// ----------------------------------------------------------------------------
// -                        Open3D: www.open3d.org                            -
// ----------------------------------------------------------------------------
// Copyright (c) 2018-2023 www.open3d.org
// SPDX-License-Identifier: MIT
// ----------------------------------------------------------------------------

#pragma once

#include <string>

#include "open3d/core/Tensor.h"
#include "open3d/core/TensorCheck.h"
#include "open3d/geometry/LineSet.h"
#include "open3d/t/geometry/BoundingVolume.h"
#include "open3d/t/geometry/DrawableGeometry.h"
#include "open3d/t/geometry/Geometry.h"
#include "open3d/t/geometry/TensorMap.h"
#include "open3d/utility/Logging.h"

namespace open3d {
namespace t {
namespace geometry {

class TriangleMesh;

/// \class LineSet
/// \brief A LineSet contains points and lines joining them and optionally
/// attributes on the points and lines.
///
/// The LineSet class stores the attribute data in key-value pairs for
/// flexibility, where the key is a string representing the attribute name and
/// value is a Tensor containing the attribute data.
///
/// By default, there are two sets of dictionaries: `point_attr_` and
/// `line_attr_`. In most cases, the length of an attribute should be equal to
/// the length of the data corresponding to the primary key. For instance,
/// `point_attr_["colors"]` should have the same length as
/// `point_attr_["positions"]`.
///
/// Although the attributes are all stored in a key-value pair dictionary, the
/// attributes have different levels:
///
/// - Default attributes {"positions", "indices"}.
///     - Created by default, required for all LineSets.
///     - The "positions" tensor must have shape {N,3} while the "indices"
///       tensor must have shape {N,2} and \ref DtypeCode Int.
///     - The device of "positions" and "indices" must be consistent and they
///       determine the device of the LineSet.
///     - Usage:
///         - LineSet::GetPointPositions()
///         - LineSet::SetPointPositions(const core::Tensor &positions)
///         - LineSet::HasPointPositions()
///         - LineSet::GetLineIndices()
///         - LineSet::SetLineIndices(const core::Tensor &indices)
///         - LineSet::HasLineIndices()
/// - Commonly used attributes: line colors.
///     - Not created by default.
///     - The tensor must be of shape {N,3}.
///     - For all attributes, the device must be consistent with the device of
///       the LineSet.
///     - Value tensors can have any \ref Dtype.
///     - Usage:
///         - Line colors (stored at line_attr_["colors"])
///             - LineSet::GetLineColors()
///             - LineSet::SetLineColors(const core::Tensor &line_colors)
///             - LineSet::HasLineColors()
/// - Custom attributes, e.g. {"labels"}.
///     - Not created by default. Users can add their own custom attributes.
///     - For all attributes, the device must be consistent with the device of
///       the LineSet.
///     - Value tensors can have any \ref Dtype.
///     - Usage:
///         - LineSet::GetPointAttr("labels")
///         - LineSet::SetPointAttr("labels", point_labels_tensor)
///         - LineSet::HasPointAttr("labels")
///         - LineSet::GetLineAttr("labels")
///         - LineSet::SetLineAttr("labels", line_labels_tensor)
///         - LineSet::HasLineAttr("labels")
///
/// Note that `{Get|Set|Has}{Point|Line}Attr()` functions also work "positions"
/// and "indices".

class LineSet : public Geometry, public DrawableGeometry {
public:
    /// Construct an empty LineSet on the provided device.
    LineSet(const core::Device &device = core::Device("CPU:0"));

    /// Construct a LineSet from points and lines.
    ///
    /// The input tensors will be directly used as the underlying storage of
    /// the line set (no memory copy). If the tensor is created in-place
    /// from a pre-allocated buffer, the tensor has a fixed size and thus
    /// the resulting LineSet will have a fixed size and calling to
    /// functions like `SynchronizedPushBack` will raise an exception.
    ///
    /// The resulting LineSet will have the same dtype and device as the
    /// tensor. The device for \p points must be consistent with
    /// \p lines.
    ///
    /// \param point_positions A tensor with element shape {3}.
    /// \param line_indices A tensor with element shape {2} and Int \ref
    /// DtypeCode.
    LineSet(const core::Tensor &point_positions,
            const core::Tensor &line_indices);

