use crate::data_types::dimens::Dimens3;
use crate::matrix::Matrix;
use crate::util::builder;
use crate::vector::Vec3;

#[derive(Debug)]
#[non_exhaustive]
pub enum Projection
{
    Perspective(Perspective),
    Orthographic(Orthographic),
}

/// Perspective projection parameters.
#[derive(Debug)]
pub struct Perspective
{
    pub fov_radians: f32,
    pub far: f32,
    pub near: f32,
}

impl Perspective
{
    /// Creates a perspective projection matrix using right-handed coordinates.
    #[inline]
    pub fn to_matrix_rh(&self, aspect: f32, clip_volume: ClipVolume)
        -> Matrix<f32, 4, 4>
    {
        let mut out = Matrix::new();

        match clip_volume {
            ClipVolume::NegOneToOne => {
                out.set_cell(0, 0, (1.0 / (self.fov_radians / 2.0).tan()) / aspect);
                out.set_cell(1, 1, 1.0 / (self.fov_radians / 2.0).tan());
                out.set_cell(2, 2, (self.near + self.far) / (self.near - self.far));
                out.set_cell(2, 3, (2.0 * self.near * self.far) / (self.near - self.far));
                out.set_cell(3, 2, -1.0);
            }
        }

        out
    }
}

impl Default for Perspective
{
    fn default() -> Self
    {
        Self {
            fov_radians: 80.0f32.to_radians(),
            far: 100.0,
            near: 0.1,
        }
    }
}

builder! {
#[builder(name = OrthographicBuilder, derives=(Debug, Clone))]
#[derive(Debug, Clone, PartialEq, PartialOrd)]
#[non_exhaustive]
pub struct Orthographic
{
    pub size: Dimens3<f32>,
}
}

impl Orthographic
{
    pub fn builder() -> OrthographicBuilder
    {
        OrthographicBuilder::default()
    }

    /// Creates a orthographic projection matrix using right-handed coordinates.
    pub fn to_matrix_rh(
        &self,
        center_pos: &Vec3<f32>,
        clip_volume: ClipVolume,
    ) -> Matrix<f32, 4, 4>
    {
        let mut result = Matrix::<f32, 4, 4>::new();

        let left = center_pos.x - (self.size.width / 2.0);
        let right = center_pos.x + (self.size.width / 2.0);
        let bottom = center_pos.y - (self.size.height / 2.0);
        let top = center_pos.y + (self.size.height / 2.0);
        let near = center_pos.z - (self.size.depth / 2.0);
        let far = center_pos.z + (self.size.depth / 2.0);

        match clip_volume {
            ClipVolume::NegOneToOne => {
                result.set_cell(0, 0, 2.0 / (right - left));
                result.set_cell(1, 1, 2.0 / (top - bottom));
                result.set_cell(2, 2, -2.0 / (far - near));
                result.set_cell(0, 3, -(right + left) / (right - left));
                result.set_cell(1, 3, -(top + bottom) / (top - bottom));
                result.set_cell(2, 3, -(far + near) / (far - near));
                result.set_cell(3, 3, 1.0);
            }
        }

        result
    }
}

impl Default for Orthographic
{
    fn default() -> Self
    {
        Self {
            size: Dimens3 {
                width: 10.0,
                height: 7.0,
                depth: 10.0,
            },
        }
    }
}

impl Default for OrthographicBuilder
{
    fn default() -> Self
    {
        let orthographic = Orthographic::default();

        OrthographicBuilder { size: orthographic.size }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[non_exhaustive]
pub enum ClipVolume
{
    /// -1 to +1. This is the OpenGL clip volume definition.
    NegOneToOne,
}