use core::time::Duration;

use nalgebra::{UnitQuaternion, Vector3};
use uom::si::angle::radian;
use uom::si::f64::{Angle, Length};
use uom::si::length::millimeter;

use bone_types::{Camera3, CubicEasing, Point3, Projection, ProjectionKind, Result, UnitVec3};

const SLERP_EPSILON: f64 = 1.0e-9;
const NEWTON_ITERATIONS: u32 = 8;
const BISECTION_ITERATIONS: u32 = 24;
const SOLVE_EPSILON: f64 = 1.0e-7;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct CameraTween {
    from: Camera3,
    to: Camera3,
    duration: Duration,
    easing: CubicEasing,
}

impl CameraTween {
    #[must_use]
    pub fn eased(from: Camera3, to: Camera3, duration: Duration, easing: CubicEasing) -> Self {
        Self {
            from,
            to,
            duration,
            easing,
        }
    }

    #[must_use]
    pub fn immediate(to: Camera3) -> Self {
        Self {
            from: to,
            to,
            duration: Duration::ZERO,
            easing: CubicEasing::LINEAR,
        }
    }

    #[must_use]
    pub fn to(self) -> Camera3 {
        self.to
    }

    #[must_use]
    pub fn is_done(self, elapsed: Duration) -> bool {
        elapsed >= self.duration
    }

    pub fn sample(self, elapsed: Duration) -> Result<Camera3> {
        let progress = if self.duration.is_zero() {
            1.0
        } else {
            (elapsed.as_secs_f64() / self.duration.as_secs_f64()).clamp(0.0, 1.0)
        };
        interpolate(self.from, self.to, ease(self.easing, progress))
    }
}

fn interpolate(from: Camera3, to: Camera3, t: f64) -> Result<Camera3> {
    let t = t.clamp(0.0, 1.0);
    if t <= 0.0 {
        return Ok(from);
    }
    if t >= 1.0 {
        return Ok(to);
    }
    let orientation = orientation_of(from)
        .try_slerp(&orientation_of(to), t, SLERP_EPSILON)
        .unwrap_or_else(|| orientation_of(to));
    let forward = orientation * Vector3::z();
    let up_axis = orientation * Vector3::y();
    let target = lerp_point(from.target(), to.target(), t);
    let distance = lerp(eye_distance(from), eye_distance(to), t);
    let eye = target
        + UnitVec3::new_unchecked(forward.x, forward.y, forward.z)
            .into_vec(Length::new::<millimeter>(-distance));
    let up = UnitVec3::new_unchecked(up_axis.x, up_axis.y, up_axis.z);
    Camera3::new(
        eye,
        target,
        up,
        lerp_projection(from.projection(), to.projection(), t)?,
    )
}

fn orientation_of(camera: Camera3) -> UnitQuaternion<f64> {
    let (ex, ey, ez) = camera.eye().coords_mm();
    let (tx, ty, tz) = camera.target().coords_mm();
    let (ux, uy, uz) = camera.up().components();
    let forward = Vector3::new(tx - ex, ty - ey, tz - ez);
    let up = Vector3::new(ux, uy, uz);
    UnitQuaternion::face_towards(&forward, &up)
}

fn eye_distance(camera: Camera3) -> f64 {
    (camera.target() - camera.eye()).norm_mm()
}

fn lerp(a: f64, b: f64, t: f64) -> f64 {
    a + (b - a) * t
}

fn lerp_point(a: Point3, b: Point3, t: f64) -> Point3 {
    let (ax, ay, az) = a.coords_mm();
    let (bx, by, bz) = b.coords_mm();
    Point3::from_mm(lerp(ax, bx, t), lerp(ay, by, t), lerp(az, bz, t))
}

fn lerp_mm(a: Length, b: Length, t: f64) -> Length {
    Length::new::<millimeter>(lerp(a.get::<millimeter>(), b.get::<millimeter>(), t))
}

