use crate::prelude::*;
use std::collections::HashMap;
use std::fs::File;
use std::io::{BufReader, BufWriter};
use std::path::Path;
#[cfg(feature = "audio")]
use std::str::FromStr;

use freecs::Entity;

use crate::ecs::audio::components::AudioSource;
#[cfg(feature = "audio")]
use crate::ecs::audio::systems::load_sound_from_cursor;
use crate::ecs::bounding_volume::components::{BoundingVolume, OrientedBoundingBox};
use crate::ecs::generational_registry::registry_entry_by_name;
use crate::ecs::lines::components::Lines;
use crate::ecs::material::resources::material_registry_insert;
use crate::ecs::mesh::components::InstancedMesh;
use crate::ecs::prefab::{import_gltf_from_path, spawn_prefab};
use crate::ecs::world::Vec3;
use crate::ecs::world::commands::RenderCommand;
use crate::ecs::world::components::{
    CastsShadow, GlobalTransform, LocalTransformDirty, MaterialRef, Name, Parent, RenderMesh,
    Visibility,
};
use crate::ecs::world::{
    ANIMATION_PLAYER, AUDIO_SOURCE, BOUNDING_VOLUME, CAMERA, CASTS_SHADOW, DECAL, GLOBAL_TRANSFORM,
    INSTANCED_MESH, LIGHT, LINES, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, MATERIAL_REF, NAME,
    NAVMESH_AGENT, PARENT, PARTICLE_EMITTER, RENDER_LAYER, RENDER_MESH, TEXT, VISIBILITY, World,
};
use crate::render::wgpu::texture_cache::texture_cache_add_reference;

use super::asset_uuid::AssetUuid;
use super::audio::ScenePrefabInstance;
use super::components::{Scene, SceneComponents, SceneEntity, SceneHdrSkybox};
use super::lighting::SceneLight;
use super::material::SceneMaterial;
use super::registry::AssetRegistry;

#[derive(Debug, thiserror::Error)]
pub enum SceneError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("JSON error: {0}")]
    Json(#[from] serde_json::Error),

    #[error("Binary error: {0}")]
    Binary(#[from] bincode::Error),

    #[error("Asset not found: {0}")]
    AssetNotFound(String),

    #[error("Invalid scene structure")]
    InvalidStructure,

    #[error("Cyclic parent reference detected")]
    CyclicReference,
}

pub fn save_scene_json(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
    scene.compute_spawn_order();
    let mut value = serde_json::to_value(&*scene)?;
    sanitize_numeric_nulls(&mut value);
    let file = File::create(path)?;
    let writer = BufWriter::new(file);
    serde_json::to_writer_pretty(writer, &value)?;
    Ok(())
}

fn sanitize_numeric_nulls(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Array(items) => {
            let any_number = items.iter().any(|item| item.is_number());
            let only_numeric_or_null = items.iter().all(|item| item.is_number() || item.is_null());
            if any_number && only_numeric_or_null {
                for item in items.iter_mut() {
                    if item.is_null() {
                        *item = serde_json::Value::from(0.0);
                    }
                }
            } else {
                for item in items.iter_mut() {
                    sanitize_numeric_nulls(item);
                }
            }
        }
        serde_json::Value::Object(map) => {
            for inner in map.values_mut() {
                sanitize_numeric_nulls(inner);
            }
        }
        _ => {}
    }
}

pub fn load_scene_json(path: &Path) -> Result<Scene, SceneError> {
    let file = File::open(path)?;
    let reader = BufReader::new(file);
    let mut scene: Scene = serde_json::from_reader(reader)?;
    scene.rebuild_uuid_index();
    Ok(scene)
}

pub fn save_scene_binary(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
    let compressed = save_scene_binary_to_bytes(scene)?;
    std::fs::write(path, compressed)?;
    Ok(())
}

pub fn save_scene_binary_to_bytes(scene: &mut Scene) -> Result<Vec<u8>, SceneError> {
    scene.compute_spawn_order();
    let data = bincode::serialize(scene)?;
    Ok(lz4_flex::compress_prepend_size(&data))
}

pub fn load_scene_binary(path: &Path) -> Result<Scene, SceneError> {
    let compressed = std::fs::read(path)?;
    let data = lz4_flex::decompress_size_prepended(&compressed)
        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
    let mut scene: Scene = bincode::deserialize(&data)?;
    scene.rebuild_uuid_index();
    Ok(scene)
}

pub fn load_scene_binary_from_bytes(bytes: &[u8]) -> Result<Scene, SceneError> {
    let data = lz4_flex::decompress_size_prepended(bytes)
        .map_err(|error| std::io::Error::new(std::io::ErrorKind::InvalidData, error))?;
    let mut scene: Scene = bincode::deserialize(&data)?;
    scene.rebuild_uuid_index();
    Ok(scene)
}

pub fn save_scene(scene: &mut Scene, path: &Path) -> Result<(), SceneError> {
    match path.extension().and_then(|extension| extension.to_str()) {
        Some("json") => save_scene_json(scene, path),
        _ => save_scene_binary(scene, path),
    }
}

pub fn load_scene(path: &Path) -> Result<Scene, SceneError> {
    match path.extension().and_then(|extension| extension.to_str()) {
        Some("json") => load_scene_json(path),
        _ => load_scene_binary(path),
    }
}

pub struct SpawnSceneResult {
    pub uuid_to_entity: HashMap<AssetUuid, Entity>,
    pub root_entities: Vec<Entity>,
    pub warnings: Vec<String>,
    /// Pairs each spawned entity back to its original uuid inside the
    /// source `.nsprefab`. Populated by `spawn_prefab_scene` from the
    /// remap it builds, and by `spawn_scene` from per-entity
    /// `SceneEntity::source_entity_uuid` fields. Editors use it to keep
    /// override / refresh links alive across saves.
    pub entity_to_source_uuid: HashMap<Entity, AssetUuid>,
}

pub fn spawn_scene(
    world: &mut World,
    scene: &Scene,
    asset_registry: Option<&AssetRegistry>,
) -> Result<SpawnSceneResult, SceneError> {
    spawn_scene_with_deserializer(world, scene, asset_registry, None)
}

/// Same as `spawn_scene`, but invokes `deserializer.populate_world`
/// after every engine-side entity has been spawned. Use this to
/// reconstruct game-side state from `Scene::game_resources` and
/// `SceneEntity::components::game_components` via the registry the
/// game owns.
pub fn spawn_scene_with_deserializer(
    world: &mut World,
    scene: &Scene,
    asset_registry: Option<&AssetRegistry>,
    deserializer: Option<&mut dyn crate::ecs::scene::SceneDeserializer>,
) -> Result<SpawnSceneResult, SceneError> {
    let mut uuid_to_entity: HashMap<AssetUuid, Entity> = HashMap::new();
    let mut root_entities: Vec<Entity> = Vec::new();
    let mut warnings: Vec<String> = Vec::new();

    world.resources.render_settings.atmosphere = scene.atmosphere;
    apply_scene_settings(world, &scene.settings);
    if scene.next_guid > world.resources.entities.next_guid {
        world.resources.entities.next_guid = scene.next_guid;
    }

    if let Some(hdr_skybox) = &scene.hdr_skybox {
        match hdr_skybox {
            SceneHdrSkybox::Embedded { data } => {
                queue_render_command(
                    world,
                    RenderCommand::LoadHdrSkybox {
                        hdr_data: data.clone(),
                    },
                );
            }
            SceneHdrSkybox::Reference { path } => {
                queue_render_command(
                    world,
                    RenderCommand::LoadHdrSkyboxFromPath {
                        path: std::path::PathBuf::from(path),
                    },
                );
            }
            SceneHdrSkybox::Asset { uuid } => {
                if let Some(registry) = asset_registry {
                    if let Some(path) = asset_registry_resolve_uuid(registry, *uuid) {
                        queue_render_command(world, RenderCommand::LoadHdrSkyboxFromPath { path });
                    } else {
                        warnings.push(format!("HDR skybox asset not found: {}", uuid));
                    }
                } else {
                    warnings.push(format!(
                        "Asset registry required to resolve HDR skybox UUID: {}",
                        uuid
                    ));
                }
            }
        }
    }

    for embedded_texture in scene.embedded_textures.values() {
        if let Some((rgba_data, width, height)) = embedded_texture.to_rgba() {
            crate::ecs::loading::queue_decoded_texture(
                world,
                embedded_texture.name.clone(),
                rgba_data,
                width,
                height,
                embedded_texture.usage,
                embedded_texture.sampler,
            );
            // queue_decoded_texture inserts an Rgba variant into
            // texture_sources. If the embedded texture was originally
            // stored as Png, overwrite with the encoded bytes so a
            // load-edit-save cycle preserves the smaller format instead
            // of inflating to raw RGBA every round trip.
            if let crate::ecs::scene::audio::EmbeddedTextureData::Png { png_data } =
                &embedded_texture.data
            {
                world.resources.assets.texture_sources.insert(
                    embedded_texture.name.clone(),
                    crate::ecs::asset_state::TextureSourceBytes {
                        data: crate::ecs::asset_state::TextureSourceData::Png(png_data.clone()),
                        usage: embedded_texture.usage,
                        sampler: embedded_texture.sampler,
                    },
                );
            }
        }
    }

    for embedded_mesh in scene.embedded_meshes.values() {
        crate::ecs::prefab::resources::mesh_cache_insert(
            &mut world.resources.assets.mesh_cache,
            embedded_mesh.name.clone(),
            embedded_mesh.mesh.clone(),
        );
    }

    let spawn_order = if !scene.spawn_order.is_empty() {
        scene.spawn_order.clone()
    } else {
        compute_spawn_order(&scene.entities)?
    };

    for scene_entity_uuid in spawn_order {
        let scene_entity = scene
            .find_entity(scene_entity_uuid)
            .ok_or(SceneError::InvalidStructure)?;

        let parent_entity = scene_entity
            .parent
            .and_then(|parent_uuid| uuid_to_entity.get(&parent_uuid).copied());

        let entity = spawn_scene_entity(
            world,
            scene,
            scene_entity,
            parent_entity,
            asset_registry,
            &mut warnings,
        );

        if scene_entity.parent.is_none() {
            root_entities.push(entity);
        }

        uuid_to_entity.insert(scene_entity.uuid, entity);
    }

    #[cfg(feature = "navmesh")]
    if let Some(navmesh) = &scene.navmesh {
        world.resources.navmesh = navmesh.to_navmesh_world();
    }

    #[cfg(feature = "physics")]
    super::commands_physics::spawn_scene_joints(
        world,
        &scene.joints,
        &uuid_to_entity,
        &mut warnings,
    );

    resolve_animation_targets(world, scene, &uuid_to_entity);

    if let Some(deserializer) = deserializer {
        deserializer.populate_world(world, &uuid_to_entity, scene);
    }

    let mut entity_to_source_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
    for scene_entity in &scene.entities {
        if let (Some(source_uuid), Some(&entity)) = (
            scene_entity.source_entity_uuid,
            uuid_to_entity.get(&scene_entity.uuid),
        ) {
            entity_to_source_uuid.insert(entity, source_uuid);
        }
    }

    Ok(SpawnSceneResult {
        uuid_to_entity,
        root_entities,
        warnings,
        entity_to_source_uuid,
    })
}

