use std::time::Duration;

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

use crate::fleet::FleetsManager;
use crate::solar_system::body::OrbitalBody;
use crate::solar_system::{self, Kilometers, SolarSystem};
use crate::timeman::Second;
use crate::ui;
use crate::ui::contact::MapContact;
use crate::wgpuctx::WgpuCtx;

pub struct Camera
{
    origin_position: Vector3<Kilometers>,
    offset_position: Vector3<f32>,
    pitch: Rad<f32>,
    yaw: Rad<f32>,
    scale: f32,

    target: Option<MapContact>,

    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],
    origin_pos: [f32;3],
    _pad0: u32,
    rel_pos: [f32;3],
    scale: f32,
}

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

impl Camera
{
    pub fn new<
        V: Into<Vector3<Kilometers>>,
        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: size_of::<CameraUniform>() as _,
                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: size_of::<CameraUniform>() as _, 
                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 {
            origin_position: position.into(),
            offset_position: Vector3::new(0.0, 0.0, 0.0),
            yaw: yaw.into(),
            pitch: pitch.into(),
            scale: 1e-7,
            target: Some(MapContact::from_body(0)),
            buffer: buffer,
            staging_buffer: staging_buffer,
            bindgroup: bind_group
        }
    }

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

    pub fn get_origin_position(&self) -> Vector3<Kilometers>
    { self.origin_position }

    pub fn get_combined_position(&self) -> Vector3<Kilometers>
    {
        self.origin_position + self.offset_position.map(|v| { v as f64 })
    }

    pub fn get_rotation(&self) -> Vector2<Rad<f32>>
    {
        Vector2::new(self.yaw, self.pitch)
    }

    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 set_target(
        &mut self,
        target: Option<MapContact>)
    { 
        if target == self.target { return; }
        if target.is_none() {
            self.origin_position = self.get_combined_position();
            self.offset_position = Vector3::new(0.0, 0.0, 0.0);
        }
        self.target = target;
    }

    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();

        let eye_pos = cgmath::point3(
            pitch_cos * yaw_cos,
            pitch_sin,
            pitch_cos * yaw_sin
        );

        if self.target.is_some() {
            cgmath::Matrix4::look_at_rh(
                eye_pos,
                cgmath::point3(0.0, 0.0, 0.0),
                Vector3::unit_y())
        }else{
            cgmath::Matrix4::look_to_rh(
                cgmath::point3(0.0, 0.0, 0.0),
                cgmath::vec3(
                    eye_pos.x, eye_pos.y, eye_pos.z
                ).normalize(),
                Vector3::unit_y()
            )
        }
    }

    pub fn uniform(
        &self,
        projection: &Projection)
    -> CameraUniform
    {
        CameraUniform {
            view: (self.view_matrix()).into(),
            proj: projection.projection_matrix().into(),
            origin_pos: [ 
                   self.origin_position.x as f32, 
                   self.origin_position.y as f32, 
                   self.origin_position.z as f32 ],
            rel_pos: [ 
                   self.offset_position.x as f32,
                   self.offset_position.y as f32,
                   self.offset_position.z as f32 ],
            scale: self.scale,

            _pad0: 0
        }
    }
} //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; },
            _ => {}
        }
    }

    fn update_orbit(
        &mut self,
        camera: &mut Camera,
        target_origin: cgmath::Vector3<Kilometers>,
        target_offset: cgmath::Vector3<Kilometers>,
        dt: Duration)
    {
        let target_radius = 1.0;
        camera.scale *= 1.0 + ((self.scale_delta.y - self.scale_delta.x) * 0.1);
        camera.scale = f32::max(1e-16, f32::min(1.0 / target_radius, camera.scale));

        camera.origin_position = target_origin;
        camera.offset_position = target_offset.map(|v| { v as f32 });

        let dt = dt.as_secs_f32();
        let speed = 1.0;

        let polar_diff = (self.position_pos_delta.z - self.position_neg_delta.z) * speed * dt;
        let azimuth_diff = (self.position_pos_delta.x - self.position_neg_delta.x) * speed * dt;

        let polar_final = camera.pitch.0 + polar_diff;
        let polar_cap = Into::<Rad<f32>>::into(cgmath::Deg(89.999)).0;
        if polar_final > polar_cap { camera.pitch.0 = polar_cap; } else
        if polar_final < -polar_cap { camera.pitch.0 = -polar_cap; } else
        { camera.pitch.0 = polar_final; }

        camera.yaw.0 += azimuth_diff;
    }

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

        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));

        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.offset_position += forward * (self.position_pos_delta.z - self.position_neg_delta.z) * speed * dt;
        camera.offset_position += right * (self.position_pos_delta.x - self.position_neg_delta.x) * speed * dt;
        camera.offset_position += up * (self.position_pos_delta.y - self.position_neg_delta.y) * speed * dt;
        
        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;
    }

    pub fn update(
        &mut self,
        camera: &mut Camera,
        star_system: &SolarSystem,
        fleets_man: &FleetsManager,
        time: Second,
        dt: Duration)
    {
        match camera.target {
            Some(target) => {
                let target_origin = target.origin_position(star_system, fleets_man, time);
                let target_offset = target.offset_position(star_system, fleets_man, time);

                self.update_orbit(
                    camera, 
                    target_origin,
                    target_offset,
                    dt);
            }
            None => self.update_pan(camera, dt)
        }
    }
} //impl CameraController

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)
    }
}