fn lerp_projection(a: Projection, b: Projection, t: f64) -> Result<Projection> {
    match (a.kind(), b.kind()) {
        (
            ProjectionKind::Orthographic { half_height: ha },
            ProjectionKind::Orthographic { half_height: hb },
        ) => Projection::orthographic(lerp_mm(ha, hb, t)),
        (
            ProjectionKind::Perspective {
                fov: fa,
                near: na,
                far: la,
            },
            ProjectionKind::Perspective {
                fov: fb,
                near: nb,
                far: lb,
            },
        ) => {
            let fov = Angle::new::<radian>(lerp(fa.get::<radian>(), fb.get::<radian>(), t));
            Projection::perspective(fov, lerp_mm(na, nb, t), lerp_mm(la, lb, t))
        }
        _ => Ok(if t < 0.5 { a } else { b }),
    }
}

fn ease(control: CubicEasing, x: f64) -> f64 {
    let x = x.clamp(0.0, 1.0);
    if x <= 0.0 || x >= 1.0 {
        return x;
    }
    bezier_axis(
        control.y1(),
        control.y2(),
        solve_for_t(control.x1(), control.x2(), x),
    )
}

fn bezier_axis(p1: f64, p2: f64, s: f64) -> f64 {
    let c = 3.0 * p1;
    let b = 3.0 * (p2 - p1) - c;
    let a = 1.0 - c - b;
    ((a * s + b) * s + c) * s
}

fn bezier_slope(p1: f64, p2: f64, s: f64) -> f64 {
    let c = 3.0 * p1;
    let b = 3.0 * (p2 - p1) - c;
    let a = 1.0 - c - b;
    (3.0 * a * s + 2.0 * b) * s + c
}

fn solve_for_t(x1: f64, x2: f64, x: f64) -> f64 {
    let newton = (0..NEWTON_ITERATIONS).try_fold(x, |guess, _| {
        let error = bezier_axis(x1, x2, guess) - x;
        if error.abs() < SOLVE_EPSILON {
            return Err(guess);
        }
        let slope = bezier_slope(x1, x2, guess);
        if slope.abs() < SOLVE_EPSILON {
            return Err(guess);
        }
        Ok(guess - error / slope)
    });
    match newton {
        Err(solved) => solved,
        Ok(guess) if (0.0..=1.0).contains(&guess) && bezier_close(x1, x2, guess, x) => guess,
        Ok(_) => bisect(x1, x2, x),
    }
}

fn bezier_close(x1: f64, x2: f64, s: f64, x: f64) -> bool {
    (bezier_axis(x1, x2, s) - x).abs() < SOLVE_EPSILON
}

