/***************************************************************************
*                                                                          *
*  Copyright (C) Golaem S.A.  All Rights Reserved.                         *
*                                                                          *
***************************************************************************/

#pragma once

#ifdef _MSC_VER
#pragma warning(push)
#pragma warning(disable : 4949)
#pragma managed(push, off)
#endif

#include "glmVector3.h"
#include "glmVector4.h"
#include "glmQuaternion.h"
#include "glmQuaternionConversions.h"
#include "glmRotationMatrix.h"
#include <ostream>

namespace glm
{
    /**
	* \brief A 4*4 matrix
	* 
	* \ingroup types
	* \author Golaem <dev@golaem.com>
	*
	*/
    class GLM_CORE_API Matrix4
    {
    public:
        GLM_CLASS_STAT_ID( glm::MemoryStatIds::GolaemShared);

        /** 
		* \brief This matrix datas. 
		*/
        Vector4 _matrix[4]; // false : do not initialize array to empty, allocate data

        //************************************************************
        //! @name Constructors/destructor
        //************************************************************
        //@{

        /** 
		* \brief Default constructor.
		* \param createIdentityMatrix If true the identity matrix is created, otherwise the zero matrix is (default is false).
		*/
        Matrix4(bool createIdentityMatrix = false);

        /**
		* \brief copy constructor
		*/
        Matrix4(const Matrix4& otherMatrix4);

        /**
		* \brief Constructor from the 16 scalar elements.
		* \param f00 Scalar at [0,0]
		* \param f01 Scalar at [0,1]
		* \param f02 Scalar at [0,2]
		* \param f03 Scalar at [0,3]
		* \param f10 Scalar at [1,0]
		* \param f11 Scalar at [1,1]
		* \param f12 Scalar at [1,2]
		* \param f13 Scalar at [1,3]
		* \param f20 Scalar at [2,0]
		* \param f21 Scalar at [2,1]
		* \param f22 Scalar at [2,2]
		* \param f23 Scalar at [2,3]
		* \param f30 Scalar at [3,0]
		* \param f31 Scalar at [3,1]
		* \param f32 Scalar at [3,2]
		* \param f33 Scalar at [3,3]
		*/
        Matrix4(
            float f00, float f01, float f02, float f03,
            float f10, float f11, float f12, float f13,
            float f20, float f21, float f22, float f23,
            float f30, float f31, float f32, float f33);

        /**
		* \brief Constructor from a float array
		*/
        Matrix4(const float* values);

        /**
		* \brief Constructor from a double array
		*/
        Matrix4(const double* values);

        /**
		* \brief
		*/
        Matrix4(Vector4 f0, Vector4 f1, Vector4 f2, Vector4 f3)
        {
            _matrix[0] = f0;
            _matrix[1] = f1;
            _matrix[2] = f2;
            _matrix[3] = f3;
        }

        /**
		* \brief constructor with one value
		*/
        Matrix4(float initialValue);

        /**
		* \brief set the matrix values
		*/
        void set(float f00, float f01, float f02, float f03,
                 float f10, float f11, float f12, float f13,
                 float f20, float f21, float f22, float f23,
                 float f30, float f31, float f32, float f33);

        void set(const float* otherMatrix);

        void set(const float (&otherMatrix)[4][4]);

        /**
		* \brief identity matrix
		*/
        static const Matrix4 identity;
        static const Matrix4 nullMatrix;
        //@}

    public:
        //------------------------------------------------------------
        /**
		* \name ValueAccessor
		* These methods can get values inside the matrix from given row and column numbers.
		*/
        //------------------------------------------------------------
        //@{

        /**
		* \brief operator[] returns const row
		*/
        const Vector4& operator[](unsigned int r) const;

        /**
		* \brief operator[] returns row
		*/
        Vector4& operator[](unsigned int r);

        /**
		* \brief get a column of the matrix 
		*/
        void column(unsigned int c, Vector4& resultColumn) const;

        /**
		* \brief float pointer
		*/
        float* floats()
        {
            return &_matrix[0].x;
        }

        const float* const_floats() const
        {
            return &_matrix[0].x;
        }

        /**
		* \brief
		*/
        Vector3 right() const
        {
            return Vector3(_matrix[0].x, _matrix[0].y, _matrix[0].z);
        }

        /**
		* \brief
		*/
        Vector3 up() const
        {
            return Vector3(_matrix[1].x, _matrix[1].y, _matrix[1].z);
        }