    virtual ~LineSet() override {}

    /// Transfer the line set to a specified device.
    /// \param device The targeted device to convert to.
    /// \param copy If true, a new line set is always created; if false,
    /// the copy is avoided when the original line set is already on the
    /// targeted device.
    LineSet To(const core::Device &device, bool copy = false) const;

    /// Returns copy of the line set on the same device.
    LineSet Clone() const { return To(GetDevice(), /*copy=*/true); }

    /// \brief Text description.
    std::string ToString() const;

    /// Getter for point_attr_ TensorMap. Used in Pybind.
    const TensorMap &GetPointAttr() const { return point_attr_; }

    /// Getter for point_attr_ TensorMap.
    TensorMap &GetPointAttr() { return point_attr_; }

    /// Get point attributes in point_attr_. Throws exception if the attribute
    /// does not exist.
    ///
    /// \param key Attribute name.
    core::Tensor &GetPointAttr(const std::string &key) {
        return point_attr_.at(key);
    }

    /// Get the value of the "positions" attribute in point_attr_.
    /// Convenience function.
    core::Tensor &GetPointPositions() { return GetPointAttr("positions"); }

    /// Getter for line_attr_ TensorMap. Used in Pybind.
    const TensorMap &GetLineAttr() const { return line_attr_; }

    /// Getter for line_attr_ TensorMap.
    TensorMap &GetLineAttr() { return line_attr_; }

    /// Get line attributes in line_attr_. Throws exception if the
    /// attribute does not exist.
    ///
    /// \param key Attribute name.
    core::Tensor &GetLineAttr(const std::string &key) {
        return line_attr_.at(key);
    }

    /// Get the value of the "indices" attribute in line_attr_.
    /// Convenience function.
    core::Tensor &GetLineIndices() { return GetLineAttr("indices"); }

    /// Get the value of the "colors" attribute in line_attr_.
    /// Convenience function.
    core::Tensor &GetLineColors() { return GetLineAttr("colors"); }

    /// Get point attributes. Throws exception if the attribute does not exist.
    ///
    /// \param key Attribute name.
    const core::Tensor &GetPointAttr(const std::string &key) const {
        return point_attr_.at(key);
    }

    /// Removes point attribute by key value. Primary attribute "positions"
    /// cannot be removed. Throws warning if attribute key does not exists.
    ///
    /// \param key Attribute name.
    void RemovePointAttr(const std::string &key) { point_attr_.Erase(key); }

    /// Get the value of the "positions" attribute in point_attr_.
    /// Convenience function.
    const core::Tensor &GetPointPositions() const {
        return GetPointAttr("positions");
    }

    /// Get line attributes in line_attr_. Throws exception if the
    /// attribute does not exist.
    ///
    /// \param key Attribute name.
    const core::Tensor &GetLineAttr(const std::string &key) const {
        return line_attr_.at(key);
    }

    /// Removes line attribute by key value. Primary attribute "indices"
    /// cannot be removed. Throws warning if attribute key does not exists.
    ///
    /// \param key Attribute name.
    void RemoveLineAttr(const std::string &key) { line_attr_.Erase(key); }

    /// Get the value of the "indices" attribute in line_attr_.
    /// Convenience function.
    const core::Tensor &GetLineIndices() const {
        return GetLineAttr("indices");
    }

    /// Get the value of the "colors" attribute in line_attr_.
    /// Convenience function.
    const core::Tensor &GetLineColors() const { return GetLineAttr("colors"); }

