use cgmath::{Deg, InnerSpace, Matrix4, One, Quaternion, Rad, Rotation, Rotation3, SquareMatrix, Vector3, Vector4, vec3, vec4};
use vulkano::buffer::TypedBufferAccess;

use crate::game::player::PlayerMovementMode::{FirstPerson, Flying};

use crate::input::InputState;
use crate::vulkan::Mesh;
use crate::vulkan::gameobject::GameObject;
use crate::vulkan::{
    gameobject::Updatable,
    VulkanRenderer
};

pub struct Camera {
    pub fov_y: f32,
    pub position: Vector3<f32>,
    pub rotation: Quaternion<f32>,
    pub view: Matrix4<f32>,
    pub proj: Matrix4<f32>,
}

impl Camera {
    pub fn new() -> Camera {
        Camera {
            fov_y: 45.0,
            position: vec3(0.0, 0.0, 0.0),
            rotation: Quaternion::one(),
            view: Matrix4::identity(),
            proj: Matrix4::identity(),
        }
    }

    pub fn update(&mut self, renderer: &mut VulkanRenderer) {
        // Don't let floating point error accumulate in rotation
        debug_assert!(f32::abs(1.0 - self.rotation.magnitude()) < 0.00001 as f32);
        self.rotation = self.rotation.normalize();

        // Create matrices
        self.view = Matrix4::from(self.rotation) * Matrix4::from_translation(self.position * -1.);

        self.proj = cgmath::perspective(
            Rad::from(Deg(self.fov_y)),
            renderer.game_data.dimensions[0] as f32 / renderer.game_data.dimensions[1] as f32,
            0.1,
            100.0
        );
        // Why?
        self.proj.y.y *= -1.0;

        // Upload
        renderer.game_data.line_push_constants.view = self.view.into();
        renderer.game_data.line_push_constants.projection = self.proj.into();
    }

    pub fn viewport_pos_to_ray_direction(&self, viewport_pos: [f64; 2], viewport_dimensions: [u32; 2]) -> Option<Vector3<f32>> {
        let normalized_x = 2. * (viewport_pos[0] as f32 / viewport_dimensions[0] as f32) - 1.;
        let normalized_y = 2. * (viewport_pos[1] as f32 / viewport_dimensions[1] as f32) - 1.;

        let click_start_screen = vec4(normalized_x, normalized_y, -1.0, 1.0);
        let click_end_screen = vec4(normalized_x, normalized_y, 0.0, 1.0);

        let proj_inverse = self.proj.invert()?;
        let view_inverse = self.view.invert()?;

        let mut ray_start_camera = proj_inverse * click_start_screen;
        ray_start_camera /= ray_start_camera.w;

        let mut ray_end_camera = proj_inverse * click_end_screen;
        ray_end_camera /= ray_end_camera.w;

        let mut ray_start_world = view_inverse * ray_start_camera;
        ray_start_world /= ray_start_world.w;

        let mut ray_end_world = view_inverse * ray_end_camera;
        ray_end_world /= ray_end_world.w;

        let ray_dir_world = (ray_end_world - ray_start_world).normalize();

        Some(ray_dir_world.truncate())
    }

    pub fn local_to_world(&self, vec:Vector3<f32>) -> Vector3<f32> {
        self.rotation.invert().rotate_vector(vec)
    }

    pub fn world_to_local(&self, vec:Vector3<f32>) -> Vector3<f32> {
        self.rotation.rotate_vector(vec)
    }
}

#[derive(PartialEq)]
pub enum PlayerMovementMode {
    FirstPerson,
    Flying,
}

pub struct Player {
    pub camera: Camera,
    pub movement_mode: PlayerMovementMode,
    pub movement_speed: f32,
    pub look_sensitivity: f32,
    pub height: f32,
    pub x_look: f32,
    pub y_look: f32
}

impl Player {
    pub fn new() -> Player {
        Player {
            camera: Camera::new(),
            movement_mode: Flying,
            movement_speed: 3.0,
            look_sensitivity: 30.0,
            height: 1.0,
            x_look: 0.0,
            y_look: 0.0
        }
    }
}

pub fn vec4_from_pos(pos: [f32; 3]) -> Vector4<f32> {
    vec4(pos[0], pos[1], pos[2], 1.0)
}