/// After every entity in a scene has been spawned and assigned to its
/// UUID, rebuild each `AnimationPlayer.bone_name_to_entity` and
/// `node_index_to_entity` from the matching `bone_name_to_node` and
/// `node_index_to_node` tables captured at save time. Without this the
/// runtime player has empty mappings and animation channels cannot
/// find their target entities — both skeletal channels (which use
/// bone_name) and non-skeletal channels (which use node_index) need
/// their target entities resolved here.
fn resolve_animation_targets(
    world: &mut World,
    scene: &Scene,
    uuid_to_entity: &HashMap<AssetUuid, Entity>,
) {
    for scene_entity in &scene.entities {
        let Some(scene_player) = scene_entity.components.animation_player.as_ref() else {
            continue;
        };
        if scene_player.bone_name_to_node.is_empty() && scene_player.node_index_to_node.is_empty() {
            continue;
        }
        let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid) else {
            continue;
        };
        let Some(player) = world.core.get_animation_player_mut(entity) else {
            continue;
        };
        for (bone_name, bone_uuid) in &scene_player.bone_name_to_node {
            if let Some(&bone_entity) = uuid_to_entity.get(bone_uuid) {
                player
                    .bone_name_to_entity
                    .insert(bone_name.clone(), bone_entity);
            }
        }
        for (&node_index, node_uuid) in &scene_player.node_index_to_node {
            if let Some(&node_entity) = uuid_to_entity.get(node_uuid) {
                player.node_index_to_entity.insert(node_index, node_entity);
            }
        }
    }
}

fn compute_spawn_order(entities: &[SceneEntity]) -> Result<Vec<AssetUuid>, SceneError> {
    let uuid_set: std::collections::HashSet<AssetUuid> = entities.iter().map(|e| e.uuid).collect();

    let mut result: Vec<AssetUuid> = Vec::with_capacity(entities.len());
    let mut visited: std::collections::HashSet<AssetUuid> = std::collections::HashSet::new();
    let mut in_stack: std::collections::HashSet<AssetUuid> = std::collections::HashSet::new();

    let entity_map: HashMap<AssetUuid, &SceneEntity> =
        entities.iter().map(|e| (e.uuid, e)).collect();

    fn visit(
        uuid: AssetUuid,
        entity_map: &HashMap<AssetUuid, &SceneEntity>,
        uuid_set: &std::collections::HashSet<AssetUuid>,
        visited: &mut std::collections::HashSet<AssetUuid>,
        in_stack: &mut std::collections::HashSet<AssetUuid>,
        result: &mut Vec<AssetUuid>,
    ) -> Result<(), SceneError> {
        if visited.contains(&uuid) {
            return Ok(());
        }

        if in_stack.contains(&uuid) {
            return Err(SceneError::CyclicReference);
        }

        in_stack.insert(uuid);

        if let Some(entity) = entity_map.get(&uuid)
            && let Some(parent_uuid) = entity.parent
            && uuid_set.contains(&parent_uuid)
        {
            visit(parent_uuid, entity_map, uuid_set, visited, in_stack, result)?;
        }

        in_stack.remove(&uuid);
        visited.insert(uuid);
        result.push(uuid);

        Ok(())
    }

    for entity in entities {
        visit(
            entity.uuid,
            &entity_map,
            &uuid_set,
            &mut visited,
            &mut in_stack,
            &mut result,
        )?;
    }

    Ok(result)
}

fn spawn_scene_entity(
    world: &mut World,
    scene: &Scene,
    scene_entity: &SceneEntity,
    parent_entity: Option<Entity>,
    asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) -> Entity {
    let mut component_mask = LOCAL_TRANSFORM | GLOBAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY;

    if scene_entity.name.is_some() {
        component_mask |= NAME;
    }
    if parent_entity.is_some() {
        component_mask |= PARENT;
    }
    if scene_entity.components.visible {
        component_mask |= VISIBILITY;
    }

    let entity = spawn_entities(world, component_mask, 1)[0];

    world
        .core
        .set_local_transform(entity, scene_entity.transform);
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    world
        .core
        .set_local_transform_dirty(entity, LocalTransformDirty);

    if let Some(name) = &scene_entity.name {
        world.core.set_name(entity, Name(name.clone()));
        world.resources.entities.names.insert(name.clone(), entity);
    }

    if !scene_entity.components.tags.is_empty() {
        world
            .resources
            .entities
            .tags
            .insert(entity, scene_entity.components.tags.clone());
    }
    if let Some(guid) = scene_entity.guid {
        crate::ecs::entity_registry::assign_guid(world, entity, crate::ecs::primitives::Guid(guid));
    }

    if let Some(parent) = parent_entity {
        world.core.set_parent(entity, Parent(Some(parent)));
        world
            .resources
            .transform_state
            .children_cache
            .entry(parent)
            .or_default()
            .push(entity);
    }

    if scene_entity.components.visible {
        world
            .core
            .set_visibility(entity, Visibility { visible: true });
    }

    apply_scene_components(
        world,
        scene,
        entity,
        &scene_entity.components,
        asset_registry,
        warnings,
    );

    entity
}

fn apply_scene_components(
    world: &mut World,
    scene: &Scene,
    entity: Entity,
    components: &SceneComponents,
    asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) {
    if let Some(mesh) = &components.mesh {
        apply_mesh_component(world, entity, mesh, asset_registry, warnings);
    }

    if let Some(instanced_mesh) = &components.instanced_mesh {
        apply_instanced_mesh_component(world, entity, instanced_mesh, asset_registry, warnings);
    }

    if !components.lines.is_empty() {
        world.core.add_components(entity, LINES);
        world
            .core
            .set_lines(entity, Lines::new(components.lines.clone()));
    }

    if let Some(light) = &components.light {
        world.core.add_components(entity, LIGHT);
        world.core.set_light(entity, light.to_light());
    }

    if let Some(camera) = &components.camera {
        world.core.add_components(entity, CAMERA);
        world.core.set_camera(entity, camera.to_camera());
    }

    #[cfg(feature = "physics")]
    if let Some(physics) = &components.physics {
        super::commands_physics::apply_physics_component(world, entity, physics);
    }

    if let Some(audio) = &components.audio {
        apply_audio_component(world, scene, entity, audio, asset_registry, warnings);
    }

    if let Some(prefab) = &components.prefab {
        apply_prefab_component(world, entity, prefab, asset_registry, warnings);
    }

    if let Some(bounding_volume) = &components.bounding_volume {
        world.core.add_components(entity, BOUNDING_VOLUME);
        world.core.set_bounding_volume(entity, *bounding_volume);
    }

    if let Some(animation_player) = &components.animation_player {
        world.core.add_components(entity, ANIMATION_PLAYER);
        let player = if animation_player.clip_refs.is_empty() {
            animation_player.to_animation_player()
        } else {
            animation_player.to_animation_player_with_resolved_clips(asset_registry, warnings)
        };
        world.core.set_animation_player(entity, player);
    }

    if components.casts_shadow {
        world.core.add_components(entity, CASTS_SHADOW);
        world.core.set_casts_shadow(entity, CastsShadow);
    }

    if let Some(scene_emitter) = &components.particle_emitter {
        world.core.add_components(entity, PARTICLE_EMITTER);
        world
            .core
            .set_particle_emitter(entity, scene_emitter.to_particle_emitter());
    }

    if let Some(decal) = &components.decal {
        world.core.add_components(entity, DECAL);
        world.core.set_decal(entity, decal.clone());
    }

    if let Some(render_layer) = &components.render_layer {
        world.core.add_components(entity, RENDER_LAYER);
        world.core.set_render_layer(entity, *render_layer);
    }

    if let Some(text) = &components.text {
        let mut text = text.clone();
        if let Some(content) = components.text_content.as_deref() {
            text.text_index = world.resources.text.cache.add_text(content);
        }
        text.dirty = true;
        world.core.add_components(entity, TEXT);
        world.core.set_text(entity, text);
    }

    #[cfg(feature = "physics")]
    if let Some(scene_cc) = &components.character_controller {
        super::commands_physics::apply_character_controller(world, entity, scene_cc);
    }

    if let Some(navmesh_agent) = &components.navmesh_agent {
        world.core.add_components(entity, NAVMESH_AGENT);
        world.core.set_navmesh_agent(entity, navmesh_agent.clone());
    }
}

