//! Runtime adapter for versioned Blacksite `.scn.ron` assets.

use std::io;

use bevy::asset::{io::Reader, AssetApp, AssetLoader, LoadContext};
use bevy::ecs::reflect::AppTypeRegistry;
use bevy::prelude::*;
use bevy::reflect::{TypePath, TypeRegistryArc};
use bevy::world_serialization::{serde::WorldDeserializer, DynamicWorld};
use serde::de::DeserializeSeed;

pub(super) struct SchemaWorldAssetPlugin;

impl Plugin for SchemaWorldAssetPlugin {
    fn build(&self, app: &mut App) {
        // Register after Bevy's stock loader so versioned Blacksite scenes use
        // this adapter while retaining the standard DynamicWorld asset type.
        app.init_asset_loader::<SchemaWorldAssetLoader>();
    }
}

#[derive(Debug, TypePath)]
struct SchemaWorldAssetLoader {
    type_registry: TypeRegistryArc,
}

impl FromWorld for SchemaWorldAssetLoader {
    fn from_world(world: &mut World) -> Self {
        Self {
            type_registry: world.resource::<AppTypeRegistry>().0.clone(),
        }
    }
}

impl AssetLoader for SchemaWorldAssetLoader {
    type Asset = DynamicWorld;
    type Settings = ();
    type Error = io::Error;

    async fn load(
        &self,
        reader: &mut dyn Reader,
        _settings: &Self::Settings,
        load_context: &mut LoadContext<'_>,
    ) -> Result<Self::Asset, Self::Error> {
        let mut bytes = Vec::new();
        reader.read_to_end(&mut bytes).await?;
        let text = std::str::from_utf8(&bytes).map_err(invalid_scene)?;
        let body = normalized_world_body(text)?;
        let mut deserializer = ron::de::Deserializer::from_str(&body).map_err(invalid_scene)?;
        let world_deserializer = WorldDeserializer {
            type_registry: &self.type_registry.read(),
            load_from_path: load_context,
        };
        world_deserializer
            .deserialize(&mut deserializer)
            .map_err(|error| invalid_scene(deserializer.span_error(error)))
    }

    fn extensions(&self) -> &[&str] {
        &["scn.ron"]
    }
}

fn normalized_world_body(text: &str) -> Result<String, io::Error> {
    scene::validate_level_text(text).map_err(invalid_scene)?;
    let migrated = scene::migrate_scene_text(text).map_err(invalid_scene)?;
    scene::strip_schema_version(&migrated).map_err(invalid_scene)
}

fn invalid_scene(error: impl std::fmt::Display) -> io::Error {
    io::Error::new(io::ErrorKind::InvalidData, error.to_string())
}

#[cfg(test)]
mod tests {
    use super::normalized_world_body;
    use super::SchemaWorldAssetPlugin;
    use bevy::asset::{AssetPlugin, AssetServer, Assets, LoadState};
    use bevy::prelude::*;
    use bevy::world_serialization::{DynamicWorld, WorldSerializationPlugin};

    #[test]
    fn versioned_world_assets_are_validated_and_unwrapped_for_bevy() {
        let body = normalized_world_body(
            r#"(schema_version: 2, resources: {}, entities: {
                1: (components: { "shared::components::LevelObject": () }),
            })"#,
        )
        .unwrap();

        assert!(body.starts_with("(resources:"));
        assert!(!body.contains("schema_version"));
    }

    #[test]
    fn invalid_hierarchy_is_rejected_before_world_deserialization() {
        let error = normalized_world_body(
            r#"(schema_version: 2, resources: {}, entities: {
                1: (components: { "bevy_ecs::hierarchy::ChildOf": (9) }),
            })"#,
        )
        .unwrap_err();

        assert!(error.to_string().contains("missing hierarchy parent `9`"));
    }

    #[test]
    fn asset_server_loads_the_committed_versioned_prefab_fixture() {
        let assets = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets");
        let mut app = App::new();
        app.add_plugins(MinimalPlugins)
            .add_plugins(AssetPlugin {
                file_path: assets.to_string_lossy().into_owned(),
                ..default()
            })
            .add_plugins(WorldSerializationPlugin)
            .register_type::<Name>()
            .register_type::<Transform>()
            .register_type::<shared::LevelObject>()
            .register_type::<shared::ActorId>()
            .register_type::<shared::ActorKind>()
            .register_type::<shared::EditorVisibility>()
            .register_type::<shared::PrefabInstance>()
            .register_type::<shared::PrefabRef>()
            .add_plugins(SchemaWorldAssetPlugin);

        let handle: Handle<DynamicWorld> = app
            .world()
            .resource::<AssetServer>()
            .load("prefabs/example_variant.scn.ron");
        for _ in 0..200 {
            app.update();
            if app
                .world()
                .resource::<Assets<DynamicWorld>>()
                .get(&handle)
                .is_some()
            {
                return;
            }
            if matches!(
                app.world()
                    .resource::<AssetServer>()
                    .load_state(handle.id()),
                LoadState::Failed(_)
            ) {
                break;
            }
            std::thread::sleep(std::time::Duration::from_millis(5));
        }

        panic!(
            "versioned prefab fixture did not load: {:?}",
            app.world()
                .resource::<AssetServer>()
                .load_state(handle.id())
        );
    }
}
