use glam::{Quat, Vec3};

/// Generic linear interpolation trait
pub trait Interpolable: Copy {
    /// Linearly interpolate between self and other at parameter t ∈ [0,1]
    fn lerp(self, other: Self, t: f32) -> Self;
}

impl Interpolable for f32 {
    #[inline]
    fn lerp(self, other: Self, t: f32) -> Self {
        self + (other - self) * t
    }
}

impl Interpolable for Vec3 {
    #[inline]
    fn lerp(self, other: Self, t: f32) -> Self {
        // Use glam's optimized lerp
        self.lerp(other, t)
    }
}

impl Interpolable for Quat {
    /// Spherical linear interpolation (SLERP)
    ///
    /// # Why SLERP over NLERP?
    /// - SLERP maintains constant angular velocity
    /// - NLERP causes speed variation (easing artifacts)
    /// - Critical for smooth character animation
    fn lerp(self, other: Self, t: f32) -> Self {
        self.slerp(other, t)
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use proptest::prelude::*;

    #[test]
    fn quat_slerp_shortest_path() {
        let q1 = Quat::IDENTITY;
        let q2 = Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI);

        let mid = q1.lerp(q2, 0.5);
        let angle = q1.angle_between(mid);

        assert!((angle - std::f32::consts::PI / 2.0).abs() < 0.01);
    }

    #[test]
    fn quat_slerp_maintains_unit_length() {
        let q1 = Quat::from_axis_angle(Vec3::X, 0.5);
        let q2 = Quat::from_axis_angle(Vec3::Y, 1.0);

        for i in 0..=10 {
            let t = i as f32 / 10.0;
            let result = q1.lerp(q2, t);
            assert!((result.length() - 1.0).abs() < 0.0001);
        }
    }

    proptest! {
        #[test]
        fn slerp_is_deterministic(
            t in 0.0f32..=1.0f32,
            angle1 in -std::f32::consts::PI..std::f32::consts::PI,
            angle2 in -std::f32::consts::PI..std::f32::consts::PI,
        ) {
            let q1 = Quat::from_axis_angle(Vec3::Y, angle1);
            let q2 = Quat::from_axis_angle(Vec3::Y, angle2);

            let result1 = q1.lerp(q2, t);
            let result2 = q1.lerp(q2, t);

            assert!((result1.dot(result2).abs() - 1.0).abs() < 0.0001);
        }
    }

    #[test]
    fn quat_slerp_identity_stays_identity() {
        let q = Quat::IDENTITY;
        let result = q.lerp(q, 0.5);

        assert_eq!(result, Quat::IDENTITY);
    }

    #[test]
    fn quat_slerp_180_degree_shortest_path() {
        let q1 = Quat::from_axis_angle(Vec3::Y, 0.0);
        let q2 = Quat::from_axis_angle(Vec3::Y, std::f32::consts::PI);

        let mid = q1.lerp(q2, 0.5);
        let angle = q1.angle_between(mid);

        // Should take shortest path (90 degrees)
        assert!((angle - std::f32::consts::PI / 2.0).abs() < 0.01);
    }

    #[test]
    fn quat_slerp_boundary_t_zero() {
        let q1 = Quat::from_axis_angle(Vec3::X, 0.5);
        let q2 = Quat::from_axis_angle(Vec3::Y, 1.0);

        let result = q1.lerp(q2, 0.0);

        // At t=0, should return q1
        assert!((result.dot(q1).abs() - 1.0).abs() < 0.0001);
    }

    #[test]
    fn quat_slerp_boundary_t_one() {
        let q1 = Quat::from_axis_angle(Vec3::X, 0.5);
        let q2 = Quat::from_axis_angle(Vec3::Y, 1.0);

        let result = q1.lerp(q2, 1.0);

        // At t=1, should return q2
        assert!((result.dot(q2).abs() - 1.0).abs() < 0.0001);
    }

    #[test]
    fn vec3_lerp_boundary_values() {
        let v1 = Vec3::new(0.0, 0.0, 0.0);
        let v2 = Vec3::new(10.0, 10.0, 10.0);

        assert_eq!(v1.lerp(v2, 0.0), v1);
        assert_eq!(v1.lerp(v2, 1.0), v2);
        assert_eq!(v1.lerp(v2, 0.5), Vec3::new(5.0, 5.0, 5.0));
    }

    #[test]
    fn vec3_lerp_component_wise() {
        let v1 = Vec3::new(1.0, 2.0, 3.0);
        let v2 = Vec3::new(5.0, 6.0, 7.0);

        let result = v1.lerp(v2, 0.25);

        assert!((result.x - 2.0).abs() < 0.0001);
        assert!((result.y - 3.0).abs() < 0.0001);
        assert!((result.z - 4.0).abs() < 0.0001);
    }

    #[test]
    fn f32_lerp_precision() {
        let a = 0.0f32;
        let b = 1.0f32;

        for i in 0..=100 {
            let t = i as f32 / 100.0;
            let result = a.lerp(b, t);
            assert!((result - t).abs() < 0.0001);
        }
    }
}