    /// Set point attributes. If the attribute key already exists, its value
    /// will be overwritten, otherwise, the new key will be created.
    ///
    /// \param key Attribute name.
    /// \param value A tensor.
    void SetPointAttr(const std::string &key, const core::Tensor &value) {
        core::AssertTensorDevice(value, device_);
        point_attr_[key] = value;
    }

    /// Set the value of the "positions" attribute in point_attr_.
    /// Convenience function.
    void SetPointPositions(const core::Tensor &value) {
        core::AssertTensorShape(value, {utility::nullopt, 3});
        SetPointAttr("positions", value);
    }

    /// Set line attributes. If the attribute key already exists, its value
    /// will be overwritten, otherwise, the new key will be created.
    ///
    /// \param key Attribute name.
    /// \param value A tensor.
    void SetLineAttr(const std::string &key, const core::Tensor &value) {
        core::AssertTensorDevice(value, device_);
        line_attr_[key] = value;
    }

    /// Set the value of the "indices" attribute in line_attr_.
    void SetLineIndices(const core::Tensor &value) {
        core::AssertTensorShape(value, {utility::nullopt, 2});
        SetLineAttr("indices", value);
    }

    /// Set the value of the "colors" attribute in line_attr_.
    /// This is a convenience function.
    void SetLineColors(const core::Tensor &value) {
        core::AssertTensorShape(value, {utility::nullopt, 3});
        SetLineAttr("colors", value);
    }

    /// Returns true if all of the following are true in point_attr_:
    /// 1) attribute key exist
    /// 2) attribute's length as "points"'s length
    /// 3) attribute's length > 0
    bool HasPointAttr(const std::string &key) const {
        return point_attr_.Contains(key) && GetPointAttr(key).GetLength() > 0 &&
               GetPointAttr(key).GetLength() == GetPointPositions().GetLength();
    }

    /// Check if the "positions" attribute's value in point_attr_ has length >
    /// 0. Convenience function.
    bool HasPointPositions() const { return HasPointAttr("positions"); }

    /// Returns true if all of the following are true in line_attr_:
    /// 1) attribute key exist
    /// 2) attribute's length as "indices"'s length
    /// 3) attribute's length > 0
    bool HasLineAttr(const std::string &key) const {
        return line_attr_.Contains(key) && GetLineAttr(key).GetLength() > 0 &&
               GetLineAttr(key).GetLength() == GetLineIndices().GetLength();
    }

    /// Check if the "indices" attribute's value in line_attr_ has
    /// length > 0.  Convenience function.
    bool HasLineIndices() const { return HasLineAttr("indices"); }

    /// Returns true if all of the following are true in line_attr_:
    /// 1) attribute "colors" exist
    /// 2) attribute "colors"'s length as "indices"'s length
    /// 3) attribute "colors"'s length > 0
    /// Convenience function.
    bool HasLineColors() const { return HasLineAttr("colors"); }

    /// Clear all data in the line set.
    LineSet &Clear() override {
        point_attr_.clear();
        line_attr_.clear();
        return *this;
    }

    /// Returns !HasPointPositions(), line indices are ignored.
    bool IsEmpty() const override { return !HasPointPositions(); }

    /// Returns the max bound for point coordinates.
    core::Tensor GetMinBound() const { return GetPointPositions().Min({0}); }

    /// Returns the max bound for point coordinates.
    core::Tensor GetMaxBound() const { return GetPointPositions().Max({0}); }

    /// Returns the center for point coordinates.
    core::Tensor GetCenter() const { return GetPointPositions().Mean({0}); }

