use crate::ecs::EditorWorld;
use crate::systems::polyhaven::Category;
use crate::systems::retained_ui::camera_grid;
use crate::systems::retained_ui::viewport::ExtraCameraTile;
use crate::systems::retained_ui::viewport_controls;
use crate::systems::{camera, random};
use nightshade::ecs::camera::components::ShadingMode;
use nightshade::prelude::*;

#[derive(Default)]
pub struct UiInteraction {
    pub actions: Vec<Action>,
}

#[derive(Debug, Clone)]
pub enum Action {
    OpenKhronosBrowser,
    OpenPolyhavenBrowser,
    #[cfg(not(target_arch = "wasm32"))]
    OpenSketchfabBrowser,
    #[cfg(not(target_arch = "wasm32"))]
    OpenKenneyBrowser,
    #[cfg(not(target_arch = "wasm32"))]
    PickKenneyDirectory,
    #[cfg(not(target_arch = "wasm32"))]
    PickKenneyZip,
    #[cfg(not(target_arch = "wasm32"))]
    LoadKenneyByIndex(usize),
    OpenMaterialsBrowser,
    OpenTagManager,
    OpenPrefabBrowser,
    ToggleTreePanel,
    ToggleInspectorPanel,
    ToggleGroundGrid,
    ToggleSky,
    ToggleLitUnlit,
    ToggleViewerMode,
    FrameScene,
    ToggleFrameOnLoad,
    ClearScene,
    SnapSelectionToFloor,
    BakeNavmesh,
    ToggleNavmeshDebug,
    SetAtmosphere(Atmosphere),
    SetGizmoMode(nightshade::ecs::gizmos::GizmoMode),
    RandomizeBoth,
    SelectEntity(Entity),
    ToggleSelectEntity(Entity),
    ClearSelection,
    AddEntityChild(Entity),
    ViewCamera(Entity),
    ShowAllCameras,
    ResetViewportLayout,
    ToggleIconSet,
    ToggleSnap,
    ToggleSkeletonView,
    ToggleShowNormals,
    OpenNormalsSettings,
    OpenSnapSettings,
    ToggleDayNightCycle,
    LoadKhronosByIndex(usize),
    LoadPolyhavenByIndex(Category, usize),
    #[cfg(not(target_arch = "wasm32"))]
    SketchfabFetch {
        token: String,
        url: String,
    },
    SpawnLines,
    SpawnMeshes,
    Spawn3DText,
    SpawnTextLattice,
    SetShadingMode(Entity, ShadingMode),
    ToggleViewportOverlays(Entity),
    ToggleViewportRealtime(Entity),
    ClothSponza,
    NewProject,
    OpenProject,
    SaveProject,
    SaveProjectAs,
    ExportGlb,
    SaveSelectedAsPrefab,
    ImportPrefab,
    RefreshSelectedPrefab,
    BreakSelectedPrefabLink,
    Undo,
    Redo,
    DeleteEntity(Entity),
    DeleteSelectedEntity,
    DuplicateEntity(Entity),
    DuplicateSelectedEntity,
    AddCube,
    AddSphere,
    AddCylinder,
    AddCone,
    AddTorus,
    AddPlane,
    AddInstancedCube,
    AddInstancedSphere,
    AddInstancedCylinder,
    AddInstancedCone,
    AddInstancedTorus,
    AddInstancedPlane,
    ConvertSimilarToInstanced,
    AddEmpty,
    AddPlayerSpawn,
    AddSavePoint,
    AddPatrolPoint,
    AddTriggerVolume,
    StampDecalAtCursor,
    LoadHdrSkybox,
    AddPointLight,
    AddSpotLight,
    AddDirectionalLight,
    AddTagToSelected(String),
    RemoveTagFromSelected(String),
    OpenCommandPalette,
    ToggleGreyboxMode,
    ApplyGreyboxToSelection,
    SnapSelectionToGrid,
    OpenCreateShape,
    MarkSelectionAsBrush,
    UpgradeSelectionWithGltf,
    ToggleShowFinal,
}

pub fn drain(editor_world: &mut EditorWorld, world: &mut World) {
    let actions = std::mem::take(&mut editor_world.resources.ui_interaction.actions);
    for action in actions {
        apply(editor_world, world, action);
    }
}

