use crate::core::{AnimationClip, InterpolationMode, Keyframe, Skeleton, TransformTrack};
use crate::error::{AnimationError, Result};
use glam::{Mat4, Quat, Vec3};
use std::collections::HashMap;
use std::path::Path;

/// Load skeleton and animation clips from a GLTF file
///
/// # Arguments
/// * `path` - Path to the .glb or .gltf file
///
/// # Returns
/// Tuple of (Skeleton, Vec<AnimationClip>) on success
///
/// # Errors
/// Returns error if:
/// - File doesn't exist or can't be read
/// - GLTF format is invalid
/// - No skeleton (skin) found in file
/// - Animation data is malformed
pub fn load_gltf(path: &str) -> Result<(Skeleton, Vec<AnimationClip>)> {
    let path_obj = Path::new(path);
    let (document, buffers, _images) =
        gltf::import(path_obj).map_err(|e| AnimationError::AssetLoadError {
            path: path.to_string(),
            source: Box::new(e),
        })?;

    let skeleton = load_skeleton(&document, &buffers)?;
    let clips = load_animations(&document, &skeleton, &buffers)?;

    Ok((skeleton, clips))
}

fn load_skeleton(document: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Result<Skeleton> {
    let skin = document.skins().next().ok_or_else(|| {
        AnimationError::InvalidFormat("No skeleton (skin) found in GLTF".to_string())
    })?;

    let mut skeleton = Skeleton::new();

    // Read inverse bind matrices
    let inverse_bind_matrices = if let Some(accessor) = skin.inverse_bind_matrices() {
        read_mat4_accessor(&accessor, buffers)
    } else {
        vec![Mat4::IDENTITY; skin.joints().count()]
    };

    // Build bone hierarchy
    let joints: Vec<_> = skin.joints().collect();

    for (joint_idx, joint) in joints.iter().enumerate() {
        let parent_index = joints
            .iter()
            .position(|j| j.children().any(|child| child.index() == joint.index()))
            .and_then(|idx| if idx < joint_idx { Some(idx) } else { None });

        let inverse_bind = inverse_bind_matrices
            .get(joint_idx)
            .copied()
            .unwrap_or(Mat4::IDENTITY);

        skeleton.add_bone(
            joint
                .name()
                .unwrap_or(&format!("Joint_{joint_idx}"))
                .to_string(),
            parent_index,
            inverse_bind,
        );
    }

    skeleton.validate()?;
    Ok(skeleton)
}

fn load_animations(
    document: &gltf::Document,
    _skeleton: &Skeleton,
    buffers: &[gltf::buffer::Data],
) -> Result<Vec<AnimationClip>> {
    let mut clips = Vec::new();

    for (anim_idx, animation) in document.animations().enumerate() {
        let name = animation
            .name()
            .unwrap_or(&format!("Animation_{anim_idx}"))
            .to_string();

        let mut bone_tracks: HashMap<String, TransformTrack> = HashMap::new();
        let mut max_time = 0.0f32;

        for channel in animation.channels() {
            let target = channel.target();
            let reader = channel.reader(|buffer| Some(&buffers[buffer.index()]));

            let bone_name = target
                .node()
                .name()
                .ok_or_else(|| {
                    AnimationError::InvalidFormat("Animation target node has no name".to_string())
                })?
                .to_string();

            let track = bone_tracks.entry(bone_name.clone()).or_default();

            // Read input times
            let times: Vec<f32> = if let Some(inputs) = reader.read_inputs() {
                inputs.collect()
            } else {
                continue;
            };

            if let Some(last_time) = times.last() {
                max_time = max_time.max(*last_time);
            }

            // Read output values based on property
            match channel.target().property() {
                gltf::animation::Property::Translation => {
                    if let Some(gltf::animation::util::ReadOutputs::Translations(translations)) =
                        reader.read_outputs()
                    {
                        for (time, translation) in times.iter().zip(translations) {
                            track.positions.push(Keyframe {
                                time: *time,
                                value: Vec3::from(translation),
                                interpolation: InterpolationMode::Linear,
                            });
                        }
                    }
                }
                gltf::animation::Property::Rotation => {
                    if let Some(gltf::animation::util::ReadOutputs::Rotations(rotations)) =
                        reader.read_outputs()
                    {
                        for (time, rotation) in times.iter().zip(rotations.into_f32()) {
                            track.rotations.push(Keyframe {
                                time: *time,
                                value: Quat::from_array(rotation),
                                interpolation: InterpolationMode::Linear,
                            });
                        }
                    }
                }
                gltf::animation::Property::Scale => {
                    if let Some(gltf::animation::util::ReadOutputs::Scales(scales)) =
                        reader.read_outputs()
                    {
                        for (time, scale) in times.iter().zip(scales) {
                            track.scales.push(Keyframe {
                                time: *time,
                                value: Vec3::from(scale),
                                interpolation: InterpolationMode::Linear,
                            });
                        }
                    }
                }
                _ => {}
            }
        }

        let duration = if max_time > 0.0 { max_time } else { 1.0 };
        let clip = AnimationClip::new(name, duration, bone_tracks)?;
        clips.push(clip);
    }

    Ok(clips)
}

fn read_mat4_accessor(accessor: &gltf::Accessor, buffers: &[gltf::buffer::Data]) -> Vec<Mat4> {
    let mut matrices = Vec::new();

    if let Some(view) = accessor.view() {
        let buffer = &buffers[view.buffer().index()];
        let start = view.offset() + accessor.offset();
        let stride = view.stride().unwrap_or(16 * std::mem::size_of::<f32>());

        for i in 0..accessor.count() {
            let offset = start + i * stride;
            let data = &buffer[offset..offset + 16 * std::mem::size_of::<f32>()];

            let mut floats = [0.0f32; 16];
            for (j, chunk) in data.chunks_exact(4).enumerate() {
                floats[j] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]);
            }

            matrices.push(Mat4::from_cols_array(&floats));
        }
    }

    matrices
}
