use crate::ecs::EditorWorld;
use crate::systems::retained_ui::UiHandles;
use nightshade::ecs::ui::components::DragPayload;
use nightshade::ecs::world::PREFAB_SOURCE;
use nightshade::prelude::*;
use std::collections::HashMap;
use std::path::PathBuf;

const PREFAB_BROWSER_RECT: Rect = Rect {
    min: Vec2::new(200.0, 200.0),
    max: Vec2::new(640.0, 620.0),
};

const MAX_PREFAB_ROWS: usize = 64;
const DRAG_PREVIEW_HOVER_HEIGHT: f32 = 0.4;

#[derive(Clone, Default)]
pub struct PrefabRow {
    pub prefab_name: String,
    pub library_path: Option<PathBuf>,
    pub thumbnail_path: Option<PathBuf>,
    pub thumbnail: Entity,
    pub label: Entity,
    pub select_button: Entity,
    pub spawn_button: Entity,
    pub refresh_button: Entity,
}

#[derive(Default, Clone)]
pub struct PrefabBrowserHandles {
    pub panel: Entity,
    pub status_label: Entity,
    pub focus_label: Entity,
    pub list_rows: Vec<PrefabRow>,
    pub scan_button: Entity,
    pub library: Vec<PathBuf>,
    pub last_find_name: Option<String>,
    pub find_cycle_index: usize,
    pub drag_state: Option<PrefabDragState>,
}

#[derive(Clone)]
pub struct PrefabDragState {
    pub prefab_name: String,
    pub library_path: Option<PathBuf>,
    pub source_entity: Entity,
    pub preview_root: Option<Entity>,
    pub ghost_material_swaps: Vec<GhostMaterialSwap>,
}

#[derive(Clone)]
pub struct GhostMaterialSwap {
    pub entity: Entity,
    pub original_name: String,
}

pub fn build(tree: &mut UiTreeBuilder) -> PrefabBrowserHandles {
    let panel = tree.add_floating_panel("prefab_browser", "Prefabs", PREFAB_BROWSER_RECT);
    ui_set_visible(tree.world_mut(), panel, false);
    let content = super::panel_content(tree, panel);
    let mut status_label = Entity::default();
    let mut focus_label = Entity::default();
    let mut scan_button = Entity::default();
    let mut list_rows: Vec<PrefabRow> = Vec::with_capacity(MAX_PREFAB_ROWS);

    tree.in_parent(content, |tree| {
        let theme = tree
            .world_mut()
            .resources
            .retained_ui
            .theme_state
            .active_theme();
        let font = theme.font_size;
        let dim = vec4(0.7, 0.7, 0.75, 1.0);

        scan_button = tree
            .add_node()
            .size(100.pct(), (24.0).px())
            .with_text("Scan directory for *.nsprefab...", font * 0.85)
            .text_center()
            .color_raw::<UiBase>(vec4(0.6, 0.85, 1.0, 1.0))
            .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
            .with_interaction()
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .entity();
        status_label = tree
            .add_node()
            .size(100.pct(), (18.0).px())
            .with_text("0 prefabs", font * 0.85)
            .text_left()
            .color_raw::<UiBase>(dim)
            .entity();
        focus_label = tree
            .add_node()
            .size(100.pct(), (18.0).px())
            .with_text("Click an entry to select its first instance.", font * 0.8)
            .text_left()
            .color_raw::<UiBase>(vec4(0.85, 0.85, 0.6, 1.0))
            .entity();

        let wrapper = tree.add_node().fill_width().flex_grow(1.0).entity();
        let scroll = tree.in_parent(wrapper, |tree| tree.add_scroll_area_fill(4.0, 4.0));
        let list_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
            .map(|d| d.content_entity)
            .unwrap_or(scroll);
        tree.in_parent(list_root, |tree| {
            for row_index in 0..MAX_PREFAB_ROWS {
                let row = tree
                    .add_node()
                    .size(100.pct(), (24.0).px())
                    .flow_with_alignment(
                        FlowDirection::Horizontal,
                        0.0,
                        6.0,
                        FlowAlignment::Start,
                        FlowAlignment::Center,
                    )
                    .with_visible(false)
                    .entity();
                let mut label = Entity::default();
                let mut select_button = Entity::default();
                let mut spawn_button = Entity::default();
                let mut refresh_button = Entity::default();
                let mut thumbnail = Entity::default();
                tree.in_parent(row, |tree| {
                    thumbnail = tree
                        .add_node()
                        .flow_child(Ab(vec2(22.0, 22.0)))
                        .color_raw::<UiBase>(vec4(0.18, 0.18, 0.2, 1.0))
                        .with_drag_source(DragPayload::Index(row_index))
                        .with_cursor_icon(winit::window::CursorIcon::Grab)
                        .entity();
                    label = tree
                        .add_node()
                        .flow_child(Ab(vec2(0.0, 20.0)))
                        .flex_grow(1.0)
                        .with_text("", font * 0.85)
                        .text_left()
                        .color_raw::<UiBase>(vec4(0.9, 0.9, 0.9, 1.0))
                        .entity();
                    spawn_button = tree
                        .add_node()
                        .flow_child(Ab(vec2(70.0, 22.0)))
                        .with_text("Spawn", font * 0.8)
                        .text_center()
                        .color_raw::<UiBase>(vec4(0.7, 1.0, 0.6, 1.0))
                        .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
                        .with_interaction()
                        .with_cursor_icon(winit::window::CursorIcon::Pointer)
                        .entity();
                    refresh_button = tree
                        .add_node()
                        .flow_child(Ab(vec2(70.0, 22.0)))
                        .with_text("Refresh", font * 0.8)
                        .text_center()
                        .color_raw::<UiBase>(vec4(1.0, 0.85, 0.55, 1.0))
                        .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
                        .with_interaction()
                        .with_cursor_icon(winit::window::CursorIcon::Pointer)
                        .entity();
                    select_button = tree
                        .add_node()
                        .flow_child(Ab(vec2(100.0, 22.0)))
                        .with_text("Find instance", font * 0.8)
                        .text_center()
                        .color_raw::<UiBase>(vec4(0.65, 0.8, 1.0, 1.0))
                        .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
                        .with_interaction()
                        .with_cursor_icon(winit::window::CursorIcon::Pointer)
                        .entity();
                });
                list_rows.push(PrefabRow {
                    prefab_name: String::new(),
                    library_path: None,
                    thumbnail_path: None,
                    thumbnail,
                    label,
                    select_button,
                    spawn_button,
                    refresh_button,
                });
            }
        });
    });

    PrefabBrowserHandles {
        panel,
        status_label,
        focus_label,
        list_rows,
        scan_button,
        library: Vec::new(),
        last_find_name: None,
        find_cycle_index: 0,
        drag_state: None,
    }
}

pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
    let select_buttons: Vec<(Entity, String)> = handles
        .prefab_browser
        .list_rows
        .iter()
        .map(|row| (row.select_button, row.prefab_name.clone()))
        .collect();
    let spawn_buttons: Vec<(Entity, String, Option<PathBuf>)> = handles
        .prefab_browser
        .list_rows
        .iter()
        .map(|row| {
            (
                row.spawn_button,
                row.prefab_name.clone(),
                row.library_path.clone(),
            )
        })
        .collect();
    let refresh_buttons: Vec<(Entity, String)> = handles
        .prefab_browser
        .list_rows
        .iter()
        .map(|row| (row.refresh_button, row.prefab_name.clone()))
        .collect();
    let scan_button = handles.prefab_browser.scan_button;
    let row_info: Vec<(Entity, String, Option<PathBuf>)> = handles
        .prefab_browser
        .list_rows
        .iter()
        .map(|row| {
            (
                row.thumbnail,
                row.prefab_name.clone(),
                row.library_path.clone(),
            )
        })
        .collect();
    let mut select_request: Option<String> = None;
    let mut spawn_request: Option<(String, Option<PathBuf>)> = None;
    let mut refresh_request: Option<String> = None;
    let mut scan_request = false;
    let mut drag_start_request: Option<(Entity, String, Option<PathBuf>)> = None;
    let mut drag_release_request: Option<Entity> = None;
    for event in ui_events(world) {
        match event {
            UiEvent::ButtonClicked(entity) => {
                if *entity == scan_button {
                    scan_request = true;
                    continue;
                }
                for (button, prefab_name) in &select_buttons {
                    if *entity == *button && !prefab_name.is_empty() {
                        select_request = Some(prefab_name.clone());
                        break;
                    }
                }
                for (button, prefab_name, path) in &spawn_buttons {
                    if *entity == *button {
                        spawn_request = Some((prefab_name.clone(), path.clone()));
                        break;
                    }
                }
                for (button, prefab_name) in &refresh_buttons {
                    if *entity == *button && !prefab_name.is_empty() {
                        refresh_request = Some(prefab_name.clone());
                        break;
                    }
                }
            }
            UiEvent::DragStarted {
                source,
                payload: DragPayload::Index(index),
            } => {
                if let Some((thumbnail, prefab_name, library_path)) = row_info.get(*index)
                    && *thumbnail == *source
                    && !prefab_name.is_empty()
                {
                    drag_start_request =
                        Some((*thumbnail, prefab_name.clone(), library_path.clone()));
                }
            }
            UiEvent::DragCancelled { source } | UiEvent::DragDropped { source, .. } => {
                drag_release_request = Some(*source);
            }
            _ => {}
        }
    }

    if let Some(source) = drag_release_request {
        finalize_drag(editor_world, world, source);
    }

    if let Some((source_entity, prefab_name, library_path)) = drag_start_request {
        begin_drag(editor_world, source_entity, prefab_name, library_path);
    }

    if scan_request {
        scan_prefab_directory(editor_world);
        return;
    }

    if let Some(prefab_name) = refresh_request {
        refresh_instances(editor_world, world, &prefab_name);
        return;
    }

    if let Some((prefab_name, library_path)) = spawn_request {
        if let Some(path) = library_path {
            crate::systems::project_io::import_prefab_from_path(editor_world, world, &path);
        } else if !prefab_name.is_empty() {
            spawn_cached_prefab(editor_world, world, &prefab_name);
        }
        return;
    }

    let Some(prefab_name) = select_request else {
        return;
    };

    let matches: Vec<Entity> = world
        .core
        .query_entities(PREFAB_SOURCE)
        .filter(|entity| {
            world
                .core
                .get_prefab_source(*entity)
                .map(|prefab| prefab.prefab_name == prefab_name)
                .unwrap_or(false)
        })
        .collect();

    let browser = &mut editor_world.resources.ui_handles.prefab_browser;
    let next_index = if browser.last_find_name.as_deref() == Some(prefab_name.as_str()) {
        browser.find_cycle_index.saturating_add(1)
    } else {
        0
    };
    browser.last_find_name = Some(prefab_name.clone());

    if matches.is_empty() {
        browser.find_cycle_index = 0;
        let label = browser.focus_label;
        ui_set_text(world, label, &format!("No live instance of {prefab_name}"));
        return;
    }

    let cycled = next_index % matches.len();
    browser.find_cycle_index = cycled;
    let entity = matches[cycled];
    let label = browser.focus_label;
    editor_world
        .resources
        .ui_interaction
        .actions
        .push(crate::systems::retained_ui::Action::SelectEntity(entity));
    ui_set_text(
        world,
        label,
        &format!(
            "Selected instance {} of {} (of {})",
            cycled + 1,
            prefab_name,
            matches.len()
        ),
    );
}

#[cfg(not(target_arch = "wasm32"))]
fn scan_prefab_directory(editor_world: &mut EditorWorld) {
    let Some(directory) = nightshade::prelude::rfd::FileDialog::new()
        .set_title("Pick prefab directory")
        .pick_folder()
    else {
        return;
    };
    let mut found: Vec<PathBuf> = Vec::new();
    walk_for_prefabs(&directory, &mut found);
    found.sort();
    editor_world.resources.ui_handles.prefab_browser.library = found;
}

#[cfg(target_arch = "wasm32")]
fn scan_prefab_directory(_editor_world: &mut EditorWorld) {}

#[cfg(not(target_arch = "wasm32"))]
fn walk_for_prefabs(directory: &std::path::Path, found: &mut Vec<PathBuf>) {
    let Ok(entries) = std::fs::read_dir(directory) else {
        return;
    };
    for entry in entries.flatten() {
        let path = entry.path();
        if path.is_dir() {
            walk_for_prefabs(&path, found);
        } else if path.extension().and_then(|s| s.to_str()) == Some("nsprefab") {
            found.push(path);
        }
    }
}

fn spawn_cached_prefab(editor_world: &mut EditorWorld, world: &mut World, prefab_name: &str) {
    let cached = world
        .resources
        .assets
        .prefab_cache
        .prefabs
        .get(prefab_name)
        .cloned();
    let Some(cached) = cached else {
        let label = editor_world.resources.ui_handles.prefab_browser.focus_label;
        ui_set_text(
            world,
            label,
            &format!("Prefab '{prefab_name}' not in cache"),
        );
        return;
    };
    let spawn_position = world
        .resources
        .active_camera
        .and_then(|camera| world.core.get_global_transform(camera))
        .map(|transform| {
            let forward = transform.forward_vector();
            transform.translation() + forward * 4.0
        })
        .unwrap_or_else(|| Vec3::new(0.0, 0.0, 0.0));
    let entity = nightshade::ecs::prefab::spawn_prefab_with_skins(
        world,
        &cached.prefab,
        &cached.animations,
        &cached.skins,
        spawn_position,
    );
    editor_world
        .resources
        .ui_interaction
        .actions
        .push(crate::systems::retained_ui::Action::SelectEntity(entity));
    let label = editor_world.resources.ui_handles.prefab_browser.focus_label;
    ui_set_text(
        world,
        label,
        &format!("Spawned new instance of {prefab_name}"),
    );
}