pub fn apply_scene_settings(world: &mut World, settings: &super::settings::SceneSettings) {
    #[cfg(feature = "physics")]
    super::commands_physics::apply_physics_settings(world, settings);

    let render = &mut world.resources.render_settings;
    render.ambient_light = settings.ambient_light;
    render.clear_color = settings.clear_color;
    render.fog = settings.fog;
    render.bloom_enabled = settings.bloom_enabled;
    render.bloom_intensity = settings.bloom_intensity;
    render.bloom_threshold = settings.bloom_threshold;
    render.color_grading = settings.color_grading;
    render.depth_of_field = settings.depth_of_field;
    render.ssao_enabled = settings.ssao_enabled;
    render.ssao_radius = settings.ssao_radius;
    render.ssao_bias = settings.ssao_bias;
    render.ssao_intensity = settings.ssao_intensity;
    render.ssao_sample_count = settings.ssao_sample_count;
    render.ssgi_enabled = settings.ssgi_enabled;
    render.ssgi_radius = settings.ssgi_radius;
    render.ssgi_intensity = settings.ssgi_intensity;
    render.ssgi_max_steps = settings.ssgi_max_steps;
    render.ssr_enabled = settings.ssr_enabled;
    render.ssr_max_steps = settings.ssr_max_steps;
    render.ssr_thickness = settings.ssr_thickness;
    render.ssr_max_distance = settings.ssr_max_distance;
    render.ssr_stride = settings.ssr_stride;
    render.ssr_fade_start = settings.ssr_fade_start;
    render.ssr_fade_end = settings.ssr_fade_end;
    render.ssr_intensity = settings.ssr_intensity;
    render.vertex_snap = settings.vertex_snap;
    render.affine_texture_mapping = settings.affine_texture_mapping;
    if let Some(hour) = settings.day_night_hour {
        world.resources.renderer_state.day_night.hour = hour;
    }
}

pub fn capture_scene_settings(world: &World) -> super::settings::SceneSettings {
    let graphics = &world.resources.render_settings;
    #[cfg(feature = "physics")]
    let (gravity, physics_timestep, physics_max_substeps) =
        super::commands_physics::capture_physics_settings(world);
    #[cfg(not(feature = "physics"))]
    let (gravity, physics_timestep, physics_max_substeps) = ([0.0, -9.81, 0.0], 1.0 / 60.0, 4);

    let day_night_hour = if matches!(
        graphics.atmosphere,
        crate::ecs::graphics::resources::Atmosphere::DayNight
    ) {
        Some(world.resources.renderer_state.day_night.hour)
    } else {
        None
    };

    super::settings::SceneSettings {
        gravity,
        physics_timestep,
        physics_max_substeps,
        ambient_light: graphics.ambient_light,
        clear_color: graphics.clear_color,
        fog: graphics.fog,
        bloom_enabled: graphics.bloom_enabled,
        bloom_intensity: graphics.bloom_intensity,
        bloom_threshold: graphics.bloom_threshold,
        color_grading: graphics.color_grading,
        depth_of_field: graphics.depth_of_field,
        ssao_enabled: graphics.ssao_enabled,
        ssao_radius: graphics.ssao_radius,
        ssao_bias: graphics.ssao_bias,
        ssao_intensity: graphics.ssao_intensity,
        ssao_sample_count: graphics.ssao_sample_count,
        ssgi_enabled: graphics.ssgi_enabled,
        ssgi_radius: graphics.ssgi_radius,
        ssgi_intensity: graphics.ssgi_intensity,
        ssgi_max_steps: graphics.ssgi_max_steps,
        ssr_enabled: graphics.ssr_enabled,
        ssr_max_steps: graphics.ssr_max_steps,
        ssr_thickness: graphics.ssr_thickness,
        ssr_max_distance: graphics.ssr_max_distance,
        ssr_stride: graphics.ssr_stride,
        ssr_fade_start: graphics.ssr_fade_start,
        ssr_fade_end: graphics.ssr_fade_end,
        ssr_intensity: graphics.ssr_intensity,
        vertex_snap: graphics.vertex_snap,
        affine_texture_mapping: graphics.affine_texture_mapping,
        day_night_hour,
    }
}

fn apply_mesh_component(
    world: &mut World,
    entity: Entity,
    mesh: &super::components::SceneMesh,
    asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) {
    let mesh_name = if let Some(name) = &mesh.mesh_name {
        name.clone()
    } else if let Some(uuid) = mesh.mesh_uuid {
        if let Some(registry) = asset_registry {
            if let Some(entry) = asset_registry_get_entry(registry, uuid) {
                entry.name.clone().unwrap_or_else(|| uuid.to_string())
            } else {
                warnings.push(format!("Mesh asset not found: {}", uuid));
                return;
            }
        } else {
            uuid.to_string()
        }
    } else {
        warnings.push("Mesh component has neither UUID nor name".to_string());
        return;
    };

    world.core.add_components(
        entity,
        RENDER_MESH | MATERIAL_REF | CASTS_SHADOW | BOUNDING_VOLUME,
    );

    let render_mesh = RenderMesh::new(mesh_name.clone());
    world
        .core
        .set_bounding_volume(entity, BoundingVolume::from_mesh_type(&mesh_name));
    if let Some(&index) = world
        .resources
        .assets
        .mesh_cache
        .registry
        .name_to_index
        .get(&render_mesh.name)
    {
        registry_add_reference(&mut world.resources.assets.mesh_cache.registry, index);
    }
    world.core.set_render_mesh(entity, render_mesh);
    world.resources.mesh_render_state.mark_entity_added(entity);

    let material_name = if let Some(scene_material) = &mesh.material {
        let mat = scene_material.to_material();
        let name = format!("Scene_{}_{}", mesh_name, entity.id);
        material_registry_insert(
            &mut world.resources.assets.material_registry,
            name.clone(),
            mat,
        );
        name
    } else {
        "Default".to_string()
    };

    if let Some(&index) = world
        .resources
        .assets
        .material_registry
        .registry
        .name_to_index
        .get(&material_name)
    {
        registry_add_reference(
            &mut world.resources.assets.material_registry.registry,
            index,
        );
    }

    let texture_names: Vec<String> = registry_entry_by_name(
        &world.resources.assets.material_registry.registry,
        &material_name,
    )
    .map(|mat| mat.texture_names().map(str::to_string).collect())
    .unwrap_or_default();
    for texture in &texture_names {
        texture_cache_add_reference(&mut world.resources.texture_cache, texture);
    }

    world
        .core
        .set_material_ref(entity, MaterialRef::new(material_name));
    world.core.set_casts_shadow(entity, CastsShadow);
}

fn compute_instanced_mesh_bounding_volume(
    mesh_name: &str,
    instances: &[super::mesh::SceneMeshInstance],
) -> BoundingVolume {
    if instances.is_empty() {
        return BoundingVolume::from_mesh_type(mesh_name);
    }

    let base_bv = BoundingVolume::from_mesh_type(mesh_name);
    let base_obb = &base_bv.obb;

    let mut min_corner = Vec3::new(f32::MAX, f32::MAX, f32::MAX);
    let mut max_corner = Vec3::new(f32::MIN, f32::MIN, f32::MIN);

    for instance in instances {
        let local_transform = instance.to_local_transform();
        let instance_matrix = nalgebra_glm::translation(&local_transform.translation)
            * nalgebra_glm::quat_to_mat4(&local_transform.rotation)
            * nalgebra_glm::scaling(&local_transform.scale);

        let transformed_obb = base_obb.transform(&instance_matrix);
        let corners = transformed_obb.get_corners();

        for corner in &corners {
            min_corner.x = min_corner.x.min(corner.x);
            min_corner.y = min_corner.y.min(corner.y);
            min_corner.z = min_corner.z.min(corner.z);
            max_corner.x = max_corner.x.max(corner.x);
            max_corner.y = max_corner.y.max(corner.y);
            max_corner.z = max_corner.z.max(corner.z);
        }
    }

    let combined_obb = OrientedBoundingBox::from_aabb(min_corner, max_corner);
    let sphere_radius = nalgebra_glm::length(&combined_obb.half_extents);

    BoundingVolume::new(combined_obb, sphere_radius)
}

fn apply_instanced_mesh_component(
    world: &mut World,
    entity: Entity,
    instanced_mesh: &super::mesh::SceneInstancedMesh,
    asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) {
    use crate::ecs::mesh::components::{InstanceCustomData, InstanceTransform};

    let mesh_name = if let Some(name) = &instanced_mesh.mesh_name {
        name.clone()
    } else if let Some(uuid) = instanced_mesh.mesh_uuid {
        if let Some(registry) = asset_registry {
            if let Some(entry) = asset_registry_get_entry(registry, uuid) {
                entry.name.clone().unwrap_or_else(|| uuid.to_string())
            } else {
                warnings.push(format!("Instanced mesh asset not found: {}", uuid));
                return;
            }
        } else {
            uuid.to_string()
        }
    } else {
        warnings.push("Instanced mesh component has neither UUID nor name".to_string());
        return;
    };

    world.core.add_components(
        entity,
        INSTANCED_MESH | MATERIAL_REF | CASTS_SHADOW | BOUNDING_VOLUME,
    );

    let bounding_volume =
        compute_instanced_mesh_bounding_volume(&mesh_name, &instanced_mesh.instances);
    world.core.set_bounding_volume(entity, bounding_volume);

    let transforms: Vec<InstanceTransform> = instanced_mesh
        .instances
        .iter()
        .map(|i| {
            let local = i.to_local_transform();
            InstanceTransform::new(local.translation, local.rotation, local.scale)
        })
        .collect();

    let mut component = InstancedMesh::with_instances(&mesh_name, transforms);

    for (index, instance) in instanced_mesh.instances.iter().enumerate() {
        if let Some(color) = instance.color {
            component.custom_data[index] = InstanceCustomData { tint: color };
        }
    }

    world.core.set_instanced_mesh(entity, component);

    let material_name = if let Some(scene_material) = &instanced_mesh.material {
        let mat = scene_material.to_material();
        let name = format!("SceneInstanced_{}_{}", mesh_name, entity.id);
        material_registry_insert(
            &mut world.resources.assets.material_registry,
            name.clone(),
            mat,
        );
        name
    } else {
        "Default".to_string()
    };

    if let Some(&index) = world
        .resources
        .assets
        .material_registry
        .registry
        .name_to_index
        .get(&material_name)
    {
        registry_add_reference(
            &mut world.resources.assets.material_registry.registry,
            index,
        );
    }

    world
        .core
        .set_material_ref(entity, MaterialRef::new(material_name));
    world.core.set_casts_shadow(entity, CastsShadow);
}

