use crate::core::interpolation::Interpolable;
use crate::error::{AnimationError, Result};
use glam::{Quat, Vec3};
use std::collections::HashMap;

/// Interpolation mode for keyframe sampling
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InterpolationMode {
    /// Linear interpolation (default for most properties)
    Linear,
    /// Step (hold first value until next keyframe)
    Step,
    /// Cubic spline (requires tangent data) - Post-MVP
    CubicSpline,
}

impl Default for InterpolationMode {
    fn default() -> Self {
        Self::Linear
    }
}

/// Single keyframe in an animation track
#[derive(Debug, Clone, Copy)]
pub struct Keyframe<T: Copy> {
    /// Time in seconds from animation start
    pub time: f32,
    /// Value at this keyframe
    pub value: T,
    /// How to interpolate to next keyframe
    pub interpolation: InterpolationMode,
}

/// Transform animation track for a single bone
#[derive(Debug, Clone, Default)]
pub struct TransformTrack {
    pub positions: Vec<Keyframe<Vec3>>,
    pub rotations: Vec<Keyframe<Quat>>,
    pub scales: Vec<Keyframe<Vec3>>,
}

impl TransformTrack {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn sample(&self, time: f32) -> (Vec3, Quat, Vec3) {
        let position = Self::sample_channel(&self.positions, time, Vec3::ZERO);
        let rotation = Self::sample_channel(&self.rotations, time, Quat::IDENTITY);
        let scale = Self::sample_channel(&self.scales, time, Vec3::ONE);

        (position, rotation, scale)
    }

    fn sample_channel<T: Interpolable>(keyframes: &[Keyframe<T>], time: f32, default: T) -> T {
        if keyframes.is_empty() {
            return default;
        }

        match keyframes.binary_search_by(|k| {
            k.time
                .partial_cmp(&time)
                .unwrap_or(std::cmp::Ordering::Equal)
        }) {
            Ok(idx) => keyframes[idx].value,
            Err(idx) => {
                if idx == 0 {
                    keyframes[0].value
                } else if idx >= keyframes.len() {
                    keyframes[keyframes.len() - 1].value
                } else {
                    let k0 = &keyframes[idx - 1];
                    let k1 = &keyframes[idx];
                    let t = (time - k0.time) / (k1.time - k0.time);

                    match k0.interpolation {
                        InterpolationMode::Linear => k0.value.lerp(k1.value, t),
                        InterpolationMode::Step => k0.value,
                        InterpolationMode::CubicSpline => {
                            // TODO: Phase 4
                            k0.value.lerp(k1.value, t)
                        }
                    }
                }
            }
        }
    }

    pub fn validate(&self) -> Result<()> {
        Self::validate_channel(&self.positions, "Positions")?;
        Self::validate_channel(&self.rotations, "Rotations")?;
        Self::validate_channel(&self.scales, "Scales")?;
        Ok(())
    }

    fn validate_channel<T: Copy>(keyframes: &[Keyframe<T>], name: &str) -> Result<()> {
        for window in keyframes.windows(2) {
            if window[0].time > window[1].time {
                return Err(AnimationError::UnsortedKeyframes(name.to_string()));
            }
        }
        Ok(())
    }
}

/// Complete animation clip
#[derive(Debug, Clone)]
pub struct AnimationClip {
    pub name: String,
    pub duration: f32,
    pub bone_tracks: HashMap<String, TransformTrack>,
}

impl AnimationClip {
    pub fn new(
        name: String,
        duration: f32,
        bone_tracks: HashMap<String, TransformTrack>,
    ) -> Result<Self> {
        if duration <= 0.0 {
            return Err(AnimationError::InvalidDuration(duration));
        }

        for (bone_name, track) in &bone_tracks {
            track
                .validate()
                .map_err(|_| AnimationError::UnsortedKeyframes(bone_name.clone()))?;
        }

        Ok(Self {
            name,
            duration,
            bone_tracks,
        })
    }

