use crate::components::{CodeType, Program, Snippet};
use crate::errors::Result;
use crate::graphics::AttaWithProgram;

use super::Transform;

use glow::HasContext;
use nalgebra::{Matrix4, Quaternion, Translation3, Unit, UnitQuaternion, Vector3};

#[derive(Debug, Clone)]
pub struct TrackballModel {
    rotation: UnitQuaternion<f32>,
    count: usize,
    model: Matrix4<f32>,
    renorm_count: usize,
    trackball_size: f32,
    x: f32,
    y: f32,
    theta: f32,
    phi: f32,
}

impl TrackballModel {
    pub fn new(theta: f32, phi: f32) -> Self {
        let mut trackball = Self {
            rotation: UnitQuaternion::identity(),
            count: 0,
            model: Matrix4::identity(),
            renorm_count: 97,
            trackball_size: 0.8,
            x: 0.0,
            y: 0.0,
            theta,
            phi,
        };
        trackball.set_orientation(theta, phi);
        trackball
    }

    fn drag_to(&mut self, x: f32, y: f32, dx: f32, dy: f32) {
        let q = self.rotate(x, y, dx, dy);
        self.rotation *= q;
        self.count += 1;
        if self.count > self.renorm_count {
            self.rotation = UnitQuaternion::new_normalize(*self.rotation.clone());
            self.count = 0;
        }
        self.model = self.rotation.to_homogeneous();
    }

    fn model(&self) -> &Matrix4<f32> {
        &self.model
    }

    fn theta(&self) -> f32 {
        self.theta
    }

    fn set_theta(&mut self, theta: f32) {
        self.set_orientation(theta % 360.0, self.phi % 360.0);
    }

    fn phi(&self) -> f32 {
        self.phi
    }

    fn set_phi(&mut self, phi: f32) {
        self.set_orientation(self.theta % 360.0, phi % 360.0);
    }

    fn get_orientation(&self) -> (f32, f32) {
        let q = self.rotation.quaternion();
        let ax = (2.0 * (q.w * q.i + q.j * q.k) / (1.0 - 2.0 * (q.i * q.i + q.j * q.j))).atan()
            * 180.0
            / std::f32::consts::PI;
        let az = (2.0 * (q.w * q.k + q.i * q.j) / (1.0 - 2.0 * (q.j * q.j + q.k * q.k))).atan()
            * 180.0
            / std::f32::consts::PI;
        (-az, ax)
    }

    fn set_orientation(&mut self, theta: f32, phi: f32) {
        self.theta = theta;
        self.phi = phi;

        let angle = self.theta * (std::f32::consts::PI / 180.0);
        let sine = (0.5 * angle).sin();
        let xrot =
            UnitQuaternion::from_quaternion(Quaternion::new((0.5 * angle).cos(), sine, 0.0, 0.0));

        let angle = self.phi * (std::f32::consts::PI / 180.0);
        let sine = (0.5 * angle).sin();
        let zrot =
            UnitQuaternion::from_quaternion(Quaternion::new((0.5 * angle).cos(), 0.0, 0.0, sine));

        self.rotation = xrot * zrot;
        self.model = self.rotation.to_homogeneous();
    }

    fn project(&self, r: f32, x: f32, y: f32) -> f32 {
        let d = (x * x + y * y).sqrt();
        if d < r * 0.70710678118654752440 {
            (r * r - d * d).sqrt()
        } else {
            let t = r / 1.41421356237309504880;
            t * t / d
        }
    }

    fn rotate(&self, x: f32, y: f32, dx: f32, dy: f32) -> UnitQuaternion<f32> {
        if dx == 0.0 && dy == 0.0 {
            return UnitQuaternion::identity();
        }

        let last = Vector3::new(x, y, self.project(self.trackball_size, x, y));
        let new = Vector3::new(
            x + dx,
            y + dy,
            self.project(self.trackball_size, x + dx, y + dy),
        );

        let a = new.cross(&last);
        let d = last - new;
        let t = d.norm() / (2.0 * self.trackball_size);
        let t = t.clamp(-1.0, 1.0);

        let phi = 2.0 * t.asin();
        UnitQuaternion::from_axis_angle(&Unit::new_normalize(a), phi)
    }
}

#[derive(Debug, Clone)]
pub struct Trackball {
    snippet: Snippet,
    model: TrackballModel,
}

impl Trackball {
    pub fn new() -> Result<Self> {
        let snippets = Snippet::new(
            "trackball",
            CodeType::<&'static str>::Path("transform/trackball.glsl".into()),
            false,
            None,
        )?;

        let model = TrackballModel::new(45.0, 45.0);

        Ok(Self {
            snippet: snippets,
            model,
        })
    }

    pub fn on_mouse_drag(&mut self, x: f32, y: f32, dx: f32, dy: f32) {
        self.model.drag_to(x, y, dx, dy);
    }

    pub fn model(&self) -> &Matrix4<f32> {
        self.model.model()
    }
}

impl Transform for Trackball {
    fn snippet(&self) -> &Snippet {
        &self.snippet
    }
}

impl AttaWithProgram for Trackball {
    fn attach_with_program(&self, gl: &glow::Context, program: &Program) -> Result<()> {
        unsafe {
            let l = program.get_uniform_location(gl, "trackball_model");
            gl.uniform_matrix_4_f32_slice(l.as_ref(), false, self.model.model().as_slice());
        }

        Ok(())
    }
}

mod test {
    use super::*;

    #[test]
    fn test_trackball() {
        // let trackball = Trackball::new().unwrap();
        // println!("{}", trackball.snippet);
    }
}