fn apply_audio_component(
    world: &mut World,
    scene: &Scene,
    entity: Entity,
    audio: &super::audio::SceneAudioSource,
    _asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) {
    world.core.add_components(entity, AUDIO_SOURCE);

    let audio_ref = if let Some(name) = &audio.audio_name {
        name.clone()
    } else if let Some(uuid) = audio.audio_uuid {
        uuid.to_string()
    } else {
        warnings.push("Audio component has neither UUID nor name".to_string());
        return;
    };

    #[cfg(feature = "audio")]
    {
        let resolved_uuid = audio
            .audio_uuid
            .or_else(|| AssetUuid::from_str(&audio_ref).ok());
        match resolved_uuid.and_then(|uuid| scene.embedded_audio.get(&uuid)) {
            Some(embedded_audio) => {
                if let Ok(sound_data) = load_sound_from_cursor(embedded_audio.data.clone()) {
                    audio_engine_load_sound(
                        &mut world.resources.audio,
                        audio_ref.clone(),
                        sound_data,
                    );
                }
            }
            None => {
                warnings.push(format!("Audio '{}' not found in scene", audio_ref));
            }
        }
    }

    #[cfg(not(feature = "audio"))]
    {
        let _ = scene;
        warnings.push("Audio feature is not enabled - audio source will not play".to_string());
    }

    let mut reverb_zones = audio.reverb_zones.clone();
    if reverb_zones.is_empty() && audio.reverb {
        reverb_zones.push(("default".to_string(), 0.0));
    }

    world.core.set_audio_source(
        entity,
        AudioSource {
            audio_ref: Some(audio_ref),
            volume: audio.volume,
            looping: audio.looping,
            playing: audio.playing,
            spatial: audio.spatial,
            bus: audio.bus,
            min_distance: audio.min_distance,
            max_distance: audio.max_distance,
            reverb_zones,
            random_clips: audio.random_clips.clone(),
            random_pick: audio.random_pick,
            playback_rate: 1.0,
        },
    );
}

fn apply_prefab_component(
    world: &mut World,
    entity: Entity,
    prefab_instance: &ScenePrefabInstance,
    asset_registry: Option<&AssetRegistry>,
    warnings: &mut Vec<String>,
) {
    // Always attach the tracking link first. The structural import below
    // may bail (no asset registry, missing source file, etc.), but the
    // editor still needs the PrefabSource link to find this instance
    // when refreshing or searching across maps.
    attach_prefab_source(world, entity, prefab_instance);

    // `.nsprefab` prefab instances carry only a tracking link — their
    // entity content was embedded into the map at save time and is
    // already being spawned via the normal scene path. Nothing
    // structural to do.
    let is_nsprefab = prefab_instance.prefab_path.as_deref().is_some_and(|path| {
        std::path::Path::new(path)
            .extension()
            .and_then(|extension| extension.to_str())
            .is_some_and(|extension| extension.eq_ignore_ascii_case("nsprefab"))
    });
    if is_nsprefab || prefab_instance.prefab_path.is_none() {
        return;
    }

    let prefab_path = if let Some(uuid) = prefab_instance.prefab_uuid {
        if let Some(registry) = asset_registry {
            asset_registry_resolve_uuid(registry, uuid)
        } else {
            warnings.push(format!(
                "Asset registry required to resolve prefab UUID: {}",
                uuid
            ));
            return;
        }
    } else if let Some(path) = &prefab_instance.prefab_path {
        Some(std::path::PathBuf::from(path))
    } else {
        warnings.push("Prefab instance has neither UUID nor path".to_string());
        return;
    };

    if let Some(path) = prefab_path {
        match import_gltf_from_path(&path) {
            Ok(mut result) => {
                crate::ecs::loading::queue_gltf_load(world, &mut result);

                if let Some(prefab) = result.prefabs.into_iter().next() {
                    let transform = world
                        .core
                        .get_local_transform(entity)
                        .copied()
                        .unwrap_or_default();
                    let prefab_root = spawn_prefab(world, &prefab, transform.translation);

                    world.core.add_components(prefab_root, PARENT);
                    world.core.set_parent(prefab_root, Parent(Some(entity)));
                    world
                        .resources
                        .transform_state
                        .children_cache
                        .entry(entity)
                        .or_default()
                        .push(prefab_root);
                } else {
                    warnings.push(format!("No prefabs found in file '{}'", path.display()));
                }
            }
            Err(error) => {
                warnings.push(format!(
                    "Failed to load prefab from '{}': {}",
                    path.display(),
                    error
                ));
            }
        }
    }
}

fn attach_prefab_source(world: &mut World, entity: Entity, prefab_instance: &ScenePrefabInstance) {
    use crate::ecs::prefab::components::PrefabSource;
    world
        .core
        .add_components(entity, crate::ecs::world::PREFAB_SOURCE);
    world.core.set_prefab_source(
        entity,
        PrefabSource {
            prefab_name: prefab_instance.prefab_name.clone().unwrap_or_default(),
            source_path: prefab_instance.prefab_path.clone(),
            source_uuid: prefab_instance.prefab_uuid,
        },
    );
}

pub fn entity_to_scene_entity(
    world: &World,
    entity: Entity,
    uuid: AssetUuid,
    parent_uuid: Option<AssetUuid>,
) -> SceneEntity {
    entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &HashMap::new())
}

/// Like `entity_to_scene_entity` but accepts an `Entity -> AssetUuid`
/// map so component data that references other entities (animation
/// player bone targets, node targets) can be serialized with the right
/// stable uuids. Callers that don't have access to a full uuid map
/// (e.g. unit tests building a single entity) should use the simpler
/// `entity_to_scene_entity` and rely on the bulk save's post-pass to
/// fill cross-entity references.
pub fn entity_to_scene_entity_with_uuids(
    world: &World,
    entity: Entity,
    uuid: AssetUuid,
    parent_uuid: Option<AssetUuid>,
    entity_to_uuid: &HashMap<Entity, AssetUuid>,
) -> SceneEntity {
    let transform = world
        .core
        .get_local_transform(entity)
        .copied()
        .unwrap_or_default();

    let name = world.core.get_name(entity).map(|n| n.0.clone());

    let mut scene_entity = SceneEntity {
        uuid,
        parent: parent_uuid,
        name,
        transform,
        layer: None,
        chunk_id: None,
        guid: world.core.get_guid(entity).map(|guid| guid.0),
        source_entity_uuid: None,
        components: SceneComponents::default(),
    };

    populate_scene_entity_components(world, entity, &mut scene_entity);
    populate_animation_target_uuids(world, entity, &mut scene_entity, entity_to_uuid);
    scene_entity
}

/// Fills `bone_name_to_node` and `node_index_to_node` on an entity's
/// `SceneAnimationPlayer` using the supplied `entity_to_uuid` map.
/// Skips entries that point at entities outside the map; the bulk save
/// path runs the same logic globally as a post-pass to catch those.
fn populate_animation_target_uuids(
    world: &World,
    entity: Entity,
    scene_entity: &mut SceneEntity,
    entity_to_uuid: &HashMap<Entity, AssetUuid>,
) {
    let Some(player) = world.core.get_animation_player(entity) else {
        return;
    };
    if player.bone_name_to_entity.is_empty() && player.node_index_to_entity.is_empty() {
        return;
    }
    let Some(scene_player) = scene_entity.components.animation_player.as_mut() else {
        return;
    };
    for (bone_name, bone_entity) in &player.bone_name_to_entity {
        if let Some(&bone_uuid) = entity_to_uuid.get(bone_entity) {
            scene_player
                .bone_name_to_node
                .insert(bone_name.clone(), bone_uuid);
        }
    }
    for (&node_index, node_entity) in &player.node_index_to_entity {
        if let Some(&node_uuid) = entity_to_uuid.get(node_entity) {
            scene_player
                .node_index_to_node
                .insert(node_index, node_uuid);
        }
    }
}