fn apply(editor_world: &mut EditorWorld, world: &mut World, action: Action) {
    match action {
        Action::OpenKhronosBrowser => {
            editor_world
                .resources
                .browsers
                .sample_browser
                .ensure_loaded();
            let panel = editor_world.resources.ui_handles.browsers.khronos.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        Action::OpenPolyhavenBrowser => {
            editor_world
                .resources
                .browsers
                .polyhaven_browser
                .ensure_loaded(Category::Hdris);
            editor_world
                .resources
                .browsers
                .polyhaven_browser
                .ensure_loaded(Category::Models);
            let panel = editor_world.resources.ui_handles.browsers.polyhaven.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::OpenSketchfabBrowser => {
            let panel = editor_world.resources.ui_handles.browsers.sketchfab.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::OpenKenneyBrowser => {
            let panel = editor_world.resources.ui_handles.browsers.kenney.panel;
            ui_set_visible(world, panel, true);
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::PickKenneyDirectory => {
            editor_world
                .resources
                .browsers
                .kenney_browser
                .pick_directory();
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::PickKenneyZip => {
            editor_world.resources.browsers.kenney_browser.pick_zip();
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::LoadKenneyByIndex(index) => {
            let entry = editor_world
                .resources
                .browsers
                .kenney_browser
                .entries()
                .and_then(|entries| entries.get(index).cloned());
            if let Some(entry) = entry {
                crate::systems::kenney_assets::load_entry(
                    &editor_world.resources.browsers.kenney_browser,
                    &entry,
                );
            }
        }
        Action::OpenMaterialsBrowser => {
            let panel = editor_world.resources.ui_handles.materials.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        Action::OpenTagManager => {
            let panel = editor_world.resources.ui_handles.tag_manager.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        Action::OpenPrefabBrowser => {
            let panel = editor_world.resources.ui_handles.prefab_browser.panel;
            ui_set_visible(world, panel, !ui_node_visible(world, panel));
        }
        Action::ToggleTreePanel => {
            editor_world.resources.ui.show_tree = !editor_world.resources.ui.show_tree;
            let panel = editor_world.resources.ui_handles.tree.panel;
            let visible = editor_world.resources.ui.show_tree;
            ui_set_visible(world, panel, visible);
            ui_context_menu_set_checked(
                world,
                editor_world.resources.ui_handles.top_bar.view_menu,
                0,
                visible,
            );
        }
        Action::ToggleInspectorPanel => {
            editor_world.resources.ui.show_inspector = !editor_world.resources.ui.show_inspector;
            let panel = editor_world.resources.ui_handles.inspector.panel;
            let visible = editor_world.resources.ui.show_inspector;
            ui_set_visible(world, panel, visible);
            ui_context_menu_set_checked(
                world,
                editor_world.resources.ui_handles.top_bar.view_menu,
                1,
                visible,
            );
        }
        Action::ToggleGroundGrid => {
            world.resources.debug_draw.show_grid = !world.resources.debug_draw.show_grid;
        }
        Action::ToggleSky => {
            world.resources.render_settings.show_sky = !world.resources.render_settings.show_sky;
        }
        Action::ToggleLitUnlit => {
            world.resources.render_settings.unlit_mode =
                !world.resources.render_settings.unlit_mode;
        }
        Action::ToggleViewerMode => {
            editor_world.resources.loading.viewer_mode =
                !editor_world.resources.loading.viewer_mode;
        }
        Action::FrameScene => camera::frame_scene(editor_world, world),
        Action::ClearScene => {
            let captured = capture_active_scene_for_undo(editor_world, world);
            crate::systems::loading::clear_scene(editor_world, world);
            if let Some(captured) = captured {
                editor_world.resources.undo.push(
                    crate::undo::UndoableOperation::EntityDeleted {
                        captured: Box::new(captured),
                    },
                    "Clear scene",
                );
            }
        }
        Action::ToggleFrameOnLoad => {
            editor_world.resources.camera.reset_camera_on_load =
                !editor_world.resources.camera.reset_camera_on_load;
        }
        Action::SetAtmosphere(atmosphere) => {
            world.resources.render_settings.atmosphere = atmosphere;
        }
        Action::SnapSelectionToFloor => {
            snap_selection_to_floor(editor_world, world);
        }
        Action::BakeNavmesh => {
            let config = nightshade::ecs::navmesh::generation::RecastNavMeshConfig::default();
            nightshade::ecs::navmesh::generation::generate_navmesh_from_world(world, &config);
        }
        Action::ToggleNavmeshDebug => {
            world.resources.navmesh.debug_draw = !world.resources.navmesh.debug_draw;
        }
        Action::SetGizmoMode(mode) => {
            world.resources.user_interface.gizmos.mode = mode;
        }
        Action::RandomizeBoth => random::request_both(editor_world),
        Action::ShowAllCameras => {
            let container = editor_world.resources.ui_handles.viewport.tile_container;

            let existing_tiles: Vec<(TileId, Entity, Entity)> = editor_world
                .resources
                .ui_handles
                .viewport
                .extra_camera_tiles
                .iter()
                .map(|tile| (tile.pane_id, tile.controls.container, tile.content_entity))
                .collect();
            for (tile_id, controls_container, content_entity) in existing_tiles {
                ui_set_visible(world, controls_container, false);
                ui_set_visible(world, content_entity, false);
                ui_tile_remove(world, container, tile_id);
            }
            editor_world
                .resources
                .ui_handles
                .viewport
                .extra_camera_tiles
                .clear();

            let active_camera = world.resources.active_camera;
            const MAX_VISIBLE_CAMERA_TILES: usize = 8;

            let mut camera_entities: Vec<Entity> = Vec::new();
            world
                .core
                .query()
                .with(nightshade::ecs::world::CAMERA)
                .iter(|entity, _, _| camera_entities.push(entity));

            let extras: Vec<(Entity, String)> = camera_entities
                .iter()
                .copied()
                .filter(|camera| Some(*camera) != active_camera)
                .take(MAX_VISIBLE_CAMERA_TILES)
                .enumerate()
                .map(|(index, camera)| {
                    let title = world
                        .core
                        .get_name(camera)
                        .map(|name| name.0.clone())
                        .filter(|name| !name.is_empty())
                        .unwrap_or_else(|| format!("Camera {}", index + 2));
                    (camera, title)
                })
                .collect();

            let main_pane = editor_world.resources.ui_handles.viewport.viewport_pane;
            let main_content = editor_world.resources.ui_handles.viewport.viewport_content;
            let titles: Vec<String> = extras.iter().map(|(_, title)| title.clone()).collect();
            let Some(layout) =
                camera_grid::rebuild_grid(world, container, main_pane, main_content, &titles)
            else {
                return;
            };

            editor_world.resources.ui_handles.viewport.viewport_pane = layout.main_pane;

            let mut new_tiles: Vec<ExtraCameraTile> = Vec::with_capacity(layout.extras.len());
            for ((pane_id, content_entity), (camera, _title)) in
                layout.extras.into_iter().zip(extras)
            {
                super::viewport::configure_viewport_pane(world, content_entity);
                viewport_controls::ensure_shading_with_mode(world, camera, ShadingMode::Solid);
                if !world
                    .core
                    .entity_has_components(camera, nightshade::ecs::world::VIEWPORT_UPDATE_MODE)
                {
                    world
                        .core
                        .add_components(camera, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
                }
                world.core.set_viewport_update_mode(
                    camera,
                    nightshade::ecs::camera::components::ViewportUpdateMode::WhenDirty,
                );
                let mut tree = UiTreeBuilder::new(world);
                let controls = viewport_controls::build(&mut tree, camera);
                tree.finish();

                new_tiles.push(ExtraCameraTile {
                    pane_id,
                    camera_entity: camera,
                    content_entity,
                    controls,
                });
            }
            editor_world
                .resources
                .ui_handles
                .viewport
                .extra_camera_tiles
                .extend(new_tiles);
        }
        Action::ToggleIconSet => {
            let new_set = match world.resources.retained_ui.icon_set {
                IconSet::Material => IconSet::Lucide,
                IconSet::Lucide => IconSet::Material,
            };
            world.resources.retained_ui.icon_set = new_set;
            let lookup = IconLookup::for_set(new_set);
            for (entity, icon_fn) in &editor_world.resources.ui_handles.top_bar.icon_entities {
                ui_set_icon(world, *entity, icon_fn(lookup));
            }
            editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
        }
        Action::ToggleDayNightCycle => {
            editor_world.resources.sun.auto_cycle = !editor_world.resources.sun.auto_cycle;
            ui_context_menu_set_checked(
                world,
                editor_world.resources.ui_handles.top_bar.view_menu,
                4,
                editor_world.resources.sun.auto_cycle,
            );
        }
        Action::ToggleSnap => {
            editor_world.resources.snap.enabled = !editor_world.resources.snap.enabled;
        }
        Action::ToggleSkeletonView => {
            let enabled = &mut editor_world.resources.skeleton_debug.enabled;
            *enabled = !*enabled;
            ui_context_menu_set_checked(
                world,
                editor_world.resources.ui_handles.top_bar.view_menu,
                7,
                *enabled,
            );
        }
        Action::OpenSnapSettings => {
            let panel = editor_world.resources.ui_handles.snap_settings.panel;
            ui_set_visible(world, panel, true);
        }
        Action::ToggleShowNormals => {
            let enabled = !world.resources.debug_draw.show_normals;
            world.resources.debug_draw.show_normals = enabled;
            ui_context_menu_set_checked(
                world,
                editor_world.resources.ui_handles.top_bar.view_menu,
                8,
                enabled,
            );
        }
        Action::OpenNormalsSettings => {
            let panel = editor_world.resources.ui_handles.normals_settings.panel;
            ui_set_visible(world, panel, true);
        }
        Action::ResetViewportLayout => {
            let container = editor_world.resources.ui_handles.viewport.tile_container;
            let viewport_content = editor_world.resources.ui_handles.viewport.viewport_content;
            let notes_content = editor_world.resources.ui_handles.viewport.notes_content;
            for tile in &editor_world
                .resources
                .ui_handles
                .viewport
                .extra_camera_tiles
            {
                ui_set_visible(world, tile.controls.container, false);
                ui_set_visible(world, tile.content_entity, false);
            }
            if let Some(new_panes) = ui_tile_reset_to_panes(
                world,
                container,
                &[(viewport_content, "Viewport"), (notes_content, "Notes")],
            ) {
                editor_world.resources.ui_handles.viewport.viewport_pane = new_panes[0];
                editor_world.resources.ui_handles.viewport.notes_pane = new_panes[1];
            }
            editor_world
                .resources
                .ui_handles
                .viewport
                .extra_camera_tiles
                .clear();
        }
        Action::SelectEntity(entity) => {
            crate::systems::selection::set_primary(editor_world, Some(entity));
            crate::systems::picking::reset_cycle(editor_world);
        }
        Action::ToggleSelectEntity(entity) => {
            crate::systems::selection::toggle(editor_world, entity);
            crate::systems::picking::reset_cycle(editor_world);
        }
        Action::ClearSelection => {
            crate::systems::selection::clear(editor_world);
            crate::systems::picking::reset_cycle(editor_world);
        }
        Action::ViewCamera(camera_entity) => {
            if world.core.entity_has_camera(camera_entity) {
                world.resources.active_camera = Some(camera_entity);
            }
        }
        Action::AddEntityChild(parent) => {
            let entity = spawn_entities(
                world,
                NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | PARENT,
                1,
            )[0];
            world.core.set_parent(entity, Parent(Some(parent)));
            world
                .core
                .set_name(entity, Name(format!("Entity {}", entity.id)));
            world.core.set_local_transform(
                entity,
                LocalTransform {
                    translation: Vec3::zeros(),
                    rotation: Quat::identity(),
                    scale: Vec3::new(1.0, 1.0, 1.0),
                },
            );
            world
                .core
                .set_local_transform_dirty(entity, LocalTransformDirty);
            world
                .core
                .set_global_transform(entity, GlobalTransform::default());
            editor_world
                .resources
                .ui_handles
                .tree
                .expanded
                .insert(parent.id);
            crate::systems::selection::set_primary(editor_world, Some(entity));
            editor_world.resources.ui_handles.tree.last_signature = 0;
            editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
            crate::systems::picking::reset_cycle(editor_world);
            crate::scene_writeback::register_subtree(
                &mut editor_world.resources.project,
                &mut editor_world.resources.editor_scene,
                world,
                entity,
            );
            let captured = crate::undo::capture_subtree(
                world,
                &mut editor_world.resources.editor_scene,
                entity,
            );
            editor_world.resources.undo.push(
                crate::undo::UndoableOperation::EntityCreated {
                    captured: Box::new(captured),
                },
                "Add entity",
            );
        }
        Action::LoadKhronosByIndex(index) => {
            let entry = editor_world
                .resources
                .browsers
                .sample_browser
                .entries()
                .and_then(|entries| entries.get(index).cloned());
            if let Some(entry) = entry {
                editor_world
                    .resources
                    .browsers
                    .sample_browser
                    .fetch_entry(&entry);
            }
        }
        Action::LoadPolyhavenByIndex(category, index) => {
            let entry = editor_world
                .resources
                .browsers
                .polyhaven_browser
                .entries(category)
                .and_then(|entries| entries.get(index).cloned());
            if let Some(entry) = entry {
                editor_world
                    .resources
                    .browsers
                    .polyhaven_browser
                    .fetch_asset(category, &entry.slug, &entry.name);
            }
        }
        #[cfg(not(target_arch = "wasm32"))]
        Action::SketchfabFetch { token, url } => {
            let browser = &mut editor_world.resources.browsers.sketchfab_browser;
            browser.token = token;
            browser.url = url;
            if browser.can_fetch() {
                browser.start_fetch();
            }
        }
        Action::SpawnLines => {
            let entity = crate::dev_tools::spawn_lines(world);
            register_dev_tool_root(editor_world, world, entity);
        }
        Action::SpawnMeshes => {
            let entity = crate::dev_tools::spawn_meshes(world);
            register_dev_tool_root(editor_world, world, entity);
        }
        Action::Spawn3DText => {
            let entity = crate::dev_tools::spawn_3d_text(world);
            register_dev_tool_root(editor_world, world, entity);
        }
        Action::SpawnTextLattice => {
            let entity = crate::dev_tools::spawn_text_lattice(world);
            register_dev_tool_root(editor_world, world, entity);
        }
        Action::SetShadingMode(camera_entity, mode) => {
            viewport_controls::ensure_shading(world, camera_entity);
            if let Some(shading) = world.core.get_viewport_shading_mut(camera_entity) {
                shading.mode = mode;
            }
        }
        Action::ToggleViewportOverlays(camera_entity) => {
            viewport_controls::ensure_shading(world, camera_entity);
            if let Some(shading) = world.core.get_viewport_shading_mut(camera_entity) {
                shading.show_overlays = !shading.show_overlays;
            }
        }
        Action::ToggleViewportRealtime(camera_entity) => {
            if !world
                .core
                .entity_has_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE)
            {
                world
                    .core
                    .add_components(camera_entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
                world.core.set_viewport_update_mode(
                    camera_entity,
                    nightshade::ecs::camera::components::ViewportUpdateMode::Always,
                );
            }
            if let Some(mode) = world.core.get_viewport_update_mode_mut(camera_entity) {
                *mode = match *mode {
                    nightshade::ecs::camera::components::ViewportUpdateMode::Always => {
                        nightshade::ecs::camera::components::ViewportUpdateMode::WhenDirty
                    }
                    _ => nightshade::ecs::camera::components::ViewportUpdateMode::Always,
                };
            }
        }
        Action::ClothSponza => {
            crate::systems::cloth_sponza::convert(editor_world, world);
        }
        Action::NewProject => {
            crate::systems::project_io::new_project(editor_world, world);
        }
        Action::OpenProject => {
            crate::systems::project_io::open_project(editor_world);
        }
        Action::SaveProject => {
            crate::systems::project_io::save_project(editor_world, world);
        }
        Action::SaveProjectAs => {
            crate::systems::project_io::save_project_as(editor_world, world);
        }
        Action::ExportGlb => {
            crate::systems::project_io::export_glb(editor_world, world);
        }
        Action::SaveSelectedAsPrefab => {
            crate::systems::project_io::save_selected_as_prefab(editor_world, world);
        }
        Action::ImportPrefab => {
            crate::systems::project_io::import_prefab(editor_world);
        }
        Action::RefreshSelectedPrefab => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                crate::systems::retained_ui::prefab_browser::refresh_selected_instance(
                    editor_world,
                    world,
                    entity,
                );
            }
        }
        Action::BreakSelectedPrefabLink => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                crate::systems::retained_ui::prefab_browser::break_instance_link(
                    editor_world,
                    world,
                    entity,
                );
            }
        }
        Action::Undo => {
            apply_history_action(editor_world, world, true);
        }
        Action::Redo => {
            apply_history_action(editor_world, world, false);
        }
        Action::DeleteEntity(entity) => {
            delete_entity(editor_world, world, entity);
        }
        Action::DeleteSelectedEntity => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                delete_entity(editor_world, world, entity);
            }
        }
        Action::DuplicateEntity(entity) => {
            duplicate_entity(editor_world, world, entity);
        }
        Action::DuplicateSelectedEntity => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                duplicate_entity(editor_world, world, entity);
            }
        }
        Action::AddCube => {
            spawn_primitive(editor_world, world, PrimitiveKind::Cube);
        }
        Action::AddSphere => {
            spawn_primitive(editor_world, world, PrimitiveKind::Sphere);
        }
        Action::AddCylinder => {
            spawn_primitive(editor_world, world, PrimitiveKind::Cylinder);
        }
        Action::AddCone => {
            spawn_primitive(editor_world, world, PrimitiveKind::Cone);
        }
        Action::AddTorus => {
            spawn_primitive(editor_world, world, PrimitiveKind::Torus);
        }
        Action::AddPlane => {
            spawn_primitive(editor_world, world, PrimitiveKind::Plane);
        }
        Action::AddInstancedCube => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cube);
        }
        Action::AddInstancedSphere => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Sphere);
        }
        Action::AddInstancedCylinder => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cylinder);
        }
        Action::AddInstancedCone => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Cone);
        }
        Action::AddInstancedTorus => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Torus);
        }
        Action::AddInstancedPlane => {
            spawn_instanced_primitive(editor_world, world, PrimitiveKind::Plane);
        }
        Action::ConvertSimilarToInstanced => {
            convert_similar_to_instanced(editor_world, world);
        }
        Action::AddEmpty => {
            spawn_empty_entity(editor_world, world);
        }
        Action::AddPlayerSpawn => {
            spawn_tagged_marker(editor_world, world, "Player Spawn", "player_spawn");
        }
        Action::AddSavePoint => {
            spawn_tagged_marker(editor_world, world, "Save Point", "save_point");
        }
        Action::AddPatrolPoint => {
            spawn_tagged_marker(editor_world, world, "Patrol Point", "patrol_point");
        }
        Action::AddTriggerVolume => {
            spawn_trigger_volume(editor_world, world);
        }
        Action::StampDecalAtCursor => {
            stamp_decal_at_cursor(editor_world, world);
        }
        Action::LoadHdrSkybox => {
            load_hdr_skybox_from_picker(editor_world, world);
        }
        Action::AddPointLight => {
            spawn_light_entity(editor_world, world, LightType::Point);
        }
        Action::AddSpotLight => {
            spawn_light_entity(editor_world, world, LightType::Spot);
        }
        Action::AddDirectionalLight => {
            spawn_light_entity(editor_world, world, LightType::Directional);
        }
        Action::AddTagToSelected(tag) => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                add_tag(editor_world, world, entity, tag);
            }
        }
        Action::RemoveTagFromSelected(tag) => {
            if let Some(entity) = editor_world.resources.ui.selected_entity {
                remove_tag(editor_world, world, entity, &tag);
            }
        }
        Action::OpenCommandPalette => {
            let palette = editor_world.resources.ui_handles.command_palette.entity;
            ui_show_command_palette(world, palette);
        }
        Action::ToggleGreyboxMode => {
            let enabled = !editor_world.resources.greybox.enabled;
            super::greybox::set_mode(editor_world, world, enabled);
        }
        Action::ApplyGreyboxToSelection => {
            super::build::apply_active_to_selection(editor_world, world);
        }
        Action::SnapSelectionToGrid => {
            super::build::snap_selection_to_grid(editor_world, world);
        }
        Action::OpenCreateShape => {
            super::create_shape::open(editor_world, world);
        }
        Action::MarkSelectionAsBrush => {
            super::greybox::mark_selection_as_brush(editor_world, world);
        }
        Action::UpgradeSelectionWithGltf => {
            super::greybox::upgrade_selection_with_gltf(editor_world, world);
        }
        Action::ToggleShowFinal => {
            super::greybox::toggle_show_final(editor_world, world);
        }
    }
}

