//! Dedicated scene hydration for rigged and skinned model geometry.

use std::fs;

use bevy::gltf::GltfAssetLabel;
use bevy::prelude::WorldAssetRoot;
use bevy::prelude::*;
use bevy::world_serialization::WorldInstanceReady;
use serde::Deserialize;

use crate::{
    asset_server_path, authoring_component_active, AuthoringComponentStates,
    HydratedRendererMaterialBinding, InspectorOrder, LevelObject, SkinnedMeshRenderer,
    ANIMATION_ARTIFACT_DIR, COMPONENT_SKINNED_MESH_RENDERER,
};

use super::static_meshes::{
    renderer_error_material, resolve_renderer_material_handle, StaticMeshArtifactCache,
};

/// Runtime hierarchy root generated for one authored [`SkinnedMeshRenderer`].
///
/// The loaded world contains Bevy's actual skinned-mesh bindings and joints. Keeping this marker
/// distinct from `HydratedModelRoot` prevents animation systems from binding to generic imported
/// scene instances.
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct HydratedSkinnedMeshRoot {
    pub owner: Entity,
}

#[derive(Component, Debug, Clone, PartialEq, Eq)]
pub(super) struct HydratedSkinnedMeshSource {
    asset_id: String,
    path: String,
    scene_index: usize,
}

impl From<&SkinnedMeshRenderer> for HydratedSkinnedMeshSource {
    fn from(renderer: &SkinnedMeshRenderer) -> Self {
        Self {
            asset_id: renderer.asset_id.clone(),
            path: renderer.path.clone(),
            scene_index: renderer.scene_index,
        }
    }
}

#[derive(Component, Default)]
pub(super) struct SkinnedMaterialsDirty;

#[derive(Deserialize)]
struct AnimationArtifactLookup {
    source: AnimationSourceLookup,
}

#[derive(Deserialize)]
struct AnimationSourceLookup {
    path: String,
}

pub fn skinned_mesh_manifest_path(asset_id: &str) -> String {
    format!("{ANIMATION_ARTIFACT_DIR}/{asset_id}.animation.ron")
}

/// Resolves the current model source from the animation artifact rather than the static-mesh
/// artifact. The authored cached path remains the fallback when a legacy/unindexed asset has no
/// generated animation metadata.
pub fn skinned_mesh_source_path(asset_id: &str) -> Result<String, String> {
    let path = skinned_mesh_manifest_path(asset_id);
    let text = fs::read_to_string(&path)
        .map_err(|error| format!("could not read skinned mesh artifact {path}: {error}"))?;
    let artifact: AnimationArtifactLookup = ron::from_str(&text)
        .map_err(|error| format!("could not parse skinned mesh artifact {path}: {error}"))?;
    Ok(artifact.source.path)
}

#[allow(clippy::type_complexity)]
pub(super) fn hydrate_skinned_mesh_renderers(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    renderers: Query<
        (
            Entity,
            &SkinnedMeshRenderer,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ),
        (
            With<LevelObject>,
            Or<(
                Added<SkinnedMeshRenderer>,
                Changed<SkinnedMeshRenderer>,
                Changed<AuthoringComponentStates>,
                Changed<InspectorOrder>,
            )>,
        ),
    >,
    generated_roots: Query<(
        Entity,
        &HydratedSkinnedMeshRoot,
        Option<&HydratedSkinnedMeshSource>,
    )>,
) {
    for (entity, renderer, states, legacy_order) in &renderers {
        if !authoring_component_active(states, legacy_order, COMPONENT_SKINNED_MESH_RENDERER) {
            for (root, generated, _) in &generated_roots {
                if generated.owner == entity {
                    commands.entity(root).despawn();
                }
            }
            continue;
        }
        let requested_source = HydratedSkinnedMeshSource::from(renderer);
        let mut matching_root = None;
        for (root, generated, source) in &generated_roots {
            if generated.owner == entity {
                if source == Some(&requested_source) {
                    matching_root = Some(root);
                } else {
                    commands.entity(root).despawn();
                }
            }
        }
        if let Some(root) = matching_root {
            commands.entity(root).insert(SkinnedMaterialsDirty);
            continue;
        }

        let resolved_path = if renderer.asset_id.trim().is_empty() {
            renderer.path.clone()
        } else {
            match skinned_mesh_source_path(&renderer.asset_id) {
                Ok(path) => path,
                Err(error) => {
                    warn!(
                        "SkinnedMeshRenderer {entity} could not resolve stable asset {}: {error}; falling back to cached path {}",
                        renderer.asset_id, renderer.path
                    );
                    renderer.path.clone()
                }
            }
        };
        let path = asset_server_path(&resolved_path);
        let handle = if path.to_ascii_lowercase().ends_with(".fbx") {
            asset_server.load(SkinnedMeshRenderer::fbx_scene_asset_path(
                &path,
                renderer.scene_index,
            ))
        } else {
            asset_server.load(GltfAssetLabel::Scene(renderer.scene_index).from_asset(path))
        };
        commands.spawn((
            Name::new("Hydrated Skinned Mesh Root"),
            HydratedSkinnedMeshRoot { owner: entity },
            requested_source,
            WorldAssetRoot(handle),
            ChildOf(entity),
        ));
    }
}