fn bisect(x1: f64, x2: f64, x: f64) -> f64 {
    let result = (0..BISECTION_ITERATIONS).try_fold((0.0_f64, 1.0_f64), |(low, high), _| {
        let mid = f64::midpoint(low, high);
        let value = bezier_axis(x1, x2, mid);
        if (value - x).abs() < SOLVE_EPSILON {
            return Err(mid);
        }
        if value < x {
            Ok((mid, high))
        } else {
            Ok((low, mid))
        }
    });
    match result {
        Err(mid) => mid,
        Ok((low, high)) => f64::midpoint(low, high),
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bone_types::Angle;
    use uom::si::angle::degree;

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

    fn front() -> Camera3 {
        ortho_camera(Point3::from_mm(0.0, -10.0, 0.0), UnitVec3::z_axis())
    }

    fn top() -> Camera3 {
        ortho_camera(Point3::from_mm(0.0, 0.0, 10.0), UnitVec3::y_axis())
    }

    fn bottom() -> Camera3 {
        ortho_camera(
            Point3::from_mm(0.0, 0.0, -10.0),
            UnitVec3::y_axis().reversed(),
        )
    }

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

    #[test]
    fn endpoints_reproduce_the_input_cameras() {
        let Ok(start) = interpolate(front(), top(), 0.0) else {
            panic!("t=0 interpolates");
        };
        let Ok(end) = interpolate(front(), top(), 1.0) else {
            panic!("t=1 interpolates");
        };
        assert!(
            close(start.eye(), front().eye()),
            "t=0 must be the start eye"
        );
        assert!(close(end.eye(), top().eye()), "t=1 must be the end eye");
        assert!(close(start.target(), front().target()));
        assert!(close(end.target(), top().target()));
    }

    #[test]
    fn immediate_tween_is_the_target_at_any_time() {
        let tween = CameraTween::immediate(top());
        let Ok(at_zero) = tween.sample(Duration::ZERO) else {
            panic!("immediate samples at zero");
        };
        let Ok(at_later) = tween.sample(Duration::from_secs(5)) else {
            panic!("immediate samples later");
        };
        assert!(close(at_zero.eye(), top().eye()));
        assert!(close(at_later.eye(), top().eye()));
        assert!(
            close(tween.to().eye(), top().eye()),
            "the endpoint is the target"
        );
        assert!(tween.is_done(Duration::ZERO), "a zero-length tween is done");
    }

    #[test]
    fn opposite_views_interpolate_without_panic() {
        let Ok(mid) = interpolate(top(), bottom(), 0.5) else {
            panic!("a 180-degree swing between top and bottom is well defined");
        };
        let span = (mid.eye() - mid.target()).norm_mm();
        assert!(span > 0.0, "the midpoint camera keeps eye and target apart");
    }

    #[test]
    fn midpoint_keeps_the_orbit_radius() {
        let Ok(mid) = interpolate(front(), top(), 0.5) else {
            panic!("midpoint interpolates");
        };
        let radius = (mid.eye() - mid.target()).norm_mm();
        assert!(
            (radius - 10.0).abs() < 1e-9,
            "equal-radius endpoints keep the radius across the slerp: {radius}",
        );
    }

    #[test]
    fn sample_runs_the_tween_from_start_to_finish() {
        let tween = CameraTween::eased(
            front(),
            top(),
            Duration::from_millis(180),
            CubicEasing::STANDARD,
        );
        let Ok(start) = tween.sample(Duration::ZERO) else {
            panic!("samples at start");
        };
        let Ok(finish) = tween.sample(Duration::from_millis(180)) else {
            panic!("samples at finish");
        };
        assert!(close(start.eye(), front().eye()));
        assert!(close(finish.eye(), top().eye()));
    }

    #[test]
    fn linear_easing_is_the_identity() {
        [0.0, 0.25, 0.5, 0.75, 1.0].into_iter().for_each(|x| {
            assert!(
                (ease(CubicEasing::LINEAR, x) - x).abs() < SOLVE_EPSILON,
                "linear ease passes x through within solver precision",
            );
        });
    }

    #[test]
    fn standard_easing_is_monotonic_and_pinned_at_the_ends() {
        assert!((ease(CubicEasing::STANDARD, 0.0)).abs() < 1e-9);
        assert!((ease(CubicEasing::STANDARD, 1.0) - 1.0).abs() < 1e-9);
        let samples: Vec<f64> = (0..=20)
            .map(|i| ease(CubicEasing::STANDARD, f64::from(i) / 20.0))
            .collect();
        samples.windows(2).for_each(|pair| {
            assert!(pair[1] >= pair[0] - 1e-9, "easing must not move backward");
        });
    }

    #[test]
    fn perspective_projection_interpolates_in_kind() {
        let Ok(near_proj) = Projection::perspective(
            Angle::new::<degree>(40.0),
            Length::new::<millimeter>(1.0),
            Length::new::<millimeter>(100.0),
        ) else {
            panic!("projection is valid");
        };
        let Ok(far_proj) = Projection::perspective(
            Angle::new::<degree>(60.0),
            Length::new::<millimeter>(2.0),
            Length::new::<millimeter>(200.0),
        ) else {
            panic!("projection is valid");
        };
        let Ok(mid) = lerp_projection(near_proj, far_proj, 0.5) else {
            panic!("two perspectives interpolate");
        };
        let ProjectionKind::Perspective { fov, near, far } = mid.kind() else {
            panic!("the interpolation stays perspective");
        };
        assert!((fov.get::<degree>() - 50.0).abs() < 1e-9);
        assert!((near.get::<millimeter>() - 1.5).abs() < 1e-9);
        assert!((far.get::<millimeter>() - 150.0).abs() < 1e-9);
    }
}
