/********************************************************************************
* OpenGL-Framework                                                              *
* Copyright (c) 2013 Daniel Chappuis                                            *
*********************************************************************************
*                                                                               *
* This software is provided 'as-is', without any express or implied warranty.   *
* In no event will the authors be held liable for any damages arising from the  *
* use of this software.                                                         *
*                                                                               *
* Permission is granted to anyone to use this software for any purpose,         *
* including commercial applications, and to alter it and redistribute it        *
* freely, subject to the following restrictions:                                *
*                                                                               *
* 1. The origin of this software must not be misrepresented; you must not claim *
*    that you wrote the original software. If you use this software in a        *
*    product, an acknowledgment in the product documentation would be           *
*    appreciated but is not required.                                           *
*                                                                               *
* 2. Altered source versions must be plainly marked as such, and must not be    *
*    misrepresented as being the original software.                             *
*                                                                               *
* 3. This notice may not be removed or altered from any source distribution.    *
*                                                                               *
********************************************************************************/

#ifndef MATRIX4_H
#define MATRIX4_H

// Libraries
#include <cmath>
#include <assert.h>
#include <iostream>
#include "Vector3.h"
#include "Vector4.h"
#include "Matrix3.h"
#include "definitions.h"

namespace openglframework {

// Class Matrix4
// This class represents a 4x4 matrix
class Matrix4 {

    public:

        // -------------------- Attributes -------------------- //

        // Elements of the matrix
        float m[4][4];

        // -------------------- Methods -------------------- //

        // Constructor
        Matrix4(float m_00=0, float m_01=0, float m_02=0, float m_03=0,
                float m_10=0, float m_11=0, float m_12=0, float m_13=0,
                float m_20=0, float m_21=0, float m_22=0, float m_23=0,
                float m_30=0, float m_31=0, float m_32=0, float m_33=0) {
            m[0][0] = m_00; m[0][1] = m_01; m[0][2] = m_02;  m[0][3] = m_03;
            m[1][0] = m_10; m[1][1] = m_11; m[1][2] = m_12;  m[1][3] = m_13;
            m[2][0] = m_20; m[2][1] = m_21; m[2][2] = m_22;  m[2][3] = m_23;
            m[3][0] = m_30; m[3][1] = m_31; m[3][2] = m_32;  m[3][3] = m_33;
        }

        // Constructor
        Matrix4(float n[4][4]) {
            m[0][0]=n[0][0]; m[0][1]=n[0][1]; m[0][2]=n[0][2]; m[0][3]=n[0][3];
            m[1][0]=n[1][0]; m[1][1]=n[1][1]; m[1][2]=n[1][2]; m[1][3]=n[1][3];
            m[2][0]=n[2][0]; m[2][1]=n[2][1]; m[2][2]=n[2][2]; m[2][3]=n[2][3];
            m[3][0]=n[3][0]; m[3][1]=n[3][1]; m[3][2]=n[3][2]; m[3][3]=n[3][3];
        }

        // Constructor
        Matrix4(const Vector3& a1, const Vector3& a2, const Vector3& a3) {
            m[0][0] = a1.x; m[0][1] = a2.x; m[0][2] = a3.x;  m[0][3] = 0.f;
            m[1][0] = a1.y; m[1][1] = a2.y; m[1][2] = a3.y;  m[1][3] = 0.f;
            m[2][0] = a1.z; m[2][1] = a2.z; m[2][2] = a3.z;  m[2][3] = 0.f;
            m[3][0] = 0.f;  m[3][1] = 0.f;  m[3][2] = 0.f;   m[3][3] = 1.f;
        }

        // Constructor
        Matrix4(const Vector4& a1, const Vector4& a2, const Vector4& a3) {
            m[0][0] = a1.x; m[0][1] = a2.x; m[0][2] = a3.x;  m[0][3] = 0.f;
            m[1][0] = a1.y; m[1][1] = a2.y; m[1][2] = a3.y;  m[1][3] = 0.f;
            m[2][0] = a1.z; m[2][1] = a2.z; m[2][2] = a3.z;  m[2][3] = 0.f;
            m[3][0] = a1.w; m[3][1] = a2.w; m[3][2] = a3.w;  m[3][3] = 1.f;
        }

        // Constructor
        Matrix4(const Matrix4& matrix) {

            setAllValues(matrix.m[0][0], matrix.m[0][1], matrix.m[0][2], matrix.m[0][3],
                         matrix.m[1][0], matrix.m[1][1], matrix.m[1][2], matrix.m[1][3],
                         matrix.m[2][0], matrix.m[2][1], matrix.m[2][2], matrix.m[2][3],
                         matrix.m[3][0], matrix.m[3][1], matrix.m[3][2], matrix.m[3][3]);
        }

