use crate::{
    camera::{self as cam, FirstPersonCamera},
    engine::{self, World},
    graphics_util::draw_cuboid,
};
use macroquad::prelude as mq;
use macroquad::ui::hash;

use rapier3d::prelude::*;
use strum::IntoEnumIterator;
mod ui;

const DEBUG_UI: bool = false;
const DEPTH_MAP_SIZE: u32 = 1024;

const LIGHT_PROJECTION_SIZE: f32 = 40.0;
const LIGHT_NEAR: f32 = 50.0;
const LIGHT_FAR: f32 = 400.0;

pub struct Light {
    projection_matrix: mq::Mat4,
    view_matrix: mq::Mat4,
    pub position: mq::Vec3,
    pub front: mq::Vec3,
}
impl Light {
    pub fn new() -> Light {
        let mut l = Light {
            projection_matrix: mq::Mat4::orthographic_rh(
                -LIGHT_PROJECTION_SIZE,
                LIGHT_PROJECTION_SIZE,
                -LIGHT_PROJECTION_SIZE,
                LIGHT_PROJECTION_SIZE,
                LIGHT_NEAR,
                LIGHT_FAR,
            ),
            view_matrix: mq::Mat4::NAN,
            position: mq::Vec3::NAN,
            front: mq::Vec3::NAN,
        };
        l.set_location(mq::vec3(5.0, 10.0, 0.0), mq::Vec3::NEG_Y);
        return l;
    }
    pub fn set_location(&mut self, position: mq::Vec3, front: mq::Vec3) {
        self.position = position;
        self.front = front;
        self.view_matrix =
            mq::Mat4::look_at_rh(self.position, self.position + self.front, mq::Vec3::Y);
    }
    pub fn get_space_matrix(&self) -> mq::Mat4 {
        return self.projection_matrix * self.view_matrix;
    }
}

pub struct Renderer {
    base_material: mq::Material,
    depth_material: mq::Material,
    depth_target: mq::RenderTarget,
    pub light: Light,
    pub camera: Box<dyn cam::CameraController>,
    ui_state: ui::MaterialUi,
    pub enable_shadows: bool,
}

#[derive(strum::EnumIter, Debug)]
pub enum MaterialUni {
    RenderNormalsBool,
    RenderShadowsBool,
    ShadowStepCount,
    ShadowStepSize,
    ShadowMinBias,
    ShadowFacBias,
    ShadowMaxBias,
    LightPosition,
    LightSpaceMatrix,
}

impl MaterialUni {
    pub fn as_str(&self) -> &'static str {
        use MaterialUni::*;
        match self {
            RenderNormalsBool => "render_normals_bool",
            RenderShadowsBool => "render_shadows_bool",
            ShadowStepCount => "shadow_step_count",
            ShadowStepSize => "shadow_step_size",
            ShadowMinBias => "shadow_min_bias",
            ShadowFacBias => "shadow_fac_bias",
            ShadowMaxBias => "shadow_max_bias",
            LightPosition => "light_position",
            LightSpaceMatrix => "light_space_matrix",
        }
    }
    pub fn as_type(&self) -> mq::UniformType {
        use mq::UniformType::*;
        use MaterialUni::*;
        match self {
            RenderNormalsBool => Int1,
            RenderShadowsBool => Int1,
            ShadowStepCount => Int1,
            ShadowStepSize => Int1,
            ShadowMinBias => Float1,
            ShadowFacBias => Float1,
            ShadowMaxBias => Float1,
            LightPosition => Float3,
            LightSpaceMatrix => Mat4,
        }
    }
    pub fn uniform_list() -> Vec<mq::UniformDesc> {
        let mut ret: Vec<mq::UniformDesc> = Default::default();
        for member in MaterialUni::iter() {
            ret.push(mq::UniformDesc::new(member.as_str(), member.as_type()));
        }
        return ret;
    }
}