    pub fn sample_bone(&self, bone_name: &str, time: f32) -> Option<(Vec3, Quat, Vec3)> {
        let wrapped_time = time.rem_euclid(self.duration);
        self.bone_tracks
            .get(bone_name)
            .map(|track| track.sample(wrapped_time))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use approx::assert_abs_diff_eq;

    #[test]
    fn transform_track_samples_correctly() {
        let mut track = TransformTrack::new();
        track.positions = vec![
            Keyframe {
                time: 0.0,
                value: Vec3::ZERO,
                interpolation: InterpolationMode::Linear,
            },
            Keyframe {
                time: 1.0,
                value: Vec3::ONE,
                interpolation: InterpolationMode::Linear,
            },
        ];

        let (pos, _, _) = track.sample(0.5);
        assert_abs_diff_eq!(pos, Vec3::splat(0.5), epsilon = 0.001);
    }

    #[test]
    fn clip_rejects_unsorted_keyframes() {
        let mut bone_tracks = HashMap::new();
        let mut track = TransformTrack::new();

        // Add keyframes out of order
        track.positions = vec![
            Keyframe {
                time: 1.0,
                value: Vec3::ONE,
                interpolation: InterpolationMode::Linear,
            },
            Keyframe {
                time: 0.0,
                value: Vec3::ZERO,
                interpolation: InterpolationMode::Linear,
            },
        ];

        bone_tracks.insert("Bone".to_string(), track);

        let result = AnimationClip::new("Test".to_string(), 1.0, bone_tracks);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            AnimationError::UnsortedKeyframes(_)
        ));
    }

    #[test]
    fn clip_accepts_empty_tracks() {
        let mut bone_tracks = HashMap::new();
        let track = TransformTrack::new(); // Empty track

        bone_tracks.insert("Bone".to_string(), track);

        let result = AnimationClip::new("Test".to_string(), 1.0, bone_tracks);
        assert!(result.is_ok());
    }

    #[test]
    fn clip_rejects_invalid_duration() {
        let bone_tracks = HashMap::new();

        let result = AnimationClip::new("Test".to_string(), 0.0, bone_tracks);
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            AnimationError::InvalidDuration(_)
        ));

        let bone_tracks2 = HashMap::new();
        let result = AnimationClip::new("Test".to_string(), -1.0, bone_tracks2);
        assert!(result.is_err());
    }

    #[test]
    fn track_samples_before_first_keyframe() {
        let mut track = TransformTrack::new();
        track.positions = vec![Keyframe {
            time: 1.0,
            value: Vec3::ONE,
            interpolation: InterpolationMode::Linear,
        }];

        let (pos, _, _) = track.sample(0.0);

        // Should return first keyframe value
        assert_eq!(pos, Vec3::ONE);
    }

    #[test]
    fn track_samples_after_last_keyframe() {
        let mut track = TransformTrack::new();
        track.positions = vec![Keyframe {
            time: 0.0,
            value: Vec3::ZERO,
            interpolation: InterpolationMode::Linear,
        }];

        let (pos, _, _) = track.sample(10.0);

        // Should return last keyframe value
        assert_eq!(pos, Vec3::ZERO);
    }

    #[test]
    fn track_step_interpolation() {
        let mut track = TransformTrack::new();
        track.positions = vec![
            Keyframe {
                time: 0.0,
                value: Vec3::ZERO,
                interpolation: InterpolationMode::Step,
            },
            Keyframe {
                time: 1.0,
                value: Vec3::ONE,
                interpolation: InterpolationMode::Step,
            },
        ];

        let (pos, _, _) = track.sample(0.5);

        // Step interpolation should hold first value
        assert_eq!(pos, Vec3::ZERO);
    }

    #[test]
    fn clip_sample_bone_wraps_time() {
        let mut bone_tracks = HashMap::new();
        let mut track = TransformTrack::new();
        track.positions = vec![
            Keyframe {
                time: 0.0,
                value: Vec3::ZERO,
                interpolation: InterpolationMode::Linear,
            },
            Keyframe {
                time: 1.0,
                value: Vec3::ONE,
                interpolation: InterpolationMode::Linear,
            },
        ];

        bone_tracks.insert("Bone".to_string(), track);
        let clip = AnimationClip::new("Test".to_string(), 1.0, bone_tracks).unwrap();

        // Sample at time > duration should wrap
        let (pos, _, _) = clip.sample_bone("Bone", 2.5).unwrap();

        // 2.5 % 1.0 = 0.5, should be halfway
        assert_abs_diff_eq!(pos, Vec3::splat(0.5), epsilon = 0.001);
    }

    #[test]
    fn clip_sample_nonexistent_bone() {
        let bone_tracks = HashMap::new();
        let clip = AnimationClip::new("Test".to_string(), 1.0, bone_tracks).unwrap();

        let result = clip.sample_bone("NonExistent", 0.5);
        assert!(result.is_none());
    }
}