        // + operator
        Matrix4 operator+(const Matrix4 &n) const {
            return Matrix4(m[0][0]+n.m[0][0], m[0][1]+n.m[0][1], m[0][2]+n.m[0][2], m[0][3]+n.m[0][3],
                           m[1][0]+n.m[1][0], m[1][1]+n.m[1][1], m[1][2]+n.m[1][2], m[1][3]+n.m[1][3],
                           m[2][0]+n.m[2][0], m[2][1]+n.m[2][1], m[2][2]+n.m[2][2], m[2][3]+n.m[2][3],
                           m[3][0]+n.m[3][0], m[3][1]+n.m[3][1], m[3][2]+n.m[3][2], m[3][3]+n.m[3][3]);
        }

        // += operator
        Matrix4& operator+=(const Matrix4 &n) {
            m[0][0]+=n.m[0][0]; m[0][1]+=n.m[0][1]; m[0][2]+=n.m[0][2]; m[0][3]+=n.m[0][3];
            m[1][0]+=n.m[1][0]; m[1][1]+=n.m[1][1]; m[1][2]+=n.m[1][2]; m[1][3]+=n.m[1][3];
            m[2][0]+=n.m[2][0]; m[2][1]+=n.m[2][1]; m[2][2]+=n.m[2][2]; m[2][3]+=n.m[2][3];
            m[3][0]+=n.m[3][0]; m[3][1]+=n.m[3][1]; m[3][2]+=n.m[3][2]; m[3][3]+=n.m[3][3];
            return *this;
        }

        // - operator
        Matrix4 operator-(const Matrix4 &n) const {
            return Matrix4(m[0][0]-n.m[0][0], m[0][1]-n.m[0][1], m[0][2]-n.m[0][2], m[0][3]-n.m[0][3],
                           m[1][0]-n.m[1][0], m[1][1]-n.m[1][1], m[1][2]-n.m[1][2], m[1][3]-n.m[1][3],
                           m[2][0]-n.m[2][0], m[2][1]-n.m[2][1], m[2][2]-n.m[2][2], m[2][3]-n.m[2][3],
                           m[3][0]-n.m[3][0], m[3][1]-n.m[3][1], m[3][2]-n.m[3][2], m[3][3]-n.m[3][3]);
        }

        // -= operator
        Matrix4& operator-=(const Matrix4 &n) {
            m[0][0]-=n.m[0][0]; m[0][1]-=n.m[0][1]; m[0][2]-=n.m[0][2]; m[0][3]-=n.m[0][3];
            m[1][0]-=n.m[1][0]; m[1][1]-=n.m[1][1]; m[1][2]-=n.m[1][2]; m[1][3]-=n.m[1][3];
            m[2][0]-=n.m[2][0]; m[2][1]-=n.m[2][1]; m[2][2]-=n.m[2][2]; m[2][3]-=n.m[2][3];
            m[3][0]-=n.m[3][0]; m[3][1]-=n.m[3][1]; m[3][2]-=n.m[3][2]; m[3][3]-=n.m[3][3];
            return *this;
        }

        // = operator
        Matrix4& operator=(const Matrix4& matrix) {
            if (&matrix != this) {
                setAllValues(matrix.m[0][0], matrix.m[0][1], matrix.m[0][2], matrix.m[0][3],
                             matrix.m[1][0], matrix.m[1][1], matrix.m[1][2], matrix.m[1][3],
                             matrix.m[2][0], matrix.m[2][1], matrix.m[2][2], matrix.m[2][3],
                             matrix.m[3][0], matrix.m[3][1], matrix.m[3][2], matrix.m[3][3]);
            }
            return *this;
        }

        // == operator
        bool operator==(const Matrix4 &n) const {
            return m[0][0]==n.m[0][0] && m[0][1]==n.m[0][1] && m[0][2]==n.m[0][2] && m[0][3]==n.m[0][3] &&
                    m[1][0]==n.m[1][0] && m[1][1]==n.m[1][1] && m[1][2]==n.m[1][2] && m[1][3]==n.m[1][3] &&
                    m[2][0]==n.m[2][0] && m[2][1]==n.m[2][1] && m[2][2]==n.m[2][2] && m[2][3]==n.m[2][3] &&
                    m[3][0]==n.m[3][0] && m[3][1]==n.m[3][1] && m[3][2]==n.m[3][2] && m[3][3]==n.m[3][3];
        }

