//! Imported model scenes from [`ModelRef`] authoring data.

use bevy::gltf::GltfAssetLabel;
use bevy::prelude::WorldAssetRoot;
use bevy::prelude::*;

use crate::{asset_server_path, LevelObject, ModelRef};

use super::static_meshes::static_mesh_source_path;

/// Runtime root generated for one authored [`ModelRef`].
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
pub struct HydratedModelRoot {
    pub owner: Entity,
}

#[allow(clippy::type_complexity)]
pub fn hydrate_models(
    mut commands: Commands,
    asset_server: Res<AssetServer>,
    models: Query<
        (Entity, &ModelRef),
        (With<LevelObject>, Or<(Added<ModelRef>, Changed<ModelRef>)>),
    >,
    generated_roots: Query<(Entity, &HydratedModelRoot)>,
) {
    for (entity, model) in &models {
        for (root, generated) in &generated_roots {
            if generated.owner == entity {
                commands.entity(root).despawn();
            }
        }

        // Remove roots created by the legacy hydration path without touching authored children.
        commands.entity(entity).remove::<WorldAssetRoot>();

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

#[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(super::super::static_meshes::static_mesh_manifest_path(
            asset_id,
        ));
        std::fs::create_dir_all(path.parent().expect("manifest has a parent directory"))
            .expect("generated static mesh directory should be writable");
        std::fs::write(
            &path,
            format!(
                r#"(
                    source: (path: "{source_path}"),
                    parts: [],
                )"#
            ),
        )
        .expect("test static mesh 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_models);
        app
    }

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

    #[test]
    fn hydration_preserves_arbitrary_authored_children() {
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((LevelObject, ModelRef::new("assets/models/first.glb")))
            .id();
        let authored_child = app
            .world_mut()
            .spawn((Name::new("Authored Child"), ChildOf(owner)))
            .id();

        app.update();

        assert!(app.world().get_entity(authored_child).is_ok());
        assert_eq!(
            app.world().get::<ChildOf>(authored_child).unwrap().parent(),
            owner
        );
        assert_eq!(generated_roots(app.world_mut(), owner).len(), 1);
    }

    #[test]
    fn model_ref_change_replaces_only_the_generated_root() {
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((LevelObject, ModelRef::new("assets/models/first.glb")))
            .id();
        let authored_child = app
            .world_mut()
            .spawn((Name::new("Authored Child"), ChildOf(owner)))
            .id();

        app.update();
        let first_root = generated_roots(app.world_mut(), owner)
            .into_iter()
            .next()
            .unwrap();

        app.world_mut().get_mut::<ModelRef>(owner).unwrap().path =
            "assets/models/second.glb".into();
        app.update();

        let roots = generated_roots(app.world_mut(), owner);
        assert_eq!(roots.len(), 1);
        assert_ne!(roots[0], first_root);
        assert!(app.world().get_entity(first_root).is_err());
        assert!(app.world().get_entity(authored_child).is_ok());
        assert_eq!(
            app.world().get::<ChildOf>(authored_child).unwrap().parent(),
            owner
        );
    }

    #[test]
    fn stable_asset_id_resolves_relocated_model_source_from_manifest() {
        const ASSET_ID: &str = "34b80e75-f00c-4ead-91cb-54bd9b66ad55";
        const CACHED_PATH: &str = "assets/models/stale_cached_path.glb";
        const MANIFEST_PATH: &str = "assets/models/relocated/source.glb";
        let _manifest = write_test_manifest(ASSET_ID, MANIFEST_PATH);
        let mut app = hydration_app();
        let owner = app
            .world_mut()
            .spawn((
                LevelObject,
                ModelRef {
                    asset_id: ASSET_ID.into(),
                    path: CACHED_PATH.into(),
                    scene_index: 3,
                },
            ))
            .id();

        app.update();

        let root_entity = generated_roots(app.world_mut(), owner)
            .into_iter()
            .next()
            .expect("stable model reference should create a hydrated root");
        let root = app
            .world()
            .get::<WorldAssetRoot>(root_entity)
            .expect("generated root should retain its world asset handle");
        let loaded_path = app
            .world()
            .resource::<AssetServer>()
            .get_path(root.0.id())
            .expect("world asset handle should retain its resolved source path");

        assert_eq!(loaded_path.path(), Path::new("models/relocated/source.glb"));
        assert_eq!(loaded_path.label(), Some("Scene3"));
        assert_ne!(
            loaded_path.path(),
            Path::new("models/stale_cached_path.glb")
        );
    }
}