        /**
		* \brief
		*/
        Vector3 dir() const
        {
            return Vector3(_matrix[2].x, _matrix[2].y, _matrix[2].z);
        }

        /**
		* \brief
		*/
        Vector3 position() const
        {
            return Vector3(_matrix[3].x, _matrix[3].y, _matrix[3].z);
        }

        /**
		* \brief
		*/
        void setPosition(const Vector3& position)
        {
            _matrix[3].x = position.x;
            _matrix[3].y = position.y;
            _matrix[3].z = position.z;
        }

        void addScale(const Vector3& scale)
        {
            _matrix[0].x *= scale.x;
            _matrix[1].x *= scale.x;
            _matrix[2].x *= scale.x;
            _matrix[0].y *= scale.y;
            _matrix[1].y *= scale.y;
            _matrix[2].y *= scale.y;
            _matrix[0].z *= scale.z;
            _matrix[1].z *= scale.z;
            _matrix[2].z *= scale.z;
        }

        void setRotation(const Quaternion& rotation);
        void setRotation(const RotationMatrix& rotation);

        /**
		* \brief get matrix determinant
		*/
        float GetDeterminant() const
        {
            return _matrix[0][0] * _matrix[1][1] * _matrix[2][2] + _matrix[0][1] * _matrix[1][2] * _matrix[2][0] + _matrix[0][2] * _matrix[1][0] * _matrix[2][1] -
                   _matrix[0][2] * _matrix[1][1] * _matrix[2][0] - _matrix[0][1] * _matrix[1][0] * _matrix[2][2] - _matrix[0][0] * _matrix[1][2] * _matrix[2][1];
        }

        inline Vector3 computeScale() const;
        //@}

        //************************************************************
        //! @name Operators
        //************************************************************
        //@{

        /**
		* \brief Addition of two matrices 
		*/
        Matrix4 operator+(const Matrix4& m) const;
        Matrix4& operator+=(const Matrix4& m);

        /**
		* \brief Subtraction of two matrices 
		*/
        Matrix4 operator-(const Matrix4& m) const;

        /**
		* \brief Multiplication of two matrices 
		*/
        Matrix4 operator*(Matrix4 const& m) const;

        /**
		* \brief Transpose of the matrix 
		*/
        void transpose(const Matrix4& other);
        Matrix4 transpose() const;

        /**
		* \brief Scalar multiplication 
		*/
        Matrix4 operator*(float v) const;

        /**
		* \brief Scalar division
		*/
        Matrix4 operator/(float v) const;

        /**
		* \brief operator= Matrix4
		*/
        Matrix4& operator=(Matrix4 const& m);

        /**
		* \brief operator= float
		*/
        Matrix4& operator=(float v);
        //@}

        //************************************************************
        //! @name Comparison operators
        //************************************************************
        //@{

        /**
		* \brief operator==
		*/
        bool operator==(const Matrix4& matrix2) const;

        /**
		* \brief operator!=
		*/
        bool operator!=(const Matrix4& matrix2) const;

        /**
		* \brief approxEqual
		*/
        static bool approxEqual(const Matrix4& mat1, const Matrix4& mat2, float precision = GLM_NUMERICAL_PRECISION);
        //@}

        //************************************************************
        //! @name Matrix creation toolkit
        //************************************************************
        //@{

        /**
		* \brief perspective matrix
		*/
        void perspective(float fovyInDegrees, float aspectRatio, float znear, float zfar);

        /**
		* \brief perspective matrix
		*/
        void frustum(float left, float right, float bottom, float top, float znear, float zfar);

        /**
		* \brief lookat matrix for view matrix
		*/
        void lookAt(const Vector3& eye, const Vector3& at, const Vector3& up);

        /**
		* \brief lookat matrix for object matrix
		*/
        void lookAtLH(const Vector3& eye, const Vector3& at, const Vector3& up);

        /**
		* \brief lookat matrix for object matrix
		*/
        void lookAtRH(const Vector3& eye, const Vector3& at, const Vector3& up);

        /**
		* \brief build orthographic projection matrix
		*/
        void ortho(const float l, float r, float b, const float t, float zn, const float zf);

        /**
		* \brief
		*/
        void rotationAxis(const Vector3& axis, float angle);

        /**
		* \brief matrix inverse
		*/
        float inverse(const Matrix4& srcMatrix, bool affine = false);

        /**
		* \brief
		*/
        void translation(const Vector3& position);