        // * operator
        Matrix4 operator*(const Matrix4 &n) const {
            Matrix4 o;
            for(int i = 0; i  < 4; i++) {
                for(int j = 0; j < 4; j++) {
                    float v = 0;
                    for(int k = 0; k < 4; k++) {
                        v += m[i][k] * n.m[k][j];
                    }
                    o.m[i][j] = v;
                }
            }
            return o;
        }

        // * operator
        Vector3 operator*(const Vector3 &v) const {
            Vector3 u =Vector3(m[0][0]*v.x + m[0][1]*v.y + m[0][2]*v.z + m[0][3],
                               m[1][0]*v.x + m[1][1]*v.y + m[1][2]*v.z + m[1][3],
                               m[2][0]*v.x + m[2][1]*v.y + m[2][2]*v.z + m[2][3]);
            float w = m[3][0]*v.x + m[3][1]*v.y + m[3][2]*v.z + m[3][3];
            return u/w;
        }

        // * operator
        Vector4 operator*(const Vector4 &v) const {
            Vector4 u = Vector4(m[0][0]*v.x + m[0][1]*v.y + m[0][2]*v.z + v.w*m[0][3],
                                m[1][0]*v.x + m[1][1]*v.y + m[1][2]*v.z + v.w*m[1][3],
                                m[2][0]*v.x + m[2][1]*v.y + m[2][2]*v.z + v.w*m[2][3],
                                m[3][0]*v.x + m[3][1]*v.y + m[3][2]*v.z + v.w*m[3][3]);
            if(u.w != 0)
                return u/u.w;
            else
                return u;
        }

        // * operator
        Matrix4 operator*(float f) const {
            return Matrix4(m[0][0]*f, m[0][1]*f, m[0][2]*f,  m[0][3]*f,
                           m[1][0]*f, m[1][1]*f, m[1][2]*f,  m[1][3]*f,
                           m[2][0]*f, m[2][1]*f, m[2][2]*f,  m[2][3]*f,
                           m[3][0]*f, m[3][1]*f, m[3][2]*f,  m[3][3]*f);
        }

        // * operator
        Matrix4 &operator*=(float f) {
            m[0][0]*=f; m[0][1]*=f; m[0][2]*=f;  m[0][3]*=f;
            m[1][0]*=f; m[1][1]*=f; m[1][2]*=f;  m[1][3]*=f;
            m[2][0]*=f; m[2][1]*=f; m[2][2]*=f;  m[2][3]*=f;
            m[3][0]*=f; m[3][1]*=f; m[3][2]*=f;  m[3][3]*=f;
            return *this;
        }

        // / operator
        Matrix4 operator/(float f) const {
            assert(f!=0);
            return Matrix4(m[0][0]/f, m[0][1]/f, m[0][2]/f,  m[0][3]/f,
                           m[1][0]/f, m[1][1]/f, m[1][2]/f,  m[1][3]/f,
                           m[2][0]/f, m[2][1]/f, m[2][2]/f,  m[2][3]/f,
                           m[3][0]/f, m[3][1]/f, m[3][2]/f,  m[3][3]/f);
        }

        // /= operator
        Matrix4 &operator/=(float f) {
            assert(f!=0);
            m[0][0]/=f; m[0][1]/=f; m[0][2]/=f;  m[0][3]/=f;
            m[1][0]/=f; m[1][1]/=f; m[1][2]/=f;  m[1][3]/=f;
            m[2][0]/=f; m[2][1]/=f; m[2][2]/=f;  m[2][3]/=f;
            m[3][0]/=f; m[3][1]/=f; m[3][2]/=f;  m[3][3]/=f;
            return *this;
        }

        // - operator
        Matrix4 operator-() const {
            return Matrix4(-m[0][0], -m[0][1], -m[0][2],  -m[0][3],
                           -m[1][0], -m[1][1], -m[1][2],  -m[1][3],
                           -m[2][0], -m[2][1], -m[2][2],  -m[2][3],
                           -m[3][0], -m[3][1], -m[3][2],  -m[3][3]);
        }

        // Return the transpose matrix
        Matrix4 getTranspose() const {
            return Matrix4(m[0][0], m[1][0], m[2][0], m[3][0],
                           m[0][1], m[1][1], m[2][1], m[3][1],
                           m[0][2], m[1][2], m[2][2], m[3][2],
                           m[0][3], m[1][3], m[2][3], m[3][3]);
        }

        // Return the 3x3 upper-left matrix
        Matrix3 getUpperLeft3x3Matrix() const {
            return Matrix3(m[0][0], m[0][1], m[0][2],
                           m[1][0], m[1][1], m[1][2],
                           m[2][0], m[2][1], m[2][2]);
        }

