#pragma once
#ifndef POSE_HPP
#define POSE_HPP 1
#define NUMCPP_NO_USE_BOOST ON
#include <iostream>
#include <numeric>
#include <iostream>
#include <algorithm> // For std::min_element, std::max_element
#include "NumCpp.hpp"
#ifdef _MSC_VER
#include <valarray>
#endif

namespace QOSM::Pose{
    using MatrixX3d = nc::NdArray<double>;
    
    /**
     * @brief
     * Vector of Nx3 points. Each point as 3 components (x, y, z) and can represent a position vector, a unit vector or a Poyinting vector.
     *
     */
    class VectorNx3 : public MatrixX3d{
        protected:
            /// Number of points (number of rows)
            size_type N;
        
        public:
            /**
             * @brief Default constructor, initializes a 1x3 vector.
             */
            VectorNx3() : NdArray(1, 3), N(1){};

            /**
             * @brief Constructs a vector with N rows and 3 columns, initializing all elements to a specific value.
             * @param _N Number of rows (vectors).
             * @param value Initial value for all components (default: 0).
             */
            VectorNx3(const size_type _N, double value = 0.0) : NdArray(_N, 3), N(_N){
                fill(double{ value });
            };

            /**
             * @brief Constructs a vector from an existing matrix of size Nx3.
             * @param data Matrix of values.
             */
            VectorNx3(const MatrixX3d data) : NdArray(data), N(data.numRows()){};

            /**
             * @brief Constructs a vector from a pointer to an array of doubles.
             * @param ptr Pointer to data.
             * @param size Total number of elements (must be a multiple of 3).
             */
            VectorNx3(const double* ptr, uint32_t size) : NdArray(ptr, size){
                N = size / 3;
            }

            /**
             * @brief Returns the number of vectors (rows).
             * @return Number of vectors.
             */
            inline nc::uint32 rows() const{
                return N;
            }

            /**
             * @brief Retrieves a column at a specific index.
             * @param idx Index of the column.
             * @return Column as an Nx1 array.
             */
            inline auto col(nc::uint32 idx){
                return (*this)(rSlice(), idx);
            }

            /**
             * @brief Retrieves a row at a specific index.
             * @param idx Index of the row.
             * @return Row as a 1x3 array.
             */
            inline auto row(nc::uint32 idx){
                return (*this)(idx, cSlice());
            }

            /**
             * @brief Extracts a column and returns it as a separate VectorNx3 instance.
             * @param idx Index of the column.
             * @return New VectorNx3 containing the column.
             */
            inline auto vcol(nc::uint32 idx){
                return VectorNx3((*this)(rSlice(), idx));
            }

            /**
             * @brief Extracts a row and returns it as a separate VectorNx3 instance.
             * @param idx Index of the row.
             * @param N Number of repetitions (default: 1).
             * @return New VectorNx3 containing the repeated row.
             */
            inline auto vrow(nc::uint32 idx, nc::uint32 N = 1){
                return VectorNx3((*this)(idx, cSlice()).repeat(N, 1));
            }

            /**
             * @brief Repeats a row N times.
             * @param idx Index of the row.
             * @return New instance with the row repeated.
             */
            inline auto rowN(nc::uint32 idx){
                return (*this)(idx, cSlice()).repeat(N, 1);
            }

            /**
             * @brief Inserts a column vector at a specified index.
             * @param idx Column index.
             * @param data Column data (Nx1 matrix).
             */
            inline void putCol(nc::uint32 idx, MatrixX3d &data){
                put(rSlice(), idx, data);
            }

            /**
             * @brief Inserts a row vector at a specified index.
             * @param idx Row index.
             * @param data Row data (1x3 matrix).
             */
            inline void putRow(nc::uint32 idx, MatrixX3d &data){
                put(idx, cSlice(), data);
            }
            
            /**
             * @brief Creates a VectorNx3 from an existing MatrixX3d.
             * @param data Matrix of values.
             * @return New VectorNx3 instance.
             */
            static VectorNx3 CreateVectorNx3(const MatrixX3d data){
                return VectorNx3(NdArray(data));
            };

            /**
             * @brief Computes the norm of each row.
             * @return Nx1 matrix containing norms.
             */
            inline MatrixX3d norm(){
                return nc::sqrt<double>(nc::sum<double>((*this)*(*this), nc::Axis::COL));
            }

