use std::time::Duration;

use cgmath::{InnerSpace, Point3, Rad, Vector2, Vector3, Vector4, Zero, perspective};
use winit::{event::ElementState, keyboard::KeyCode};

use crate::{solar_system::BodyId, wgpuctx::WgpuCtx};

pub const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4<f32> = cgmath::Matrix4::from_cols(
    Vector4::new(1.0, 0.0, 0.0, 0.0),
    Vector4::new(0.0, 1.0, 0.0, 0.0),
    Vector4::new(0.0, 0.0, 0.5, 0.0),
    Vector4::new(0.0, 0.0, 0.5, 1.0)
);

pub struct Camera
{
    position: Point3<f32>,
    pitch: Rad<f32>,
    yaw: Rad<f32>,
    scale: f32,

    target: Option<BodyId>,

    buffer: wgpu::Buffer,
    staging_buffer: wgpu::Buffer,
    bindgroup: wgpu::BindGroup
}

pub struct CameraController
{
    position_pos_delta: Vector3<f32>,
    position_neg_delta: Vector3<f32>,
    rotation_pos_delta: Vector2<f32>,
    rotation_neg_delta: Vector2<f32>,
    scale_delta: Vector2<f32>,
}

#[repr(C)]
#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
pub struct CameraUniform
{
    view: [[f32;4];4],
    proj: [[f32;4];4],
    scale: f32
}

pub struct Projection
{
    aspect: f32,
    fovy: Rad<f32>,
    clip: (f32, f32)
}

impl Camera
{
    pub fn new<
        V: Into<Point3<f32>>,
        Y: Into<Rad<f32>>,
        P: Into<Rad<f32>>
    >(
        wgpuctx: &WgpuCtx,
        position: V,
        yaw: Y,
        pitch: P)
    -> Self {
        use wgpu::BufferUsages;
        let buffer = wgpuctx.create_buffer(
            &wgpu::BufferDescriptor {
                label: Some("Camera buffer"),
                size: 144,
                usage: BufferUsages::UNIFORM | BufferUsages::COPY_DST,
                mapped_at_creation: false
            }
        );

        let staging_buffer = wgpuctx.create_buffer(
            &wgpu::wgt::BufferDescriptor { 
                label: Some("Camera staging buffer"),
                size: 144, 
                usage: BufferUsages::COPY_SRC | BufferUsages::COPY_DST, 
                mapped_at_creation: false
            }
        );

        let bind_group_layout = Camera::bindgroup_layout(wgpuctx);
        let bind_group = wgpuctx.device().create_bind_group(
            &wgpu::BindGroupDescriptor { 
                label: Some("Camera bind group"), 
                layout: &bind_group_layout, 
                entries: &[
                    wgpu::BindGroupEntry {
                        binding: 0,
                        resource: buffer.as_entire_binding()
                    }
                ]
            }
        );
        Self {
            position: position.into(),
            yaw: yaw.into(),
            pitch: pitch.into(),
            scale: 1.0,
            target: None,
            buffer: buffer,
            staging_buffer: staging_buffer,
            bindgroup: bind_group
        }
    }

    pub fn get_scale(&self) -> f32
    { self.scale }

    pub fn bindgroup_layout(wgpuctx: &WgpuCtx)
    -> wgpu::BindGroupLayout
    {
        wgpuctx.device().create_bind_group_layout(
            &wgpu::BindGroupLayoutDescriptor { 
                label: Some("Camera bind group layout"),
                entries: &[
                    wgpu::BindGroupLayoutEntry {
                        binding: 0,
                        visibility: wgpu::ShaderStages::VERTEX,
                        ty: wgpu::BindingType::Buffer {
                            ty: wgpu::BufferBindingType::Uniform,
                            has_dynamic_offset: false,
                            min_binding_size: None
                        },
                        count: None
                    }
                ]
            }
        )
    }

    pub fn bindgroup(&self)
    -> &wgpu::BindGroup
    { &self.bindgroup }

    pub fn stage_changes(
        &self,
        encoder: &mut wgpu::CommandEncoder)
    {
        let buffer_size = size_of::<CameraUniform>();
        encoder.copy_buffer_to_buffer(&self.staging_buffer, 0, &self.buffer, 0, Some(buffer_size as u64));
    }