#[derive(Clone, Copy)]
enum PrimitiveKind {
    Cube,
    Sphere,
    Cylinder,
    Cone,
    Torus,
    Plane,
}

fn primitive_mesh_name(kind: PrimitiveKind) -> &'static str {
    match kind {
        PrimitiveKind::Cube => "Cube",
        PrimitiveKind::Sphere => "Sphere",
        PrimitiveKind::Cylinder => "Cylinder",
        PrimitiveKind::Cone => "Cone",
        PrimitiveKind::Torus => "Torus",
        PrimitiveKind::Plane => "Plane",
    }
}

fn spawn_primitive(editor_world: &mut EditorWorld, world: &mut World, kind: PrimitiveKind) {
    let mut position = camera::focus_point(editor_world, world);
    let greybox_enabled = editor_world.resources.greybox.enabled;
    if greybox_enabled {
        let step = super::build::snap_step(editor_world);
        position = super::build::snap_translation(position, step);
    }
    let entity = match kind {
        PrimitiveKind::Cube => spawn_cube_at(world, position),
        PrimitiveKind::Sphere => spawn_sphere_at(world, position),
        PrimitiveKind::Cylinder => spawn_cylinder_at(world, position),
        PrimitiveKind::Cone => spawn_cone_at(world, position),
        PrimitiveKind::Torus => spawn_torus_at(world, position),
        PrimitiveKind::Plane => spawn_plane_at(world, position),
    };
    if greybox_enabled {
        let index = editor_world.resources.build.active_palette;
        super::build::apply_to_entity(world, index, entity);
    }
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Add primitive",
    );
    select_new_entity(editor_world, entity);
}