        // Return the inversed matrix
        Matrix4 getInverse() const {
            int indxc[4], indxr[4];
            int ipiv[4] = { 0, 0, 0, 0 };
            float minv[4][4];
            float temp;

            for (int s=0; s<4; s++) {
                for (int t=0; t<4; t++) {
                    minv[s][t] = m[s][t];
                }
            }

            for (int i = 0; i < 4; i++) {
                int irow = -1, icol = -1;
                float big = 0.;
                // Choose pivot
                for (int j = 0; j < 4; j++) {
                    if (ipiv[j] != 1) {
                        for (int k = 0; k < 4; k++) {
                            if (ipiv[k] == 0) {
                                if (fabs(minv[j][k]) >= big) {
                                    big = float(fabs(minv[j][k]));
                                    irow = j;
                                    icol = k;
                                }
                            }
                            else if (ipiv[k] > 1) {
                                std::cout << "ERROR: Singular matrix in MatrixInvert\n";
                            }
                        }
                    }
                }
                ++ipiv[icol];
                // Swap rows _irow_ and _icol_ for pivot
                if (irow != icol) {
                    for (int k = 0; k < 4; ++k){
                        temp = minv[irow][k];
                        minv[irow][k] = minv[icol][k];
                        minv[icol][k] = temp;
                    }
                }
                indxr[i] = irow;
                indxc[i] = icol;
                if (minv[icol][icol] == 0.){
                    std::cout << "Singular matrix in MatrixInvert\n";
                }
                // Set $m[icol][icol]$ to one by scaling row _icol_ appropriately
                float pivinv = 1.f / minv[icol][icol];
                minv[icol][icol] = 1.f;
                for (int j = 0; j < 4; j++) {
                    minv[icol][j] *= pivinv;
                }

                // Subtract this row from others to zero out their columns
                for (int j = 0; j < 4; j++) {
                    if (j != icol) {
                        float save = minv[j][icol];
                        minv[j][icol] = 0;
                        for (int k = 0; k < 4; k++) {
                            minv[j][k] -= minv[icol][k]*save;
                        }
                    }
                }
            }
            // Swap columns to reflect permutation
            for (int j = 3; j >= 0; j--) {
                if (indxr[j] != indxc[j]) {
                    for (int k = 0; k < 4; k++){
                        temp = minv[k][indxr[j]];
                        minv[k][indxr[j]] = minv[k][indxc[j]];
                        minv[k][indxc[j]] = temp;
                    }
                }
            }
            return Matrix4(minv);
        }

        // Method to set all the values in the matrix
        void setAllValues(float a1, float a2, float a3, float a4,
                          float b1, float b2, float b3, float b4,
                          float c1, float c2, float c3, float c4,
                          float d1, float d2, float d3, float d4) {
            m[0][0] = a1; m[0][1] = a2; m[0][2] = a3, m[0][3] = a4;
            m[1][0] = b1; m[1][1] = b2; m[1][2] = b3; m[1][3] = b4;
            m[2][0] = c1; m[2][1] = c2; m[2][2] = c3; m[2][3] = c4;
            m[3][0] = d1; m[3][1] = d2; m[3][2] = d3; m[3][3] = d4;
        }

        // Set the matrix to the identity matrix
        Matrix4 setToIdentity() {
            m[0][0] = 1.f; m[0][1] = 0.f; m[0][2] = 0.f;  m[0][3] = 0.f;
            m[1][0] = 0.f; m[1][1] = 1.f; m[1][2] = 0.f;  m[1][3] = 0.f;
            m[2][0] = 0.f; m[2][1] = 0.f; m[2][2] = 1.f;  m[2][3] = 0.f;
            m[3][0] = 0.f; m[3][1] = 0.f; m[3][2] = 0.f;  m[3][3] = 1.f;
            return *this;
        }

        // Display the matrix
        void print() const {
            for (int i=0; i<4; i++) {
                for (int j=0; j<4; j++) {
                    std::cout << m[i][j];
                }
                std::cout << std::endl;
            }
        }

        // Return the pointer to the data array of the matrix
        float* dataBlock() {
            return m[0];
        }

        // Return the constant pointer to the data array of the matrix
        const float* dataBlock() const {
            return m[0];
        }

        // Return a given value from the matrix
        float getValue(int i, int j) const {
            assert(i >= 0 && i<4 && j >= 0 && j<4);
            return m[i][j];
        }

        // Return the trace of the matrix
        float getTrace() const {
            // Compute and return the trace
            return (m[0][0] + m[1][1] + m[2][2] + m[3][3]);
        }