fn resolve_prefab_scene(
    prefab_source: &nightshade::ecs::prefab::components::PrefabSource,
    imported_scenes: &HashMap<nightshade::ecs::scene::AssetUuid, nightshade::ecs::scene::Scene>,
) -> Option<nightshade::ecs::scene::Scene> {
    if let Some(uuid) = prefab_source.source_uuid
        && let Some(scene) = imported_scenes.get(&uuid)
    {
        return Some(scene.clone());
    }
    #[cfg(not(target_arch = "wasm32"))]
    {
        if let Some(path) = prefab_source.source_path.as_deref() {
            let path = std::path::Path::new(path);
            if path.exists()
                && let Ok(bytes) = std::fs::read(path)
                && let Ok(scene) =
                    nightshade::ecs::scene::commands::load_scene_binary_from_bytes(&bytes)
            {
                return Some(scene);
            }
        }
    }
    None
}

fn ensure_prefab_cached(
    editor_world: &mut EditorWorld,
    prefab_source: &nightshade::ecs::prefab::components::PrefabSource,
) -> Option<nightshade::ecs::scene::Scene> {
    let scene = resolve_prefab_scene(
        prefab_source,
        &editor_world.resources.prefab_instance_links.imported_scenes,
    )?;
    if let Some(uuid) = scene.header.prefab_id {
        editor_world
            .resources
            .prefab_instance_links
            .imported_scenes
            .entry(uuid)
            .or_insert_with(|| scene.clone());
    }
    Some(scene)
}

fn resolvable_prefab(
    prefab_name: &str,
    world: &World,
    imported_scenes: &HashMap<nightshade::ecs::scene::AssetUuid, nightshade::ecs::scene::Scene>,
) -> bool {
    world
        .core
        .query_entities(PREFAB_SOURCE)
        .filter_map(|entity| world.core.get_prefab_source(entity))
        .any(|source| {
            source.prefab_name == prefab_name
                && (resolve_prefab_scene(source, imported_scenes).is_some()
                    || source_path_exists(source))
        })
}

pub fn source_reachable(
    editor_world: &EditorWorld,
    source: &nightshade::ecs::prefab::components::PrefabSource,
) -> bool {
    if let Some(uuid) = source.source_uuid
        && editor_world
            .resources
            .prefab_instance_links
            .imported_scenes
            .contains_key(&uuid)
    {
        return true;
    }
    source_path_exists(source)
}

fn source_path_exists(source: &nightshade::ecs::prefab::components::PrefabSource) -> bool {
    #[cfg(not(target_arch = "wasm32"))]
    {
        source
            .source_path
            .as_deref()
            .map(|path| std::path::Path::new(path).exists())
            .unwrap_or(false)
    }
    #[cfg(target_arch = "wasm32")]
    {
        let _ = source;
        false
    }
}

fn refresh_instances(editor_world: &mut EditorWorld, world: &mut World, prefab_name: &str) {
    let matches: Vec<Entity> = world
        .core
        .query_entities(PREFAB_SOURCE)
        .filter(|entity| {
            world
                .core
                .get_prefab_source(*entity)
                .map(|source| source.prefab_name == prefab_name)
                .unwrap_or(false)
        })
        .collect();

    let label = editor_world.resources.ui_handles.prefab_browser.focus_label;
    if matches.is_empty() {
        ui_set_text(
            world,
            label,
            &format!("No live instance of {prefab_name} to refresh"),
        );
        return;
    }

    let mut steps: Vec<crate::undo::UndoableOperation> = Vec::new();
    let mut refreshed_count = 0usize;
    let mut last_failure: Option<String> = None;
    for entity in matches {
        let Some(source) = world.core.get_prefab_source(entity).cloned() else {
            continue;
        };
        let Some(prefab_scene) = ensure_prefab_cached(editor_world, &source) else {
            last_failure = Some(format!(
                "{} has no cached source and source file is unreachable",
                source.prefab_name
            ));
            continue;
        };

        match refresh_single_instance(editor_world, world, entity, &source, &prefab_scene) {
            Some((captured_old, captured_new)) => {
                steps.push(crate::undo::UndoableOperation::EntityDeleted {
                    captured: Box::new(captured_old),
                });
                steps.push(crate::undo::UndoableOperation::EntityCreated {
                    captured: Box::new(captured_new),
                });
                refreshed_count += 1;
            }
            None => {
                last_failure = Some(format!(
                    "Refresh produced no root entity for {}",
                    source.prefab_name
                ));
            }
        }
    }

    if !steps.is_empty() {
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::Batch { steps },
            format!("Refresh {prefab_name}"),
        );
        editor_world.resources.project.mark_modified();
    }

    let message = match (refreshed_count, last_failure) {
        (0, Some(reason)) => format!("Refresh {prefab_name} failed: {reason}"),
        (count, Some(reason)) => format!("Refreshed {count} {prefab_name} (warnings: {reason})"),
        (count, None) => format!("Refreshed {count} instance(s) of {prefab_name}"),
    };
    ui_set_text(world, label, &message);
}

pub fn refresh_selected_instance(
    editor_world: &mut EditorWorld,
    world: &mut World,
    selected: Entity,
) {
    let Some(source) = world.core.get_prefab_source(selected).cloned() else {
        return;
    };
    let Some(prefab_scene) = ensure_prefab_cached(editor_world, &source) else {
        tracing::warn!(
            "Cannot refresh '{}' — no cached source and source file unreachable.",
            source.prefab_name
        );
        return;
    };
    let Some((captured_old, captured_new)) =
        refresh_single_instance(editor_world, world, selected, &source, &prefab_scene)
    else {
        return;
    };
    let prefab_name = source.prefab_name.clone();
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::Batch {
            steps: vec![
                crate::undo::UndoableOperation::EntityDeleted {
                    captured: Box::new(captured_old),
                },
                crate::undo::UndoableOperation::EntityCreated {
                    captured: Box::new(captured_new),
                },
            ],
        },
        format!("Refresh {prefab_name}"),
    );
    editor_world.resources.project.mark_modified();
}

pub fn break_instance_link(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
    if world.core.get_prefab_source(entity).is_none() {
        return;
    }
    let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity) else {
        return;
    };
    let snapshot_old = crate::undo::ComponentSnapshot::capture(
        world,
        entity,
        crate::undo::SnapshotKind::PrefabSource,
    );
    world
        .core
        .remove_components(entity, nightshade::ecs::world::PREFAB_SOURCE);
    crate::scene_writeback::sync_entity(
        &mut editor_world.resources.project,
        &editor_world.resources.editor_scene,
        world,
        entity,
    );
    editor_world
        .resources
        .prefab_instance_links
        .entity_to_source
        .remove(&entity);
    editor_world.resources.project.mark_modified();
    if let (Some(old), Some(new)) = (
        snapshot_old,
        crate::undo::ComponentSnapshot::capture(
            world,
            entity,
            crate::undo::SnapshotKind::PrefabSource,
        ),
    ) {
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::ComponentChanged {
                uuid,
                old: Box::new(old),
                new: Box::new(new),
            },
            "Break prefab link",
        );
    }
}