            /**
             * @brief Computes the squared norm of each row.
             * @return Nx1 matrix containing squared norms.
             */
            inline MatrixX3d norm2(){
                return nc::sum<double>((*this)*(*this), nc::Axis::COL);
            }

            /**
             * @brief Normalizes each row in place.
             * @return Nx1 matrix of normalization factors.
             */
            nc::NdArray<double> normalise(){
                const auto norm = (nc::sqrt<double>(nc::sum<double>((*this)*(*this), nc::Axis::COL))).reshape(N, 1);
                *this /= norm;
                return norm;
            }

            /**
             * @brief Returns a normalized copy of the vector.
             * @return New VectorNx3 instance with normalized rows.
             */
            VectorNx3 normalised() const{
                auto n = nc::sqrt<double>(nc::sum<double>((*this)*(*this), nc::Axis::COL)).reshape(N, 1);
                return VectorNx3((*this) / n);
            }
            
            /**
             * @brief Computes the dot product of each row with another VectorNx3.
             * @warning Both vectors must have the same number of rows.
             * @return Nx1 matrix of dot product values.
             */
            inline MatrixX3d dot(const VectorNx3 &b){
                return nc::sum<double>((*this)*b, nc::Axis::COL);
            }
            
            /**
             * @brief Computes the cross product of each row with another VectorNx3.
             * @warning Both vectors must have the same number of rows.
             * @return New VectorNx3 containing cross product results.
             */
            VectorNx3 cross(const VectorNx3 &v2){
                const VectorNx3 &v1 = *this;
                auto res = VectorNx3(v1.numRows());
                auto r = v1.rSlice();
                res.put(r, 0, v1(r, 1) * v2(r, 2) - v1(r, 2) * v2(r, 1));
                res.put(r, 1, v1(r, 2) * v2(r, 0) - v1(r, 0) * v2(r, 2));
                res.put(r, 2, v1(r, 0) * v2(r, 1) - v1(r, 1) * v2(r, 0));
                return res;
            }

            /**
             * @brief Flips the direction of all vectors.
             * @return New VectorNx3 with reversed directions.
             */
            VectorNx3 flip(){
                MatrixX3d res = *this * -1.;
                return VectorNx3(res);
            }

            /** @brief Scalar multiplication. */
            friend VectorNx3 operator*(VectorNx3 lhs, const double &rhs){
                auto res = lhs * rhs;
                return VectorNx3(res);
            }

            /** @brief Scalar division. */
            VectorNx3 &operator*=(const double &rhs){
                *this *= rhs;
                return *this;
            }

            /** @brief Scalar addition. */
            friend VectorNx3 operator/(VectorNx3 lhs, const double &rhs){
                auto res = lhs / rhs;
                return VectorNx3(res);
            }

            /** @brief Scalar subtraction. */
            VectorNx3 &operator/=(const double &rhs){
                *this /= rhs;
                return *this;
            }

            friend VectorNx3 operator+(VectorNx3 lhs, const double &rhs){
                auto res = lhs + rhs;
                return VectorNx3(res);
            }
            VectorNx3 &operator+=(const double &rhs){
                *this += rhs;
                return *this;
            }

            friend VectorNx3 operator-(VectorNx3 lhs, const double &rhs){
                auto res = lhs - rhs;
                return VectorNx3(res);
            }
            VectorNx3 &operator-=(const double &rhs){
                *this -= rhs;
                return *this;
            }
    };
    #ifndef _MSC_VER
    typedef double v4df __attribute__ ((vector_size (32)));
    #else
    using v4df = std::valarray<double>;
    #endif

    
    /**
     * @brief
     * 3D Vector of doubles (x, y, z). It can represent a position vector, a unit vector or a Poyinting vector.
     *
     */
    class Vec3{
        protected:
            v4df data = {0., 0., 0., 0.};
            Vec3(const v4df &v){
                data[0] = v[0];
                data[1] = v[1];
                data[2] = v[2];
            }
        
        public:        
            /**
             * @brief Default constructor initializing the vector to (0,0,0).
             */
            Vec3(double x=0., double y=0., double z=0.){
                data[0] = x;
                data[1] = y;
                data[2] = z;
            }

            /**
             * @brief Constructor initializing from an nc::NdArray.
             * @param inVector Input array.
             * @param idx Row index to extract the vector from.
             */
            Vec3(const nc::NdArray<double> &inVector, const std::size_t idx){
                data[0] = inVector(idx, 0);
                data[1] = inVector(idx, 1);
                data[2] = inVector(idx, 2);
            }
            