    /// \brief Transforms the points and lines of the LineSet.
    ///
    /// Custom attributes (e.g.: point or line normals) are not transformed.
    ///
    /// Transformation matrix is a 4x4 matrix.
    ///  T (4x4) =   [[ R(3x3)  t(3x1) ],
    ///               [ O(1x3)  s(1x1) ]]
    ///  (s = 1 for Transformation without scaling)
    ///
    ///  It applies the following general transform to each `positions` and
    ///  `normals`.
    ///   |x'|   | R(0,0) R(0,1) R(0,2) t(0)|   |x|
    ///   |y'| = | R(1,0) R(1,1) R(1,2) t(1)| @ |y|
    ///   |z'|   | R(2,0) R(2,1) R(2,2) t(2)|   |z|
    ///   |w'|   | O(0,0) O(0,1) O(0,2)  s  |   |1|
    ///
    ///   [x, y, z] = [x', y', z'] / w'
    ///
    /// \param transformation Transformation [Tensor of shape {4,4}].
    /// \return Transformed line set.
    LineSet &Transform(const core::Tensor &transformation);

    /// \brief Translates the points and lines of the LineSet.
    /// \param translation Translation tensor of shape {3}
    /// \param relative If true (default) translates relative to center of
    /// LineSet.
    /// \return Translated line set.
    LineSet &Translate(const core::Tensor &translation, bool relative = true);

    /// \brief Scales the points and lines of the LineSet.
    /// \param scale Scale magnitude.
    /// \param center Center [Tensor of shape {3}] about which the LineSet is
    /// \return Scaled line set.
    LineSet &Scale(double scale, const core::Tensor &center);

    /// \brief Rotates the points and lines of the line set. Custom attributes
    /// (e.g.: point or line normals) are not transformed.
    /// \param R Rotation [Tensor of shape {3,3}].
    /// \param center Center [Tensor of shape {3}] about which the LineSet is
    /// to be scaled.
    /// \return Rotated line set.
    LineSet &Rotate(const core::Tensor &R, const core::Tensor &center);

    /// \brief Returns the device attribute of this LineSet.
    core::Device GetDevice() const override { return device_; }

    /// Create a LineSet from a legacy Open3D LineSet.
    /// \param lineset_legacy Legacy Open3D LineSet.
    /// \param float_dtype Float32 or Float64, used to store floating point
    /// values, e.g. points, normals, colors.
    /// \param int_dtype Int32 or Int64, used to store index values, e.g.
    /// line indices.
    /// \param device The device where the resulting LineSet resides.
    static geometry::LineSet FromLegacy(
            const open3d::geometry::LineSet &lineset_legacy,
            core::Dtype float_dtype = core::Float32,
            core::Dtype int_dtype = core::Int64,
            const core::Device &device = core::Device("CPU:0"));

    /// Convert to a legacy Open3D LineSet.
    open3d::geometry::LineSet ToLegacy() const;

    /// Create an axis-aligned bounding box from point attribute "positions".
    AxisAlignedBoundingBox GetAxisAlignedBoundingBox() const;

    /// Create an oriented bounding box from point attribute "positions".
    OrientedBoundingBox GetOrientedBoundingBox() const;

    /// Sweeps the line set rotationally about an axis.
    /// \param angle The rotation angle in degree.
    /// \param axis The rotation axis.
    /// \param resolution The resolution defines the number of intermediate
    /// sweeps about the rotation axis.
    /// \param translation The translation along the rotation axis.
    /// \param capping If true adds caps to the mesh.
    /// \return A triangle mesh with the result of the sweep operation.
    TriangleMesh ExtrudeRotation(double angle,
                                 const core::Tensor &axis,
                                 int resolution = 16,
                                 double translation = 0.0,
                                 bool capping = true) const;

    /// Sweeps the line set along a direction vector.
    /// \param vector The direction vector.
    /// \param scale Scalar factor which essentially scales the direction
    /// vector. \param capping If true adds caps to the mesh. \return A triangle
    /// mesh with the result of the sweep operation.
    TriangleMesh ExtrudeLinear(const core::Tensor &vector,
                               double scale = 1.0,
                               bool capping = true) const;

protected:
    core::Device device_ = core::Device("CPU:0");
    TensorMap point_attr_;
    TensorMap line_attr_;
};

}  // namespace geometry
}  // namespace t
}  // namespace open3d