fn refresh_single_instance(
    editor_world: &mut EditorWorld,
    world: &mut World,
    live_root: Entity,
    source: &nightshade::ecs::prefab::components::PrefabSource,
    prefab_scene: &nightshade::ecs::scene::Scene,
) -> Option<(crate::undo::CapturedSubtree, crate::undo::CapturedSubtree)> {
    let captured_old =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, live_root);

    let old_transform = world
        .core
        .get_local_transform(live_root)
        .copied()
        .unwrap_or_default();
    let old_name = world.core.get_name(live_root).cloned();
    let old_parent = world.core.get_parent(live_root).and_then(|parent| parent.0);
    let preserved_root_uuid = editor_world.resources.editor_scene.uuid_for(live_root);

    let overrides = collect_overrides(editor_world, world, live_root, prefab_scene);

    let descendants = nightshade::ecs::transform::queries::query_descendants(world, live_root);
    let added_entities: std::collections::HashSet<Entity> =
        overrides.added.iter().map(|a| a.entity).collect();
    let source_linked_descendants: Vec<Entity> = descendants
        .into_iter()
        .filter(|entity| !added_entities.contains(entity))
        .collect();
    for descendant in &source_linked_descendants {
        editor_world
            .resources
            .prefab_instance_links
            .entity_to_source
            .remove(descendant);
        crate::scene_writeback::remove_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            *descendant,
        );
    }
    editor_world
        .resources
        .prefab_instance_links
        .entity_to_source
        .remove(&live_root);
    crate::scene_writeback::remove_entity(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        live_root,
    );

    let mut despawn_list = vec![live_root];
    despawn_list.extend(source_linked_descendants);
    nightshade::ecs::world::commands::despawn_entities_with_cache_cleanup(world, &despawn_list);
    world.resources.transform_state.children_cache_valid = false;

    let spawn_result =
        nightshade::ecs::scene::commands::spawn_prefab_scene(world, prefab_scene, old_parent)
            .ok()?;
    let new_root = *spawn_result.root_entities.first()?;

    if let Some(transform) = world.core.get_local_transform_mut(new_root) {
        *transform = old_transform;
    }
    if let Some(name) = old_name {
        world.core.set_name(new_root, name);
    }
    mark_local_transform_dirty(world, new_root);

    let prefab_source_component = nightshade::ecs::prefab::components::PrefabSource {
        prefab_name: source.prefab_name.clone(),
        source_path: source.source_path.clone(),
        source_uuid: prefab_scene.header.prefab_id.or(source.source_uuid),
    };
    world
        .core
        .add_components(new_root, nightshade::ecs::world::PREFAB_SOURCE);
    world
        .core
        .set_prefab_source(new_root, prefab_source_component);

    apply_overrides(
        world,
        new_root,
        &spawn_result.entity_to_source_uuid,
        &overrides,
        prefab_scene,
    );

    let source_uuid_to_new_entity: HashMap<nightshade::ecs::scene::AssetUuid, Entity> =
        spawn_result
            .entity_to_source_uuid
            .iter()
            .map(|(entity, source_uuid)| (*source_uuid, *entity))
            .collect();
    reattach_added_entities(world, &overrides, new_root, &source_uuid_to_new_entity);

    for (&entity, &source_uuid) in &spawn_result.entity_to_source_uuid {
        if overrides.removed.contains(&source_uuid) {
            continue;
        }
        editor_world
            .resources
            .prefab_instance_links
            .entity_to_source
            .insert(entity, source_uuid);
    }

    register_subtree_preserving_root_uuid(editor_world, world, new_root, preserved_root_uuid);

    let captured_new =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, new_root);

    Some((captured_old, captured_new))
}

fn register_subtree_preserving_root_uuid(
    editor_world: &mut EditorWorld,
    world: &mut World,
    root: Entity,
    preserved_root_uuid: Option<nightshade::ecs::scene::AssetUuid>,
) {
    let root_uuid = preserved_root_uuid.unwrap_or_else(nightshade::ecs::scene::AssetUuid::random);
    editor_world.resources.editor_scene.insert(root_uuid, root);
    crate::scene_writeback::sync_entity(
        &mut editor_world.resources.project,
        &editor_world.resources.editor_scene,
        world,
        root,
    );
    let descendants = nightshade::ecs::transform::queries::query_descendants(world, root);
    for descendant in descendants {
        if editor_world
            .resources
            .editor_scene
            .is_scaffolding(descendant)
        {
            continue;
        }
        if editor_world
            .resources
            .editor_scene
            .uuid_for(descendant)
            .is_none()
        {
            let descendant_uuid = nightshade::ecs::scene::AssetUuid::random();
            editor_world
                .resources
                .editor_scene
                .insert(descendant_uuid, descendant);
        }
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            descendant,
        );
    }
}

struct Overrides {
    live: HashMap<nightshade::ecs::scene::AssetUuid, nightshade::ecs::scene::SceneEntity>,
    /// Entities the user added inside the instance subtree. Keyed by
    /// the source uuid of the entity they were parented under so the
    /// refresh can reattach them after spawn. Each value is the live
    /// SceneEntity captured before despawn. Adopted entities outside
    /// the prefab boundary still have their world Entity ids preserved
    /// because we never despawn them.
    added: Vec<AddedEntity>,
    /// Source uuids the user removed from this instance. Empty unless
    /// the user deleted entities from the prefab tree.
    removed: std::collections::HashSet<nightshade::ecs::scene::AssetUuid>,
}

struct AddedEntity {
    parent_source_uuid: Option<nightshade::ecs::scene::AssetUuid>,
    entity: Entity,
}

fn collect_overrides(
    editor_world: &EditorWorld,
    world: &World,
    live_root: Entity,
    prefab_scene: &nightshade::ecs::scene::Scene,
) -> Overrides {
    let mut live: HashMap<nightshade::ecs::scene::AssetUuid, nightshade::ecs::scene::SceneEntity> =
        HashMap::new();
    let mut added: Vec<AddedEntity> = Vec::new();
    let mut entities = vec![live_root];
    entities.extend(nightshade::ecs::transform::queries::query_descendants(
        world, live_root,
    ));
    let mut present: std::collections::HashSet<nightshade::ecs::scene::AssetUuid> =
        std::collections::HashSet::new();
    for entity in &entities {
        if let Some(&source_uuid) = editor_world
            .resources
            .prefab_instance_links
            .entity_to_source
            .get(entity)
        {
            present.insert(source_uuid);
            let scene_entity =
                nightshade::ecs::scene::entity_to_scene_entity(world, *entity, source_uuid, None);
            live.insert(source_uuid, scene_entity);
        } else {
            let parent_source_uuid = world
                .core
                .get_parent(*entity)
                .and_then(|parent| parent.0)
                .and_then(|parent_entity| {
                    editor_world
                        .resources
                        .prefab_instance_links
                        .entity_to_source
                        .get(&parent_entity)
                        .copied()
                });
            added.push(AddedEntity {
                parent_source_uuid,
                entity: *entity,
            });
        }
    }
    let mut removed = std::collections::HashSet::new();
    for source_entity in &prefab_scene.entities {
        if !present.contains(&source_entity.uuid) {
            removed.insert(source_entity.uuid);
        }
    }
    Overrides {
        live,
        added,
        removed,
    }
}

