use glam::{Mat4, Quat, Vec2, Vec3};
use std::time::Instant;
use winit::event::{ElementState, VirtualKeyCode};
use std::f32::consts::LN_2;

pub trait Camera {
    fn get_eye(&self) -> [f32; 4];
    fn get_vp(&self) -> [[f32; 4]; 4];
}

pub struct Flycam {
    // input
    // currently held keys
    is_world_up_pressed: bool,
    is_world_down_pressed: bool,
    is_cam_up_pressed: bool,
    is_cam_down_pressed: bool,
    is_forward_pressed: bool,
    is_backward_pressed: bool,
    is_left_pressed: bool,
    is_right_pressed: bool,
    // accumulated mouse movement yet to be processed
    mouse_dx: f32,
    mouse_dy: f32,

    // state
    // timestamp
    last_update: Instant,
    // camera orientation
    euler_x: f32,
    euler_y: f32,
    // camera movement state
    velocity: Vec3,
    position: Vec3,

    // constants
    // camera movement
    turn_sensitivity: f32,  // coefficient for mouse_dx/dy -> euler_x/y
    thrust_mag: f32,        // coefficient for thrust acceleration vector
    damping_coeff: f32,     // coefficient for damping acceleration vector
    // camera frustum
    aspect: f32,
    fovy: f32,
    znear: f32,
    zfar: f32,
}

impl Flycam {
    /// thrust_speed: top speed when using a single thruster, in units/second
    /// duration to halve difference between current and target velocity, in seconds
    pub fn new(turn_sensitivity: f32, thrust_speed: f32, damper_half_life: f32) -> Self {
        Self {
            is_world_up_pressed: false,
            is_world_down_pressed: false,
            is_cam_up_pressed: false,
            is_cam_down_pressed: false,
            is_forward_pressed: false,
            is_backward_pressed: false,
            is_left_pressed: false,
            is_right_pressed: false,
            mouse_dx: 0.0,
            mouse_dy: 0.0,
            last_update: Instant::now(),
            euler_x: 0.0,
            euler_y: 0.0,
            velocity: Vec3::new(0.0, 0.0, 0.0),
            position: Vec3::new(0.0, 0.5, 1.0),
            turn_sensitivity,
            thrust_mag: thrust_speed / damper_half_life * LN_2,
            damping_coeff: LN_2 / damper_half_life,
            aspect: 1.0, // TODO compute from size
            fovy: std::f32::consts::FRAC_PI_2,
            znear: 0.01,
            zfar: 100.0,
        }
    }
}

impl Flycam {
    /// update stored keyboard state for use in update()
    pub fn process_keyboard(&mut self, key: VirtualKeyCode, state: ElementState) {
        let is_pressed = state == ElementState::Pressed;
        match key {
            VirtualKeyCode::Space => {
                self.is_world_up_pressed = is_pressed;
            }
            VirtualKeyCode::LShift => {
                self.is_world_down_pressed = is_pressed;
            }
            VirtualKeyCode::Q => {
                self.is_cam_down_pressed = is_pressed;
            }
            VirtualKeyCode::E => {
                self.is_cam_up_pressed = is_pressed;
            }
            VirtualKeyCode::W | VirtualKeyCode::Up => {
                self.is_forward_pressed = is_pressed;
            }
            VirtualKeyCode::A | VirtualKeyCode::Left => {
                self.is_left_pressed = is_pressed;
            }
            VirtualKeyCode::S | VirtualKeyCode::Down => {
                self.is_backward_pressed = is_pressed;
            }
            VirtualKeyCode::D | VirtualKeyCode::Right => {
                self.is_right_pressed = is_pressed;
            }
            _ => {}
        }
    }

    /// update accumulated mouse movement for use in update()
    pub fn process_mouse(&mut self, mouse_dx: f64, mouse_dy: f64) {
        self.mouse_dx += mouse_dx as f32;
        self.mouse_dy += mouse_dy as f32;
    }

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