            /**
             * @brief Constructor initializing from a VectorNx3.
             * @param inVector Input matrix.
             * @param idx Row index to extract the vector from.
             */
            Vec3(const VectorNx3 &inVector, const std::size_t idx){
                data[0] = inVector(idx, 0);
                data[1] = inVector(idx, 1);
                data[2] = inVector(idx, 2);
            }
            
            /**
             * @brief Constructor initializing from a 3-element std::array.
             * @param v Input array.
             */
            Vec3(const std::array<double, 3> &v){
                auto v_ptr = v.data();
                data[0] = v_ptr[0];
                data[1] = v_ptr[1];
                data[2] = v_ptr[2];
            }
    
            /**
             * @brief Constructor initializing from a std::vector.
             * @param vec Input vector (must have at least 3 elements).
             * @throws std::runtime_error if the vector has less than 3 elements.
             */
            Vec3(const std::vector<double>& vec) {
                if (vec.size() < 3)
                    throw std::runtime_error("Invalid vector size (<3)");
                auto it = vec.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it;
            }

            /**
             * @brief Constructor initializing from an initializer list.
             * @param values List of 3 values.
             * @throws std::runtime_error if the list has less than 3 elements.
             */
            Vec3(std::initializer_list<double> values) {
                if (values.size() < 3)
                    throw std::runtime_error("Invalid list size (<3)");
                auto it = values.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it;
            }

            /**
             * @brief Assigns values using an initializer list.
             * @param values List of 3 values.
             * @throws std::runtime_error if the list has less than 3 elements.
             */
            void assign(std::initializer_list<double> values) {
                if (values.size() < 3)
                    throw std::runtime_error("Invalid list size (<3)");
                auto it = values.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it;
            }

            /**
             * @brief Computes the dot product with another vector.
             * @param rhl The other vector.
             * @return The dot product value.
             */
            inline double dot(const Vec3 &rhl) const{
                return data[0]*rhl.data[0] + data[1]*rhl.data[1] + data[2]*rhl.data[2];
            }

            /**
             * @brief Computes the squared norm of the vector.
             * @return The squared norm value.
             */
            inline double norm2() const{
                return data[0]*data[0] + data[1]*data[1] + data[2]*data[2];
            }