fn populate_scene_entity_components(world: &World, entity: Entity, scene_entity: &mut SceneEntity) {
    if let Some(tags) = world.resources.entities.tags.get(&entity) {
        scene_entity.components.tags = tags.clone();
    }

    if let Some(render_mesh) = world.core.get_render_mesh(entity) {
        let material = world
            .core
            .get_material_ref(entity)
            .and_then(|mat_ref| {
                registry_entry_by_name(
                    &world.resources.assets.material_registry.registry,
                    &mat_ref.name,
                )
            })
            .map(SceneMaterial::from);

        scene_entity.components.mesh = Some(super::components::SceneMesh {
            mesh_uuid: None,
            mesh_name: Some(render_mesh.name.clone()),
            material,
        });
    }

    if let Some(instanced_mesh) = world.core.get_instanced_mesh(entity) {
        let material = world
            .core
            .get_material_ref(entity)
            .and_then(|mat_ref| {
                registry_entry_by_name(
                    &world.resources.assets.material_registry.registry,
                    &mat_ref.name,
                )
            })
            .map(SceneMaterial::from);

        let instances: Vec<super::mesh::SceneMeshInstance> = instanced_mesh
            .instances
            .iter()
            .enumerate()
            .map(|(index, transform)| {
                let tint = instanced_mesh
                    .custom_data
                    .get(index)
                    .map(|cd| cd.tint)
                    .filter(|t| *t != [1.0, 1.0, 1.0, 1.0]);
                super::mesh::SceneMeshInstance {
                    translation: [
                        transform.translation.x,
                        transform.translation.y,
                        transform.translation.z,
                    ],
                    rotation: [
                        transform.rotation.i,
                        transform.rotation.j,
                        transform.rotation.k,
                        transform.rotation.w,
                    ],
                    scale: [transform.scale.x, transform.scale.y, transform.scale.z],
                    color: tint,
                }
            })
            .collect();

        scene_entity.components.instanced_mesh = Some(super::mesh::SceneInstancedMesh {
            mesh_uuid: None,
            mesh_name: Some(instanced_mesh.mesh_name.clone()),
            instances,
            material,
        });
    }

    if let Some(light) = world.core.get_light(entity) {
        scene_entity.components.light = Some(SceneLight::from(light));
    }

    if let Some(camera) = world.core.get_camera(entity) {
        scene_entity.components.camera = Some(super::lighting::SceneCamera::from(camera));
    }

    #[cfg(feature = "physics")]
    super::commands_physics::export_entity_physics(world, entity, &mut scene_entity.components);

    if let Some(audio_source) = world.core.get_audio_source(entity) {
        let reverb_legacy = audio_source
            .reverb_zones
            .iter()
            .any(|(name, _)| name == "default");
        scene_entity.components.audio = Some(super::audio::SceneAudioSource {
            audio_uuid: None,
            audio_name: audio_source.audio_ref.clone(),
            volume: audio_source.volume,
            looping: audio_source.looping,
            playing: audio_source.playing,
            spatial: audio_source.spatial,
            reverb: reverb_legacy,
            bus: audio_source.bus,
            min_distance: audio_source.min_distance,
            max_distance: audio_source.max_distance,
            reverb_zones: audio_source.reverb_zones.clone(),
            random_clips: audio_source.random_clips.clone(),
            random_pick: audio_source.random_pick,
        });
    }

    if let Some(bounding_volume) = world.core.get_bounding_volume(entity) {
        scene_entity.components.bounding_volume = Some(*bounding_volume);
    }

    if let Some(animation_player) = world.core.get_animation_player(entity) {
        scene_entity.components.animation_player = Some(
            super::animation::SceneAnimationPlayer::from(animation_player),
        );
    }

    scene_entity.components.casts_shadow = world.core.get_casts_shadow(entity).is_some();

    if let Some(visibility) = world.core.get_visibility(entity) {
        scene_entity.components.visible = visibility.visible;
    }

    if let Some(emitter) = world.core.get_particle_emitter(entity) {
        scene_entity.components.particle_emitter =
            Some(super::particles::SceneParticleEmitter::from(emitter));
    }

    if let Some(decal) = world.core.get_decal(entity) {
        scene_entity.components.decal = Some(decal.clone());
    }

    if let Some(render_layer) = world.core.get_render_layer(entity) {
        scene_entity.components.render_layer = Some(*render_layer);
    }

    if let Some(text) = world.core.get_text(entity) {
        scene_entity.components.text = Some(text.clone());
        scene_entity.components.text_content = world
            .resources
            .text
            .cache
            .get_text(text.text_index)
            .map(str::to_string);
    }

    if let Some(navmesh_agent) = world.core.get_navmesh_agent(entity) {
        scene_entity.components.navmesh_agent = Some(navmesh_agent.clone());
    }

    if let Some(prefab_source) = world.core.get_prefab_source(entity) {
        scene_entity.components.prefab = Some(ScenePrefabInstance {
            prefab_uuid: prefab_source.source_uuid,
            prefab_path: prefab_source.source_path.clone(),
            prefab_name: if prefab_source.prefab_name.is_empty() {
                None
            } else {
                Some(prefab_source.prefab_name.clone())
            },
        });
    }
}

pub fn world_to_scene(world: &World, name: impl Into<String>) -> Scene {
    world_to_scene_with_uuids(world, name, &HashMap::new())
}

/// Same as `world_to_scene`, but lets the caller populate the scene's
/// typed game-side payloads via a `SceneSerializer`.
pub fn world_to_scene_with_serializer(
    world: &World,
    name: impl Into<String>,
    serializer: Option<&dyn crate::ecs::scene::SceneSerializer>,
) -> Scene {
    world_to_scene_with_uuids_and_serializer(world, name, &HashMap::new(), serializer)
}

/// Captures a single entity subtree (root + descendants) as a `Scene`,
/// suitable for writing as a `.nsprefab` file. Skips world-level state
/// like atmosphere, scene settings, navmesh, and joints that involve
/// entities outside the subtree — a prefab only owns its own entities.
/// The root entity in the resulting scene becomes the prefab's logical
/// root and has its parent stripped.
pub fn subtree_to_scene(world: &World, root: Entity, name: impl Into<String>) -> Scene {
    let mut scene = Scene::new(name);

    let mut entities = vec![root];
    entities.extend(crate::ecs::transform::queries::query_descendants(
        world, root,
    ));

    let mut entity_to_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
    for entity in &entities {
        entity_to_uuid.insert(*entity, AssetUuid::random());
    }

    for (index, entity) in entities.iter().copied().enumerate() {
        let uuid = entity_to_uuid[&entity];
        // The first entity is the subtree root; its parent in the world
        // is outside the prefab so it serializes as a root entity in the
        // prefab scene.
        let parent_uuid = if index == 0 {
            None
        } else {
            world
                .core
                .get_parent(entity)
                .and_then(|parent| parent.0)
                .and_then(|parent_entity| entity_to_uuid.get(&parent_entity).copied())
        };
        let scene_entity =
            entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &entity_to_uuid);
        scene.add_entity(scene_entity);
    }

    embed_referenced_meshes(world, &mut scene);
    embed_referenced_textures(world, &mut scene);
    capture_animation_targets(world, &mut scene, &entity_to_uuid);

    scene
}

/// Spawns a prefab scene as a subtree under `parent` (or as a root if
/// `parent` is None), remapping every uuid in the prefab so that
/// multiple instances of the same prefab don't collide. Returns the
/// root entities that were created.
pub fn spawn_prefab_scene(
    world: &mut World,
    prefab_scene: &Scene,
    parent: Option<Entity>,
) -> Result<SpawnSceneResult, SceneError> {
    // Mint a fresh uuid per entity in the prefab so the embedded uuids
    // never alias with whatever's already in the loaded map.
    let mut remap: HashMap<AssetUuid, AssetUuid> = HashMap::new();
    let mut remapped_scene = prefab_scene.clone();
    for scene_entity in &mut remapped_scene.entities {
        let new_uuid = AssetUuid::random();
        remap.insert(scene_entity.uuid, new_uuid);
        scene_entity.uuid = new_uuid;
    }
    for scene_entity in &mut remapped_scene.entities {
        if let Some(parent_uuid) = scene_entity.parent
            && let Some(&new_parent) = remap.get(&parent_uuid)
        {
            scene_entity.parent = Some(new_parent);
        }
    }
    for scene_entity in &mut remapped_scene.entities {
        if let Some(player) = scene_entity.components.animation_player.as_mut() {
            let original_bones = std::mem::take(&mut player.bone_name_to_node);
            for (bone_name, old_uuid) in original_bones {
                if let Some(&new_uuid) = remap.get(&old_uuid) {
                    player.bone_name_to_node.insert(bone_name, new_uuid);
                }
            }
            let original_nodes = std::mem::take(&mut player.node_index_to_node);
            for (node_index, old_uuid) in original_nodes {
                if let Some(&new_uuid) = remap.get(&old_uuid) {
                    player.node_index_to_node.insert(node_index, new_uuid);
                }
            }
        }
        // ScenePrefabInstance.prefab_uuid identifies the originating
        // .nsprefab file, not an in-scene entity, so it stays as-is
        // through the remap. The instance link should still point at
        // the source prefab after spawn.
    }
    remapped_scene.rebuild_uuid_index();
    remapped_scene.compute_spawn_order();

    let mut result = spawn_scene(world, &remapped_scene, None)?;

    for (source_uuid, new_uuid) in &remap {
        if let Some(entity) = result.uuid_to_entity.get(new_uuid) {
            result.entity_to_source_uuid.insert(*entity, *source_uuid);
        }
    }

    if let Some(parent) = parent {
        for &root_entity in &result.root_entities {
            world
                .core
                .add_components(root_entity, crate::ecs::world::PARENT);
            world.core.set_parent(
                root_entity,
                crate::ecs::world::components::Parent(Some(parent)),
            );
            world
                .resources
                .transform_state
                .children_cache
                .entry(parent)
                .or_default()
                .push(root_entity);
        }
    }

    Ok(result)
}

/// Builds a `Scene` from the current `World`, preserving the supplied
/// `Entity -> AssetUuid` mapping so callers (the editor) retain stable
/// entity identifiers across saves. Entities not present in the map
/// are assigned fresh UUIDs.
///
/// All meshes and source textures referenced by `RenderMesh` /
/// `InstancedMesh` components on exported entities are embedded into
/// `scene.embedded_meshes` / `embedded_textures` so the resulting file
/// is self-contained and game-loadable without external asset
/// dependencies.
pub fn world_to_scene_with_uuids(
    world: &World,
    name: impl Into<String>,
    stable_uuids: &HashMap<Entity, AssetUuid>,
) -> Scene {
    world_to_scene_with_uuids_and_serializer(world, name, stable_uuids, None)
}