    /// apply input and update camera movement
    pub fn update(&mut self) {
        let dt = self.last_update.elapsed();
        self.last_update = Instant::now();
        let dt = dt.as_micros() as f32 / 1_000_000.0;

        self.update_orientation(dt);
        self.update_kinematic(dt);
    }

    fn update_orientation(&mut self, dt: f32) {
        let t = self.turn_sensitivity;

        self.euler_x -= t * self.mouse_dy;  // mouse +y = 2D plane down  = look down         = 3d space -x
        self.euler_y -= t * self.mouse_dx;  // mouse +x = 2D plane right = look to the right = 3d space -y

        self.mouse_dx = 0.0;
        self.mouse_dy = 0.0;

        // Clamp euler_x to [-pi/2, pi/2]
        let euler_x_limit = std::f32::consts::FRAC_PI_2;
        if self.euler_x < -euler_x_limit {
            self.euler_x = -euler_x_limit;
        } else if self.euler_x > euler_x_limit {
            self.euler_x = euler_x_limit;
        }
    }

    /// update velocity and position from acceleration using forward differences
    fn update_kinematic(&mut self, dt: f32) {
        let net_acc = self.get_thrust_acc() + self.get_damping_acc();

        let delta_vel = net_acc * dt;
        self.velocity += delta_vel;

        let delta_pos = self.velocity * dt;
        self.position += delta_pos;
    }

    /// use keyboard key pairs to trigger directional thrusters in camera and world coordinates
    /// thrust_speed is the max speed (under drag) with a single thruster, but combinations can
    /// produce higher speeds (e.g. forward and right, camera down and world down)
    fn get_thrust_acc(&self) -> glam::Vec3 {
        let axis = Self::key_axis;

        let thruster_cam_x   =  axis(self.is_left_pressed, self.is_right_pressed);
        let thruster_cam_y   =  axis(self.is_cam_down_pressed, self.is_cam_up_pressed);
        let thruster_cam_z   = -axis(self.is_backward_pressed, self.is_forward_pressed);  // forward is -z
        let thruster_world_y =  axis(self.is_world_down_pressed, self.is_world_up_pressed);

        let thrusters_cam = Vec3::new(thruster_cam_x, thruster_cam_y, thruster_cam_z);
        let thrusters_world = Vec3::new(0.0, thruster_world_y, 0.0);

        let cam_to_world = self.get_orientation();
        let thrusters_total = thrusters_world + cam_to_world * thrusters_cam;

        self.thrust_mag * thrusters_total
    }

    /// calculate a damping force (proportional to velocity)
    /// the damping coefficient is calculated in the constructor, which is parameterized in terms
    /// of more physically meaningful values
    fn get_damping_acc(&self) -> glam::Vec3 {
        self.damping_coeff * -self.velocity
    }

    /// a helper function to turn a pair of key states into a sign for thruster direction
    fn key_axis(negative: bool, positive: bool) -> f32 {
        if negative {
            if positive {
                0.0  // positive + negative cancel out
            } else {
                -1.0  // negative only
            }
        } else {
            if positive {
                1.0  // positive only
            } else {
                0.0  // neutral
            }
        }
    }

    /// the current camera orientation, which can be seen as a rotation (in quaternion form) from
    /// camera axes to world axes
    /// glam's YXZ ordering matches the standard roll-pitch-yaw Euler angles
    fn get_orientation(&self) -> glam::Quat {
        Quat::from_euler(glam::EulerRot::YXZ, self.euler_y, self.euler_x, 0.0)
    }
}

impl Camera for Flycam {
    fn get_eye(&self) -> [f32; 4] {
        self.position.extend(0.0).to_array()
    }

    fn get_vp(&self) -> [[f32; 4]; 4] {
        // view matrix is inverted camera pose (world space to camera space)
        let rotation = Mat4::from_quat(self.get_orientation().inverse());
        let translation = Mat4::from_translation(-self.position);
        let view = rotation * translation;

        // perspective projection
        let proj = Mat4::perspective_rh_gl(self.fovy, self.aspect, self.znear, self.zfar);

        let vp = proj * view;
        vp.to_cols_array_2d()
    }
}
