use std::ops::Mul;
use crate::{functions::cross, prelude::*};
#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub struct Matrix4 {
    pub data: [[f64; 4]; 4],
}
impl Matrix4 {
    
    pub fn zeroes() -> Matrix4 {
        Matrix4 {
            data: [
                [0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0],
                [0.0, 0.0, 0.0, 0.0],
            ],
        }
    }
    
    pub fn translation(x: f64, y: f64, z: f64) -> Matrix4 {
        Matrix4 {
            data: [
                [1.0, 0.0, 0.0, x],
                [0.0, 1.0, 0.0, y],
                [0.0, 0.0, 1.0, z],
                [0.0, 0.0, 0.0, 1.0],
            ],
        }
    }
    
    pub fn scale(x: f64, y: f64, z: f64) -> Matrix4 {
        Matrix4 {
            data: [
                [x, 0.0, 0.0, 0.0],
                [0.0, y, 0.0, 0.0],
                [0.0, 0.0, z, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
        }
    }
    
    pub fn rotation(x: Rad, y: Rad, z: Rad) -> Matrix4 {
        let (mut mx, mut my, mut mz) = (Matrix4::default(), Matrix4::default(), Matrix4::default());
        let (rad_x, rad_y, rad_z) = (x.val, y.val, z.val);
        mx.data[0][0] = 1.0;
        mx.data[0][1] = 0.0;
        mx.data[0][2] = 0.0;
        mx.data[0][3] = 0.0;
        mx.data[1][0] = 0.0;
        mx.data[1][1] = rad_x.cos();
        mx.data[1][2] = -rad_x.sin();
        mx.data[1][3] = 0.0;
        mx.data[2][0] = 0.0;
        mx.data[2][1] = rad_x.sin();
        mx.data[2][2] = rad_x.cos();
        mx.data[2][3] = 0.0;
        mx.data[3][0] = 0.0;
        mx.data[3][1] = 0.0;
        mx.data[3][2] = 0.0;
        mx.data[3][3] = 1.0;
        my.data[0][0] = rad_y.cos();
        my.data[0][1] = 0.0;
        my.data[0][2] = rad_y.sin();
        my.data[0][3] = 0.0;
        my.data[1][0] = 0.0;
        my.data[1][1] = 1.0;
        my.data[1][2] = 0.0;
        my.data[1][3] = 0.0;
        my.data[2][0] = -rad_y.sin();
        my.data[2][1] = 0.0;
        my.data[2][2] = rad_y.cos();
        my.data[2][3] = 0.0;
        my.data[3][0] = 0.0;
        my.data[3][1] = 0.0;
        my.data[3][2] = 0.0;
        my.data[3][3] = 1.0;
        mz.data[0][0] = rad_z.cos();
        mz.data[0][1] = -rad_z.sin();
        mz.data[0][2] = 0.0;
        mz.data[0][3] = 0.0;
        mz.data[1][0] = rad_z.sin();
        mz.data[1][1] = rad_z.cos();
        mz.data[1][2] = 0.0;
        mz.data[1][3] = 0.0;
        mz.data[2][0] = 0.0;
        mz.data[2][1] = 0.0;
        mz.data[2][2] = 1.0;
        mz.data[2][3] = 0.0;
        mz.data[3][0] = 0.0;
        mz.data[3][1] = 0.0;
        mz.data[3][2] = 0.0;
        mz.data[3][3] = 1.0;
        mx * my * mz
    }
    
    pub fn rotation_axis<N>(axis: &N, r: Rad) -> Matrix4
    where
        N: IsNormalized3D,
    {
        let rad = r.val;
        let ref u = axis;
        let mut result = Matrix4::default();
        result.data[0][0] = rad.cos() + u.x() * u.x() * (1.0 - rad.cos());
        result.data[0][1] = u.x() * u.y() * (1.0 - rad.cos()) - u.z() * rad.sin();
        result.data[0][2] = u.x() * u.z() * (1.0 - rad.cos()) + u.y() * rad.sin();
        result.data[0][3] = 0.0;
        result.data[1][0] = u.y() * u.x() * (1.0 - rad.cos()) + u.z() * rad.sin();
        result.data[1][1] = rad.cos() + u.y() * u.y() * (1.0 - rad.cos());
        result.data[1][2] = u.y() * u.z() * (1.0 - rad.cos()) - u.x() * rad.sin();
        result.data[1][3] = 0.0;
        result.data[2][0] = u.z() * u.x() * (1.0 - rad.cos()) - u.y() * rad.sin();
        result.data[2][1] = u.z() * u.y() * (1.0 - rad.cos()) + u.x() * rad.sin();
        result.data[2][2] = rad.cos() + u.z() * u.z() * (1.0 - rad.cos());
        result.data[2][3] = 0.0;
        result.data[3][0] = 0.0;
        result.data[3][1] = 0.0;
        result.data[3][2] = 0.0;
        result.data[3][3] = 1.0;
        result
    }
    
    pub fn perspective(close: f64, away: f64, fov: Rad) -> Matrix4 {
        let fov_rad = fov.val;
        let range = close - away;
        let tan_fov_half = (fov_rad / 2.0).tan();
        let mut result = Matrix4::default();
        result.data[0][0] = 1.0 / (tan_fov_half * away);
        result.data[0][1] = 0.0;
        result.data[0][2] = 0.0;
        result.data[0][3] = 0.0;
        result.data[1][0] = 0.0;
        result.data[1][1] = 1.0 / tan_fov_half;
        result.data[1][2] = 0.0;
        result.data[1][3] = 0.0;
        result.data[2][0] = 0.0;
        result.data[2][1] = 0.0;
        result.data[2][2] = (-close - away) / range;
        result.data[2][3] = 2.0 * away * close / range;
        result.data[3][0] = 0.0;
        result.data[3][1] = 0.0;
        result.data[3][2] = 1.0;
        result.data[3][3] = 1.0;
        result
    }
    
    pub fn look_at<P, N>(target: &P, up: &N) -> Result<Matrix4>
    where
        P: IsBuildable3D,
        N: IsNormalized3D,
    {
        let n = target.clone().normalized()?;
        let u = cross(&*up, target);
        let v = cross(&n, &u);
        let mut result = Matrix4::default();
        result.data[0][0] = u.x();
        result.data[0][1] = u.y();
        result.data[0][2] = u.z();
        result.data[0][3] = 0.0;
        result.data[1][0] = v.x();
        result.data[1][1] = v.y();
        result.data[1][2] = v.z();
        result.data[1][3] = 0.0;
        result.data[2][0] = n.x();
        result.data[2][1] = n.y();
        result.data[2][2] = n.z();
        result.data[2][3] = 0.0;
        result.data[3][0] = 0.0;
        result.data[3][1] = 0.0;
        result.data[3][2] = 0.0;
        result.data[3][3] = 1.0;
        Ok(result)
    }
}
impl Default for Matrix4 {
    fn default() -> Self {
        Matrix4 {
            data: [
                [1.0, 0.0, 0.0, 0.0],
                [0.0, 1.0, 0.0, 0.0],
                [0.0, 0.0, 1.0, 0.0],
                [0.0, 0.0, 0.0, 1.0],
            ],
        }
    }
}
impl Mul for Matrix4 {
    type Output = Self;
    fn mul(self, other: Self) -> Self {
        let mut result = Matrix4::default();
        for i in 0..4 {
            for j in 0..4 {
                result.data[i][j] = self.data[i][0] * other.data[0][j]
                    + self.data[i][1] * other.data[1][j]
                    + self.data[i][2] * other.data[2][j]
                    + self.data[i][3] * other.data[3][j];
            }
        }
        result
    }
}
impl Mul<f64> for Matrix4 {
    type Output = Self;
    fn mul(self, other: f64) -> Self {
        let mut result = Matrix4::default();
        for i in 0..4 {
            for j in 0..4 {
                result.data[i][j] = other * self.data[i][j];
            }
        }
        result
    }
}