fn ensure_primitive_mesh_cached(world: &mut World, mesh_name: &str) {
    use nightshade::ecs::mesh::components::{
        create_cone_mesh, create_cube_mesh, create_cylinder_mesh, create_plane_mesh,
        create_sphere_mesh, create_torus_mesh,
    };
    if world
        .resources
        .assets
        .mesh_cache
        .registry
        .name_to_index
        .contains_key(mesh_name)
    {
        return;
    }
    let mesh = match mesh_name {
        "Cube" => Some(create_cube_mesh()),
        "Sphere" => Some(create_sphere_mesh(1.0, 16)),
        "Plane" => Some(create_plane_mesh(2.0)),
        "Torus" => Some(create_torus_mesh(1.0, 0.3, 32, 16)),
        "Cylinder" => Some(create_cylinder_mesh(0.5, 1.0, 16)),
        "Cone" => Some(create_cone_mesh(0.5, 1.0, 16)),
        _ => None,
    };
    if let Some(mesh) = mesh {
        mesh_cache_insert(
            &mut world.resources.assets.mesh_cache,
            mesh_name.to_string(),
            mesh,
        );
    }
}

fn spawn_instanced_primitive(
    editor_world: &mut EditorWorld,
    world: &mut World,
    kind: PrimitiveKind,
) {
    let mesh_name = primitive_mesh_name(kind);
    ensure_primitive_mesh_cached(world, mesh_name);

    let position = camera::focus_point(editor_world, world);
    let instances = vec![nightshade::ecs::mesh::components::InstanceTransform::default()];
    let entity = spawn_instanced_mesh_with_material(world, mesh_name, instances, "Default");
    world
        .core
        .set_name(entity, Name(format!("Instanced {mesh_name}")));
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.translation = position;
    }
    mark_local_transform_dirty(world, entity);
    world
        .core
        .set_bounding_volume(entity, BoundingVolume::from_mesh_type(mesh_name));

    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Add instanced primitive",
    );
    select_new_entity(editor_world, entity);
}