/// Defers material application until the imported world has produced its actual draw entities.
pub(super) fn mark_skinned_materials_ready(
    ready: On<WorldInstanceReady>,
    roots: Query<(), With<HydratedSkinnedMeshRoot>>,
    mut commands: Commands,
) {
    if roots.get(ready.entity).is_ok() {
        commands.entity(ready.entity).insert(SkinnedMaterialsDirty);
    }
}

#[allow(clippy::too_many_arguments)]
pub(super) fn apply_skinned_renderer_material_changes(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    mut materials: ResMut<Assets<StandardMaterial>>,
    mut artifact_cache: ResMut<StaticMeshArtifactCache>,
    dirty_roots: Query<(Entity, &HydratedSkinnedMeshRoot), With<SkinnedMaterialsDirty>>,
    renderers: Query<&SkinnedMeshRenderer>,
    children: Query<&Children>,
    parents: Query<&ChildOf>,
    names: Query<&Name>,
    meshes: Query<(Entity, &Mesh3d)>,
) {
    for (root_entity, root) in &dirty_roots {
        let Ok(renderer) = renderers.get(root.owner) else {
            commands
                .entity(root_entity)
                .remove::<SkinnedMaterialsDirty>();
            continue;
        };
        let descendants = collect_descendants(root_entity, &children);
        let mut candidates = Vec::new();
        for entity in descendants {
            let Ok((_, mesh)) = meshes.get(entity) else {
                continue;
            };
            let label = asset_server
                .get_path(mesh.id())
                .and_then(|path| path.label().map(str::to_string));
            let hierarchy_path = hierarchy_path(entity, root_entity, &parents, &names);
            candidates.push((entity, label, hierarchy_path));
        }

        let mut used = std::collections::HashSet::new();
        for slot in &renderer.materials.slots {
            let Some(binding) = artifact_cache.resolve_draw_binding(&renderer.asset_id, &slot.id)
            else {
                warn!(
                    "Skinned material slot '{}' has no generated draw binding for asset {}",
                    slot.id.0, renderer.asset_id
                );
                continue;
            };
            let matched = candidates
                .iter()
                .filter(|(entity, label, _)| {
                    !used.contains(entity) && label.as_deref() == Some(binding.mesh_label.as_str())
                })
                .find(|(_, _, path)| {
                    binding
                        .source_node
                        .as_deref()
                        .is_none_or(|source| path.ends_with(source))
                })
                .or_else(|| {
                    candidates.iter().find(|(entity, label, _)| {
                        !used.contains(entity)
                            && label.as_deref() == Some(binding.mesh_label.as_str())
                    })
                });
            let Some((entity, _, _)) = matched else {
                warn!(
                    "Skinned material slot '{}' could not find hydrated mesh {} ({:?})",
                    slot.id.0, binding.mesh_label, binding.source_node
                );
                continue;
            };
            used.insert(*entity);
            let effective = slot.effective_material().cloned();
            let Some(reference) = effective.as_ref() else {
                commands
                    .entity(*entity)
                    .insert(HydratedRendererMaterialBinding {
                        owner: root.owner,
                        slot_id: slot.id.clone(),
                        effective_material: None,
                    });
                continue;
            };
            let handle = resolve_renderer_material_handle(
                &asset_server,
                &mut materials,
                &mut artifact_cache,
                reference,
            )
            .unwrap_or_else(|| renderer_error_material(&mut materials));
            commands.entity(*entity).insert((
                MeshMaterial3d(handle),
                HydratedRendererMaterialBinding {
                    owner: root.owner,
                    slot_id: slot.id.clone(),
                    effective_material: effective,
                },
            ));
        }
        commands
            .entity(root_entity)
            .remove::<SkinnedMaterialsDirty>();
    }
}

fn collect_descendants(root: Entity, children: &Query<&Children>) -> Vec<Entity> {
    let mut result = Vec::new();
    let mut pending = vec![root];
    while let Some(entity) = pending.pop() {
        if entity != root {
            result.push(entity);
        }
        if let Ok(entity_children) = children.get(entity) {
            pending.extend(entity_children.iter());
        }
    }
    result
}

fn hierarchy_path(
    mut entity: Entity,
    root: Entity,
    parents: &Query<&ChildOf>,
    names: &Query<&Name>,
) -> String {
    let mut segments = Vec::new();
    while entity != root {
        if let Ok(name) = names.get(entity) {
            segments.push(name.as_str().to_string());
        }
        let Ok(parent) = parents.get(entity) else {
            break;
        };
        entity = parent.parent();
    }
    segments.reverse();
    segments.join("/")
}