impl Renderer {
    pub fn new(camera: Box<dyn cam::CameraController>) -> Renderer {
        let material = mq::load_material(
            mq::ShaderSource::Glsl {
                vertex: include_str!("shaders/shader.vert"),
                fragment: include_str!("shaders/shader.frag"),
            },
            mq::MaterialParams {
                pipeline_params: mq::PipelineParams {
                    cull_face: macroquad::miniquad::CullFace::Front,
                    depth_test: mq::Comparison::LessOrEqual,
                    depth_write: true,
                    ..Default::default()
                },
                uniforms: MaterialUni::uniform_list(),
                textures: vec![String::from("shadow_map")],
                ..Default::default()
            },
        )
        .unwrap();
        material.set_uniform(MaterialUni::RenderNormalsBool.as_str(), 0);
        material.set_uniform(MaterialUni::RenderShadowsBool.as_str(), true as i32);

        let depth_material = mq::load_material(
            mq::ShaderSource::Glsl {
                vertex: include_str!("shaders/depth.vert"),
                fragment: include_str!("shaders/depth.frag"),
            },
            mq::MaterialParams {
                pipeline_params: mq::PipelineParams {
                    // cull_face: macroquad::miniquad::CullFace::Back,
                    depth_test: mq::Comparison::LessOrEqual,
                    depth_write: true,
                    ..Default::default()
                },
                uniforms: vec![mq::UniformDesc::new(
                    "light_space_matrix",
                    mq::UniformType::Mat4,
                )],
                ..Default::default()
            },
        )
        .unwrap();

        let depth_map_texture = mq::render_target_ex(
            DEPTH_MAP_SIZE,
            DEPTH_MAP_SIZE,
            mq::RenderTargetParams {
                sample_count: 1,
                depth: true,
            },
        );
        depth_map_texture
            .texture
            .set_filter(mq::FilterMode::Nearest);

        let light = Light::new();
        let mut r = Renderer {
            base_material: material,
            depth_material,
            depth_target: depth_map_texture,
            light,
            camera,
            ui_state: ui::MaterialUi::new(),
            enable_shadows: true,
        };
        r.light
            .set_location(mq::vec3(10.0, 40.0, 10.0), mq::Vec3::NEG_Y);
        r.apply_config();
        return r;
    }
    pub fn apply_config(&mut self) {
        self.ui_state.apply(&self.base_material);
        self.enable_shadows = self.ui_state.render_shadows_bool;
    }
    pub fn update_camera(&mut self, world: &World) {
        self.camera.update(mq::get_frame_time(), world);
    }
    pub fn update_light(&mut self, world: &World) {
        let light_view = self.light.view_matrix;

        let mut min_ls = mq::vec3(f32::MAX, 0.0, f32::MAX);
        let mut max_ls = mq::vec3(f32::MIN, 50.0, f32::MIN);

        // 1. iterate all world objects
        for (handle, _) in world.colliders.iter() {
            let (min_ws, max_ws) = engine::collider_world_aabb(world, handle);

            // 2. get 8 corners of the world-space AABB
            for &corner in &[
                mq::vec3(min_ws.x, min_ws.y, min_ws.z),
                mq::vec3(min_ws.x, min_ws.y, max_ws.z),
                mq::vec3(min_ws.x, max_ws.y, min_ws.z),
                mq::vec3(min_ws.x, max_ws.y, max_ws.z),
                mq::vec3(max_ws.x, min_ws.y, min_ws.z),
                mq::vec3(max_ws.x, min_ws.y, max_ws.z),
                mq::vec3(max_ws.x, max_ws.y, min_ws.z),
                mq::vec3(max_ws.x, max_ws.y, max_ws.z),
            ] {
                // 3. transform to light space
                let p = light_view.transform_point3(corner);

                min_ls.x = min_ls.x.min(p.x);
                min_ls.y = min_ls.y.min(p.y);
                min_ls.z = min_ls.z.min(p.z);

                max_ls.x = max_ls.x.max(p.x);
                max_ls.y = max_ls.y.max(p.y);
                max_ls.z = max_ls.z.max(p.z);
            }
        }

        // 4. build tight-fitting orthographic projection
        self.light.projection_matrix = mq::Mat4::orthographic_rh(
            min_ls.x, max_ls.x, // left/right
            min_ls.y, max_ls.y, // bottom/top
            -max_ls.z, -min_ls.z, // near/far (RH coordinate system)
        );

        // 5. upload to materials
        let light_space_matrix = self.light.get_space_matrix();
        self.base_material
            .set_uniform("light_position", self.light.position);
        self.base_material
            .set_uniform("light_space_matrix", light_space_matrix);
        self.depth_material
            .set_uniform("light_space_matrix", light_space_matrix);
    }