fn decompose_global_transform(
    matrix: &Mat4,
) -> nightshade::ecs::mesh::components::InstanceTransform {
    let translation = Vec3::new(matrix[(0, 3)], matrix[(1, 3)], matrix[(2, 3)]);
    let column_x = Vec3::new(matrix[(0, 0)], matrix[(1, 0)], matrix[(2, 0)]);
    let column_y = Vec3::new(matrix[(0, 1)], matrix[(1, 1)], matrix[(2, 1)]);
    let column_z = Vec3::new(matrix[(0, 2)], matrix[(1, 2)], matrix[(2, 2)]);
    let scale = Vec3::new(column_x.norm(), column_y.norm(), column_z.norm());
    let safe_scale = Vec3::new(
        if scale.x.abs() < 1.0e-6 { 1.0 } else { scale.x },
        if scale.y.abs() < 1.0e-6 { 1.0 } else { scale.y },
        if scale.z.abs() < 1.0e-6 { 1.0 } else { scale.z },
    );
    let rotation_basis = nalgebra_glm::Mat3::new(
        column_x.x / safe_scale.x,
        column_y.x / safe_scale.y,
        column_z.x / safe_scale.z,
        column_x.y / safe_scale.x,
        column_y.y / safe_scale.y,
        column_z.y / safe_scale.z,
        column_x.z / safe_scale.x,
        column_y.z / safe_scale.y,
        column_z.z / safe_scale.z,
    );
    let rotation = nalgebra_glm::mat3_to_quat(&rotation_basis);
    nightshade::ecs::mesh::components::InstanceTransform::new(translation, rotation, scale)
}

fn convert_similar_to_instanced(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(selected) = editor_world.resources.ui.selected_entity else {
        return;
    };
    let Some(mesh) = world.core.get_render_mesh(selected).cloned() else {
        return;
    };
    let mesh_name = mesh.name.clone();
    let selected_material_name = world
        .core
        .get_material_ref(selected)
        .map(|reference| reference.name.clone());

    let candidate_entities: Vec<Entity> = world
        .core
        .query_entities(
            nightshade::ecs::world::RENDER_MESH | nightshade::ecs::world::GLOBAL_TRANSFORM,
        )
        .collect();
    let mut matches: Vec<Entity> = Vec::new();
    for entity in candidate_entities {
        if editor_world.resources.editor_scene.is_scaffolding(entity) {
            continue;
        }
        let Some(render_mesh) = world.core.get_render_mesh(entity) else {
            continue;
        };
        if render_mesh.name != mesh_name {
            continue;
        }
        let entity_material = world
            .core
            .get_material_ref(entity)
            .map(|reference| reference.name.clone());
        if entity_material != selected_material_name {
            continue;
        }
        if !nightshade::ecs::transform::queries::query_descendants(world, entity).is_empty() {
            continue;
        }
        matches.push(entity);
    }

    if matches.len() < 2 {
        return;
    }

    let mut instances: Vec<nightshade::ecs::mesh::components::InstanceTransform> =
        Vec::with_capacity(matches.len());
    for entity in &matches {
        let Some(global) = world.core.get_global_transform(*entity).copied() else {
            continue;
        };
        instances.push(decompose_global_transform(&global.0));
    }
    if instances.is_empty() {
        return;
    }

    let material_name = selected_material_name.unwrap_or_else(|| "Default".to_string());

    ensure_primitive_mesh_cached(world, &mesh_name);
    let new_entity =
        spawn_instanced_mesh_with_material(world, &mesh_name, instances, &material_name);
    world
        .core
        .set_name(new_entity, Name(format!("Instanced {mesh_name}")));
    world
        .core
        .set_bounding_volume(new_entity, BoundingVolume::from_mesh_type(&mesh_name));

    let captured_originals = capture_entities_for_undo(editor_world, world, &matches);

    for entity in &matches {
        let descendants = nightshade::ecs::transform::queries::query_descendants(world, *entity);
        crate::scene_writeback::remove_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            *entity,
        );
        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, *entity);
    }

    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        new_entity,
    );
    let new_captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, new_entity);

    let mut steps: Vec<crate::undo::UndoableOperation> = Vec::with_capacity(2);
    if let Some(captured_originals) = captured_originals {
        steps.push(crate::undo::UndoableOperation::EntityDeleted {
            captured: Box::new(captured_originals),
        });
    }
    steps.push(crate::undo::UndoableOperation::EntityCreated {
        captured: Box::new(new_captured),
    });
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::Batch { steps },
        "Convert to instanced",
    );

    select_new_entity(editor_world, new_entity);
}

