use crate::error::{AnimationError, Result};
use glam::Mat4;
use std::collections::HashMap;

/// Single bone in a skeleton
#[derive(Debug, Clone)]
pub struct Bone {
    pub name: String,
    pub parent_index: Option<usize>,
    pub inverse_bind_pose: Mat4,
}

/// Complete skeletal rig
#[derive(Debug, Clone)]
pub struct Skeleton {
    pub bones: Vec<Bone>,
    pub bone_name_to_index: HashMap<String, usize>,
}

impl Skeleton {
    pub fn new() -> Self {
        Self {
            bones: Vec::new(),
            bone_name_to_index: HashMap::new(),
        }
    }

    pub fn add_bone(
        &mut self,
        name: String,
        parent_index: Option<usize>,
        inverse_bind_pose: Mat4,
    ) -> usize {
        let index = self.bones.len();

        self.bone_name_to_index.insert(name.clone(), index);
        self.bones.push(Bone {
            name,
            parent_index,
            inverse_bind_pose,
        });

        index
    }

    pub fn validate(&self) -> Result<()> {
        if self.bones.is_empty() {
            return Err(AnimationError::EmptySkeleton);
        }

        for (child_idx, bone) in self.bones.iter().enumerate() {
            if let Some(parent_idx) = bone.parent_index {
                if parent_idx >= self.bones.len() {
                    return Err(AnimationError::BoneIndexOutOfBounds(
                        parent_idx,
                        self.bones.len(),
                    ));
                }

                if parent_idx >= child_idx {
                    return Err(AnimationError::InvalidTopology {
                        parent: parent_idx,
                        child: child_idx,
                    });
                }
            }
        }

        Ok(())
    }

    pub fn get_bone_index(&self, name: &str) -> Result<usize> {
        self.bone_name_to_index
            .get(name)
            .copied()
            .ok_or_else(|| AnimationError::BoneNotFound {
                bone: name.to_string(),
            })
    }

    pub fn calculate_world_matrix(
        &self,
        bone_index: usize,
        local_transforms: &[(glam::Vec3, glam::Quat, glam::Vec3)],
    ) -> Mat4 {
        let (pos, rot, scale) = local_transforms[bone_index];

        let local_mat = Mat4::from_scale_rotation_translation(scale, rot, pos);

        if let Some(parent_idx) = self.bones[bone_index].parent_index {
            let parent_world = self.calculate_world_matrix(parent_idx, local_transforms);
            parent_world * local_mat
        } else {
            local_mat
        }
    }
}

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

#[cfg(test)]
mod tests {
    use super::*;
    use glam::{Quat, Vec3};

    #[test]
    fn valid_skeleton_passes() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);
        skeleton.add_bone("Child1".to_string(), Some(0), Mat4::IDENTITY);

        assert!(skeleton.validate().is_ok());
    }

    #[test]
    fn empty_skeleton_fails() {
        let skeleton = Skeleton::new();

        let result = skeleton.validate();
        assert!(result.is_err());
        assert!(matches!(result.unwrap_err(), AnimationError::EmptySkeleton));
    }

    #[test]
    fn invalid_topology_parent_after_child() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);
        skeleton.add_bone("Child".to_string(), Some(0), Mat4::IDENTITY);

        // Manually break topology: make parent point to child
        skeleton.bones[0].parent_index = Some(1);

        let result = skeleton.validate();
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            AnimationError::InvalidTopology { .. }
        ));
    }

    #[test]
    fn parent_index_out_of_bounds() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);

        // Manually set invalid parent index
        skeleton.bones[0].parent_index = Some(999);

        let result = skeleton.validate();
        assert!(result.is_err());
        assert!(matches!(
            result.unwrap_err(),
            AnimationError::BoneIndexOutOfBounds(_, _)
        ));
    }

    #[test]
    fn get_bone_index_by_name() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);
        skeleton.add_bone("Hips".to_string(), Some(0), Mat4::IDENTITY);

        assert_eq!(skeleton.get_bone_index("Root").unwrap(), 0);
        assert_eq!(skeleton.get_bone_index("Hips").unwrap(), 1);
        assert!(skeleton.get_bone_index("NonExistent").is_err());
    }

    #[test]
    fn world_matrix_calculation() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);
        skeleton.add_bone("Child".to_string(), Some(0), Mat4::IDENTITY);

        // Root at (0, 0, 0), Child at (1, 0, 0) relative to root
        let local_transforms = vec![
            (Vec3::ZERO, Quat::IDENTITY, Vec3::ONE),
            (Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, Vec3::ONE),
        ];

        let root_world = skeleton.calculate_world_matrix(0, &local_transforms);
        let child_world = skeleton.calculate_world_matrix(1, &local_transforms);

        // Root should be at origin
        assert_eq!(root_world.w_axis.truncate(), Vec3::ZERO);

        // Child should be at (1, 0, 0) in world space
        assert_eq!(child_world.w_axis.truncate(), Vec3::new(1.0, 0.0, 0.0));
    }

    #[test]
    fn hierarchical_transform_chain() {
        let mut skeleton = Skeleton::new();
        skeleton.add_bone("Root".to_string(), None, Mat4::IDENTITY);
        skeleton.add_bone("Child1".to_string(), Some(0), Mat4::IDENTITY);
        skeleton.add_bone("Child2".to_string(), Some(1), Mat4::IDENTITY);

        // Each bone offset by (1, 0, 0)
        let local_transforms = vec![
            (Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, Vec3::ONE),
            (Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, Vec3::ONE),
            (Vec3::new(1.0, 0.0, 0.0), Quat::IDENTITY, Vec3::ONE),
        ];

        let child2_world = skeleton.calculate_world_matrix(2, &local_transforms);

        // Child2 should be at (3, 0, 0) = sum of all offsets
        assert!((child2_world.w_axis.x - 3.0).abs() < 0.001);
    }
}