    fn render_depth(&mut self, mut world: &mut World) {
        mq::set_camera(&mq::Camera3D {
            position: self.light.position,
            target: self.light.position + self.light.front,
            up: mq::Vec3::Y,
            render_target: Some(self.depth_target.clone()),
            ..Default::default()
        });
        mq::gl_use_material(&self.depth_material);
        mq::clear_background(mq::BLACK);
        draw_world(&mut world, None);
        mq::gl_use_default_material();
        mq::set_default_camera();

        self.base_material
            .set_texture("shadow_map", self.depth_target.texture.clone());
    }
    fn render_default(&mut self, mut world: &mut World) {
        mq::clear_background(mq::DARKGRAY);
        self.camera.apply();

        draw_world(&mut world, Some(&self.base_material));

        // mq::draw_sphere(self.light.position, 0.25, None, mq::WHITE);

        mq::draw_grid(20, 1.5, mq::BLACK, mq::GRAY);
        mq::gl_use_default_material();
        mq::set_default_camera();
    }
    fn render_ui(&mut self) {
        mq::set_default_camera();

        if DEBUG_UI {
            mq::draw_texture_ex(
                &self.depth_target.texture,
                320.,
                20.,
                mq::WHITE,
                mq::DrawTextureParams {
                    flip_y: true,
                    dest_size: Some(mq::vec2(200.0, 200.0)),
                    ..Default::default()
                },
            );
            macroquad::ui::root_ui().window(
                hash!("MaterialWindow"),
                mq::vec2(20.0, 20.0),
                mq::vec2(300.0, 300.0),
                |ui| {
                    self.ui_state.ui(ui);
                },
            );
        }
    }
    pub fn draw(&mut self, mut world: &mut World) {
        if self.enable_shadows {
            self.render_depth(&mut world);
        }

        self.render_default(&mut world);

        self.render_ui();
    }
}

fn draw_world(world: &mut World, material: Option<&mq::Material>) {
    for (handle, coll) in world.colliders.iter() {
        let position: glam::Vec3 = world.position_of_collider(handle).unwrap().into();
        let rotation = *coll.rotation();
        unsafe {
            let context = mq::get_internal_gl().quad_gl;

            context.push_model_matrix(mq::Mat4::from_rotation_translation(
                rotation.into(),
                position,
            ));
        }
        let color = world.collider_data.get(&handle).unwrap().color;
        match coll.shape().shape_type() {
            ShapeType::Ball => {
                mq::draw_sphere(
                    mq::vec3(0.0, 0.0, 0.0),
                    coll.shape().as_ball().unwrap().radius,
                    None,
                    color,
                );
            }
            ShapeType::Cuboid => {
                match material {
                    Some(mat) => {
                        mq::gl_use_material(&mat);
                    }
                    None => {}
                }
                draw_cuboid(
                    mq::vec3(0.0, 0.0, 0.0),
                    (coll.shape().as_cuboid().unwrap().half_extents * 2.0).into(),
                    color,
                );
                match material {
                    Some(_) => {
                        mq::gl_use_default_material();
                    }
                    None => {}
                }
            }
            _ => println!("Not implemented.. Skipping"),
        }
        unsafe {
            let context = mq::get_internal_gl().quad_gl;
            context.pop_model_matrix();
        }
    }
}