pub fn intersection_distance(ray_origin: Vector3<f32>, ray_direction: Vector3<f32>, mesh: &Mesh, game_object: &GameObject) -> Option<f32> {
    let index_lock = mesh.index_buffer.read().unwrap();
    let vertex_lock = mesh.vertex_buffer.read().unwrap();
    (0..mesh.index_buffer.len() / 3).map(|tri_idx| {
        let vtx_a = game_object.get_model_matrix() * vec4_from_pos(vertex_lock[index_lock[tri_idx * 3    ] as usize].position);
        let vtx_b = game_object.get_model_matrix() * vec4_from_pos(vertex_lock[index_lock[tri_idx * 3 + 1] as usize].position);
        let vtx_c = game_object.get_model_matrix() * vec4_from_pos(vertex_lock[index_lock[tri_idx * 3 + 2] as usize].position);
        intersect_triangle(ray_origin, ray_direction, vtx_a.truncate().into(), vtx_b.truncate().into(), vtx_c.truncate().into())
    }).fold(None, |acc, x| {
        if let Some(smallest) = acc {
            if let Some(new) = x {
                if new < smallest {
                    Some(new)
                } else {
                    Some(smallest)
                }
            } else {
                acc
            }
        } else {
            x
        }
    })
}

// https://en.wikipedia.org/wiki/M%C3%B6ller%E2%80%93Trumbore_intersection_algorithm
pub fn intersect_triangle(ray_origin: Vector3<f32>, ray_direction: Vector3<f32>, vtx_a: [f32; 3], vtx_b: [f32; 3], vtx_c: [f32; 3]) -> Option<f32> {
    let edge_1 = vec3(vtx_b[0] - vtx_a[0], vtx_b[1] - vtx_a[1], vtx_b[2] - vtx_a[2]);
    let edge_2 = vec3(vtx_c[0] - vtx_a[0], vtx_c[1] - vtx_a[1], vtx_c[2] - vtx_a[2]);
    let h = ray_direction.cross(edge_2);
    let a = edge_1.dot(h);
    if a > -f32::EPSILON && a < f32::EPSILON { return None; }

    let f = 1. / a;
    let s: Vector3<f32> = ray_origin - Vector3::from(vtx_a);
    let u = f * s.dot(h);
    if u < 0. || u > 1. { return None; }

    let q = s.cross(edge_1);
    let v = f * ray_direction.dot(q);
    if v < 0. || u + v > 1. { return None; }

    let t = f * edge_2.dot(q);
    if t > f32::EPSILON { return Some(t); }

    None
}

impl Updatable for Player {
    fn update(&mut self, delta_time: f32, input: &InputState, renderer: &mut VulkanRenderer) {
        // Edit mode
        if input.button_just_pressed("toggle_edit") {
            if self.movement_mode == FirstPerson {
                self.movement_mode = Flying;
            } else {
                self.movement_mode = FirstPerson;
            }
        }

        if input.button_just_pressed("select") {
            let ray_direction = self.camera.viewport_pos_to_ray_direction(input.mouse_position, renderer.game_data.dimensions).unwrap();
            for game_object in &mut renderer.game_data.game_objects {
                let mesh = &renderer.game_data.meshes[game_object.mesh_index];
                if let Some(dist) = intersection_distance(self.camera.position, ray_direction, mesh, game_object) {
                    game_object.is_selected = !game_object.is_selected;
                    println!("distance: {}", dist);
                }
            }
        }

        // Rotation
        self.x_look += input.get_axis("look_vertical")   * delta_time * self.look_sensitivity;
        self.y_look += input.get_axis("look_horizontal") * delta_time * self.look_sensitivity;
        let x_rot = Quaternion::from_angle_x(Deg(self.x_look));
        let y_rot = Quaternion::from_angle_y(Deg(self.y_look));
        self.camera.rotation = x_rot * y_rot;

        // Movement
        let local_input = vec3(input.get_axis("move_sideways"), 0.0, -input.get_axis("move_forward")) * self.movement_speed * delta_time;

        if self.movement_mode == FirstPerson {
            let mut world_input = self.camera.local_to_world(local_input);
            world_input.y = 0.0;
            self.camera.position += world_input.normalize();
            self.camera.position.y = self.height;
        } else if self.movement_mode == Flying {
            self.camera.position += self.camera.local_to_world(local_input);
        }

        // Update
        self.camera.update(renderer);
    }
}