/// Removes generated roots if the authoring renderer is removed without another renderer change.
pub(super) fn cleanup_removed_skinned_mesh_renderers(
    mut commands: Commands,
    renderers: Query<
        (Option<&AuthoringComponentStates>, Option<&InspectorOrder>),
        With<SkinnedMeshRenderer>,
    >,
    roots: Query<(Entity, &HydratedSkinnedMeshRoot)>,
) {
    for (root_entity, root) in &roots {
        let active = renderers
            .get(root.owner)
            .is_ok_and(|(states, legacy_order)| {
                authoring_component_active(states, legacy_order, COMPONENT_SKINNED_MESH_RENDERER)
            });
        if !active {
            commands.entity(root_entity).despawn();
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use bevy::asset::{AssetApp, AssetPlugin};
    use bevy::prelude::WorldAsset;
    use std::path::{Path, PathBuf};

    struct TestManifest(PathBuf);

    impl Drop for TestManifest {
        fn drop(&mut self) {
            let _ = std::fs::remove_file(&self.0);
        }
    }

    fn write_test_manifest(asset_id: &str, source_path: &str) -> TestManifest {
        let path = PathBuf::from(skinned_mesh_manifest_path(asset_id));
        std::fs::create_dir_all(path.parent().expect("manifest has a parent directory"))
            .expect("generated animation directory should be writable");
        std::fs::write(&path, format!(r#"(source: (path: "{source_path}"))"#))
            .expect("test animation manifest should be writable");
        TestManifest(path)
    }

    fn hydration_app() -> App {
        let mut app = App::new();
        app.add_plugins((MinimalPlugins, AssetPlugin::default()))
            .init_asset::<WorldAsset>()
            .add_systems(
                Update,
                (
                    hydrate_skinned_mesh_renderers,
                    cleanup_removed_skinned_mesh_renderers,
                )
                    .chain(),
            );
        app
    }

    fn generated_roots(world: &mut World, owner: Entity) -> Vec<Entity> {
        world
            .query::<(Entity, &HydratedSkinnedMeshRoot)>()
            .iter(world)
            .filter_map(|(entity, root)| (root.owner == owner).then_some(entity))
            .collect()
    }

    #[test]
    fn renderer_hydrates_a_dedicated_root_and_preserves_authored_children() {
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((
                LevelObject,
                SkinnedMeshRenderer::new("assets/models/character.glb"),
            ))
            .id();
        let authored_child = app
            .world_mut()
            .spawn((Name::new("Socket"), ChildOf(owner)))
            .id();

        app.update();

        assert_eq!(generated_roots(app.world_mut(), owner).len(), 1);
        assert!(app.world().get_entity(authored_child).is_ok());
    }

    #[test]
    fn stable_asset_id_uses_animation_artifact_source() {
        const ASSET_ID: &str = "fe1ea62f-9ca8-40c6-a747-a1f40567b8c6";
        let _manifest = write_test_manifest(ASSET_ID, "assets/models/relocated/hero.glb");
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((
                LevelObject,
                SkinnedMeshRenderer::new("assets/models/stale.glb").with_asset_id(ASSET_ID),
            ))
            .id();

        app.update();

        let root_entity = generated_roots(app.world_mut(), owner)[0];
        let root = app.world().get::<WorldAssetRoot>(root_entity).unwrap();
        let loaded_path = app
            .world()
            .resource::<AssetServer>()
            .get_path(root.0.id())
            .unwrap();
        assert_eq!(loaded_path.path(), Path::new("models/relocated/hero.glb"));
        assert_eq!(loaded_path.label(), Some("Scene0"));
    }

    #[test]
    fn removing_renderer_despawns_only_its_generated_root() {
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((
                LevelObject,
                SkinnedMeshRenderer::new("assets/models/character.glb"),
            ))
            .id();
        app.update();
        let root = generated_roots(app.world_mut(), owner)[0];

        app.world_mut()
            .entity_mut(owner)
            .remove::<SkinnedMeshRenderer>();
        app.update();

        assert!(app.world().get_entity(root).is_err());
        assert!(app.world().get_entity(owner).is_ok());
    }

    #[test]
    fn disabling_renderer_removes_hydrated_root_and_reenable_restores_it() {
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((
                LevelObject,
                SkinnedMeshRenderer::new("assets/models/character.glb"),
                AuthoringComponentStates::default(),
            ))
            .id();
        app.update();
        assert_eq!(generated_roots(app.world_mut(), owner).len(), 1);

        app.world_mut()
            .get_mut::<AuthoringComponentStates>(owner)
            .unwrap()
            .set_component_active(COMPONENT_SKINNED_MESH_RENDERER, false);
        app.update();
        assert!(generated_roots(app.world_mut(), owner).is_empty());

        app.world_mut()
            .get_mut::<AuthoringComponentStates>(owner)
            .unwrap()
            .set_component_active(COMPONENT_SKINNED_MESH_RENDERER, true);
        app.update();
        assert_eq!(generated_roots(app.world_mut(), owner).len(), 1);
    }
}
