use super::DepthCalculation;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::Mat4;
use bevy_reflect::{Reflect, ReflectDeserialize};
use serde::{Deserialize, Serialize};

pub trait CameraProjection {
    fn get_projection_matrix(&self) -> Mat4;
    fn update(&mut self, width: f32, height: f32);
    fn depth_calculation(&self) -> DepthCalculation;
    fn far(&self) -> f32;
}

#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub struct PerspectiveProjection {
    pub fov: f32,
    pub aspect_ratio: f32,
    pub near: f32,
    pub far: f32,
}

impl CameraProjection for PerspectiveProjection {
    fn get_projection_matrix(&self) -> Mat4 {
        Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near)
    }

    fn update(&mut self, width: f32, height: f32) {
        self.aspect_ratio = width / height;
    }

    fn depth_calculation(&self) -> DepthCalculation {
        DepthCalculation::Distance
    }

    fn far(&self) -> f32 {
        self.far
    }
}

impl Default for PerspectiveProjection {
    fn default() -> Self {
        PerspectiveProjection {
            fov: std::f32::consts::PI / 4.0,
            near: 0.1,
            far: 1000.0,
            aspect_ratio: 1.0,
        }
    }
}

// TODO: make this a component instead of a property
#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect_value(Serialize, Deserialize)]
pub enum WindowOrigin {
    Center,
    BottomLeft,
}

#[derive(Debug, Clone, Reflect, Serialize, Deserialize)]
#[reflect_value(Serialize, Deserialize)]
pub enum ScalingMode {
    /// Manually specify left/right/top/bottom values.
    /// Ignore window resizing; the image will stretch.
    None,
    /// Match the window size. 1 world unit = 1 pixel.
    WindowSize,
    /// Keep vertical axis constant; resize horizontal with aspect ratio.
    FixedVertical,
    /// Keep horizontal axis constant; resize vertical with aspect ratio.
    FixedHorizontal,
}

#[derive(Component, Debug, Clone, Reflect)]
#[reflect(Component)]
pub struct OrthographicProjection {
    pub left: f32,
    pub right: f32,
    pub bottom: f32,
    pub top: f32,
    pub near: f32,
    pub far: f32,
    pub window_origin: WindowOrigin,
    pub scaling_mode: ScalingMode,
    pub scale: f32,
    pub depth_calculation: DepthCalculation,
}

impl CameraProjection for OrthographicProjection {
    fn get_projection_matrix(&self) -> Mat4 {
        Mat4::orthographic_rh(
            self.left * self.scale,
            self.right * self.scale,
            self.bottom * self.scale,
            self.top * self.scale,
            // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0]
            // This is for interoperability with pipelines using infinite reverse perspective projections.
            self.far,
            self.near,
        )
    }

    fn update(&mut self, width: f32, height: f32) {
        match (&self.scaling_mode, &self.window_origin) {
            (ScalingMode::WindowSize, WindowOrigin::Center) => {
                let half_width = width / 2.0;
                let half_height = height / 2.0;
                self.left = -half_width;
                self.right = half_width;
                self.top = half_height;
                self.bottom = -half_height;
            }
            (ScalingMode::WindowSize, WindowOrigin::BottomLeft) => {
                self.left = 0.0;
                self.right = width;
                self.top = height;
                self.bottom = 0.0;
            }
            (ScalingMode::FixedVertical, WindowOrigin::Center) => {
                let aspect_ratio = width / height;
                self.left = -aspect_ratio;
                self.right = aspect_ratio;
                self.top = 1.0;
                self.bottom = -1.0;
            }
            (ScalingMode::FixedVertical, WindowOrigin::BottomLeft) => {
                let aspect_ratio = width / height;
                self.left = 0.0;
                self.right = aspect_ratio;
                self.top = 1.0;
                self.bottom = 0.0;
            }
            (ScalingMode::FixedHorizontal, WindowOrigin::Center) => {
                let aspect_ratio = height / width;
                self.left = -1.0;
                self.right = 1.0;
                self.top = aspect_ratio;
                self.bottom = -aspect_ratio;
            }
            (ScalingMode::FixedHorizontal, WindowOrigin::BottomLeft) => {
                let aspect_ratio = height / width;
                self.left = 0.0;
                self.right = 1.0;
                self.top = aspect_ratio;
                self.bottom = 0.0;
            }
            (ScalingMode::None, _) => {}
        }
    }

    fn depth_calculation(&self) -> DepthCalculation {
        self.depth_calculation
    }

    fn far(&self) -> f32 {
        self.far
    }
}

impl Default for OrthographicProjection {
    fn default() -> Self {
        OrthographicProjection {
            left: -1.0,
            right: 1.0,
            bottom: -1.0,
            top: 1.0,
            near: 0.0,
            far: 1000.0,
            window_origin: WindowOrigin::Center,
            scaling_mode: ScalingMode::WindowSize,
            scale: 1.0,
            depth_calculation: DepthCalculation::Distance,
        }
    }
}