        /**
		* \brief
		*/
        void scale(const Vector3& scale);
        //@}
    };

    //---------------------------------------------------------
    /**
	* \brief Multiplication of a matrix with a vector (mat*vec)
	* \param matrix matrix (left operande)
	* \param vector vector (right operande)
	* \return the resulting vector
	*/
    //---------------------------------------------------------
    inline Vector4 operator*(const Matrix4& matrix, const Vector4& vector)
    {
        Vector4 result;
        result[0] = matrix[0] * vector;
        result[1] = matrix[1] * vector;
        result[2] = matrix[2] * vector;
        result[3] = matrix[3] * vector;
        return result;
    }

    //----------------------------------------------------------------------------
    // multVectMat
    //----------------------------------------------------------------------------
    /*	\param matrix matrix (right operande)
	*	\return the resulting vector
	*/
    //---------------------------------------------------------
    inline Vector4 operator*(const Vector4& vector, const Matrix4& matrix)
    {
        Vector4 result;
        Vector4 column;
        for (unsigned int i = 0; i < 4; i++)
        {
            matrix.column(i, column);
            result[i] = vector * column;
        }

        return result;
    }

    //----------------------------------------------------------------------------
    // set
    //----------------------------------------------------------------------------
    inline void Matrix4::set(
        float f00, float f01, float f02, float f03,
        float f10, float f11, float f12, float f13,
        float f20, float f21, float f22, float f23,
        float f30, float f31, float f32, float f33)
    {
        _matrix[0].setValues(f00, f01, f02, f03);
        _matrix[1].setValues(f10, f11, f12, f13);
        _matrix[2].setValues(f20, f21, f22, f23);
        _matrix[3].setValues(f30, f31, f32, f33);
    }

    //----------------------------------------------------------------------------
    // set
    //----------------------------------------------------------------------------
    inline void Matrix4::set(const float* otherMatrix)
    {
        _matrix[0].setValues(otherMatrix[0], otherMatrix[1], otherMatrix[2], otherMatrix[3]);
        _matrix[1].setValues(otherMatrix[4], otherMatrix[5], otherMatrix[6], otherMatrix[7]);
        _matrix[2].setValues(otherMatrix[8], otherMatrix[9], otherMatrix[10], otherMatrix[11]);
        _matrix[3].setValues(otherMatrix[12], otherMatrix[13], otherMatrix[14], otherMatrix[15]);
    }

    //----------------------------------------------------------------------------
    // set
    //----------------------------------------------------------------------------
    inline void Matrix4::set(const float (&otherMatrix)[4][4])
    {
        _matrix[0].setValues(otherMatrix[0]);
        _matrix[1].setValues(otherMatrix[1]);
        _matrix[2].setValues(otherMatrix[2]);
        _matrix[3].setValues(otherMatrix[3]);
    }

    //----------------------------------------------------------------------------
    inline Vector3 Matrix4::computeScale() const
    {
        return Vector3(
            Vector3(&_matrix[0][0]).norm(),
            Vector3(&_matrix[1][0]).norm(),
            Vector3(&_matrix[2][0]).norm());
    }

    //-------------------------------------------------------------------------
    // TransformVector
    //-------------------------------------------------------------------------
    inline Vector4
    transformVector(const Vector4& src, const Matrix4& matrix)
    {
        Vector4 out;

        out.x = src.x * matrix._matrix[0][0] + src.y * matrix._matrix[1][0] + src.z * matrix._matrix[2][0];
        out.y = src.x * matrix._matrix[0][1] + src.y * matrix._matrix[1][1] + src.z * matrix._matrix[2][1];
        out.z = src.x * matrix._matrix[0][2] + src.y * matrix._matrix[1][2] + src.z * matrix._matrix[2][2];
        out.w = src.x * matrix._matrix[0][3] + src.y * matrix._matrix[1][3] + src.z * matrix._matrix[2][3];

        return out;
    }

    //-------------------------------------------------------------------------
    // TransformVector
    //-------------------------------------------------------------------------
    inline Vector3
    transformVector(const Vector3& src, const Matrix4& matrix)
    {
        Vector3 out;

        out.x = src.x * matrix._matrix[0][0] + src.y * matrix._matrix[1][0] + src.z * matrix._matrix[2][0];
        out.y = src.x * matrix._matrix[0][1] + src.y * matrix._matrix[1][1] + src.z * matrix._matrix[2][1];
        out.z = src.x * matrix._matrix[0][2] + src.y * matrix._matrix[1][2] + src.z * matrix._matrix[2][2];

        return out;
    }

