use bone_types::{AxisAngle, Camera3, OrbitState, Result, ZoomFactor};

use crate::camera::ViewportExtent;
use crate::camera3::{
    ViewportPoint, arcball_rotation, orbit_about_point, pan_pixels, roll_about_view,
    zoom_about_pixel,
};

const ZOOM_DRAG_PER_PIXEL: f64 = 1.0075;

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct DragModifiers {
    ctrl: bool,
    shift: bool,
    alt: bool,
}

impl DragModifiers {
    pub const NONE: Self = Self {
        ctrl: false,
        shift: false,
        alt: false,
    };

    #[must_use]
    pub const fn with_ctrl(self) -> Self {
        Self {
            ctrl: true,
            shift: self.shift,
            alt: self.alt,
        }
    }

    #[must_use]
    pub const fn with_shift(self) -> Self {
        Self {
            ctrl: self.ctrl,
            shift: true,
            alt: self.alt,
        }
    }

    #[must_use]
    pub const fn with_alt(self) -> Self {
        Self {
            ctrl: self.ctrl,
            shift: self.shift,
            alt: true,
        }
    }

    #[must_use]
    pub const fn gesture(self) -> NavGesture {
        if self.ctrl {
            NavGesture::Pan
        } else if self.shift {
            NavGesture::Zoom
        } else if self.alt {
            NavGesture::Roll
        } else {
            NavGesture::Orbit
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum NavGesture {
    Orbit,
    Pan,
    Roll,
    Zoom,
}

#[derive(Copy, Clone, Debug, PartialEq)]
struct Drag {
    gesture: NavGesture,
    last: ViewportPoint,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ViewportNavigator {
    drag: Option<Drag>,
    orbit: OrbitState,
}

impl ViewportNavigator {
    #[must_use]
    pub fn new() -> Self {
        Self {
            drag: None,
            orbit: OrbitState::identity(),
        }
    }

    #[must_use]
    pub fn orbit_rotation(&self) -> AxisAngle {
        self.orbit.rotation()
    }

    #[must_use]
    pub fn is_dragging(&self) -> bool {
        self.drag.is_some()
    }

    pub fn begin_drag(&mut self, gesture: NavGesture, cursor: ViewportPoint) {
        self.drag = Some(Drag {
            gesture,
            last: cursor,
        });
    }

    pub fn end_drag(&mut self) {
        self.drag = None;
    }

    pub fn drag_to(
        &mut self,
        cursor: ViewportPoint,
        camera: Camera3,
        extent: ViewportExtent,
    ) -> Result<Camera3> {
        let Some(drag) = self.drag else {
            return Ok(camera);
        };
        let next = match drag.gesture {
            NavGesture::Orbit => self.orbit_step(camera, extent, cursor, drag.last)?,
            NavGesture::Pan => pan_pixels(camera, extent, drag.last, cursor)?,
            NavGesture::Roll => roll_about_view(camera, extent, cursor, drag.last)?,
            NavGesture::Zoom => {
                let factor = ZoomFactor::new(ZOOM_DRAG_PER_PIXEL.powf(drag.last.y() - cursor.y()))?;
                zoom_about_pixel(camera, extent, cursor, factor)?
            }
        };
        self.drag = Some(Drag {
            gesture: drag.gesture,
            last: cursor,
        });
        Ok(next)
    }

    pub fn orbit_pixels(
        &mut self,
        camera: Camera3,
        extent: ViewportExtent,
        dx: f64,
        dy: f64,
    ) -> Result<Camera3> {
        let cx = f64::from(extent.width().value()) * 0.5;
        let cy = f64::from(extent.height().value()) * 0.5;
        let from = ViewportPoint::new(cx, cy)?;
        let to = ViewportPoint::new(cx + dx, cy + dy)?;
        self.orbit_step(camera, extent, to, from)
    }

    fn orbit_step(
        &mut self,
        camera: Camera3,
        extent: ViewportExtent,
        from: ViewportPoint,
        to: ViewportPoint,
    ) -> Result<Camera3> {
        let delta = arcball_rotation(camera, extent, from, to)?;
        let oriented = orbit_about_point(camera, camera.target(), delta)?;
        self.orbit = self.orbit.rotated(delta);
        Ok(oriented)
    }
}

impl Default for ViewportNavigator {
    fn default() -> Self {
        Self::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::camera::ViewportPx;
    use bone_types::{Point3, Projection, UnitVec3};
    use uom::si::f64::Length;
    use uom::si::length::millimeter;

    fn extent() -> ViewportExtent {
        ViewportExtent::square(ViewportPx::new(256))
    }

    fn camera() -> Camera3 {
        let Ok(projection) = Projection::orthographic(Length::new::<millimeter>(2.0)) else {
            panic!("half height is positive");
        };
        let Ok(camera) = Camera3::new(
            Point3::from_mm(0.0, -10.0, 0.0),
            Point3::origin(),
            UnitVec3::z_axis(),
            projection,
        ) else {
            panic!("camera is non-degenerate");
        };
        camera
    }

    fn vp(x: f64, y: f64) -> ViewportPoint {
        let Ok(point) = ViewportPoint::new(x, y) else {
            panic!("pixel coordinates are finite");
        };
        point
    }

    fn close(a: Point3, b: Point3, tol: f64) -> bool {
        let (ax, ay, az) = a.coords_mm();
        let (bx, by, bz) = b.coords_mm();
        (ax - bx).abs() < tol && (ay - by).abs() < tol && (az - bz).abs() < tol
    }

    #[test]
    fn modifiers_select_the_gesture() {
        assert_eq!(DragModifiers::NONE.gesture(), NavGesture::Orbit);
        assert_eq!(DragModifiers::NONE.with_ctrl().gesture(), NavGesture::Pan);
        assert_eq!(DragModifiers::NONE.with_shift().gesture(), NavGesture::Zoom);
        assert_eq!(DragModifiers::NONE.with_alt().gesture(), NavGesture::Roll);
    }

    #[test]
    fn ctrl_outranks_shift_outranks_alt() {
        assert_eq!(
            DragModifiers::NONE.with_ctrl().with_shift().gesture(),
            NavGesture::Pan,
            "ctrl pans even when shift is also held"
        );
        assert_eq!(
            DragModifiers::NONE.with_shift().with_alt().gesture(),
            NavGesture::Zoom,
            "shift zooms even when alt is also held"
        );
        assert_eq!(
            DragModifiers::NONE.with_alt().with_ctrl().gesture(),
            NavGesture::Pan,
            "the precedence is independent of the order the modifiers were set"
        );
    }

    #[test]
    fn drag_without_a_begin_is_a_no_op() {
        let mut nav = ViewportNavigator::new();
        assert!(!nav.is_dragging());
        let Ok(after) = nav.drag_to(vp(10.0, 10.0), camera(), extent()) else {
            panic!("an idle navigator passes the camera through");
        };
        assert_eq!(after, camera());
    }

    #[test]
    fn orbit_drag_holds_the_target_and_accumulates_rotation() {
        let mut nav = ViewportNavigator::new();
        nav.begin_drag(NavGesture::Orbit, vp(128.0, 128.0));
        assert!(nav.is_dragging());
        let Ok(orbited) = nav.drag_to(vp(190.0, 128.0), camera(), extent()) else {
            panic!("an orbit drag transforms the camera");
        };
        assert!(
            close(orbited.target(), camera().target(), 1e-9),
            "the orbit pivots on the target, so it cannot move"
        );
        assert_ne!(
            orbited.eye(),
            camera().eye(),
            "a horizontal orbit drag must move the eye"
        );
        let rotated = nav.orbit_rotation().angle().get::<uom::si::angle::radian>();
        assert!(
            rotated.abs() > 1e-3,
            "the orbit state accumulates the drag rotation: {rotated}"
        );
    }

    #[test]
    fn orbit_drag_right_pulls_the_grabbed_model_with_the_cursor() {
        let mut nav = ViewportNavigator::new();
        nav.begin_drag(NavGesture::Orbit, vp(128.0, 128.0));
        let Ok(orbited) = nav.drag_to(vp(190.0, 128.0), camera(), extent()) else {
            panic!("an orbit drag transforms the camera");
        };
        let (ex, _, _) = orbited.eye().coords_mm();
        assert!(
            ex < 0.0,
            "dragging right swings the eye to -x so the grabbed face follows the cursor like pan: {ex}"
        );
    }

    #[test]
    fn pan_drag_keeps_the_grabbed_point_under_the_cursor() {
        let mut nav = ViewportNavigator::new();
        let from = vp(100.0, 110.0);
        let to = vp(160.0, 140.0);
        let Ok(grabbed) = crate::camera3::world_on_focal_plane(camera(), extent(), from) else {
            panic!("the grabbed pixel has a focal point");
        };
        nav.begin_drag(NavGesture::Pan, from);
        let Ok(panned) = nav.drag_to(to, camera(), extent()) else {
            panic!("a pan drag transforms the camera");
        };
        let Ok(landed) = crate::camera3::world_on_focal_plane(panned, extent(), to) else {
            panic!("the released pixel has a focal point");
        };
        assert!(
            close(grabbed, landed, 1e-6),
            "the grabbed world point should sit under the release cursor"
        );
    }

    #[test]
    fn roll_drag_spins_up_about_the_view_without_moving_the_eye() {
        let mut nav = ViewportNavigator::new();
        nav.begin_drag(NavGesture::Roll, vp(200.0, 128.0));
        let Ok(rolled) = nav.drag_to(vp(128.0, 200.0), camera(), extent()) else {
            panic!("a roll drag transforms the camera");
        };
        assert!(
            close(rolled.eye(), camera().eye(), 1e-9)
                && close(rolled.target(), camera().target(), 1e-9),
            "a roll keeps the eye and target fixed"
        );
        assert_ne!(rolled.up(), camera().up(), "a roll reorients the up vector");
    }

    #[test]
    fn scroll_orbit_holds_the_target_and_accumulates_rotation() {
        let mut nav = ViewportNavigator::new();
        let Ok(orbited) = nav.orbit_pixels(camera(), extent(), 60.0, 0.0) else {
            panic!("a scroll delta orbits the camera");
        };
        assert!(
            close(orbited.target(), camera().target(), 1e-9),
            "a scroll orbit pivots on the target"
        );
        assert_ne!(
            orbited.eye(),
            camera().eye(),
            "a horizontal scroll orbit moves the eye"
        );
        let rotated = nav.orbit_rotation().angle().get::<uom::si::angle::radian>();
        assert!(
            rotated.abs() > 1e-3,
            "a scroll orbit accumulates rotation like a drag: {rotated}"
        );
    }

    #[test]
    fn zoom_drag_down_enlarges_the_orthographic_view() {
        use bone_types::ProjectionKind;
        let mut nav = ViewportNavigator::new();
        nav.begin_drag(NavGesture::Zoom, vp(128.0, 100.0));
        let Ok(zoomed) = nav.drag_to(vp(128.0, 160.0), camera(), extent()) else {
            panic!("a zoom drag transforms the camera");
        };
        let ProjectionKind::Orthographic { half_height } = zoomed.projection().kind() else {
            panic!("the seed camera is orthographic");
        };
        assert!(
            half_height.get::<uom::si::length::millimeter>() > 2.0,
            "dragging the zoom gesture downward zooms out, growing the half height"
        );
    }

    #[test]
    fn end_drag_clears_the_active_gesture() {
        let mut nav = ViewportNavigator::new();
        nav.begin_drag(NavGesture::Orbit, vp(40.0, 40.0));
        assert!(nav.is_dragging());
        nav.end_drag();
        assert!(
            !nav.is_dragging(),
            "losing focus or releasing the button drops the in-flight drag"
        );
        let Ok(after) = nav.drag_to(vp(80.0, 80.0), camera(), extent()) else {
            panic!("a cleared navigator passes the camera through");
        };
        assert_eq!(
            after,
            camera(),
            "a move after the drag clears must not orbit"
        );
    }
}