fn capture_entities_for_undo(
    editor_world: &mut EditorWorld,
    world: &World,
    entities: &[Entity],
) -> Option<crate::undo::CapturedSubtree> {
    let mut combined = nightshade::ecs::scene::Scene::new("convert_capture");
    let mut root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = Vec::new();
    let mut root_parents: Vec<Option<nightshade::ecs::scene::AssetUuid>> = Vec::new();
    for entity in entities {
        if !world.core.entity_has_local_transform(*entity) {
            continue;
        }
        let captured =
            crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, *entity);
        for scene_entity in captured.scene.entities {
            combined.add_entity(scene_entity);
        }
        root_uuids.extend(captured.root_uuids);
        root_parents.extend(captured.root_parents);
    }
    if root_uuids.is_empty() {
        return None;
    }
    combined.compute_spawn_order();
    Some(crate::undo::CapturedSubtree {
        scene: combined,
        root_uuids,
        root_parents,
    })
}

fn snap_selection_to_floor(editor_world: &mut EditorWorld, world: &mut World) {
    let selected: Vec<Entity> = editor_world.resources.ui.selected_entities.clone();
    if selected.is_empty() {
        return;
    }
    let mut undo_steps: Vec<crate::undo::UndoableOperation> = Vec::new();
    for entity in selected {
        let Some(global) = world.core.get_global_transform(entity).copied() else {
            continue;
        };
        let Some(local) = world.core.get_local_transform(entity).copied() else {
            continue;
        };
        let world_position = global.translation();
        let origin = Vec3::new(world_position.x, world_position.y + 0.01, world_position.z);
        let direction = Vec3::new(0.0, -1.0, 0.0);
        let hit = nightshade::ecs::physics::resources::physics_world_cast_ray(
            &world.resources.physics,
            origin,
            direction,
            10_000.0,
            Some(entity),
        );
        let target_y = hit.map(|hit| hit.point.y).unwrap_or(0.0);
        let delta_y_world = target_y - world_position.y;
        if delta_y_world.abs() < 1e-6 {
            continue;
        }
        let new_local_translation = Vec3::new(
            local.translation.x,
            local.translation.y + delta_y_world,
            local.translation.z,
        );
        let new_local = LocalTransform {
            translation: new_local_translation,
            rotation: local.rotation,
            scale: local.scale,
        };
        let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity) else {
            continue;
        };
        if let Some(transform) = world.core.get_local_transform_mut(entity) {
            transform.translation = new_local_translation;
        }
        mark_local_transform_dirty(world, entity);
        crate::scene_writeback::sync_entity(
            &mut editor_world.resources.project,
            &editor_world.resources.editor_scene,
            world,
            entity,
        );
        undo_steps.push(crate::undo::UndoableOperation::TransformChanged {
            uuid,
            old: local,
            new: new_local,
        });
    }
    if undo_steps.is_empty() {
        return;
    }
    if undo_steps.len() == 1 {
        editor_world
            .resources
            .undo
            .push(undo_steps.into_iter().next().unwrap(), "Snap to floor");
    } else {
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::Batch { steps: undo_steps },
            "Snap to floor",
        );
    }
    editor_world.resources.ui_handles.inspector.dirty = true;
}

#[cfg(not(target_arch = "wasm32"))]
fn load_hdr_skybox_from_picker(editor_world: &mut EditorWorld, world: &mut World) {
    let picked = nightshade::prelude::rfd::FileDialog::new()
        .set_title("Select HDR skybox")
        .add_filter("HDR image", &["hdr", "exr"])
        .pick_file();
    let Some(path) = picked else { return };
    nightshade::ecs::world::commands::queue_render_command(
        world,
        nightshade::ecs::world::commands::RenderCommand::LoadHdrSkyboxFromPath {
            path: path.clone(),
        },
    );
    crate::scene_writeback::sync_hdr_skybox(
        &mut editor_world.resources.project,
        Some(nightshade::ecs::scene::SceneHdrSkybox::Reference {
            path: path.to_string_lossy().to_string(),
        }),
    );
    world.resources.render_settings.atmosphere = nightshade::prelude::Atmosphere::Hdr;
}

#[cfg(target_arch = "wasm32")]
fn load_hdr_skybox_from_picker(_editor_world: &mut EditorWorld, _world: &mut World) {}

fn stamp_decal_at_cursor(editor_world: &mut EditorWorld, world: &mut World) {
    let Some(camera_entity) = world.resources.active_camera else {
        return;
    };
    let mouse = nightshade::ecs::input::access::mouse_for_active(world);
    let mouse_position = mouse.position;
    let viewport = match world
        .resources
        .window
        .camera_tile_rects
        .get(&camera_entity)
        .copied()
        .or(world.resources.window.active_viewport_rect)
    {
        Some(rect) if rect.width > 0.0 && rect.height > 0.0 => rect,
        _ => return,
    };
    if !viewport.contains(mouse_position) {
        return;
    }
    let Some(camera) = world.core.get_camera(camera_entity).copied() else {
        return;
    };
    let Some(camera_transform) = world.core.get_global_transform(camera_entity) else {
        return;
    };
    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;
    }
    let near_world = Vec3::new(near_clip.x, near_clip.y, near_clip.z) / near_clip.w;
    let direction = (near_world - camera_position).normalize();
    let hit = nightshade::ecs::physics::resources::physics_world_cast_ray(
        &world.resources.physics,
        camera_position,
        direction,
        10_000.0,
        None,
    );
    let (hit_point, hit_normal) = match hit {
        Some(hit) => (hit.point, hit.normal),
        None => {
            let denominator = direction.y;
            if denominator.abs() < 1e-4 {
                return;
            }
            let parameter = -camera_position.y / denominator;
            if parameter <= 0.0 {
                return;
            }
            (
                camera_position + direction * parameter,
                Vec3::new(0.0, 1.0, 0.0),
            )
        }
    };
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | DECAL,
        1,
    )[0];
    world
        .core
        .set_name(entity, Name(format!("Decal {}", entity.id)));
    let projection_direction = -hit_normal;
    let up_reference = if projection_direction.y.abs() > 0.95 {
        Vec3::new(0.0, 0.0, 1.0)
    } else {
        Vec3::new(0.0, 1.0, 0.0)
    };
    let rotation = nalgebra_glm::quat_look_at_rh(&projection_direction, &up_reference);
    world.core.set_local_transform(
        entity,
        LocalTransform {
            translation: hit_point + hit_normal * 0.01,
            rotation,
            scale: Vec3::new(1.0, 1.0, 1.0),
        },
    );
    world
        .core
        .set_local_transform_dirty(entity, LocalTransformDirty);
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    world
        .core
        .set_decal(entity, nightshade::ecs::decal::components::Decal::default());
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Stamp decal",
    );
    select_new_entity(editor_world, entity);
}