pub fn world_to_scene_with_uuids_and_serializer(
    world: &World,
    name: impl Into<String>,
    stable_uuids: &HashMap<Entity, AssetUuid>,
    serializer: Option<&dyn crate::ecs::scene::SceneSerializer>,
) -> Scene {
    let mut scene = Scene::new(name);
    scene.atmosphere = world.resources.render_settings.atmosphere;
    scene.settings = capture_scene_settings(world);
    scene.next_guid = world.resources.entities.next_guid;

    let mut entity_to_uuid: HashMap<Entity, AssetUuid> = HashMap::new();
    let mut entities: Vec<Entity> = Vec::new();
    world
        .core
        .query()
        .with(LOCAL_TRANSFORM)
        .iter(|entity, _, _| {
            let uuid = stable_uuids
                .get(&entity)
                .copied()
                .unwrap_or_else(AssetUuid::random);
            entity_to_uuid.insert(entity, uuid);
            entities.push(entity);
        });

    for entity in &entities {
        let entity = *entity;
        let uuid = entity_to_uuid[&entity];

        let parent_uuid = world
            .core
            .get_parent(entity)
            .and_then(|p| p.0)
            .and_then(|parent_entity| entity_to_uuid.get(&parent_entity).copied());

        let scene_entity =
            entity_to_scene_entity_with_uuids(world, entity, uuid, parent_uuid, &entity_to_uuid);
        scene.add_entity(scene_entity);
    }

    embed_referenced_meshes(world, &mut scene);
    embed_referenced_textures(world, &mut scene);
    capture_animation_targets(world, &mut scene, &entity_to_uuid);

    #[cfg(feature = "physics")]
    super::commands_physics::export_scene_joints(world, &entity_to_uuid, &mut scene);

    #[cfg(feature = "navmesh")]
    if !world.resources.navmesh.triangles.is_empty() {
        scene.navmesh = Some(super::navigation::SceneNavMesh::from_navmesh_world(
            &world.resources.navmesh,
        ));
    }

    if let Some(serializer) = serializer {
        serializer.populate_scene(world, &entity_to_uuid, &mut scene);
    }

    scene
}

/// Walks the entities in `scene`, collects every unique mesh name they
/// reference (via `SceneMesh`/`SceneInstancedMesh`), and embeds the mesh
/// data from `world.resources.assets.mesh_cache` into `scene.embedded_meshes`.
/// Sets `mesh_uuid` on each component so loaders can route via UUID rather
/// than relying on shared `mesh_cache` state across runs.
pub fn embed_referenced_meshes(world: &World, scene: &mut Scene) {
    use crate::ecs::generational_registry::registry_entry_by_name;

    let mut name_to_uuid: HashMap<String, AssetUuid> = HashMap::new();

    for scene_entity in &mut scene.entities {
        if let Some(mesh) = scene_entity.components.mesh.as_mut()
            && let Some(mesh_name) = mesh.mesh_name.clone()
        {
            let uuid = *name_to_uuid
                .entry(mesh_name.clone())
                .or_insert_with(AssetUuid::random);
            mesh.mesh_uuid = Some(uuid);
        }
        if let Some(instanced) = scene_entity.components.instanced_mesh.as_mut()
            && let Some(mesh_name) = instanced.mesh_name.clone()
        {
            let uuid = *name_to_uuid
                .entry(mesh_name.clone())
                .or_insert_with(AssetUuid::random);
            instanced.mesh_uuid = Some(uuid);
        }
    }

    for (mesh_name, uuid) in name_to_uuid {
        if scene.embedded_meshes.contains_key(&uuid) {
            continue;
        }
        if let Some(mesh) =
            registry_entry_by_name(&world.resources.assets.mesh_cache.registry, &mesh_name)
        {
            scene.embedded_meshes.insert(
                uuid,
                super::mesh::EmbeddedMesh::new(mesh_name, mesh.clone()),
            );
        }
    }
}

/// Mirrors `AnimationPlayer.bone_name_to_entity` and `node_index_to_entity`
/// runtime maps into the matching `SceneAnimationPlayer.bone_name_to_node`
/// and `node_index_to_node` fields, swapping `Entity` for `AssetUuid`.
/// Without this, animations reload with empty target-entity maps and
/// channels go nowhere — both skeletal (bone_name) and non-skeletal
/// (node_index) channels need their target entities tracked across
/// save and load.
fn capture_animation_targets(
    world: &World,
    scene: &mut Scene,
    entity_to_uuid: &HashMap<Entity, AssetUuid>,
) {
    let uuid_to_scene_index: HashMap<AssetUuid, usize> = scene
        .entities
        .iter()
        .enumerate()
        .map(|(index, entity)| (entity.uuid, index))
        .collect();

    for (entity, uuid) in entity_to_uuid {
        let Some(player) = world.core.get_animation_player(*entity) else {
            continue;
        };
        if player.bone_name_to_entity.is_empty() && player.node_index_to_entity.is_empty() {
            continue;
        }
        let Some(&scene_index) = uuid_to_scene_index.get(uuid) else {
            continue;
        };
        let Some(scene_player) = scene.entities[scene_index]
            .components
            .animation_player
            .as_mut()
        else {
            continue;
        };
        for (bone_name, bone_entity) in &player.bone_name_to_entity {
            if let Some(&bone_uuid) = entity_to_uuid.get(bone_entity) {
                scene_player
                    .bone_name_to_node
                    .insert(bone_name.clone(), bone_uuid);
            }
        }
        for (&node_index, node_entity) in &player.node_index_to_entity {
            if let Some(&node_uuid) = entity_to_uuid.get(node_entity) {
                scene_player
                    .node_index_to_node
                    .insert(node_index, node_uuid);
            }
        }
    }
}