fn apply_overrides(
    world: &mut World,
    new_root: Entity,
    entity_to_source_uuid: &HashMap<Entity, nightshade::ecs::scene::AssetUuid>,
    overrides: &Overrides,
    prefab_scene: &nightshade::ecs::scene::Scene,
) {
    let mut despawn_due_to_removal: Vec<Entity> = Vec::new();
    for (&entity, &source_uuid) in entity_to_source_uuid {
        if overrides.removed.contains(&source_uuid) {
            despawn_due_to_removal.push(entity);
            continue;
        }
        let Some(live) = overrides.live.get(&source_uuid) else {
            continue;
        };
        let Some(source) = prefab_scene.find_entity(source_uuid) else {
            continue;
        };
        let is_root = entity == new_root;
        apply_entity_diff(world, entity, live, source, is_root);
    }
    for entity in despawn_due_to_removal {
        despawn_recursive_immediate(world, entity);
    }
}

fn reattach_added_entities(
    world: &mut World,
    overrides: &Overrides,
    new_root: Entity,
    source_uuid_to_new_entity: &HashMap<nightshade::ecs::scene::AssetUuid, Entity>,
) {
    for added in &overrides.added {
        let new_parent = match added.parent_source_uuid {
            Some(uuid) => source_uuid_to_new_entity
                .get(&uuid)
                .copied()
                .unwrap_or(new_root),
            None => new_root,
        };
        world
            .core
            .add_components(added.entity, nightshade::ecs::world::PARENT);
        world
            .core
            .set_parent(added.entity, Parent(Some(new_parent)));
        mark_local_transform_dirty(world, added.entity);
    }
}

fn apply_entity_diff(
    world: &mut World,
    entity: Entity,
    live: &nightshade::ecs::scene::SceneEntity,
    source: &nightshade::ecs::scene::SceneEntity,
    is_root: bool,
) {
    if !is_root && !transforms_equal(&live.transform, &source.transform) {
        if let Some(slot) = world.core.get_local_transform_mut(entity) {
            *slot = live.transform;
        }
        mark_local_transform_dirty(world, entity);
    }
    if !is_root && live.name != source.name {
        match &live.name {
            Some(name) => {
                world
                    .core
                    .add_components(entity, nightshade::ecs::world::NAME);
                world.core.set_name(entity, Name(name.clone()));
            }
            None => {
                world
                    .core
                    .remove_components(entity, nightshade::ecs::world::NAME);
            }
        }
    }
    if live.components.visible != source.components.visible {
        world
            .core
            .add_components(entity, nightshade::ecs::world::VISIBILITY);
        world.core.set_visibility(
            entity,
            Visibility {
                visible: live.components.visible,
            },
        );
    }
    if live.components.casts_shadow != source.components.casts_shadow {
        if live.components.casts_shadow {
            world
                .core
                .add_components(entity, nightshade::ecs::world::CASTS_SHADOW);
            world.core.set_casts_shadow(entity, CastsShadow);
        } else {
            world
                .core
                .remove_components(entity, nightshade::ecs::world::CASTS_SHADOW);
        }
    }
    apply_material_diff(
        world,
        entity,
        &live.components.mesh,
        &source.components.mesh,
    );
    apply_instanced_material_diff(
        world,
        entity,
        &live.components.instanced_mesh,
        &source.components.instanced_mesh,
    );
    apply_light_diff(
        world,
        entity,
        &live.components.light,
        &source.components.light,
    );
    apply_camera_diff(
        world,
        entity,
        &live.components.camera,
        &source.components.camera,
    );
    apply_audio_diff(
        world,
        entity,
        &live.components.audio,
        &source.components.audio,
    );
    apply_particle_diff(
        world,
        entity,
        &live.components.particle_emitter,
        &source.components.particle_emitter,
    );
    apply_decal_diff(
        world,
        entity,
        &live.components.decal,
        &source.components.decal,
    );
    apply_render_layer_diff(
        world,
        entity,
        &live.components.render_layer,
        &source.components.render_layer,
    );
    apply_navmesh_agent_diff(
        world,
        entity,
        &live.components.navmesh_agent,
        &source.components.navmesh_agent,
    );
    apply_animation_player_diff(
        world,
        entity,
        &live.components.animation_player,
        &source.components.animation_player,
    );
    apply_text_diff(
        world,
        entity,
        &live.components.text,
        &live.components.text_content,
        &source.components.text,
        &source.components.text_content,
    );
    apply_tags_diff(
        world,
        entity,
        &live.components.tags,
        &source.components.tags,
    );
}

fn apply_material_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneMesh>,
    source: &Option<nightshade::ecs::scene::SceneMesh>,
) {
    let live_material = live.as_ref().and_then(|mesh| mesh.material.clone());
    let source_material = source.as_ref().and_then(|mesh| mesh.material.clone());
    if scene_value_eq(live_material.as_ref(), source_material.as_ref()) {
        return;
    }
    let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
        return;
    };
    if let Some(scene_material) = live_material {
        let material = scene_material.to_material();
        nightshade::ecs::material::resources::material_registry_insert(
            &mut world.resources.assets.material_registry,
            material_ref.name.clone(),
            material,
        );
        world.resources.mesh_render_state.request_full_rebuild();
    }
}

fn apply_instanced_material_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneInstancedMesh>,
    source: &Option<nightshade::ecs::scene::SceneInstancedMesh>,
) {
    let live_material = live.as_ref().and_then(|mesh| mesh.material.clone());
    let source_material = source.as_ref().and_then(|mesh| mesh.material.clone());
    if scene_value_eq(live_material.as_ref(), source_material.as_ref()) {
        return;
    }
    let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
        return;
    };
    if let Some(scene_material) = live_material {
        let material = scene_material.to_material();
        nightshade::ecs::material::resources::material_registry_insert(
            &mut world.resources.assets.material_registry,
            material_ref.name.clone(),
            material,
        );
        world.resources.mesh_render_state.request_full_rebuild();
    }
}

fn apply_light_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneLight>,
    source: &Option<nightshade::ecs::scene::SceneLight>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(scene_light) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::LIGHT);
        world.core.set_light(entity, scene_light.to_light());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::LIGHT);
    }
}

fn apply_camera_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneCamera>,
    source: &Option<nightshade::ecs::scene::SceneCamera>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(scene_camera) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::CAMERA);
        world.core.set_camera(entity, scene_camera.to_camera());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::CAMERA);
    }
}

fn apply_audio_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneAudioSource>,
    source: &Option<nightshade::ecs::scene::SceneAudioSource>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(audio) = live {
        let audio_ref = audio
            .audio_name
            .clone()
            .or_else(|| audio.audio_uuid.map(|uuid| uuid.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
            .add_components(entity, nightshade::ecs::world::AUDIO_SOURCE);
        world.core.set_audio_source(
            entity,
            nightshade::ecs::audio::components::AudioSource {
                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,
            },
        );
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::AUDIO_SOURCE);
    }
}

fn apply_particle_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneParticleEmitter>,
    source: &Option<nightshade::ecs::scene::SceneParticleEmitter>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(emitter) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::PARTICLE_EMITTER);
        world
            .core
            .set_particle_emitter(entity, emitter.to_particle_emitter());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::PARTICLE_EMITTER);
    }
}