    //-------------------------------------------------------------------------
    // TransformVector
    //-------------------------------------------------------------------------
    inline Quaternion
    transformQuaternion(const Quaternion& src, const Matrix4& matrix)
    {
        // go through the axes, quaternion axis is not precise enough
        // getColumnX/Y/Z
        float x2(src.x + src.x);
        float y2(src.y + src.y);
        float z2(src.z + src.z);
        Vector3 columnX(1 - ((src.y * y2) + (src.z * z2)), (src.x * y2) + (src.w * z2), (src.x * z2) - (src.w * y2));
        Vector3 columnY((src.x * y2) - (src.w * z2), 1 - ((src.x * x2) + (src.z * z2)), (src.y * z2) + (src.w * x2));
        Vector3 columnZ((src.x * z2) + (src.w * y2), (src.y * z2) - (src.w * x2), 1 - ((src.x * x2) + (src.y * y2)));

        Vector3 moveForwardVectorTransformed = transformVector(columnX, matrix);
        Vector3 moveSideVectorTransformed = transformVector(columnY, matrix);
        Vector3 moveUpVectorTransformed = transformVector(columnZ, matrix);

        RotationMatrix temp(moveForwardVectorTransformed.x, moveSideVectorTransformed.x, moveUpVectorTransformed.x,
                            moveForwardVectorTransformed.y, moveSideVectorTransformed.y, moveUpVectorTransformed.y,
                            moveForwardVectorTransformed.z, moveSideVectorTransformed.z, moveUpVectorTransformed.z);

        Quaternion out;
        toQuaternion(out, temp);
        out.normalizeSelf();

        return out;

        //// transform the Quaternion axis part
        //Quaternion out;

        //out.x = src.x * matrix._matrix[0][0] + src.y * matrix._matrix[1][0] + src.z * matrix._matrix[2][0];
        //out.y = src.x * matrix._matrix[0][1] + src.y * matrix._matrix[1][1] + src.z * matrix._matrix[2][1];
        //out.z = src.x * matrix._matrix[0][2] + src.y * matrix._matrix[1][2] + src.z * matrix._matrix[2][2];
        //out.w = src.w;

        //return out;
    }

    //-------------------------------------------------------------------------
    // TransformPoint
    //-------------------------------------------------------------------------
    inline Vector4
    transformPoint(const Vector4& src, const Matrix4& matrix)
    {
        Vector4 out;

        out.x = src.x * matrix._matrix[0][0] + src.y * matrix._matrix[1][0] + src.z * matrix._matrix[2][0] + matrix._matrix[3][0];
        out.y = src.x * matrix._matrix[0][1] + src.y * matrix._matrix[1][1] + src.z * matrix._matrix[2][1] + matrix._matrix[3][1];
        out.z = src.x * matrix._matrix[0][2] + src.y * matrix._matrix[1][2] + src.z * matrix._matrix[2][2] + matrix._matrix[3][2];
        out.w = src.x * matrix._matrix[0][3] + src.y * matrix._matrix[1][3] + src.z * matrix._matrix[2][3] + matrix._matrix[3][3];

        return out;
    }

    //-------------------------------------------------------------------------
    // TransformPoint
    //-------------------------------------------------------------------------
    inline Vector3
    transformPoint(const Vector3& src, const Matrix4& matrix)
    {
        Vector3 out;

        out.x = src.x * matrix._matrix[0][0] + src.y * matrix._matrix[1][0] + src.z * matrix._matrix[2][0] + matrix._matrix[3][0];
        out.y = src.x * matrix._matrix[0][1] + src.y * matrix._matrix[1][1] + src.z * matrix._matrix[2][1] + matrix._matrix[3][1];
        out.z = src.x * matrix._matrix[0][2] + src.y * matrix._matrix[1][2] + src.z * matrix._matrix[2][2] + matrix._matrix[3][2];

        return out;
    }

    //-----------------------------------------------------------------------------
    inline std::ostream& operator<<(std::ostream& out, const Matrix4& matrix)
    {
        out << "\n[" << matrix[0] << ",\n"
            << matrix[1] << ",\n"
            << matrix[2] << ",\n"
            << matrix[3] << "]\n";
        return out;
    }
} // namespace glm

#ifdef _MSC_VER
#pragma managed(pop)
#pragma warning(pop)
#endif