            /**
             * @brief Computes the norm (magnitude) of the vector.
             * @return The norm value.
             */
            inline double norm() const{
                return std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2]);
            }

            /**
             * @brief Normalizes the vector in place.
             * @return The original norm before normalization.
             */
            inline double normalise(){
                const double norm = std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2]);
                if (norm != 0.) data /= norm;
                return norm;
            }

            /**
             * @brief Returns a normalized copy of the vector.
             * @return A normalized vector.
             */
            inline Vec3 normalised() const{
                const double n = std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2]);
                return n != 0. ? Vec3(data / n) : Vec3();
            }

            /**
             * @brief Computes the cross product with another vector.
             * @param rhl The other vector.
             * @return The resulting cross product vector.
             */
            Vec3 cross(const Vec3 &rhl) const{
                return Vec3(
                    data[1]*rhl.data[2] - data[2]*rhl.data[1],
                    data[2]*rhl.data[0] - data[0]*rhl.data[2],
                    data[0]*rhl.data[1] - data[1]*rhl.data[0]
                );
            }
            inline Vec3& operator+=(const Vec3& rhl){
                data += rhl.data;
                return *this;
            }
            inline Vec3& operator-=(const Vec3& rhl){
                data -= rhl.data;
                return *this;
            }
            inline Vec3& operator*=(const Vec3& rhl){
                data *= rhl.data;
                return *this;
            }
            inline Vec3& operator/=(const Vec3& rhl){
                data /= rhl.data;
                return *this;
            }
            inline Vec3& operator+=(const double& rhl){
                data += rhl;
                return *this;
            }
            inline Vec3& operator-=(const double& rhl){
                data -= rhl;
                return *this;
            }
            inline Vec3& operator*=(const double& rhl){
                data *= rhl;
                return *this;
            }
            inline Vec3& operator/=(const double& rhl){
                data /= rhl;
                return *this;
            }
            inline double &operator[](short dim){
                return data[dim];
            }

            inline double operator[](short dim) const{
                return data[dim];
            }

            friend Vec3 operator+(const Vec3& lhl, const Vec3& rhl){
                return Vec3(lhl.data + rhl.data);
            }
            friend Vec3 operator-(const Vec3& lhl, const Vec3& rhl){
                return Vec3(lhl.data - rhl.data);
            }
            friend Vec3 operator*(const Vec3& lhl, const Vec3& rhl){
                return Vec3(lhl.data * rhl.data);
            }
            friend Vec3 operator/(const Vec3& lhl, const Vec3& rhl){
                return Vec3(lhl.data / rhl.data);
            }

            friend Vec3 operator+(const Vec3& lhl, const double rhl){
                return Vec3(lhl.data + rhl);
            }
            friend Vec3 operator-(const Vec3& lhl, const double rhl){
                return Vec3(lhl.data - rhl);
            }
            friend Vec3 operator*(const Vec3& lhl, const double rhl){
                return Vec3(lhl.data * rhl);
            }
            friend Vec3 operator/(const Vec3& lhl, const double rhl){
                return Vec3(lhl.data / rhl);
            }

            friend Vec3 operator+(const double lhl, const Vec3& rhl){
                return Vec3(lhl + rhl.data);
            }
            friend Vec3 operator-(const double lhl, const Vec3& rhl){
                return Vec3(lhl - rhl.data);
            }
            friend Vec3 operator*(const double lhl, const Vec3& rhl){
                return Vec3(lhl * rhl.data);
            }
            friend Vec3 operator/(const double lhl, const Vec3& rhl){
                return Vec3(lhl / rhl.data);
            }

            /**
             * @brief Computes the minimum component of the vector.
             * @return The minimum value.
             */
            double min() const {
                return (data[0] < data[1]) 
                    ? (data[0] < data[2] ? data[0] : data[1])
                    : (data[1] < data[2] ? data[1] : data[2]);
            }

            /**
             * @brief Computes the maximum component of the vector.
             * @return The maximum value.
             */
            double max() const {
                return (data[0] > data[1]) 
                    ? (data[0] > data[2] ? data[0] : data[1])
                    : (data[1] > data[2] ? data[1] : data[2]);
            }
            
            /**
             * @brief Computes the minimum between two vectors.
             * @return A vector containing the element-wise min.
             */
            Vec3 minimum(const Vec3& other) const {
                return {
                    std::min(data[0], other.data[0]),
                    std::min(data[1], other.data[1]),
                    std::min(data[2], other.data[2])
                };
            }

            /**
             * @brief Computes the maximum between two vectors.
             * @return A vector containing the element-wise max.
             */
            Vec3 maximum(const Vec3& other) const {
                return {
                    std::max(data[0], other.data[0]),
                    std::max(data[1], other.data[1]),
                    std::max(data[2], other.data[2])
                };
            }

            /**
             * @brief Finds the index of the minimum element.
             * @return Index of the minimum value.
             */
            int argmin() const {
                return (data[0] < data[1]) 
                    ? (data[0] < data[2] ? 0 : 1)
                    : (data[1] < data[2] ? 1 : 2);
            }

            /**
             * @brief Finds the index of the maximum element.
             * @return Index of the maximum value.
             */
            int argmax() const {
                return (data[0] > data[1]) 
                    ? (data[0] > data[2] ? 0 : 1)
                    : (data[1] > data[2] ? 1 : 2);
            }

            /**
             * @brief Converts the vector to a VectorNx3 representation.
             * @param N Number of rows.
             * @return A VectorNx3 with repeated rows.
             */
            VectorNx3 toNd(const std::size_t N) const{
                VectorNx3 res(N);
                res.put(res.rSlice(), 0, data[0]);
                res.put(res.rSlice(), 1, data[1]);
                res.put(res.rSlice(), 2, data[2]);
                return res;
            }

            /**
             * @brief Accessors for individual components.
             */
            double& getx(){
                return data[0];
            }

            /**
             * @brief Accessors for individual components.
             */
            double& gety(){
                return data[1];
            }

            /**
             * @brief Accessors for individual components.
             */
            double& getz(){
                return data[2];
            }

            /**
             * @brief Mutators for individual components.
             */
            void setx(double &val){
                data[0] = val;
            }

            /**
             * @brief Mutators for individual components.
             */
            void sety(double &val){
                data[1] = val;
            }

            /**
             * @brief Mutators for individual components.
             */
            void setz(double &val){
                data[2] = val;
            }

            /**
             * @brief Creates a copy of the vector.
             * @return A new Vec3 instance with the same values.
             */
            Vec3 copy() const{
                return Vec3(data[0], data[1], data[2]);
            }
            
            /**
             * @brief Overloaded stream output operator.
             */
            friend std::ostream& operator<<(std::ostream& os, const Vec3& o){
                os << "Vector: [" << o.data[0] << ", " << o.data[1] << ", " << o.data[2] << "]";
                return os;
            }

            /**
             * @brief Converts the vector to a string representation.
             * @return A formatted string representing the vector.
             */
            std::string __str__() const {
                return "Vector: [" + std::to_string(data[0]) + ", " + std::to_string(data[1]) + ", " + std::to_string(data[2]) + "] ";
            }
    };

    
    /**
     * @class Quaternion
     * @brief Represents a quaternion, using the European convention (w, x, y, z).
     *
     * Quaternions are used for representing rotations in 3D space and provide
     * an efficient way to handle rotations without suffering from gimbal lock.
     * This class supports various constructors, arithmetic operations, and
     * rotation transformations.
     *
     * @note The class uses a four-element vector (v4df) to store the quaternion components.
     */
    class Quaternion{
        protected:
            v4df data = {0., 0., 0., 0.};
            Quaternion(const v4df &q){
                data[0] = q[0];
                data[1] = q[1];
                data[2] = q[2];
                data[3] = q[3];
            }
        
        public:        
            /**
             * @brief Default constructor, initializes a quaternion.
             * @param qw Scalar (real) component, default is 1.
             * @param qx X-component, default is 0.
             * @param qy Y-component, default is 0.
             * @param qz Z-component, default is 0.
             */
            Quaternion(const double& qw=1., const double& qx=0., const double& qy=0., const double& qz=0.){
                data[0] = qw;
                data[1] = qx;
                data[2] = qy;
                data[3] = qz;
            }
            
            /**
             * @brief Constructs a quaternion from a scalar and a 3D vector.
             * @param w Scalar component.
             * @param v 3D vector representing the imaginary part.
             */
            Quaternion(const double& w, const Vec3& v){
                data[0] = w;
                data[1] = v[0];
                data[2] = v[1];
                data[3] = v[2];
            }
        
            /**
             * @brief Constructs a quaternion from an axis-angle representation.
             * @param axis Rotation axis (must be normalized).
             * @param angle Rotation angle in radians (or degrees if deg=true).
             * @param deg Whether the angle is given in degrees (default: false).
             */
            Quaternion(const Vec3& axis, const double& angle, const bool& deg=false){
                const double _angle = (deg ? angle * nc::constants::pi / 180. : angle) * .5;
                const double sAngle = std::sin(_angle);
                data[0] = std::cos(_angle);
                data[1] = axis[0] * sAngle;
                data[2] = axis[1] * sAngle;
                data[3] = axis[2] * sAngle;
            }
        
            /**
             * @brief Constructs a quaternion from a rotation vector.
             * @param rotVec A vector representing the axis and angle of rotation.
             * @param deg Whether the rotation vector is in degrees (default: false).
             */
            Quaternion(const Vec3& rotVec, const bool& deg=false){
                const double angle = rotVec.norm() * (deg ? nc::constants::pi / 180. : 1.) * .5;
                const double sAngle = std::sin(angle);
                const Vec3 axis = rotVec.normalised();
                data[0] = std::cos(angle);
                data[1] = axis[0] * sAngle;
                data[2] = axis[1] * sAngle;
                data[3] = axis[2] * sAngle;
            }
        
            /**
             * @brief Constructs a quaternion from a 4-element array.
             * @param q An array of 4 double values.
             */
            Quaternion(const std::array<double, 4> &q){
                auto q_ptr = q.data();
                data[0] = q_ptr[0];
                data[1] = q_ptr[1];
                data[2] = q_ptr[2];
                data[3] = q_ptr[3];
            }
        
            /**
             * @brief Constructs a quaternion from a vector of at least 4 elements.
             * @param q A std::vector of doubles (must have at least 4 elements).
             * @throws std::runtime_error if the vector size is < 4.
             */
            Quaternion(const std::vector<double>& q) {
                if (q.size() < 4)
                    throw std::runtime_error("Invalid quaternion size (<4)");
                auto it = q.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it++;
                data[3] = *it;
            }

            /**
             * @brief Constructs a quaternion from an initializer list.
             * @param values A list of 4 double values.
             * @throws std::runtime_error if the list size is < 4.
             */
            Quaternion(std::initializer_list<double> values) {
                if (values.size() < 4)
                    throw std::runtime_error("Invalid list size (<4)");
                auto it = values.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it++;
                data[3] = *it;
            }

            /**
             * @brief Assigns new values to the quaternion from an initializer list.
             * @param values A list of 4 double values.
             * @throws std::runtime_error if the list size is < 4.
             */
            void assign(std::initializer_list<double> values) {
                if (values.size() < 4)
                    throw std::runtime_error("Invalid list size (<4)");
                auto it = values.begin();
                data[0] = *it++;
                data[1] = *it++;
                data[2] = *it++;
                data[3] = *it;
            }

            /**
             * @brief Returns the vector (imaginary) part of the quaternion.
             * @return Vec3 The imaginary part (x, y, z).
             */
            inline Vec3 toVec3() const{
                return {data[1], data[2], data[3]};
            }

            /** @brief Accessor for the scalar (real) part of the quaternion. */
            double& getw(){
                return data[0];
            }

            /** @brief Accessor for the x-component of the quaternion. */
            double& getx(){
                return data[1];
            }

            /** @brief Accessor for the y-component of the quaternion. */
            double& gety(){
                return data[2];
            }

            /** @brief Accessor for the z-component of the quaternion. */
            double& getz(){
                return data[3];
            }

            /**
             * @brief Computes the conjugate of the quaternion.
             * @return Quaternion with the same real part but negated imaginary parts.
             */
            inline Quaternion conj() const{
                return {data[0], -data[1], -data[2], -data[3]};
            }

            /**
             * @brief Computes the dot product with another quaternion.
             * @param rhl The other quaternion.
             * @return Dot product value.
             */
            inline double dot(const Quaternion &rhl) const{
                return data[0]*rhl.data[0] + data[1]*rhl.data[1] + data[2]*rhl.data[2];
            }

            /**
             * @brief Computes the squared norm of the quaternion.
             * @return The squared norm.
             */
            inline double norm2() const{
                return data[0]*data[0] + data[1]*data[1] + data[2]*data[2] + data[3]*data[3];
            }

            /**
             * @brief Computes the norm (magnitude) of the quaternion.
             * @return The norm of the quaternion.
             */
            inline double norm() const{
                return std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2] + data[3]*data[3]);
            }

            /**
             * @brief Normalizes the quaternion in place.
             */
            inline void normalise(){
                data /= std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2] + data[3]*data[3]);
            }

            /**
             * @brief Returns a normalized copy of the quaternion.
             * @return A normalized quaternion.
             */
            inline Quaternion normalised() const{
                const double n = std::sqrt(data[0]*data[0] + data[1]*data[1] + data[2]*data[2] + data[3]*data[3]);
                return Quaternion(data / n);
            }

            /**
             * @brief Rotates a 3D vector using the quaternion.
             * @param v The vector to rotate.
             * @return The rotated vector.
             */
            inline Vec3 rotate(const Vec3& v) const{
                const Quaternion qv(0., v[0], v[1], v[2]);
                const Quaternion res = (*this) * (qv * (*this).conj());
                return {res[1], res[2], res[3]};
            }

            /**
             * @brief Overloaded rotation method for modifying the input vector.
             */
            inline Vec3 rotate(Vec3& v) const{
                const Quaternion qv(0., v[0], v[1], v[2]);
                const Quaternion res = (*this) * (qv * (*this).conj());
                return {res[1], res[2], res[3]};
            }

            /**
             * @brief Rotates a set of 3D vectors.
             * @param v The vectors to rotate.
             * @return The rotated vectors.
             */
            inline VectorNx3 rotate(const VectorNx3& v) const{
                const auto N = v.numRows();
                VectorNx3 res(N);
                for(int i = 0; i < N; i++){
                    const Quaternion qv(0., v(i, 0), v(i, 1), v(i, 2));
                    const Quaternion qres = (*this) * (qv * (*this).conj());
                    MatrixX3d a = {qres[1], qres[2], qres[3]};
                    res.putRow(i, a);
                }
                return res;
            }

            /**
             * @brief Overloaded rotation method for modifying a set of vectors.
             */
            inline VectorNx3 rotate(VectorNx3& v) const{
                const auto N = v.numRows();
                VectorNx3 res(N);
                for(int i = 0; i < N; i++){
                    const Quaternion qv(0., v(i, 0), v(i, 1), v(i, 2));
                    const Quaternion qres = (*this) * (qv * (*this).conj());
                    MatrixX3d a = {qres[1], qres[2], qres[3]};
                    res.putRow(i, a);
                }
                return res;
            }

            inline double &operator[](short dim){
                return data[dim];
            }

            inline double operator[](short dim) const{
                return data[dim];
            }

            friend Quaternion operator+(const Quaternion& lhl, const Quaternion& rhl){
                return Quaternion(lhl.data + rhl.data);
            }
            friend Quaternion operator-(const Quaternion& lhl, const Quaternion& rhl){
                return Quaternion(lhl.data - rhl.data);
            }
            friend Quaternion operator*(const Quaternion& lhl, const Quaternion& rhl){
                return {
                    lhl.data[0] * rhl.data[0] - lhl.data[1] * rhl.data[1] - lhl.data[2] * rhl.data[2] - lhl.data[3] * rhl.data[3], // w
                    lhl.data[0] * rhl.data[1] + lhl.data[1] * rhl.data[0] + lhl.data[2] * rhl.data[3] - lhl.data[3] * rhl.data[2], // x
                    lhl.data[0] * rhl.data[2] - lhl.data[1] * rhl.data[3] + lhl.data[2] * rhl.data[0] + lhl.data[3] * rhl.data[1], // y
                    lhl.data[0] * rhl.data[3] + lhl.data[1] * rhl.data[2] - lhl.data[2] * rhl.data[1] + lhl.data[3] * rhl.data[0]  // z
                };
            }

            friend Quaternion operator*(const Quaternion& lhl, const double rhl){
                return Quaternion(lhl.data * rhl);
            }
            friend Quaternion operator/(const Quaternion& lhl, const double rhl){
                return Quaternion(lhl.data / rhl);
            }
            friend Quaternion operator*(const double lhl, const Quaternion& rhl){
                return Quaternion(lhl * rhl.data);
            }
            
            friend std::ostream& operator<<(std::ostream& os, const Quaternion& o){
                os << "Quaternion: [" << o.data[0] << ", " << o.data[1] << ", " << o.data[2] << ", " << o.data[3] << "]";
                return os;
            }

            // Define the __str__() method for printing the object
            std::string __str__() const {
                return "Quaternion: [" + std::to_string(data[0]) + ", " + std::to_string(data[1]) + ", " + std::to_string(data[2]) + ", " + std::to_string(data[3]) + "] ";
            }
    };
    
    /**
     * @struct Pose
     * @brief Represents a 3D pose consisting of position and orientation.
     *
     * The Pose structure combines a position vector (`ref_r_ref_ori`) and an 
     * orientation quaternion (`q_ref_ori`). It provides multiple constructors 
     * for initialization using initializer lists, arrays, or explicit parameters.
     */
    struct Pose{
        Vec3 ref_r_ref_ori; ///< Position vector (x, y, z) in reference frame.
        Quaternion q_ref_ori; ///< Orientation as a quaternion.

        /**
         * @brief Constructs a Pose from an initializer list.
         * @param values A list of 7 double values (3 for position, 4 for quaternion).
         * @throws std::runtime_error if the list size is not 7.
         */
        Pose(const std::initializer_list<double>& values) {
            if (values.size() != 7)
                throw std::runtime_error("Invalid list size (must be 7)");
            auto it = values.begin();
            ref_r_ref_ori = {*it++, *it++, *it++};
            q_ref_ori = {*it++, *it++, *it++, *it++};
        }

        /**
         * @brief Constructs a Pose from a 7-element array.
         * @param values A std::array of 7 doubles (3 for position, 4 for quaternion).
         */
        Pose(const std::array<double, 7>& values) {
            auto it = values.begin();
            ref_r_ref_ori = {*it++, *it++, *it++};
            q_ref_ori = {*it++, *it++, *it++, *it++};
        }

        /**
         * @brief Constructs a Pose from explicit position and quaternion values.
         * @param v The position vector (default: zero vector).
         * @param q The orientation quaternion (default: identity quaternion).
         */
        Pose(Vec3 v = Vec3(), Quaternion q = Quaternion()) {
            ref_r_ref_ori = v;
            q_ref_ori = q;
        }
    };

    /**
     * Write the pose of b (written in frame ref) wrt the frame a
     * @param pose_ref_a Pose of a wrt frame ref
     * @param pose_ref_b Pose of b wrt frame ref
     * @return pose_a_b Pose of b wrt frale a
    */
    Pose changeFrame(const Pose &pose_ref_a, const Pose &pose_ref_b);
    
    /**
     * @brief
     * Grid of points on a planar surface
     */
    class MeshGrid{
        private:
            /// @brief Scale to apply on the points coordinates
            double scale;
        public:
            /// @brief Matrices of points (meshgrid)
            MatrixX3d U, V;

            /// @brief number of point along u and v axis
            long nu, nv;

            /**
             * Create a Meshgrid 
             * @param u Meshgrid for u axis
             * @param v Meshgrid for v axis
             * @param scale Scale to apply to the grid
             */
            MeshGrid(MatrixX3d u, MatrixX3d v, const double scale=1.) : scale(scale){
                nu = u.size();
                nv = v.size();
                auto grid = nc::meshgrid(u, v);
                U = grid.first * scale;
                V = grid.second * scale;
            }

            /**
             * Get the number of points in the grid
             * @return the number of points as an integer
             */
            inline auto size(){
                return nu * nv;
            }

            /**
             * Get an 1-D vector of the meshgrid U
             * @return the flatten vector of the mesgrid U
             */
            MatrixX3d flattenU(){
                return U.reshape(nu*nv, 1);
            }

            /**
             * Get an 1-D vector of the meshgrid V
             * @return the flatten vector of the mesgrid V
             */
            MatrixX3d flattenV(){
                return V.reshape(nu*nv, 1);
            }
            
            /**
             * Return a Vector of points for an XY plane located at z
             * @param z position along the z-axis of the XY plane
             * @return a Vector of points lying in the XY plane
             */
            VectorNx3 xyPlane(double z=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, U.reshape(nu*nv, 1));
                r_pts.put(rows, 1, V.reshape(nu*nv, 1));
                r_pts.put(rows, 2, z * scale);
                return r_pts;
            }
            
            /**
             * Return a Vector of points for an XZ plane located at y
             * @param y position along the y-axis of the XY plane
             * @return a Vector of points lying in the XZ plane
             */
            VectorNx3 xzPlane(double y=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, U.reshape(nu*nv, 1));
                r_pts.put(rows, 1, y * scale);
                r_pts.put(rows, 2, V.reshape(nu*nv, 1));
                return r_pts;
            }
            
            /**
             * Return a Vector of points for an YZ plane located at x
             * @param x position along the x-axis of the XY plane
             * @return a Vector of points lying in the YZ plane
             */
            VectorNx3 yzPlane(double x=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, x * scale);
                r_pts.put(rows, 1, U.reshape(nu*nv, 1));
                r_pts.put(rows, 2, V.reshape(nu*nv, 1));
                return r_pts;
            }
            
            /**
             * Return a Vector of points for an YX plane located at z
             * @param z position along the z-axis of the YX plane
             * @return a Vector of points lying in the YX plane
             */
            VectorNx3 yxPlane(double z=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, V.reshape(nu*nv, 1));
                r_pts.put(rows, 1, U.reshape(nu*nv, 1));
                r_pts.put(rows, 2, z * scale);
                return r_pts;
            }
            
            /**
             * Return a Vector of points for an ZY plane located at x
             * @param x position along the z-axis of the XY plane
             * @return a Vector of points lying in the ZY plane
             */
            VectorNx3 zyPlane(double x=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, x * scale);
                r_pts.put(rows, 1, V.reshape(nu*nv, 1));
                r_pts.put(rows, 2, U.reshape(nu*nv, 1));
                return r_pts;
            }
            
            /**
             * Return a Vector of points for an ZX plane located at z
             * @param y position along the z-axis of the ZX plane
             * @return a Vector of points lying in the ZX plane
             */
            VectorNx3 zxPlane(double y=0.){
                VectorNx3 r_pts(nu*nv);
                auto rows = nc::Slice(0, nu*nv);
                r_pts.put(rows, 0, V.reshape(nu*nv, 1));
                r_pts.put(rows, 1, y * scale);
                r_pts.put(rows, 2, U.reshape(nu*nv, 1));
                return r_pts;
            }
            
    };
}
#endif