fn apply_decal_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::decal::components::Decal>,
    source: &Option<nightshade::ecs::decal::components::Decal>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(decal) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::DECAL);
        world.core.set_decal(entity, decal.clone());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::DECAL);
    }
}

fn apply_render_layer_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::primitives::RenderLayer>,
    source: &Option<nightshade::ecs::primitives::RenderLayer>,
) {
    if live == source {
        return;
    }
    if let Some(layer) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::RENDER_LAYER);
        world.core.set_render_layer(entity, *layer);
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::RENDER_LAYER);
    }
}

fn apply_navmesh_agent_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<NavMeshAgent>,
    source: &Option<NavMeshAgent>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(agent) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::NAVMESH_AGENT);
        world.core.set_navmesh_agent(entity, agent.clone());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::NAVMESH_AGENT);
    }
}

fn apply_animation_player_diff(
    world: &mut World,
    entity: Entity,
    live: &Option<nightshade::ecs::scene::SceneAnimationPlayer>,
    source: &Option<nightshade::ecs::scene::SceneAnimationPlayer>,
) {
    if scene_value_eq(live.as_ref(), source.as_ref()) {
        return;
    }
    if let Some(player) = live {
        world
            .core
            .add_components(entity, nightshade::ecs::world::ANIMATION_PLAYER);
        world
            .core
            .set_animation_player(entity, player.to_animation_player());
    } else {
        world
            .core
            .remove_components(entity, nightshade::ecs::world::ANIMATION_PLAYER);
    }
}

fn apply_text_diff(
    world: &mut World,
    entity: Entity,
    live_text: &Option<nightshade::ecs::text::components::Text>,
    live_content: &Option<String>,
    source_text: &Option<nightshade::ecs::text::components::Text>,
    source_content: &Option<String>,
) {
    let text_changed = scene_value_eq(live_text.as_ref(), source_text.as_ref());
    let content_changed = live_content == source_content;
    if text_changed && content_changed {
        return;
    }
    match live_text {
        Some(text) => {
            let mut text = text.clone();
            if let Some(content) = live_content.as_deref() {
                text.text_index = world.resources.text.cache.add_text(content);
            }
            text.dirty = true;
            world
                .core
                .add_components(entity, nightshade::ecs::world::TEXT);
            world.core.set_text(entity, text);
        }
        None => {
            world
                .core
                .remove_components(entity, nightshade::ecs::world::TEXT);
        }
    }
}

fn apply_tags_diff(world: &mut World, entity: Entity, live: &[String], source: &[String]) {
    if live == source {
        return;
    }
    if live.is_empty() {
        world.resources.entities.tags.remove(&entity);
    } else {
        world.resources.entities.tags.insert(entity, live.to_vec());
    }
}

fn transforms_equal(a: &LocalTransform, b: &LocalTransform) -> bool {
    const EPSILON: f32 = 1e-5;
    let translations_eq = (a.translation - b.translation).magnitude() < EPSILON;
    let scales_eq = (a.scale - b.scale).magnitude() < EPSILON;
    let rotations_eq = (a.rotation.coords - b.rotation.coords).magnitude() < EPSILON
        || (a.rotation.coords + b.rotation.coords).magnitude() < EPSILON;
    translations_eq && scales_eq && rotations_eq
}

fn scene_value_eq<T: serde::Serialize>(a: Option<&T>, b: Option<&T>) -> bool {
    match (a, b) {
        (None, None) => true,
        (Some(left), Some(right)) => {
            bincode::serialize(left).ok() == bincode::serialize(right).ok()
        }
        _ => false,
    }
}

const PREVIEW_GHOST_ALPHA: f32 = 0.45;

fn apply_preview_ghost(world: &mut World, root: Entity) -> Vec<GhostMaterialSwap> {
    let entities: Vec<Entity> = std::iter::once(root)
        .chain(nightshade::ecs::transform::queries::query_descendants(
            world, root,
        ))
        .collect();
    let mut swaps: Vec<GhostMaterialSwap> = Vec::new();
    for entity in entities {
        let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
            continue;
        };
        let Some(material) = nightshade::ecs::generational_registry::registry_entry_by_name(
            &world.resources.assets.material_registry.registry,
            &material_ref.name,
        )
        .cloned() else {
            continue;
        };
        let mut ghost = material;
        ghost.base_color[3] *= PREVIEW_GHOST_ALPHA;
        ghost.alpha_mode = nightshade::ecs::material::components::AlphaMode::Blend;
        let ghost_name = format!("PrefabPreviewGhost_{}", entity.id);
        nightshade::ecs::material::resources::material_registry_insert(
            &mut world.resources.assets.material_registry,
            ghost_name.clone(),
            ghost,
        );
        world.core.set_material_ref(
            entity,
            nightshade::ecs::world::components::MaterialRef::new(ghost_name),
        );
        swaps.push(GhostMaterialSwap {
            entity,
            original_name: material_ref.name.clone(),
        });
    }
    world.resources.mesh_render_state.request_full_rebuild();
    swaps
}

fn revert_preview_ghost(world: &mut World, swaps: &[GhostMaterialSwap]) {
    if swaps.is_empty() {
        return;
    }
    for swap in swaps {
        world.core.set_material_ref(
            swap.entity,
            nightshade::ecs::world::components::MaterialRef::new(swap.original_name.clone()),
        );
    }
    world.resources.mesh_render_state.request_full_rebuild();
}

fn begin_drag(
    editor_world: &mut EditorWorld,
    source_entity: Entity,
    prefab_name: String,
    library_path: Option<PathBuf>,
) {
    let browser = &mut editor_world.resources.ui_handles.prefab_browser;
    if browser
        .drag_state
        .as_ref()
        .is_some_and(|state| state.source_entity == source_entity)
    {
        return;
    }
    browser.drag_state = Some(PrefabDragState {
        prefab_name,
        library_path,
        source_entity,
        preview_root: None,
        ghost_material_swaps: Vec::new(),
    });
}

fn finalize_drag(editor_world: &mut EditorWorld, world: &mut World, source: Entity) {
    let drag = match editor_world
        .resources
        .ui_handles
        .prefab_browser
        .drag_state
        .take()
    {
        Some(state) if state.source_entity == source => state,
        Some(state) => {
            editor_world.resources.ui_handles.prefab_browser.drag_state = Some(state);
            return;
        }
        None => return,
    };

    let Some(preview_root) = drag.preview_root else {
        return;
    };

    let in_viewport = cursor_in_viewport(world);
    let label = editor_world.resources.ui_handles.prefab_browser.focus_label;
    if in_viewport {
        revert_preview_ghost(world, &drag.ghost_material_swaps);
        if let Some(transform) = world.core.get_local_transform_mut(preview_root) {
            transform.translation.y -= DRAG_PREVIEW_HOVER_HEIGHT;
        }
        mark_local_transform_dirty(world, preview_root);
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            preview_root,
        );
        editor_world.resources.ui_interaction.actions.push(
            crate::systems::retained_ui::Action::SelectEntity(preview_root),
        );
        ui_set_text(
            world,
            label,
            &format!("Dropped instance of {}", drag.prefab_name),
        );
        push_create_undo(editor_world, world, preview_root, drag.prefab_name.clone());
    } else {
        despawn_preview(editor_world, world, preview_root);
        ui_set_text(
            world,
            label,
            &format!("Cancelled drop of {}", drag.prefab_name),
        );
    }
}