fn spawn_tagged_marker(editor_world: &mut EditorWorld, world: &mut World, name: &str, tag: &str) {
    let position = camera::focus_point(editor_world, world);
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
        1,
    )[0];
    world
        .core
        .set_name(entity, Name(format!("{name} {}", entity.id)));
    world.core.set_local_transform(
        entity,
        LocalTransform {
            translation: position,
            rotation: Quat::identity(),
            scale: Vec3::new(0.25, 0.25, 0.25),
        },
    );
    world
        .core
        .set_local_transform_dirty(entity, LocalTransformDirty);
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    world
        .resources
        .entities
        .tags
        .entry(entity)
        .or_default()
        .push(tag.to_string());
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        format!("Add {name}"),
    );
    select_new_entity(editor_world, entity);
}

fn spawn_trigger_volume(editor_world: &mut EditorWorld, world: &mut World) {
    let position = camera::focus_point(editor_world, world);
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM | COLLIDER,
        1,
    )[0];
    world
        .core
        .set_name(entity, Name(format!("Trigger Volume {}", entity.id)));
    world.core.set_local_transform(
        entity,
        LocalTransform {
            translation: position,
            rotation: Quat::identity(),
            scale: Vec3::new(1.0, 1.0, 1.0),
        },
    );
    world
        .core
        .set_local_transform_dirty(entity, LocalTransformDirty);
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    let mut collider = ColliderComponent::new_cuboid(1.0, 1.0, 1.0);
    collider.is_sensor = true;
    world.core.set_collider(entity, collider);
    world
        .core
        .add_components(entity, nightshade::ecs::world::COLLISION_LISTENER);
    world.core.set_collision_listener(
        entity,
        nightshade::ecs::physics::components::CollisionListener,
    );
    world
        .resources
        .entities
        .tags
        .entry(entity)
        .or_default()
        .push("trigger".to_string());
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Add trigger volume",
    );
    select_new_entity(editor_world, entity);
}

fn spawn_empty_entity(editor_world: &mut EditorWorld, world: &mut World) {
    let position = camera::focus_point(editor_world, world);
    let entity = spawn_entities(
        world,
        NAME | LOCAL_TRANSFORM | LOCAL_TRANSFORM_DIRTY | GLOBAL_TRANSFORM,
        1,
    )[0];
    world
        .core
        .set_name(entity, Name(format!("Empty {}", entity.id)));
    world.core.set_local_transform(
        entity,
        LocalTransform {
            translation: position,
            rotation: Quat::identity(),
            scale: Vec3::new(1.0, 1.0, 1.0),
        },
    );
    world
        .core
        .set_local_transform_dirty(entity, LocalTransformDirty);
    world
        .core
        .set_global_transform(entity, GlobalTransform::default());
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Add empty",
    );
    select_new_entity(editor_world, entity);
}

fn spawn_light_entity(editor_world: &mut EditorWorld, world: &mut World, light_type: LightType) {
    let position = camera::focus_point(editor_world, world) + Vec3::new(0.0, 2.0, 0.0);
    let color = Vec3::new(1.0, 0.95, 0.8);
    let entity = spawn_sphere_at(world, position);
    let (name, intensity, range, material_prefix) = match light_type {
        LightType::Point => ("Point Light", 5.0_f32, 10.0_f32, "PointLightMaterial"),
        LightType::Spot => ("Spot Light", 10.0_f32, 15.0_f32, "SpotLightMaterial"),
        LightType::Directional => (
            "Directional Light",
            3.0_f32,
            0.0_f32,
            "DirectionalLightMaterial",
        ),
        LightType::Area => ("Area Light", 10.0_f32, 10.0_f32, "AreaLightMaterial"),
    };
    world.core.set_name(entity, Name(name.to_string()));
    if let Some(transform) = world.core.get_local_transform_mut(entity) {
        transform.scale = Vec3::new(0.15, 0.15, 0.15);
    }
    mark_local_transform_dirty(world, entity);
    world.core.add_components(entity, LIGHT);
    world.core.set_light(
        entity,
        Light {
            light_type,
            color,
            intensity,
            range,
            ..Default::default()
        },
    );
    let material_name = format!("{material_prefix}_{}", entity.id);
    spawn_material(
        world,
        entity,
        material_name,
        nightshade::ecs::material::components::Material {
            base_color: [color.x, color.y, color.z, 1.0],
            emissive_factor: [color.x, color.y, color.z],
            emissive_strength: 4.0,
            roughness: 1.0,
            metallic: 0.0,
            unlit: false,
            ..Default::default()
        },
    );
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Add light",
    );
    select_new_entity(editor_world, entity);
}

fn delete_entity(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
    if editor_world.resources.editor_scene.is_scaffolding(entity) {
        return;
    }
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    let descendants = nightshade::ecs::transform::queries::query_descendants(world, entity);
    crate::scene_writeback::remove_entity(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        entity,
    );
    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, entity);
    world.resources.mesh_render_state.request_full_rebuild();
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityDeleted {
            captured: Box::new(captured),
        },
        "Delete entity",
    );
    crate::systems::selection::remove(editor_world, entity);
    crate::systems::picking::reset_cycle(editor_world);
    editor_world.resources.ui_handles.tree.last_signature = 0;
    editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
}