        // Return a 4x4 translation matrix
        static Matrix4 translationMatrix(const Vector3& v);

        // Return a 4x4 rotation matrix
        static Matrix4 rotationMatrix(const Vector3& axis, float angle);

        // Return a 4x4 perspective projection matrix
        static Matrix4 perspectiveProjectionMatrix(float near, float far,
                                                   int width, int height,
                                                   float fieldOfView);

        // Return a 4x4 orthographic projection matrix
        static Matrix4 orthoProjectionMatrix(float near, float far, int width,
                                             int height);

        // Return the identity matrix
        static Matrix4 identity();
};

// * operator
inline Matrix4 operator*(float f, const Matrix4 & m) {
	return (m * f);
}

// Return a 4x4 translation matrix
inline Matrix4 Matrix4::translationMatrix(const Vector3& v) {
    return Matrix4(1, 0, 0, v.x,
                   0, 1, 0, v.y,
                   0, 0, 1, v.z,
                   0, 0, 0, 1);
}

// Return a 4x4 rotation matrix
inline Matrix4 Matrix4::rotationMatrix(const Vector3& axis, float angle) {

    float cosA = cos(angle);
    float sinA = sin(angle);
    Matrix4 rotationMatrix;
    rotationMatrix.setToIdentity();

    rotationMatrix.m[0][0] = cosA + (1-cosA) * axis.x * axis.x;
    rotationMatrix.m[0][1] = (1-cosA) * axis.x * axis.y - axis.z * sinA;
    rotationMatrix.m[0][2] = (1-cosA) * axis.x * axis.z + axis.y * sinA;
    rotationMatrix.m[0][3] = 0.f;

    rotationMatrix.m[1][0] = (1-cosA) * axis.x * axis.y + axis.z * sinA;
    rotationMatrix.m[1][1] = cosA + (1-cosA) * axis.y * axis.y;
    rotationMatrix.m[1][2] = (1-cosA) * axis.y * axis.z - axis.x * sinA;
    rotationMatrix.m[1][3] = 0.f;

    rotationMatrix.m[2][0] = (1-cosA) * axis.x * axis.z - axis.y * sinA;
    rotationMatrix.m[2][1] = (1-cosA) * axis.y * axis.z + axis.x * sinA;
    rotationMatrix.m[2][2] = cosA + (1-cosA) * axis.z * axis.z;
    rotationMatrix.m[2][3] = 0.f;

    rotationMatrix.m[3][0] = 0.f;
    rotationMatrix.m[3][1] = 0.f;
    rotationMatrix.m[3][2] = 0.f;
    rotationMatrix.m[3][3] = 1.f;

    return rotationMatrix;
}

// Return a 4x4 perspective projection matrix
inline Matrix4 Matrix4::perspectiveProjectionMatrix(float nearDistance, float farDistance, int width, int height,
                                                    float fieldOfView) {

    // Compute the aspect ratio
    float aspect = float(width) / float(height);

    float top = nearDistance * tan((fieldOfView / 2.0f) * (float(PI) / 180.0f));
    float bottom = -top;
    float left = bottom * aspect;
    float right = top * aspect;

    float fx = 2.0f * nearDistance / (right - left);
    float fy = 2.0f * nearDistance / (top - bottom);
    float fz = -(farDistance + nearDistance) / (farDistance - nearDistance);
    float fw = -2.0f * farDistance * nearDistance / (farDistance - nearDistance);

    // Compute the projection matrix
    return Matrix4(fx, 0, 0, 0,
                   0, fy, 0, 0,
                   0, 0, fz, fw,
                   0, 0, -1, 0);
}

// Return a 4x4 orthographic projection matrix
inline Matrix4 Matrix4::orthoProjectionMatrix(float nearDistance, float farDistance, int width, int height) {

    // Compute the aspect ratio
    float aspect = float(width) / float(height);

    float top = height * 0.5f;
    float bottom = -top;
    float left = bottom * aspect;
    float right = top * aspect;

    float fx = 2.0f / (right - left);
    float fy = 2.0f / (top - bottom);
    float fz = -2.0f / (farDistance - nearDistance);
    float fw = -(farDistance + nearDistance) / (farDistance - nearDistance);

    // Compute the projection matrix
    return Matrix4(fx, 0, 0, 0,
                   0, fy, 0, 0,
                   0, 0, fz, fw,
                   0, 0, 0, 1);
}

// Return the identity matrix
inline Matrix4 Matrix4::identity() {
    return Matrix4(1, 0, 0, 0,
                   0, 1, 0, 0,
                   0, 0, 1, 0,
                   0, 0, 0, 1);
}

}

#endif //_MATRIX4_H