/// Walks materials referenced by `SceneMesh`/`SceneInstancedMesh` and
/// embeds the source bytes of every texture they reference, pulling
/// from `world.resources.assets.texture_sources` (populated at import
/// time). Each texture is embedded under a fresh UUID and keeps its
/// original name so material references resolve after load.
pub fn embed_referenced_textures(world: &World, scene: &mut Scene) {
    use crate::ecs::asset_state::TextureSourceData;
    use crate::ecs::scene::audio::EmbeddedTexture;

    let mut texture_names: std::collections::HashSet<String> = std::collections::HashSet::new();
    for scene_entity in &scene.entities {
        if let Some(mesh) = scene_entity.components.mesh.as_ref()
            && let Some(material) = mesh.material.as_ref()
        {
            texture_names.extend(material.texture_names().map(|name| name.to_string()));
        }
        if let Some(instanced) = scene_entity.components.instanced_mesh.as_ref()
            && let Some(material) = instanced.material.as_ref()
        {
            texture_names.extend(material.texture_names().map(|name| name.to_string()));
        }
    }

    for texture_name in texture_names {
        let Some(source) = world.resources.assets.texture_sources.get(&texture_name) else {
            continue;
        };
        let base = match &source.data {
            TextureSourceData::Png(bytes) => {
                EmbeddedTexture::from_png(&texture_name, bytes.clone())
            }
            TextureSourceData::Rgba {
                rgba,
                width,
                height,
            } => EmbeddedTexture::from_rgba(&texture_name, rgba.clone(), *width, *height),
        };
        let embedded = base.with_usage(source.usage).with_sampler(source.sampler);
        scene
            .embedded_textures
            .insert(AssetUuid::random(), embedded);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::ecs::asset_state::TextureSourceBytes;
    use crate::ecs::material::components::Material;
    use crate::ecs::material::resources::material_registry_insert;
    use crate::ecs::world::commands::spawn_mesh_at;
    use crate::ecs::world::components::MaterialRef;
    use crate::ecs::world::{MATERIAL_REF, World};

    /// Textures imported into the world surface in `embedded_textures`
    /// when world_to_scene runs, keyed by a fresh UUID and tagged with
    /// the original texture name. After decoding the scene back into a
    /// fresh world, the texture is registered in `texture_cache` under
    /// that name so material lookups resolve.
    #[test]
    fn binary_round_trip_preserves_referenced_textures() {
        let mut source_world = World::default();
        let entity = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );

        // Pretend an importer stashed source bytes for this texture.
        source_world.resources.assets.texture_sources.insert(
            "test_diffuse".to_string(),
            TextureSourceBytes {
                data: crate::ecs::asset_state::TextureSourceData::Rgba {
                    rgba: vec![255u8; 4],
                    width: 1,
                    height: 1,
                },
                usage: crate::render::wgpu::texture_cache::TextureUsage::Color,
                sampler: crate::render::wgpu::texture_cache::SamplerSettings::DEFAULT,
            },
        );

        // Install a material that references the texture and bind it
        // to the entity.
        let material = Material {
            base_texture: Some("test_diffuse".to_string()),
            ..Default::default()
        };
        material_registry_insert(
            &mut source_world.resources.assets.material_registry,
            "TestMaterial".to_string(),
            material,
        );
        source_world.core.add_components(entity, MATERIAL_REF);
        source_world
            .core
            .set_material_ref(entity, MaterialRef::new("TestMaterial"));

        let mut scene = world_to_scene(&source_world, "TextureRoundTrip");
        assert_eq!(
            scene.embedded_textures.len(),
            1,
            "world_to_scene must embed referenced textures"
        );
        let embedded = scene
            .embedded_textures
            .values()
            .next()
            .expect("texture must be embedded");
        assert_eq!(embedded.name, "test_diffuse");

        let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");
        let decoded_texture = decoded
            .embedded_textures
            .values()
            .next()
            .expect("texture must survive bincode round-trip");
        assert_eq!(decoded_texture.name, "test_diffuse");
        assert!(decoded_texture.to_rgba().is_some());

        let mut destination_world = World::default();
        spawn_scene(&mut destination_world, &decoded, None).expect("spawn");
        // After spawn_scene queues the texture, the texture_cache should
        // have reserved an id under the original name.
        assert!(
            destination_world
                .resources
                .texture_cache
                .registry
                .name_to_index
                .contains_key("test_diffuse"),
            "embedded texture must land in destination texture_cache under its original name"
        );
    }

    /// End-to-end sanity check: build a world with referenced meshes,
    /// export it to a binary scene blob, decode it into a fresh world,
    /// and confirm the mesh data, entity references, transforms, names,
    /// and tags survived the round trip. This is the regression test
    /// that proves "game writes a map; editor / fresh game can load it".
    #[test]
    fn binary_round_trip_preserves_meshes_and_entities() {
        let mut source_world = World::default();
        let entity = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(1.0, 2.0, 3.0),
            Vec3::new(1.5, 1.5, 1.5),
        );
        source_world.resources.entities.tags.insert(
            entity,
            vec!["round_trip_marker".to_string(), "player_spawn".to_string()],
        );

        let mut scene = world_to_scene(&source_world, "RoundTrip");
        assert!(
            !scene.embedded_meshes.is_empty(),
            "world_to_scene must embed referenced meshes"
        );
        let embedded_cube_uuid = scene
            .embedded_meshes
            .iter()
            .find(|(_, mesh)| mesh.name == "Cube")
            .map(|(uuid, _)| *uuid)
            .expect("Cube mesh must be embedded under its original name");
        assert!(
            scene
                .entities
                .iter()
                .any(
                    |entity| entity.components.mesh.as_ref().is_some_and(|mesh| {
                        mesh.mesh_name.as_deref() == Some("Cube")
                            && mesh.mesh_uuid == Some(embedded_cube_uuid)
                    })
                )
        );

        let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");

        assert_eq!(decoded.embedded_meshes.len(), 1);
        let decoded_cube = decoded
            .embedded_meshes
            .get(&embedded_cube_uuid)
            .expect("Cube mesh must survive bincode round-trip");
        assert_eq!(decoded_cube.name, "Cube");
        assert!(!decoded_cube.mesh.vertices.is_empty());
        assert!(!decoded_cube.mesh.indices.is_empty());

        let mut destination_world = World::default();
        let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
        assert_eq!(result.uuid_to_entity.len(), 1);
        assert!(
            destination_world
                .resources
                .assets
                .mesh_cache
                .registry
                .name_to_index
                .contains_key("Cube"),
            "embedded mesh must land in destination mesh_cache before entities spawn"
        );

        let spawned_entity = *result
            .uuid_to_entity
            .values()
            .next()
            .expect("spawned entity");
        let render_mesh = destination_world
            .core
            .get_render_mesh(spawned_entity)
            .expect("entity must have RenderMesh after spawn_scene");
        assert_eq!(render_mesh.name, "Cube");
        let transform = destination_world
            .core
            .get_local_transform(spawned_entity)
            .expect("entity must have LocalTransform");
        assert_eq!(transform.translation, Vec3::new(1.0, 2.0, 3.0));
        assert_eq!(transform.scale, Vec3::new(1.5, 1.5, 1.5));
        let tags = destination_world
            .resources
            .entities
            .tags
            .get(&spawned_entity)
            .expect("tags must survive round-trip");
        assert!(tags.contains(&"round_trip_marker".to_string()));
        assert!(tags.contains(&"player_spawn".to_string()));
    }

    /// Typed game-side payloads attached via `SceneSerializer` /
    /// `SceneDeserializer` survive both binary and JSON scene
    /// round-trips. Proves the typed-JSON channel decodes back to
    /// the exact same struct without engine knowledge of its type.
    #[test]
    fn typed_components_round_trip_through_scene() {
        use crate::ecs::scene::{
            Persistence, SceneDeserializer, SceneSerializer, TypedComponent, TypedPayload,
            TypedResource,
        };
        use serde::{Deserialize, Serialize};
        use std::collections::HashMap;

        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
        struct TestPayload {
            label: String,
            amplitude: f32,
            offsets: [f32; 3],
        }

        #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
        struct TestResource {
            score: u32,
            note: String,
        }

        struct CaptureSerializer {
            entity_payload: HashMap<Entity, TestPayload>,
            resource_payload: TestResource,
        }

        impl SceneSerializer for CaptureSerializer {
            fn populate_scene(
                &self,
                _world: &World,
                entity_uuids: &HashMap<Entity, AssetUuid>,
                scene: &mut Scene,
            ) {
                for (entity, payload) in &self.entity_payload {
                    let Some(uuid) = entity_uuids.get(entity) else {
                        continue;
                    };
                    let Some(scene_entity) = scene.find_entity_mut(*uuid) else {
                        continue;
                    };
                    scene_entity
                        .components
                        .game_components
                        .push(TypedComponent {
                            name: "test_payload".to_string(),
                            data: TypedPayload(
                                serde_json::to_value(payload).expect("serialize entity payload"),
                            ),
                            persistence: Persistence::Both,
                            field_overrides: Vec::new(),
                        });
                }
                scene.game_resources.push(TypedResource {
                    name: "test_resource".to_string(),
                    data: TypedPayload(
                        serde_json::to_value(&self.resource_payload)
                            .expect("serialize resource payload"),
                    ),
                    persistence: Persistence::Runtime,
                });
            }
        }

        struct CaptureDeserializer {
            entity_payloads: HashMap<Entity, TestPayload>,
            resource_payload: Option<TestResource>,
        }

        impl SceneDeserializer for CaptureDeserializer {
            fn populate_world(
                &mut self,
                _world: &mut World,
                uuid_to_entity: &HashMap<AssetUuid, Entity>,
                scene: &Scene,
            ) {
                for scene_entity in &scene.entities {
                    let Some(&entity) = uuid_to_entity.get(&scene_entity.uuid) else {
                        continue;
                    };
                    for typed in &scene_entity.components.game_components {
                        if typed.name == "test_payload" {
                            let payload: TestPayload = serde_json::from_value(typed.data.0.clone())
                                .expect("deserialize entity payload");
                            self.entity_payloads.insert(entity, payload);
                        }
                    }
                }
                for resource in &scene.game_resources {
                    if resource.name == "test_resource" {
                        self.resource_payload = Some(
                            serde_json::from_value(resource.data.0.clone())
                                .expect("deserialize resource payload"),
                        );
                    }
                }
            }
        }

        let payload = TestPayload {
            label: "north_orb".to_string(),
            amplitude: 0.42,
            offsets: [1.0, -2.5, 3.75],
        };
        let resource = TestResource {
            score: 7,
            note: "checkpoint".to_string(),
        };

        for binary in [false, true] {
            let mut source_world = World::default();
            let entity = spawn_mesh_at(
                &mut source_world,
                "Cube",
                Vec3::new(0.0, 0.0, 0.0),
                Vec3::new(1.0, 1.0, 1.0),
            );

            let mut entity_payload = HashMap::new();
            entity_payload.insert(entity, payload.clone());
            let serializer = CaptureSerializer {
                entity_payload,
                resource_payload: resource.clone(),
            };

            let mut scene =
                world_to_scene_with_serializer(&source_world, "TypedRoundTrip", Some(&serializer));

            let decoded = if binary {
                let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
                load_scene_binary_from_bytes(&encoded).expect("decode scene")
            } else {
                let encoded = serde_json::to_vec(&scene).expect("encode json");
                let mut parsed: Scene = serde_json::from_slice(&encoded).expect("decode json");
                parsed.rebuild_uuid_index();
                parsed
            };

            let mut destination_world = World::default();
            let mut deserializer = CaptureDeserializer {
                entity_payloads: HashMap::new(),
                resource_payload: None,
            };
            let result = spawn_scene_with_deserializer(
                &mut destination_world,
                &decoded,
                None,
                Some(&mut deserializer),
            )
            .expect("spawn scene");
            let spawned_entity = *result
                .uuid_to_entity
                .values()
                .next()
                .expect("spawned entity");

            assert_eq!(
                deserializer.entity_payloads.get(&spawned_entity),
                Some(&payload),
                "entity payload must survive {} round-trip",
                if binary { "binary" } else { "json" }
            );
            assert_eq!(
                deserializer.resource_payload.as_ref(),
                Some(&resource),
                "resource payload must survive {} round-trip",
                if binary { "binary" } else { "json" }
            );
        }
    }

    /// Component descriptors registered through `register_component_descriptor`
    /// are findable by name via `component_descriptor` and round-trip
    /// every field with their kind tag, giving an editor inspector
    /// enough to render a widget per registered field without baking
    /// game-specific schemas into the engine.
    #[test]
    fn component_descriptors_register_and_lookup() {
        use crate::ecs::scene::{
            ComponentDescriptor, FieldDescriptor, FieldKind, Persistence, component_descriptor,
            register_component_descriptor,
        };

        let mut world = World::default();
        register_component_descriptor(
            &mut world,
            ComponentDescriptor {
                name: "door".to_string(),
                display_name: Some("Door".to_string()),
                persistence: Persistence::Both,
                fields: vec![
                    FieldDescriptor {
                        name: "required_key".to_string(),
                        kind: FieldKind::Bool,
                        display_name: Some("Requires Key".to_string()),
                    },
                    FieldDescriptor {
                        name: "open_y".to_string(),
                        kind: FieldKind::F32,
                        display_name: None,
                    },
                ],
            },
        );

        let descriptor =
            component_descriptor(&world, "door").expect("door descriptor must be findable");
        assert_eq!(descriptor.name, "door");
        assert_eq!(descriptor.persistence, Persistence::Both);
        assert_eq!(descriptor.fields.len(), 2);
        assert_eq!(descriptor.fields[0].name, "required_key");
        assert_eq!(descriptor.fields[0].kind, FieldKind::Bool);
        assert_eq!(descriptor.fields[1].name, "open_y");
        assert_eq!(descriptor.fields[1].kind, FieldKind::F32);
        assert!(component_descriptor(&world, "unknown").is_none());
    }

    /// Field-level overrides layered on top of an authored payload
    /// produce the merged value on the receiving side. Reuses the
    /// `FieldOverride` shape that `PrefabOverrides` already exposes,
    /// proving the typed-component channel and prefab overrides
    /// share one delta mechanism.
    #[test]
    fn field_overrides_layer_onto_authored_payload() {
        use crate::ecs::prefab::components::FieldOverride;
        use crate::ecs::scene::{Persistence, TypedComponent, TypedPayload, apply_field_overrides};

        let authored = serde_json::json!({
            "kind": "Lever",
            "credit_reward": 15,
            "activated": false,
            "lever_progress": 0.0,
        });

        let runtime_overrides = vec![
            FieldOverride {
                field_name: "activated".to_string(),
                value: TypedPayload(serde_json::Value::Bool(true)),
            },
            FieldOverride {
                field_name: "lever_progress".to_string(),
                value: TypedPayload(serde_json::json!(0.75)),
            },
        ];

        let component = TypedComponent {
            name: "world_prop".to_string(),
            data: TypedPayload(authored.clone()),
            persistence: Persistence::Both,
            field_overrides: runtime_overrides,
        };

        for binary in [false, true] {
            let decoded: TypedComponent = if binary {
                let encoded = bincode::serialize(&component).expect("encode component");
                bincode::deserialize(&encoded).expect("decode component")
            } else {
                let encoded = serde_json::to_string(&component).expect("encode component");
                serde_json::from_str(&encoded).expect("decode component")
            };

            let merged = apply_field_overrides(&decoded.data.0, &decoded.field_overrides);
            assert_eq!(merged["kind"], serde_json::json!("Lever"));
            assert_eq!(merged["credit_reward"], serde_json::json!(15));
            assert_eq!(merged["activated"], serde_json::json!(true));
            assert_eq!(merged["lever_progress"], serde_json::json!(0.75));
        }
    }

    #[test]
    fn guid_round_trips_through_scene() {
        use crate::ecs::entity_registry::{ensure_guid, find_entity_by_guid};

        let mut source_world = World::default();
        let entity = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        let original_guid = ensure_guid(&mut source_world, entity);
        let original_next = source_world.resources.entities.next_guid;
        assert_eq!(original_guid.0, original_next);

        let mut scene = world_to_scene(&source_world, "GuidRoundTrip");
        assert_eq!(scene.next_guid, original_next);
        let scene_entity = scene
            .entities
            .iter()
            .find(|scene_entity| scene_entity.guid == Some(original_guid.0))
            .expect("guid must be written into scene");
        let _ = scene_entity;

        let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");

        let mut destination_world = World::default();
        let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
        let spawned_entity = *result
            .uuid_to_entity
            .values()
            .next()
            .expect("spawned entity");

        let restored = destination_world
            .core
            .get_guid(spawned_entity)
            .copied()
            .expect("guid component must round-trip");
        assert_eq!(restored, original_guid);
        assert_eq!(
            destination_world.resources.entities.next_guid,
            original_next
        );
        assert_eq!(
            find_entity_by_guid(&destination_world, original_guid),
            Some(spawned_entity)
        );
    }

    #[test]
    fn text_content_round_trips_through_scene() {
        use crate::ecs::text::components::Text;
        use crate::ecs::world::{
            GLOBAL_TRANSFORM, LOCAL_TRANSFORM, LOCAL_TRANSFORM_DIRTY, TEXT, VISIBILITY,
        };

        let mut source_world = World::default();
        let content = "Welcome to Nightshade";
        let text_index = source_world.resources.text.cache.add_text(content);
        let entity = spawn_entities(
            &mut source_world,
            TEXT | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | VISIBILITY,
            1,
        )[0];
        source_world.core.set_text(
            entity,
            Text {
                text_index,
                billboard: true,
                ..Default::default()
            },
        );

        let mut scene = world_to_scene(&source_world, "TextRoundTrip");
        let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode scene");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode scene");

        let mut destination_world = World::default();
        let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn scene");
        let spawned_entity = *result
            .uuid_to_entity
            .values()
            .next()
            .expect("spawned entity");
        let restored_text = destination_world
            .core
            .get_text(spawned_entity)
            .expect("text component must survive round-trip");
        let restored_content = destination_world
            .resources
            .text
            .cache
            .get_text(restored_text.text_index)
            .expect("text cache must hold the restored content");
        assert_eq!(restored_content, content);
    }

    /// PrefabSource on an entity round-trips through a map save: the
    /// source uuid and path survive encode/decode and the entity gets
    /// the component back when the map is spawned into a fresh world.
    /// This is the link the editor relies on to track which prefab an
    /// instance came from.
    #[test]
    fn prefab_source_link_survives_map_round_trip() {
        use crate::ecs::prefab::components::PrefabSource;
        use crate::ecs::world::PREFAB_SOURCE;

        let mut source_world = World::default();
        let entity = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        let prefab_uuid = AssetUuid::random();
        source_world.core.add_components(entity, PREFAB_SOURCE);
        source_world.core.set_prefab_source(
            entity,
            PrefabSource {
                prefab_name: "Dancer".to_string(),
                source_path: Some("prefabs/dancer.nsprefab".to_string()),
                source_uuid: Some(prefab_uuid),
            },
        );

        let mut scene = world_to_scene(&source_world, "MapWithPrefab");
        // The save side must have written ScenePrefabInstance.
        let scene_entity = scene
            .entities
            .iter()
            .find(|entity| entity.components.prefab.is_some())
            .expect("entity with prefab source must produce ScenePrefabInstance");
        let scene_prefab = scene_entity.components.prefab.as_ref().unwrap();
        assert_eq!(scene_prefab.prefab_uuid, Some(prefab_uuid));
        assert_eq!(
            scene_prefab.prefab_path.as_deref(),
            Some("prefabs/dancer.nsprefab")
        );

        let encoded = save_scene_binary_to_bytes(&mut scene).expect("encode");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");

        let mut destination_world = World::default();
        let result = spawn_scene(&mut destination_world, &decoded, None).expect("spawn");

        let spawned_entity = *result
            .uuid_to_entity
            .values()
            .next()
            .expect("entity must spawn");
        let attached = destination_world
            .core
            .get_prefab_source(spawned_entity)
            .expect("PrefabSource must be attached after .nsprefab round-trip");
        assert_eq!(attached.source_uuid, Some(prefab_uuid));
        assert_eq!(
            attached.source_path.as_deref(),
            Some("prefabs/dancer.nsprefab")
        );
        assert_eq!(attached.prefab_name, "Dancer");
    }

    /// Capturing a subtree as a prefab scene and spawning it twice into
    /// a fresh world produces two independent entity trees with
    /// non-aliasing uuids, the same mesh data, and survives binary
    /// encode/decode.
    #[test]
    fn prefab_subtree_round_trip_spawns_multiple_instances() {
        let mut source_world = World::default();
        let root = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );

        let mut prefab_scene = subtree_to_scene(&source_world, root, "test_prefab");
        assert_eq!(prefab_scene.entities.len(), 1);
        assert!(!prefab_scene.embedded_meshes.is_empty());

        // Round-trip the prefab through binary encode.
        let encoded = save_scene_binary_to_bytes(&mut prefab_scene).expect("encode");
        let decoded_prefab = load_scene_binary_from_bytes(&encoded).expect("decode");

        let mut destination_world = World::default();
        let first = spawn_prefab_scene(&mut destination_world, &decoded_prefab, None)
            .expect("first instance");
        let second = spawn_prefab_scene(&mut destination_world, &decoded_prefab, None)
            .expect("second instance");

        // Each instance has its own freshly minted uuids that do not
        // collide with each other.
        for first_uuid in first.uuid_to_entity.keys() {
            assert!(
                !second.uuid_to_entity.contains_key(first_uuid),
                "prefab instances must mint fresh uuids"
            );
        }
        assert_eq!(first.root_entities.len(), 1);
        assert_eq!(second.root_entities.len(), 1);
        assert_ne!(first.root_entities[0], second.root_entities[0]);

        // Mesh was registered in the destination's cache before spawn.
        assert!(
            destination_world
                .resources
                .assets
                .mesh_cache
                .registry
                .name_to_index
                .contains_key("Cube"),
        );
    }

    /// A prefab that contains another prefab instance as a descendant
    /// must preserve the inner PrefabSource link after save and reload
    /// through subtree_to_scene + spawn_prefab_scene. Without this, the
    /// refresh-from-source workflow can't trace inner instances back to
    /// their authoring file.
    #[test]
    fn nested_prefab_source_survives_subtree_round_trip() {
        use crate::ecs::prefab::components::PrefabSource;
        use crate::ecs::world::PARENT;
        use crate::ecs::world::PREFAB_SOURCE;
        use crate::ecs::world::components::Parent;

        let mut source_world = World::default();
        let outer_root = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        let inner = spawn_mesh_at(
            &mut source_world,
            "Cube",
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 1.0),
        );
        source_world.core.add_components(inner, PARENT);
        source_world
            .core
            .set_parent(inner, Parent(Some(outer_root)));

        let inner_prefab_uuid = AssetUuid::random();
        source_world.core.add_components(inner, PREFAB_SOURCE);
        source_world.core.set_prefab_source(
            inner,
            PrefabSource {
                prefab_name: "Door".to_string(),
                source_path: Some("prefabs/door.nsprefab".to_string()),
                source_uuid: Some(inner_prefab_uuid),
            },
        );

        let mut prefab_scene = subtree_to_scene(&source_world, outer_root, "outer_prefab");
        let encoded = save_scene_binary_to_bytes(&mut prefab_scene).expect("encode");
        let decoded = load_scene_binary_from_bytes(&encoded).expect("decode");

        let mut destination_world = World::default();
        let result =
            spawn_prefab_scene(&mut destination_world, &decoded, None).expect("spawn outer");

        let inner_descendants: Vec<Entity> = destination_world
            .core
            .query_entities(PREFAB_SOURCE)
            .collect();
        let inner_match = inner_descendants
            .iter()
            .find_map(|entity| {
                destination_world
                    .core
                    .get_prefab_source(*entity)
                    .filter(|source| source.source_uuid == Some(inner_prefab_uuid))
                    .map(|source| (*entity, source.clone()))
            })
            .expect("nested PrefabSource must survive subtree round trip");
        assert!(
            !result.root_entities.contains(&inner_match.0),
            "inner instance should remain a descendant, not become a root"
        );
        assert_eq!(inner_match.1.prefab_name, "Door");
        assert_eq!(
            inner_match.1.source_path.as_deref(),
            Some("prefabs/door.nsprefab")
        );
    }
}