    pub fn update_buffer(
        &self,
        wgpuctx: &WgpuCtx,
        projection: &Projection)
    {
        wgpuctx.queue().write_buffer(&self.staging_buffer, 0, bytemuck::cast_slice(&[self.uniform(projection)]));
    }

    pub fn view_matrix(
        &self)
    -> cgmath::Matrix4<f32> {

        let (yaw_sin, yaw_cos) = self.yaw.0.sin_cos();
        let (pitch_sin, pitch_cos) = self.pitch.0.sin_cos();

        cgmath::Matrix4::look_to_rh(
            self.position,
            Vector3::new(
                pitch_cos * yaw_cos,
                pitch_sin,
                pitch_cos * yaw_sin
            ).normalize(),
            Vector3::unit_y()
        )
    }

    pub fn uniform(
        &self,
        projection: &Projection)
    -> CameraUniform
    {
        CameraUniform {
            view: (self.view_matrix()).into(),
            proj: (OPENGL_TO_WGPU_MATRIX * projection.projection_matrix()).into(),
            scale: self.scale
        }
    }
} //impl Camera

impl CameraController
{
    pub fn new() -> Self {
        Self {
            position_pos_delta: Vector3::new(0.0, 0.0, 0.0),
            position_neg_delta: Vector3::new(0.0, 0.0, 0.0),
            rotation_pos_delta: Vector2::new(0.0, 0.0),
            rotation_neg_delta: Vector2::new(0.0, 0.0),
            scale_delta: Vector2::new(0.0, 0.0),
        }
    }

    pub fn keyboard_input(
        &mut self,
        key_code: KeyCode,
        key_state: ElementState)
    {
        let press_q = if key_state == ElementState::Pressed { 1.0 } else { 0.0 };
        match key_code {
            KeyCode::KeyW => { self.position_pos_delta.z = press_q; },
            KeyCode::KeyS => { self.position_neg_delta.z = press_q; },
            KeyCode::KeyA => { self.position_neg_delta.x = press_q; },
            KeyCode::KeyD => { self.position_pos_delta.x = press_q; },
            KeyCode::Space => { self.position_pos_delta.y = press_q; },
            KeyCode::ShiftLeft => { self.position_neg_delta.y = press_q; },

            KeyCode::KeyQ => { self.scale_delta.x = press_q; },
            KeyCode::KeyE => { self.scale_delta.y = press_q; },
            _ => {}
        }
    }

    pub fn update(
        &mut self,
        camera: &mut Camera,
        dt: Duration)
    {
        let dt = dt.as_secs_f32();
        let speed = 1.0;

        camera.pitch.0 += (self.rotation_pos_delta.x - self.rotation_neg_delta.x) * speed * dt;
        camera.yaw.0 += (self.rotation_pos_delta.y - self.rotation_neg_delta.y) * speed * dt;

        let (yaw_sin, yaw_cos) = camera.yaw.0.sin_cos();
        let forward = Vector3::new(yaw_cos, 0.0, yaw_sin).normalize();
        let right = Vector3::new(-yaw_sin, 0.0, yaw_cos).normalize();
        let up = Vector3::new(0.0, 1.0, 0.0);

        camera.position += forward * (self.position_pos_delta.z - self.position_neg_delta.z) * speed * dt;
        camera.position += right * (self.position_pos_delta.x - self.position_neg_delta.x) * speed * dt;
        camera.position += up * (self.position_pos_delta.y - self.position_neg_delta.y) * speed * dt;

        camera.scale *= 1.0 + ((self.scale_delta.y - self.scale_delta.x) * 0.1);
        camera.scale = f32::max(1e-16, f32::min(1.0, camera.scale));
    }
}

impl Projection {
    pub fn new<
        F: Into<Rad<f32>>
    >(
        width: u32,
        height: u32,
        fovy: F,
        clip: (f32, f32))
    -> Self {
        Self {
            aspect: width as f32 / height as f32,
            fovy: fovy.into(),
            clip: clip
        }
    }

    pub fn resize(
        &mut self,
        width: u32,
        height: u32)
    {
        self.aspect = width as f32 / height as f32;
    }

    pub fn projection_matrix(
        &self)
    -> cgmath::Matrix4<f32> {
        perspective(self.fovy, self.aspect, self.clip.0, self.clip.1)
    }
}
