#pragma once

#include "Vec3.h"
#include "Mat3.h"
#include <iostream>

namespace Framework
{
    template< typename T >
    // Eine 4x4 Matrix
    class Mat4
    {
    public:
        T elements[ 4 ][ 4 ]; // Die Elemente der Matrix
        // Kopiert alle Werte einer anderen Matrix
        //  r: Die andere Matrix
        Mat4 &operator=( const Mat4 &r )
        {
            memcpy( elements, r.elements, sizeof( elements ) );
            return *this;
        }
        // Skalliert die Matrix
        //  r: der Faktor
        Mat4 &operator*=( const T r )
        {
            for( T &e : elements )
                e *= r;
            return *this;
        }
        // Multipliziert die MAtrix mit einer anderen
        //  r: Die andere Matrix
        Mat4 &operator*=( const Mat4 &r )
        {
            return *this = *this * r;
        }
        // Skalliert die Matrix ohne sie zu ver�ndern
        //  r: der Faktor
        Mat4 operator*( const T r ) const
        {
            Mat4 result = *this;
            return result *= r;
        }
        // Multipliziert zwei Matrizen
        //  r: Die andere Matrix
        Mat4 operator*( const Mat4 &r ) const
        {
            Mat4 result;
            for( int j = 0; j < 4; j++ )
            {
                for( int k = 0; k < 4; k++ )
                {
                    T sum = 0;
                    for( int i = 0; i < 4; i++ )
                        sum += elements[ j ][ i ] * r.elements[ i ][ k ];
                    result.elements[ j ][ k ] = sum;
                }
            }
            return result;
        }
        // Multiplziert die Matrix mit einem Vektor
        //  r: Der Vektor
        Vec3< T > operator*( const Vec3< T > &r ) const
        {
            Vec3< T > result;
            result.x = elements[ 0 ][ 0 ] * r.x + elements[ 0 ][ 1 ] * r.y + elements[ 0 ][ 2 ] * r.z + elements[ 0 ][ 3 ];
            result.y = elements[ 1 ][ 0 ] * r.x + elements[ 1 ][ 1 ] * r.y + elements[ 1 ][ 2 ] * r.z + elements[ 1 ][ 3 ];
            result.z = elements[ 2 ][ 0 ] * r.x + elements[ 2 ][ 1 ] * r.y + elements[ 2 ][ 2 ] * r.z + elements[ 2 ][ 3 ];
            return  result;
        }
        // Berechnet die inverse Matrix
        Mat4 getInverse()
        {
            Mat4 ret;
            ret.elements[ 0 ][ 0 ] =
                elements[ 1 ][ 1 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 1 ][ 1 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 2 ][ 1 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 2 ][ 1 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 3 ][ 1 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 3 ] -
                elements[ 3 ][ 1 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 2 ];

            ret.elements[ 1 ][ 0 ] =
                -elements[ 1 ][ 0 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 1 ][ 0 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 2 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 2 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 3 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 3 ] +
                elements[ 3 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 2 ];

            ret.elements[ 2 ][ 0 ] =
                elements[ 1 ][ 0 ] * elements[ 2 ][ 1 ] * elements[ 3 ][ 3 ] -
                elements[ 1 ][ 0 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 1 ] -
                elements[ 2 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 3 ][ 3 ] +
                elements[ 2 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 1 ] +
                elements[ 3 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 2 ][ 3 ] -
                elements[ 3 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 1 ];

            ret.elements[ 3 ][ 0 ] =
                -elements[ 1 ][ 0 ] * elements[ 2 ][ 1 ] * elements[ 3 ][ 2 ] +
                elements[ 1 ][ 0 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 1 ] +
                elements[ 2 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 3 ][ 2 ] -
                elements[ 2 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 1 ] -
                elements[ 3 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 2 ][ 2 ] +
                elements[ 3 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 1 ];

            ret.elements[ 0 ][ 1 ] =
                -elements[ 0 ][ 1 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 0 ][ 1 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 2 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 2 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 3 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 3 ] +
                elements[ 3 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 2 ];

            ret.elements[ 1 ][ 1 ] =
                elements[ 0 ][ 0 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 0 ][ 0 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 3 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 2 ];

            ret.elements[ 2 ][ 1 ] =
                -elements[ 0 ][ 0 ] * elements[ 2 ][ 1 ] * elements[ 3 ][ 3 ] +
                elements[ 0 ][ 0 ] * elements[ 2 ][ 3 ] * elements[ 3 ][ 1 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 3 ][ 3 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 1 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 2 ][ 3 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 1 ];

            ret.elements[ 3 ][ 1 ] =
                elements[ 0 ][ 0 ] * elements[ 2 ][ 1 ] * elements[ 3 ][ 2 ] -
                elements[ 0 ][ 0 ] * elements[ 2 ][ 2 ] * elements[ 3 ][ 1 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 3 ][ 2 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 1 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 2 ][ 2 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 1 ];

            ret.elements[ 0 ][ 2 ] =
                elements[ 0 ][ 1 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 0 ][ 1 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 1 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 1 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 3 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 3 ] -
                elements[ 3 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 2 ];

            ret.elements[ 1 ][ 2 ] =
                -elements[ 0 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 3 ] +
                elements[ 0 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 2 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 3 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 2 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 3 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 2 ];

            ret.elements[ 2 ][ 2 ] =
                elements[ 0 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 3 ][ 3 ] -
                elements[ 0 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 3 ][ 1 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 3 ][ 3 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 3 ][ 1 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 1 ][ 3 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 1 ];

            ret.elements[ 3 ][ 2 ] =
                -elements[ 0 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 3 ][ 2 ] +
                elements[ 0 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 3 ][ 1 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 3 ][ 2 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 3 ][ 1 ] -
                elements[ 3 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 1 ][ 2 ] +
                elements[ 3 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 1 ];

            ret.elements[ 0 ][ 3 ] =
                -elements[ 0 ][ 1 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 3 ] +
                elements[ 0 ][ 1 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 2 ] +
                elements[ 1 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 3 ] -
                elements[ 1 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 2 ] -
                elements[ 2 ][ 1 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 3 ] +
                elements[ 2 ][ 1 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 2 ];

            ret.elements[ 1 ][ 3 ] =
                elements[ 0 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 3 ] -
                elements[ 0 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 2 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 3 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 2 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 3 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 2 ];

            ret.elements[ 2 ][ 3 ] =
                -elements[ 0 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 2 ][ 3 ] +
                elements[ 0 ][ 0 ] * elements[ 1 ][ 3 ] * elements[ 2 ][ 1 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 2 ][ 3 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 2 ][ 1 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 1 ][ 3 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 3 ] * elements[ 1 ][ 1 ];

            ret.elements[ 3 ][ 3 ] =
                elements[ 0 ][ 0 ] * elements[ 1 ][ 1 ] * elements[ 2 ][ 2 ] -
                elements[ 0 ][ 0 ] * elements[ 1 ][ 2 ] * elements[ 2 ][ 1 ] -
                elements[ 1 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 2 ][ 2 ] +
                elements[ 1 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 2 ][ 1 ] +
                elements[ 2 ][ 0 ] * elements[ 0 ][ 1 ] * elements[ 1 ][ 2 ] -
                elements[ 2 ][ 0 ] * elements[ 0 ][ 2 ] * elements[ 1 ][ 1 ];

            T det = elements[ 0 ][ 0 ] * ret.elements[ 0 ][ 0 ] + elements[ 0 ][ 1 ] * ret.elements[ 1 ][ 0 ] + elements[ 0 ][ 2 ] * ret.elements[ 2 ][ 0 ] + elements[ 0 ][ 3 ] * ret.elements[ 3 ][ 0 ];

            if( det == 0 )
            {
                std::cout << "Fehler beim erstellen der Inversen Matrix";
                return ret;
            }
            det = 1.0f / det;

            for( int i = 0; i < 16; i++ )
                ret.elements[ i / 4 ][ i % 4 ] = ret.elements[ i / 4 ][ i % 4 ] * det;

            return ret;
        }
        // Erzeugt eine Matrix, die einen Vektor um die Z Achse dreht, wenn sie mit ihm multipliziert wird
        //  radian: Der Winkel in Bogenmas
        static Mat4 rotationZ( T radian )
        {
            const T cosTheta = (T)lowPrecisionCos( radian );
            const T sinTheta = (T)lowPrecisionSin( radian );
            Mat4 r = { cosTheta, -sinTheta, 0, 0, sinTheta, cosTheta, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
            return r;
        }
        // Erzeugt eine Matrix, die einen Vektor um die X Achse dreht, wenn sie mit ihm multipliziert wird
        //  radian: Der Winkel in Bogenmas
        static Mat4 rotationX( T radian )
        {
            const T cosTheta = (T)lowPrecisionCos( radian );
            const T sinTheta = (T)lowPrecisionSin( radian );
            Mat4 r = { 1, 0, 0, 0, 0, cosTheta, -sinTheta, 0, 0, sinTheta, cosTheta, 0, 0, 0, 0, 1 };
            return r;
        }
        // Erzeugt eine Matrix, die einen Vektor um die Y Achse dreht, wenn sie mit ihm multipliziert wird
        //  radian: Der Winkel in Bogenmas
        static Mat4 rotationY( T radian )
        {
            const T cosTheta = (T)lowPrecisionCos( radian );
            const T sinTheta = (T)lowPrecisionSin( radian );
            Mat4 r = { cosTheta, 0, sinTheta, 0, 0, 1, 0, 0, -sinTheta, 0, cosTheta, 0, 0, 0, 0, 1 };
            return r;
        }
        // Erzeugt eine Matrix, die einen Vektor Skalliert, wenn sie mit ihm multipliziert wird
        //  faktor: Der Faktor
        static Mat4 scaling( T faktor )
        {
            Mat4 s = { faktor, 0, 0, 0, 0, faktor, 0, 0, 0, 0, faktor, 0, 0, 0, 1 };
            return s;
        }
        // Erzeugt eine Matrix, die einen Vektor Skalliert, wenn sie mit ihm multipliziert wird
        //  faktorX: Der Faktor f�r die X Komponente des Vektors
        //  faktorY: Der Faktor f�r die Y Komponente des Vektors
        //  faktorZ: Der Faktor f�r die Z Komponente des Vektors
        static Mat4 scaling( T faktorX, T faktorY, T faktorZ )
        {
            Mat4 s = { faktorX, 0, 0, 0, 0, faktorY, 0, 0, 0, 0, faktorZ, 0, 0, 0, 1 };
            return s;
        }
        // Erzeugt eine Matrix, die einen Vektor verchiebt, wenn sie mit ihm multipliziert wird
        //  offset: Die Koordinaten, um die der Vektor verschoben werden soll
        static Mat4 translation( const Vec3< T > offset )
        {
            Mat4 t = { 1, 0, 0, offset.x, 0, 1, 0, offset.y, 0, 0, 1, offset.z, 0, 0, 0, 1 };
            return t;
        }
        // Erzeugt eine Matrix, die einen Vektor auf den Bildschirm Projeziert
        //  openingAngle: Der �ffnungswinkel der Kamera im Bogenmas
        //  bildschirmXY: Das Seitenverh�ltnis des Rechtecks auf dem Bildschirm, in dem gezeichnet werden soll. (Breite / H�he)
        //  minz: Der Mindestabstand zur Kamera, ab dem gezeichnet wird
        //  maxZ: Der Maximalabstand zur Kamera, ab dem nicht mehr gezeichnet wird
        static Mat4 projektion( float openingAngle, float bildschirmXY, float minZ, float maxZ )
        {
            Mat4 p = { (float)( 1 / tan( openingAngle / 2 ) ) / bildschirmXY, 0, 0, 0,
                0, (float)( 1 / tan( openingAngle / 2 ) ), 0, 0,
                0, 0, maxZ / ( maxZ - minZ ), -( minZ * maxZ ) / ( maxZ - minZ ),
                0, 0, 1, 0 };
            return p;
        }
        // Erzeugt eine Matrix, die mit allem Multipliziert werden kann ohne es zu �ndern
        static Mat4 identity()
        {
            Mat4 i = { 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1 };
            return i;
        }

        // Gibt eine Rotationsmatrix zur�ck, so dass vector a wenn man ihn damit dreht in richtung des vectors b zeigt
        //  a: der vector der gedreht werden soll
        //  b: der vector zu dem gedreht werden soll
        static Mat4 rotationTo( Vec3<T> &a, Vec3<T> &b )
        {
            Vec3<T> aNorm = Vec3<T>( a ).normalize();
            Vec3<T> bNorm = Vec3<T>( b ).normalize();
            Vec3<T> v = aNorm.crossProduct( bNorm );
            T s = v.getLengthSq();
            T c = aNorm * bNorm;
            T m = ( 1 - c ) / s;
            Mat3<T> cpm( { 0, -v.z, v.y, v.z, 0, -v.x, -v.y, v.x, 0 } );
            Mat3<T> cpm2 = cpm * cpm;
            Mat3<T> res = Mat3<T>::identity() + cpm + cpm2 * m;
            return Mat4( { res.elements[ 0 ][ 0 ], res.elements[ 0 ][ 1 ], res.elements[ 0 ][ 2 ], 0,
                           res.elements[ 1 ][ 0 ], res.elements[ 1 ][ 1 ], res.elements[ 1 ][ 2 ], 0,
                           res.elements[ 2 ][ 0 ], res.elements[ 2 ][ 1 ], res.elements[ 2 ][ 2 ], 0,
                           0, 0, 0, 1 } );
        }
    };
}