fn duplicate_entity(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
    if editor_world.resources.editor_scene.is_scaffolding(entity) {
        return;
    }
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    let mut clone_scene = nightshade::ecs::scene::Scene::new("duplicate");
    clone_scene.atmosphere = world.resources.render_settings.atmosphere;
    clone_scene.settings = nightshade::ecs::scene::commands::capture_scene_settings(world);
    let mut uuid_remap: std::collections::HashMap<
        nightshade::ecs::scene::AssetUuid,
        nightshade::ecs::scene::AssetUuid,
    > = std::collections::HashMap::new();
    for source in &captured.scene.entities {
        let new_uuid = nightshade::ecs::scene::AssetUuid::random();
        uuid_remap.insert(source.uuid, new_uuid);
    }
    for source in &captured.scene.entities {
        let mut clone = source.clone();
        clone.uuid = uuid_remap[&source.uuid];
        clone.parent = source
            .parent
            .and_then(|parent| uuid_remap.get(&parent).copied());
        if uuid_remap.contains_key(&source.uuid) && captured.root_uuids.contains(&source.uuid) {
            clone.transform.translation += Vec3::new(0.5, 0.0, 0.5);
        }
        clone_scene.add_entity(clone);
    }
    clone_scene.compute_spawn_order();
    let new_root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = captured
        .root_uuids
        .iter()
        .filter_map(|uuid| uuid_remap.get(uuid).copied())
        .collect();
    let result = match nightshade::ecs::scene::spawn_scene(world, &clone_scene, None) {
        Ok(result) => result,
        Err(error) => {
            tracing::error!("duplicate failed: {error}");
            return;
        }
    };
    for (uuid, runtime_entity) in &result.uuid_to_entity {
        editor_world
            .resources
            .editor_scene
            .insert(*uuid, *runtime_entity);
    }
    let parent_uuid = world
        .core
        .get_parent(entity)
        .and_then(|p| p.0)
        .and_then(|parent| editor_world.resources.editor_scene.uuid_for(parent));
    if let Some(parent_uuid) = parent_uuid {
        for root_uuid in &new_root_uuids {
            let Some(child) = result.uuid_to_entity.get(root_uuid).copied() else {
                continue;
            };
            if let Some(parent) = editor_world.resources.editor_scene.entity_for(parent_uuid) {
                world.core.set_parent(
                    child,
                    nightshade::ecs::transform::components::Parent(Some(parent)),
                );
            }
        }
    }
    for runtime_entity in result.uuid_to_entity.values() {
        crate::scene_writeback::add_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            world,
            *runtime_entity,
        );
    }
    let new_capture = crate::undo::CapturedSubtree {
        scene: clone_scene,
        root_uuids: new_root_uuids.clone(),
        root_parents: vec![parent_uuid; new_root_uuids.len()],
    };
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(new_capture),
        },
        "Duplicate",
    );
    if let Some(first_uuid) = new_root_uuids.first()
        && let Some(new_entity) = result.uuid_to_entity.get(first_uuid)
    {
        select_new_entity(editor_world, *new_entity);
    }
}

fn add_tag(editor_world: &mut EditorWorld, world: &mut World, entity: Entity, tag: String) {
    if tag.is_empty() {
        return;
    }
    let tags = world.resources.entities.tags.entry(entity).or_default();
    if !tags.contains(&tag) {
        tags.push(tag);
    }
    crate::scene_writeback::sync_entity(
        &mut editor_world.resources.project,
        &editor_world.resources.editor_scene,
        world,
        entity,
    );
    editor_world.resources.ui_handles.inspector.dirty = true;
}

fn remove_tag(editor_world: &mut EditorWorld, world: &mut World, entity: Entity, tag: &str) {
    if let Some(tags) = world.resources.entities.tags.get_mut(&entity) {
        tags.retain(|existing| existing != tag);
        if tags.is_empty() {
            world.resources.entities.tags.remove(&entity);
        }
    }
    crate::scene_writeback::sync_entity(
        &mut editor_world.resources.project,
        &editor_world.resources.editor_scene,
        world,
        entity,
    );
    editor_world.resources.ui_handles.inspector.dirty = true;
}

fn select_new_entity(editor_world: &mut EditorWorld, entity: Entity) {
    crate::systems::selection::set_primary(editor_world, Some(entity));
    editor_world.resources.ui_handles.tree.last_signature = 0;
    editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
    crate::systems::picking::reset_cycle(editor_world);
}

fn apply_history_action(editor_world: &mut EditorWorld, world: &mut World, is_undo: bool) {
    crate::systems::picking::reset_cycle(editor_world);
    let mut history = std::mem::take(&mut editor_world.resources.undo);
    let result = {
        let mut ctx = crate::undo::UndoCtx {
            world,
            editor_scene: &mut editor_world.resources.editor_scene,
            project: &mut editor_world.resources.project,
        };
        if is_undo {
            history.undo(&mut ctx)
        } else {
            history.redo(&mut ctx)
        }
    };
    editor_world.resources.undo = history;
    if let Some(result) = result {
        if let Some(entity) = result.select_entity {
            crate::systems::selection::set_primary(editor_world, Some(entity));
        }
        editor_world.resources.ui_handles.tree.last_signature = 0;
        editor_world.resources.ui_handles.tree.last_visible_start = usize::MAX;
        editor_world.resources.writeback_state.mark_full_resync();
    }
}

fn capture_active_scene_for_undo(
    editor_world: &mut EditorWorld,
    world: &World,
) -> Option<crate::undo::CapturedSubtree> {
    let model_roots = editor_world.resources.loading.model_entities.clone();
    let dev_roots = editor_world.resources.loading.dev_tool_entities.clone();
    let mut roots: Vec<Entity> = Vec::with_capacity(model_roots.len() + dev_roots.len());
    roots.extend(model_roots);
    roots.extend(dev_roots);
    if roots.is_empty() {
        return None;
    }
    let mut combined = nightshade::ecs::scene::Scene::new("undo_capture");
    let mut root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = Vec::new();
    let mut root_parents: Vec<Option<nightshade::ecs::scene::AssetUuid>> = Vec::new();
    for root in roots {
        if !world.core.entity_has_local_transform(root) {
            continue;
        }
        let captured =
            crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
        for entity in captured.scene.entities {
            combined.add_entity(entity);
        }
        root_uuids.extend(captured.root_uuids);
        root_parents.extend(captured.root_parents);
    }
    if root_uuids.is_empty() {
        return None;
    }
    combined.compute_spawn_order();
    Some(crate::undo::CapturedSubtree {
        scene: combined,
        root_uuids,
        root_parents,
    })
}

fn register_dev_tool_root(editor_world: &mut EditorWorld, world: &mut World, entity: Entity) {
    editor_world
        .resources
        .loading
        .dev_tool_entities
        .push(entity);
    crate::scene_writeback::register_subtree(
        &mut editor_world.resources.project,
        &mut editor_world.resources.editor_scene,
        world,
        entity,
    );
    let captured =
        crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, entity);
    editor_world.resources.undo.push(
        crate::undo::UndoableOperation::EntityCreated {
            captured: Box::new(captured),
        },
        "Spawn",
    );
}