fn push_create_undo(
    editor_world: &mut EditorWorld,
    world: &mut World,
    root: Entity,
    prefab_name: String,
) {
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        format!("Drag spawn {prefab_name}"),
    );
}

fn despawn_preview(editor_world: &mut EditorWorld, world: &mut World, root: Entity) {
    let descendants = nightshade::ecs::transform::queries::query_descendants(world, root);
    crate::scene_writeback::remove_entity(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        root,
    );
    for descendant in descendants {
        crate::scene_writeback::remove_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            descendant,
        );
    }
    despawn_recursive_immediate(world, root);
}

fn update_drag_preview(editor_world: &mut EditorWorld, world: &mut World) {
    if world.resources.retained_ui.drag.active.is_none() {
        return;
    }
    if editor_world
        .resources
        .ui_handles
        .prefab_browser
        .drag_state
        .is_none()
    {
        return;
    }

    let Some(hit) = cursor_world_hit(world) else {
        return;
    };

    let mut state = editor_world
        .resources
        .ui_handles
        .prefab_browser
        .drag_state
        .take()
        .expect("drag_state is_some checked above");

    let time_seconds = world.resources.window.timing.uptime_milliseconds as f32 / 1000.0;
    let bob = (time_seconds * std::f32::consts::TAU * 1.5).sin() * 0.05;
    let hover = hit + Vec3::new(0.0, DRAG_PREVIEW_HOVER_HEIGHT + bob, 0.0);
    if let Some(preview) = state.preview_root {
        if let Some(transform) = world.core.get_local_transform_mut(preview) {
            transform.translation = hover;
            mark_local_transform_dirty(world, preview);
        }
    } else if let Some(spawned) = spawn_preview(editor_world, world, &state, hover) {
        state.preview_root = Some(spawned);
        state.ghost_material_swaps = apply_preview_ghost(world, spawned);
    }

    editor_world.resources.ui_handles.prefab_browser.drag_state = Some(state);
}

fn spawn_preview(
    editor_world: &mut EditorWorld,
    world: &mut World,
    state: &PrefabDragState,
    position: Vec3,
) -> Option<Entity> {
    if let Some(path) = state.library_path.as_deref() {
        return spawn_preview_from_library(editor_world, world, path, position);
    }
    spawn_preview_from_cache(editor_world, world, &state.prefab_name, position)
}

fn spawn_preview_from_cache(
    editor_world: &mut EditorWorld,
    world: &mut World,
    prefab_name: &str,
    position: Vec3,
) -> Option<Entity> {
    let cached = world
        .resources
        .assets
        .prefab_cache
        .prefabs
        .get(prefab_name)
        .cloned()?;
    let entity = nightshade::ecs::prefab::spawn_prefab_with_skins(
        world,
        &cached.prefab,
        &cached.animations,
        &cached.skins,
        position,
    );
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    editor_world.resources.loading.model_entities.push(entity);
    Some(entity)
}

#[cfg(not(target_arch = "wasm32"))]
fn spawn_preview_from_library(
    editor_world: &mut EditorWorld,
    world: &mut World,
    path: &std::path::Path,
    position: Vec3,
) -> Option<Entity> {
    let entity_count_before = world.core.get_all_entities().len();
    crate::systems::project_io::import_prefab_from_path(editor_world, world, path);
    let new_entities: Vec<Entity> = world
        .core
        .get_all_entities()
        .into_iter()
        .skip(entity_count_before)
        .collect();
    let candidate = editor_world
        .resources
        .loading
        .pending_fit_roots
        .last()
        .copied()
        .or_else(|| new_entities.into_iter().next())?;
    if let Some(transform) = world.core.get_local_transform_mut(candidate) {
        transform.translation = position;
    }
    mark_local_transform_dirty(world, candidate);
    editor_world.resources.loading.pending_fit_roots.clear();
    editor_world.resources.loading.pending_fit_frames = 0;
    Some(candidate)
}

#[cfg(target_arch = "wasm32")]
fn spawn_preview_from_library(
    _editor_world: &mut EditorWorld,
    _world: &mut World,
    _path: &std::path::Path,
    _position: Vec3,
) -> Option<Entity> {
    None
}

fn cursor_in_viewport(world: &World) -> bool {
    let mouse = nightshade::ecs::input::access::mouse_for_active(world);
    let Some(rect) = world.resources.window.active_viewport_rect else {
        return false;
    };
    rect.width > 0.0 && rect.height > 0.0 && rect.contains(mouse.position)
}

fn cursor_world_hit(world: &World) -> Option<Vec3> {
    let camera_entity = world.resources.active_camera?;
    let mouse = nightshade::ecs::input::access::mouse_for_active(world);
    let viewport = world
        .resources
        .window
        .camera_tile_rects
        .get(&camera_entity)
        .copied()
        .or(world.resources.window.active_viewport_rect)?;
    if viewport.width <= 0.0 || viewport.height <= 0.0 {
        return None;
    }
    if !viewport.contains(mouse.position) {
        return None;
    }
    let camera = world.core.get_camera(camera_entity).copied()?;
    let camera_transform = world.core.get_global_transform(camera_entity)?;
    let camera_position = camera_transform.translation();
    let view_matrix = camera_transform
        .0
        .try_inverse()
        .unwrap_or_else(Mat4::identity);
    let aspect = viewport.width / viewport.height;
    let view_projection = camera.projection.matrix_with_aspect(aspect) * view_matrix;
    let inverse_view_projection = view_projection.try_inverse().unwrap_or_else(Mat4::identity);
    let ndc_x = ((mouse.position.x - viewport.x) / viewport.width) * 2.0 - 1.0;
    let ndc_y = 1.0 - ((mouse.position.y - viewport.y) / viewport.height) * 2.0;
    let near_clip = inverse_view_projection * nalgebra_glm::Vec4::new(ndc_x, ndc_y, 1.0, 1.0);
    if near_clip.w.abs() < 1e-6 {
        return None;
    }
    let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
    let direction = (near_world - camera_position).normalize();
    if let Some(hit) = nightshade::ecs::physics::resources::physics_world_cast_ray(
        &world.resources.physics,
        camera_position,
        direction,
        10_000.0,
        None,
    ) {
        return Some(hit.point);
    }
    let denominator = direction.y;
    if denominator.abs() < 1e-4 {
        return None;
    }
    let parameter = -camera_position.y / denominator;
    if parameter <= 0.0 {
        return None;
    }
    Some(camera_position + direction * parameter)
}

pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    let panel = editor_world.resources.ui_handles.prefab_browser.panel;
    if !world
        .ui
        .get_ui_layout_node(panel)
        .map(|node| node.visible)
        .unwrap_or(false)
    {
        return;
    }

    let mut counts: HashMap<String, u32> = HashMap::new();
    let mut total_instances: u32 = 0;
    for entity in world.core.query_entities(PREFAB_SOURCE) {
        let Some(prefab) = world.core.get_prefab_source(entity) else {
            continue;
        };
        *counts.entry(prefab.prefab_name.clone()).or_insert(0) += 1;
        total_instances += 1;
    }
    let cached_names: Vec<String> = nightshade::ecs::prefab::resources::prefab_cache_names(
        &world.resources.assets.prefab_cache,
    )
    .cloned()
    .collect();
    for name in cached_names {
        counts.entry(name).or_insert(0);
    }

    let mut sorted: Vec<(String, u32)> = counts.into_iter().collect();
    sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));

    let status = editor_world
        .resources
        .ui_handles
        .prefab_browser
        .status_label;
    let cached_count = sorted
        .iter()
        .filter(|(name, _)| {
            world
                .resources
                .assets
                .prefab_cache
                .prefabs
                .contains_key(name)
        })
        .count();
    ui_set_text(
        world,
        status,
        &format!(
            "{} unique prefabs ({} cached), {} live instances",
            sorted.len(),
            cached_count,
            total_instances
        ),
    );

    let library_entries: Vec<(String, PathBuf, Option<PathBuf>)> = editor_world
        .resources
        .ui_handles
        .prefab_browser
        .library
        .iter()
        .map(|path| {
            let name = path
                .file_stem()
                .map(|stem| stem.to_string_lossy().to_string())
                .unwrap_or_default();
            let thumbnail = thumbnail_path_for(path);
            (name, path.clone(), thumbnail)
        })
        .collect();

    queue_prefab_thumbnails(editor_world, &library_entries);

    let row_count = editor_world
        .resources
        .ui_handles
        .prefab_browser
        .list_rows
        .len();
    for index in 0..row_count {
        let mut owned_rows =
            std::mem::take(&mut editor_world.resources.ui_handles.prefab_browser.list_rows);
        if let Some(row) = owned_rows.get_mut(index) {
            if index < sorted.len() {
                let (name, count) = &sorted[index];
                let cached = world
                    .resources
                    .assets
                    .prefab_cache
                    .prefabs
                    .contains_key(name);
                let resolvable = resolvable_prefab(
                    name,
                    world,
                    &editor_world.resources.prefab_instance_links.imported_scenes,
                );
                let badge = if cached { "[cached]" } else { "" };
                row.prefab_name = name.clone();
                row.library_path = None;
                row.thumbnail_path = None;
                ui_set_text(world, row.label, &format!("{name} {badge}  x{count}"));
                ui_set_visible(world, row.label, true);
                ui_set_visible(world, row.select_button, *count > 0);
                ui_set_visible(world, row.spawn_button, cached);
                ui_set_visible(world, row.refresh_button, *count > 0 && resolvable);
                ui_set_visible(world, row.thumbnail, true);
            } else {
                let library_index = index - sorted.len();
                if let Some((name, path, thumbnail)) = library_entries.get(library_index) {
                    row.prefab_name = name.clone();
                    row.library_path = Some(path.clone());
                    row.thumbnail_path = thumbnail.clone();
                    ui_set_text(world, row.label, &format!("{name} [library]"));
                    ui_set_visible(world, row.label, true);
                    ui_set_visible(world, row.select_button, false);
                    ui_set_visible(world, row.spawn_button, true);
                    ui_set_visible(world, row.refresh_button, false);
                    ui_set_visible(world, row.thumbnail, true);
                } else {
                    row.prefab_name.clear();
                    row.library_path = None;
                    row.thumbnail_path = None;
                    ui_set_visible(world, row.label, false);
                    ui_set_visible(world, row.select_button, false);
                    ui_set_visible(world, row.spawn_button, false);
                    ui_set_visible(world, row.refresh_button, false);
                    ui_set_visible(world, row.thumbnail, false);
                }
            }
        }
        editor_world.resources.ui_handles.prefab_browser.list_rows = owned_rows;
    }

    patch_thumbnails(editor_world, world);
    update_drag_preview(editor_world, world);
}

fn thumbnail_path_for(prefab_path: &std::path::Path) -> Option<PathBuf> {
    let candidate = prefab_path.with_extension("thumb.png");
    if candidate.exists() {
        Some(candidate)
    } else {
        None
    }
}

fn thumbnail_slot_key(prefab_name: &str) -> String {
    format!("prefab_thumb_{prefab_name}")
}

fn queue_prefab_thumbnails(
    editor_world: &mut EditorWorld,
    entries: &[(String, PathBuf, Option<PathBuf>)],
) {
    let Ok(mut queue) = editor_world.resources.browsers.thumbnail_queue.lock() else {
        return;
    };
    for (name, _, thumbnail_path) in entries {
        let Some(path) = thumbnail_path else { continue };
        let slot_key = thumbnail_slot_key(name);
        if editor_world
            .resources
            .browsers
            .thumbnail_slots
            .contains_key(&slot_key)
        {
            continue;
        }
        queue.queue_texture(slot_key, path.to_string_lossy().to_string());
    }
}

fn patch_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
    let entries: Vec<(Entity, Option<crate::ecs::ThumbnailSlot>)> = editor_world
        .resources
        .ui_handles
        .prefab_browser
        .list_rows
        .iter()
        .map(|row| {
            let slot = if row.prefab_name.is_empty() {
                None
            } else {
                editor_world
                    .resources
                    .browsers
                    .thumbnail_slots
                    .get(&thumbnail_slot_key(&row.prefab_name))
                    .copied()
            };
            (row.thumbnail, slot)
        })
        .collect();
    let mut dirty = false;
    for (thumbnail_entity, slot) in entries {
        match slot {
            Some(thumb) => {
                if patch_thumbnail_image(world, thumbnail_entity, thumb) {
                    dirty = true;
                }
            }
            None => {
                if clear_thumbnail_image(world, thumbnail_entity) {
                    dirty = true;
                }
            }
        }
    }
    if dirty {
        ui_mark_render_dirty(world);
    }
}

fn patch_thumbnail_image(
    world: &mut World,
    image_entity: Entity,
    thumb: crate::ecs::ThumbnailSlot,
) -> bool {
    let already_matches = matches!(
        world.ui.get_ui_node_content(image_entity),
        Some(UiNodeContent::Image { texture_index, uv_min, uv_max })
            if *texture_index == thumb.layer && *uv_min == thumb.uv_min && *uv_max == thumb.uv_max
    );
    if already_matches {
        return false;
    }
    if let Some(content) = world.ui.get_ui_node_content_mut(image_entity) {
        *content = UiNodeContent::Image {
            texture_index: thumb.layer,
            uv_min: thumb.uv_min,
            uv_max: thumb.uv_max,
        };
    }
    ui_mark_layout_dirty(world);
    true
}

fn clear_thumbnail_image(world: &mut World, image_entity: Entity) -> bool {
    let already_blank = matches!(
        world.ui.get_ui_node_content(image_entity),
        Some(UiNodeContent::None) | None
    );
    if already_blank {
        return false;
    }
    if let Some(content) = world.ui.get_ui_node_content_mut(image_entity) {
        *content = UiNodeContent::None;
    }
    ui_mark_layout_dirty(world);
    true
}
