//! Hierarchy outliner: tree view, drag-and-drop, visibility, and lock toggles.

use bevy::prelude::*;
use bevy_egui::egui;
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
use egui::containers::scroll_area::ScrollSource;
use egui_phosphor_icons::icons;
use shared::{EditorVisibility, HydratedPrefabMember};

use crate::history::{
    create_empty_child_with_history, delete_entities_with_history, duplicate_entities_with_history,
    group_selection_with_history, rename_entity_with_history, reorder_siblings_with_history,
    reparent_with_history, set_editor_visibility_with_history,
};
use crate::project_io::UserPreferences;
use crate::selection::SelectedEntity;
use crate::ui::helpers::{
    entity_name, focus_editor_camera_on_selection, is_level_object, HierarchyNodeKind,
};
use crate::ui::hierarchy_ops::{
    authored_children, editor_visibility, entity_hierarchy_path, entity_matches_filter,
    hierarchy_drop_violation, hierarchy_label_for_entity, is_ancestor, is_entity_locked,
    level_object_roots, mutable_authored_children, sort_siblings, HierarchyDropViolation,
    HierarchyLabel,
};
use crate::ui::hierarchy_state::{HierarchyPanelState, HierarchySort};
use crate::ui::selection_ops::is_linked_read_only;
use crate::ui::theme::{
    overlay_chip_frame, panel_heading, ACCENT, ERROR, PANEL_BG_DARK, SELECTION, SELECTION_BG_MUTED,
    SELECTION_SECONDARY, TEXT, TEXT_DIM, TEXT_MUTED, TEXT_SELECTED,
};
use crate::ui::widgets::phosphor_icon;

#[derive(Clone)]
struct HierarchyDragPayload {
    entities: Vec<Entity>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DropTarget {
    Root,
    On(Entity),
    Before(Entity),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum DropBlock {
    Hierarchy(HierarchyDropViolation),
    LockedTarget(Entity),
}

pub fn hierarchy_panel_ui(
    world: &mut World,
    ui: &mut egui::Ui,
    selected_entities: &mut SelectedEntities,
    renaming_entity: &mut Option<Entity>,
    rename_buffer: &mut String,
) {
    let (filter, sort_mode, show_generated, show_runtime, _expanded, _locked, prefs_paths) = {
        let state = world.resource::<HierarchyPanelState>();
        (
            state.filter.clone(),
            state.sort_mode,
            state.show_generated,
            state.show_runtime,
            state.expanded.clone(),
            state.locked.clone(),
            world
                .resource::<UserPreferences>()
                .hierarchy_expanded_paths
                .clone(),
        )
    };

    if _expanded.is_empty() && !prefs_paths.is_empty() {
        let synced = crate::ui::hierarchy_state::expanded_entities_for_paths(world, &prefs_paths);
        world.resource_mut::<HierarchyPanelState>().expanded = synced;
    }

    let expanded = world.resource::<HierarchyPanelState>().expanded.clone();
    let locked = world.resource::<HierarchyPanelState>().locked.clone();

    let mut filter = filter;
    ui.horizontal(|ui| {
        ui.label(icons::MAGNIFYING_GLASS.regular());
        if ui
            .add(
                egui::TextEdit::singleline(&mut filter)
                    .hint_text("Search hierarchy...")
                    .desired_width(f32::INFINITY),
            )
            .changed()
        {
            world.resource_mut::<HierarchyPanelState>().filter = filter.clone();
            world.resource_mut::<HierarchyPanelState>().mark_dirty();
        }
    });

    ui.horizontal(|ui| {
        ui.label("Sort:");
        let mut sort = sort_mode;
        egui::ComboBox::from_id_salt("hierarchy_sort")
            .selected_text(sort_label(sort))
            .show_ui(ui, |ui| {
                for candidate in [
                    HierarchySort::Manual,
                    HierarchySort::Name,
                    HierarchySort::Type,
                ] {
                    if ui
                        .selectable_value(&mut sort, candidate, sort_label(candidate))
                        .clicked()
                    {
                        world.resource_mut::<HierarchyPanelState>().sort_mode = candidate;
                        world.resource_mut::<HierarchyPanelState>().mark_dirty();
                    }
                }
            });
        let mut show_gen = show_generated;
        if ui.checkbox(&mut show_gen, "Generated").changed() {
            world.resource_mut::<HierarchyPanelState>().show_generated = show_gen;
            world.resource_mut::<HierarchyPanelState>().mark_dirty();
        }
        let mut show_rt = show_runtime;
        if ui.checkbox(&mut show_rt, "Runtime").changed() {
            world.resource_mut::<HierarchyPanelState>().show_runtime = show_rt;
            world.resource_mut::<HierarchyPanelState>().mark_dirty();
        }
    });

    ui.add_space(4.0);
    ui.label(panel_heading("Scene"));

    if ui.input(|input| input.key_pressed(egui::Key::F2)) {
        if let Some(entity) = selected_entities.as_slice().first().copied() {
            if is_level_object(world, entity) && !is_entity_locked(&locked, entity) {
                *renaming_entity = Some(entity);
                *rename_buffer = entity_name(world, entity);
            }
        }
    }

    let filter_lower = filter.trim().to_lowercase();
    let mut drop_target = None;
    let mut drop_block = None;

    let active_drag = world.resource::<HierarchyPanelState>().active_drag.clone();
    let dragging = active_drag.is_some();

    egui::ScrollArea::vertical()
        .id_salt("hierarchy_tree")
        .scroll_source(ScrollSource {
            scroll_bar: true,
            drag: !dragging,
            mouse_wheel: true,
        })
        .show(ui, |ui| {
            ui.spacing_mut().item_spacing.y = 2.0;

            if dragging {
                draw_root_drop_zone(ui, active_drag.as_deref(), &mut drop_target);
            }

            let mut roots = level_object_roots(world);
            sort_siblings(world, &mut roots, sort_mode);

            let visible_roots: Vec<Entity> = roots
                .into_iter()
                .filter(|entity| subtree_matches_filter(world, *entity, &filter_lower, sort_mode))
                .collect();

            for entity in visible_roots {
                draw_authored_node(
                    world,
                    ui,
                    entity,
                    0,
                    selected_entities,
                    renaming_entity,
                    rename_buffer,
                    &filter_lower,
                    sort_mode,
                    show_generated,
                    active_drag.as_deref(),
                    &expanded,
                    &locked,
                    &prefs_paths,
                    &mut drop_target,
                    &mut drop_block,
                );
            }

            if show_runtime {
                let runtime_roots = runtime_roots(world);
                if !runtime_roots.is_empty() {
                    ui.add_space(6.0);
                    ui.small(egui::RichText::new("Runtime").weak());
                    for entity in runtime_roots {
                        draw_runtime_node(world, ui, entity, 0, selected_entities, show_runtime);
                    }
                }
            }
        });

    draw_hierarchy_drag_preview(world, ui, active_drag.as_deref(), drop_target, drop_block);

    if ui.ctx().input(|input| input.pointer.any_released()) {
        if let Some(target) = drop_target {
            let drag_entities = world
                .resource_mut::<HierarchyPanelState>()
                .active_drag
                .take();
            if let Some(entities) = drag_entities {
                let payload = HierarchyDragPayload { entities };
                apply_drop(world, &payload, target, drop_block);
            }
        } else {
            world.resource_mut::<HierarchyPanelState>().active_drag = None;
        }
    }

    if selected_entities.as_slice().first().copied() != world.resource::<SelectedEntity>().0 {
        world.resource_mut::<SelectedEntity>().0 = selected_entities.as_slice().first().copied();
    }
}

fn sort_label(sort: HierarchySort) -> &'static str {
    match sort {
        HierarchySort::Manual => "Manual",
        HierarchySort::Name => "Name",
        HierarchySort::Type => "Type",
    }
}

fn draw_root_drop_zone(
    ui: &mut egui::Ui,
    drag_entities: Option<&[Entity]>,
    drop_target: &mut Option<DropTarget>,
) {
    let (rect, response) =
        ui.allocate_exact_size(egui::vec2(ui.available_width(), 27.0), egui::Sense::hover());
    let hovered = drag_entities.is_some()
        && (response.contains_pointer()
            || response
                .dnd_hover_payload::<HierarchyDragPayload>()
                .is_some());
    ui.painter().rect(
        rect,
        2.0,
        if hovered {
            SELECTION_BG_MUTED
        } else {
            PANEL_BG_DARK
        },
        egui::Stroke::new(1.0, if hovered { SELECTION } else { TEXT_MUTED }),
        egui::StrokeKind::Inside,
    );
    ui.painter().text(
        rect.left_center() + egui::vec2(9.0, 0.0),
        egui::Align2::LEFT_CENTER,
        icons::TREE_STRUCTURE.as_str(),
        egui::FontId::new(13.0, egui::FontFamily::Name("phosphor-regular".into())),
        if hovered { SELECTION } else { TEXT_DIM },
    );
    ui.painter().text(
        rect.left_center() + egui::vec2(29.0, 0.0),
        egui::Align2::LEFT_CENTER,
        "SCENE ROOT",
        egui::FontId::new(11.0, egui::FontFamily::Proportional),
        if hovered { TEXT_SELECTED } else { TEXT_DIM },
    );
    if hovered {
        *drop_target = Some(DropTarget::Root);
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "recursive hierarchy rendering threads interaction state explicitly"
)]
fn draw_authored_node(
    world: &mut World,
    ui: &mut egui::Ui,
    entity: Entity,
    depth: usize,
    selected_entities: &mut SelectedEntities,
    renaming_entity: &mut Option<Entity>,
    rename_buffer: &mut String,
    filter_lower: &str,
    sort_mode: HierarchySort,
    show_generated: bool,
    drag_entities: Option<&[Entity]>,
    expanded: &std::collections::HashSet<Entity>,
    locked: &std::collections::HashSet<Entity>,
    prefs_paths: &[String],
    drop_target: &mut Option<DropTarget>,
    drop_block: &mut Option<DropBlock>,
) {
    let path = entity_hierarchy_path(world, entity);
    let mut is_expanded = expanded.contains(&entity) || prefs_paths.iter().any(|p| p == &path);
    let has_children = authored_children(world, entity)
        .iter()
        .any(|child| subtree_matches_filter(world, *child, filter_lower, sort_mode))
        || (show_generated
            && world.get::<Children>(entity).is_some_and(|children| {
                children
                    .iter()
                    .any(|child| is_hierarchy_generated_child(world, child))
            }));

    if sort_mode == HierarchySort::Manual {
        draw_drop_before(
            world,
            ui,
            entity,
            depth,
            drag_entities,
            locked,
            drop_target,
            drop_block,
        );
    }

    ui.horizontal(|ui| {
        ui.add_space(depth as f32 * 14.0);
        if has_children {
            let twistie = if is_expanded {
                icons::CARET_DOWN
            } else {
                icons::CARET_RIGHT
            };
            if ui.small_button(twistie.regular()).clicked() {
                let path = path.clone();
                let expanding = !world
                    .resource::<HierarchyPanelState>()
                    .expanded
                    .contains(&entity);
                {
                    let mut state = world.resource_mut::<HierarchyPanelState>();
                    if expanding {
                        state.expanded.insert(entity);
                    } else {
                        state.expanded.remove(&entity);
                    }
                    state.mark_dirty();
                }
                let mut prefs = world.resource_mut::<UserPreferences>();
                if expanding {
                    if !prefs
                        .hierarchy_expanded_paths
                        .iter()
                        .any(|entry| entry == &path)
                    {
                        prefs.hierarchy_expanded_paths.push(path);
                    }
                } else {
                    prefs
                        .hierarchy_expanded_paths
                        .retain(|entry| entry != &path);
                }
                is_expanded = expanding;
            }
        } else {
            ui.add_space(18.0);
        }

        let linked_read_only = is_linked_read_only(world, entity);
        let is_locked = is_entity_locked(locked, entity);
        let visible = editor_visibility(world, entity).visible;
        let eye_icon = if visible {
            icons::EYE.regular()
        } else {
            icons::EYE_SLASH.regular()
        };
        let prefab_member = world.get::<HydratedPrefabMember>(entity).is_some();
        if ui
            .add_enabled(
                !is_locked && (!linked_read_only || prefab_member),
                egui::Button::new(eye_icon),
            )
            .clicked()
        {
            if prefab_member {
                crate::assets::prefab_overrides::set_prefab_component_override(
                    world,
                    entity,
                    EditorVisibility { visible: !visible },
                );
            } else {
                set_editor_visibility_with_history(
                    world,
                    entity,
                    EditorVisibility { visible: !visible },
                );
            }
        }

        let lock_icon = if is_locked {
            icons::LOCK.regular()
        } else {
            icons::LOCK_OPEN.regular()
        };
        if ui.small_button(lock_icon).clicked() {
            let mut state = world.resource_mut::<HierarchyPanelState>();
            if is_locked {
                state.locked.remove(&entity);
            } else {
                state.locked.insert(entity);
            }
        }

        if *renaming_entity == Some(entity) {
            let response = ui.text_edit_singleline(rename_buffer);
            let commit =
                response.lost_focus() || ui.input(|input| input.key_pressed(egui::Key::Enter));
            if commit && !is_locked && !linked_read_only {
                rename_entity_with_history(world, entity, rename_buffer.clone());
                *renaming_entity = None;
            }
        } else {
            let selected = selected_entities.contains(entity);
            let primary = selected_entities.as_slice().first().copied() == Some(entity);
            let label = hierarchy_label_for_entity(world, entity, HierarchyNodeKind::Authored);
            let color = if selected { TEXT_SELECTED } else { TEXT };
            let response = ui.add_sized(
                egui::vec2(ui.available_width().max(72.0), 24.0),
                egui::Button::selectable(selected, hierarchy_label_text(label, color))
                    .sense(egui::Sense::click_and_drag()),
            );
            if selected {
                ui.painter().line_segment(
                    [response.rect.left_top(), response.rect.left_bottom()],
                    egui::Stroke::new(
                        if primary { 3.0 } else { 2.0 },
                        if primary {
                            SELECTION
                        } else {
                            SELECTION_SECONDARY
                        },
                    ),
                );
            }
            if response.clicked() && !is_locked {
                let additive = ui.input(|input| {
                    input.modifiers.shift || input.modifiers.command || input.modifiers.ctrl
                });
                if additive {
                    selected_entities.select_maybe_add(entity, true);
                } else {
                    selected_entities.select_replace(entity);
                }
                world.resource_mut::<SelectedEntity>().0 = Some(entity);
            }
            if response.double_clicked()
                && is_level_object(world, entity)
                && !is_locked
                && !linked_read_only
            {
                *renaming_entity = Some(entity);
                *rename_buffer = entity_name(world, entity);
            }

            if response.drag_started() && !is_locked && (!linked_read_only || prefab_member) {
                let candidates: Vec<Entity> = if selected_entities.contains(entity) {
                    selected_entities.iter().collect()
                } else {
                    vec![entity]
                };
                let drag_entities = draggable_selection(world, &candidates, locked);
                response.dnd_set_drag_payload(HierarchyDragPayload {
                    entities: drag_entities.clone(),
                });
                world.resource_mut::<HierarchyPanelState>().active_drag = Some(drag_entities);
            }

            let hover_payload = response.dnd_hover_payload::<HierarchyDragPayload>();
            let hovered_entities = hover_payload
                .as_deref()
                .map(|payload| payload.entities.as_slice())
                .or_else(|| drag_entities.filter(|_| response.contains_pointer()));
            if let Some(entities) = hovered_entities {
                *drop_block = drop_block_for_target(world, locked, entities, Some(entity), entity);
                *drop_target = Some(DropTarget::On(entity));
                draw_attach_target_feedback(ui, response.rect, drop_block.is_some());
            }

            response.context_menu(|ui| {
                authored_context_menu(
                    world,
                    ui,
                    entity,
                    selected_entities,
                    renaming_entity,
                    rename_buffer,
                    locked,
                )
            });
        }
    });

    if !is_expanded {
        return;
    }

    let mut children = authored_children(world, entity);
    sort_siblings(world, &mut children, sort_mode);
    for child in children {
        if subtree_matches_filter(world, child, filter_lower, sort_mode) {
            draw_authored_node(
                world,
                ui,
                child,
                depth + 1,
                selected_entities,
                renaming_entity,
                rename_buffer,
                filter_lower,
                sort_mode,
                show_generated,
                drag_entities,
                expanded,
                locked,
                prefs_paths,
                drop_target,
                drop_block,
            );
        }
    }

    if show_generated {
        if let Some(children) = world.get::<Children>(entity) {
            for child in children.iter() {
                if is_hierarchy_generated_child(world, child) {
                    draw_generated_node(world, ui, child, depth + 1, expanded, show_generated);
                }
            }
        }
    }
}

#[expect(
    clippy::too_many_arguments,
    reason = "immediate-mode drop feedback threads the current drag, lock, and target state"
)]
fn draw_drop_before(
    world: &World,
    ui: &mut egui::Ui,
    entity: Entity,
    depth: usize,
    drag_entities: Option<&[Entity]>,
    locked: &std::collections::HashSet<Entity>,
    drop_target: &mut Option<DropTarget>,
    drop_block: &mut Option<DropBlock>,
) {
    let Some(drag_entities) = drag_entities else {
        return;
    };

    ui.horizontal(|ui| {
        ui.add_space(depth as f32 * 14.0 + 18.0);
        let (rect, response) =
            ui.allocate_exact_size(egui::vec2(ui.available_width(), 7.0), egui::Sense::hover());
        let hovered = response.contains_pointer()
            || response
                .dnd_hover_payload::<HierarchyDragPayload>()
                .is_some();
        if hovered {
            let parent = world
                .get::<ChildOf>(entity)
                .map(|child_of| child_of.parent());
            *drop_block = if world.get::<HydratedPrefabMember>(entity).is_some()
                || drag_entities
                    .iter()
                    .any(|dragged| world.get::<HydratedPrefabMember>(*dragged).is_some())
            {
                let root =
                    crate::assets::prefab_overrides::prefab_member_override_target(world, entity)
                        .map(|(root, _)| root)
                        .unwrap_or(entity);
                Some(DropBlock::Hierarchy(
                    HierarchyDropViolation::PrefabBoundary { root },
                ))
            } else {
                drop_block_for_target(world, locked, drag_entities, parent, entity)
            };
            *drop_target = Some(DropTarget::Before(entity));
            let color = if drop_block.is_some() {
                ERROR
            } else {
                SELECTION
            };
            ui.painter().line_segment(
                [rect.left_center(), rect.right_center()],
                egui::Stroke::new(2.0, color),
            );
            ui.painter().circle_filled(rect.left_center(), 3.5, color);
        }
    });
}

fn draw_attach_target_feedback(ui: &egui::Ui, rect: egui::Rect, invalid: bool) {
    let color = if invalid { ERROR } else { ACCENT };
    ui.painter().rect_filled(
        rect,
        2.0,
        if invalid {
            egui::Color32::from_rgba_premultiplied(105, 25, 29, 68)
        } else {
            egui::Color32::from_rgba_premultiplied(92, 66, 18, 62)
        },
    );
    ui.painter().rect_stroke(
        rect,
        2.0,
        egui::Stroke::new(2.0, color),
        egui::StrokeKind::Inside,
    );
    if rect.width() > 150.0 {
        ui.painter().text(
            rect.right_center() - egui::vec2(7.0, 0.0),
            egui::Align2::RIGHT_CENTER,
            if invalid { "BLOCKED" } else { "ATTACH" },
            egui::FontId::new(9.0, egui::FontFamily::Proportional),
            color,
        );
    }
}

fn draw_generated_node(
    world: &World,
    ui: &mut egui::Ui,
    entity: Entity,
    depth: usize,
    expanded: &std::collections::HashSet<Entity>,
    show_generated: bool,
) {
    let _ = show_generated;
    let has_children = world.get::<Children>(entity).is_some_and(|children| {
        children
            .iter()
            .any(|child| is_hierarchy_generated_child(world, child))
    });
    let mut is_expanded = expanded.contains(&entity);
    ui.horizontal(|ui| {
        ui.add_space(depth as f32 * 14.0);
        if has_children {
            let twistie = if is_expanded {
                icons::CARET_DOWN
            } else {
                icons::CARET_RIGHT
            };
            if ui.small_button(twistie.regular()).clicked() {
                is_expanded = !is_expanded;
            }
        } else {
            ui.add_space(18.0);
        }
        ui.add_enabled(
            false,
            egui::Label::new(hierarchy_label_text(
                hierarchy_label_for_entity(world, entity, HierarchyNodeKind::Generated),
                TEXT_DIM,
            )),
        );
    });
    if !is_expanded {
        return;
    }
    if let Some(children) = world.get::<Children>(entity) {
        for child in children.iter() {
            if is_hierarchy_generated_child(world, child) {
                draw_generated_node(world, ui, child, depth + 1, expanded, show_generated);
            }
        }
    }
}

fn draw_runtime_node(
    world: &World,
    ui: &mut egui::Ui,
    entity: Entity,
    depth: usize,
    selected_entities: &mut SelectedEntities,
    show_runtime: bool,
) {
    let _ = show_runtime;
    ui.horizontal(|ui| {
        ui.add_space(depth as f32 * 14.0);
        ui.add_space(18.0);
        let label = hierarchy_label_for_entity(world, entity, HierarchyNodeKind::Runtime);
        let selected = selected_entities.contains(entity);
        let primary = selected_entities.as_slice().first().copied() == Some(entity);
        let color = if selected { TEXT_SELECTED } else { TEXT_DIM };
        let response = ui.selectable_label(selected, hierarchy_label_text(label, color));
        if selected {
            ui.painter().line_segment(
                [response.rect.left_top(), response.rect.left_bottom()],
                egui::Stroke::new(
                    if primary { 3.0 } else { 2.0 },
                    if primary {
                        SELECTION
                    } else {
                        SELECTION_SECONDARY
                    },
                ),
            );
        }
        if response.clicked() {
            selected_entities.select_replace(entity);
        }
        response.context_menu(|ui| {
            if ui.button("Focus").clicked() {
                selected_entities.select_replace(entity);
                ui.close();
            }
        });
    });
    if let Some(children) = world.get::<Children>(entity) {
        for child in children.iter() {
            if is_runtime_hierarchy_entity(world, child) {
                draw_runtime_node(world, ui, child, depth + 1, selected_entities, show_runtime);
            }
        }
    }
}

fn hierarchy_label_text(label: HierarchyLabel, color: egui::Color32) -> egui::WidgetText {
    let mut job = egui::text::LayoutJob::default();
    job.append(
        label.icon,
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(13.0, egui::FontFamily::Name("phosphor-regular".into())),
            color,
            ..Default::default()
        },
    );
    job.append(
        &format!(" {}", label.text),
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(13.0, egui::FontFamily::Proportional),
            color,
            ..Default::default()
        },
    );
    job.into()
}

fn authored_context_menu(
    world: &mut World,
    ui: &mut egui::Ui,
    entity: Entity,
    selected_entities: &mut SelectedEntities,
    renaming_entity: &mut Option<Entity>,
    rename_buffer: &mut String,
    locked: &std::collections::HashSet<Entity>,
) {
    let entity_locked = is_entity_locked(locked, entity);
    let linked_read_only = is_linked_read_only(world, entity);
    let prefab_member = world.get::<HydratedPrefabMember>(entity).is_some();
    let entity_mutable = !entity_locked && !linked_read_only;
    let selection_mutable = selected_entities.iter().all(|selected| {
        !is_entity_locked(locked, selected) && !is_linked_read_only(world, selected)
    });
    if entity_locked {
        ui.small(egui::RichText::new("Actor locked").color(TEXT_DIM));
        ui.separator();
    } else if linked_read_only {
        ui.small(egui::RichText::new("Linked source member").color(TEXT_DIM));
        ui.separator();
    }
    if ui
        .add_enabled(entity_mutable, egui::Button::new("Rename"))
        .clicked()
    {
        *renaming_entity = Some(entity);
        *rename_buffer = entity_name(world, entity);
        ui.close();
    }
    if ui
        .add_enabled(entity_mutable, egui::Button::new("Duplicate"))
        .clicked()
    {
        duplicate_entities_with_history(world, &[entity]);
        ui.close();
    }
    if ui
        .add_enabled(entity_mutable, egui::Button::new("Delete"))
        .clicked()
    {
        delete_entities_with_history(world, &[entity]);
        ui.close();
    }
    if prefab_member && !entity_locked {
        if ui.button("Remove from prefab instance").clicked() {
            crate::assets::prefab_overrides::remove_prefab_source_child(world, entity);
            selected_entities.remove(entity);
            ui.close();
            return;
        }
        if ui.button("Unparent override").clicked() {
            crate::assets::prefab_overrides::reparent_prefab_source_child(world, entity, None);
            ui.close();
            return;
        }
    }
    if selected_entities.len() >= 2
        && selected_entities.contains(entity)
        && ui
            .add_enabled(selection_mutable, egui::Button::new("Group"))
            .clicked()
    {
        let entities: Vec<Entity> = selected_entities
            .iter()
            .filter(|e| is_level_object(world, *e))
            .collect();
        group_selection_with_history(world, &entities);
        ui.close();
    }
    if ui
        .add_enabled(entity_mutable, egui::Button::new("Unparent"))
        .clicked()
    {
        reparent_with_history(world, entity, None);
        ui.close();
    }
    let create_child = ui
        .add_enabled(entity_mutable, egui::Button::new("Create child Empty"))
        .on_disabled_hover_text("Locked and generated source actors cannot be modified");
    if create_child.clicked() {
        create_empty_child_with_history(world, entity);
        ui.close();
    }
    if ui.button("Focus").clicked() {
        selected_entities.select_replace(entity);
        world.resource_mut::<SelectedEntity>().0 = Some(entity);
        focus_editor_camera_on_selection(world);
        ui.close();
    }
}

fn top_level_drag_entities(world: &World, candidates: &[Entity]) -> Vec<Entity> {
    candidates
        .iter()
        .copied()
        .filter(|entity| {
            !candidates
                .iter()
                .copied()
                .any(|other| other != *entity && is_ancestor(world, other, *entity))
        })
        .collect()
}

fn draggable_selection(
    world: &World,
    candidates: &[Entity],
    locked: &std::collections::HashSet<Entity>,
) -> Vec<Entity> {
    let editable: Vec<_> = candidates
        .iter()
        .copied()
        .filter(|entity| {
            is_level_object(world, *entity)
                && !is_entity_locked(locked, *entity)
                && (!is_linked_read_only(world, *entity)
                    || world.get::<HydratedPrefabMember>(*entity).is_some())
        })
        .collect();
    top_level_drag_entities(world, &editable)
}

fn draw_hierarchy_drag_preview(
    world: &World,
    ui: &egui::Ui,
    drag_entities: Option<&[Entity]>,
    target: Option<DropTarget>,
    block: Option<DropBlock>,
) {
    let Some(entities) = drag_entities.filter(|entities| !entities.is_empty()) else {
        return;
    };
    let Some(pointer) = ui.input(|input| input.pointer.hover_pos()) else {
        return;
    };

    let title = if entities.len() == 1 {
        entity_name(world, entities[0])
    } else {
        format!("{} ACTORS", entities.len())
    };
    let action = if let Some(block) = block {
        match block {
            DropBlock::Hierarchy(HierarchyDropViolation::Cycle) => "Cannot attach: hierarchy cycle",
            DropBlock::Hierarchy(HierarchyDropViolation::PrefabBoundary { .. }) => {
                "Cannot attach: prefab contents are read-only"
            }
            DropBlock::LockedTarget(_) => "Cannot attach: target is locked",
        }
    } else {
        match target {
            Some(DropTarget::Root) => "Move to scene root",
            Some(DropTarget::On(parent)) => {
                return draw_hierarchy_drag_preview_card(
                    ui,
                    pointer,
                    &title,
                    &format!("Attach to {}", entity_name(world, parent)),
                    false,
                );
            }
            Some(DropTarget::Before(sibling)) => {
                return draw_hierarchy_drag_preview_card(
                    ui,
                    pointer,
                    &title,
                    &format!("Insert before {}", entity_name(world, sibling)),
                    false,
                );
            }
            None => "Drop on an actor to attach",
        }
    };
    draw_hierarchy_drag_preview_card(ui, pointer, &title, action, block.is_some());
}

fn draw_hierarchy_drag_preview_card(
    ui: &egui::Ui,
    pointer: egui::Pos2,
    title: &str,
    action: &str,
    invalid: bool,
) {
    let content = ui.ctx().content_rect();
    let pos = egui::pos2(
        (pointer.x + 18.0).min(content.right() - 248.0),
        (pointer.y + 18.0).min(content.bottom() - 58.0),
    );
    egui::Area::new(egui::Id::new("hierarchy_drag_preview"))
        .fixed_pos(pos)
        .default_size(egui::vec2(236.0, 56.0))
        .order(egui::Order::Tooltip)
        .interactable(false)
        .show(ui.ctx(), |ui| {
            ui.set_min_size(egui::vec2(236.0, 56.0));
            ui.set_max_size(egui::vec2(236.0, 56.0));
            overlay_chip_frame().show(ui, |ui| {
                ui.set_width(218.0);
                ui.horizontal(|ui| {
                    ui.label(
                        phosphor_icon(icons::TREE_STRUCTURE, 18.0).color(if invalid {
                            ERROR
                        } else {
                            ACCENT
                        }),
                    );
                    ui.vertical(|ui| {
                        ui.spacing_mut().item_spacing.y = 1.0;
                        ui.label(egui::RichText::new(title).strong().color(TEXT_SELECTED));
                        ui.label(egui::RichText::new(action).size(11.0).color(if invalid {
                            ERROR
                        } else {
                            TEXT_DIM
                        }));
                    });
                });
            });
        });
}

fn apply_drop(
    world: &mut World,
    drag: &HierarchyDragPayload,
    target: DropTarget,
    block: Option<DropBlock>,
) {
    if block.is_some() || drag.entities.is_empty() {
        return;
    }
    let prefab_members = drag
        .entities
        .iter()
        .all(|entity| world.get::<HydratedPrefabMember>(*entity).is_some());
    if prefab_members {
        match target {
            DropTarget::Root => {
                crate::assets::prefab_overrides::reparent_prefab_source_children(
                    world,
                    &drag.entities,
                    None,
                );
            }
            DropTarget::On(parent) => {
                crate::assets::prefab_overrides::reparent_prefab_source_children(
                    world,
                    &drag.entities,
                    Some(parent),
                );
                expand_parent_after_drop(world, parent);
            }
            DropTarget::Before(_) => {}
        }
        return;
    }
    match target {
        DropTarget::Root => {
            reorder_siblings_with_history(world, &drag.entities, None, i32::MAX);
        }
        DropTarget::On(parent) => {
            reorder_siblings_with_history(world, &drag.entities, Some(parent), i32::MAX);
            expand_parent_after_drop(world, parent);
        }
        DropTarget::Before(before) => {
            let parent = world
                .get::<ChildOf>(before)
                .map(|child_of| child_of.parent());
            let siblings = match parent {
                Some(parent) => mutable_authored_children(world, parent),
                None => level_object_roots(world)
                    .into_iter()
                    .filter(|entity| !is_linked_read_only(world, *entity))
                    .collect(),
            };
            let insert_index = siblings
                .iter()
                .position(|entity| *entity == before)
                .unwrap_or(siblings.len()) as i32;
            reorder_siblings_with_history(world, &drag.entities, parent, insert_index);
        }
    }
}

fn drop_block_for_target(
    world: &World,
    locked: &std::collections::HashSet<Entity>,
    drag_entities: &[Entity],
    new_parent: Option<Entity>,
    target: Entity,
) -> Option<DropBlock> {
    if is_entity_locked(locked, target)
        || new_parent.is_some_and(|parent| is_entity_locked(locked, parent))
    {
        return Some(DropBlock::LockedTarget(target));
    }
    if is_linked_read_only(world, target) {
        if world.get::<HydratedPrefabMember>(target).is_some()
            && hierarchy_drop_violation(world, drag_entities, new_parent).is_none()
        {
            return None;
        }
        let root = crate::assets::prefab_overrides::prefab_member_override_target(world, target)
            .map(|(root, _)| root)
            .unwrap_or(target);
        return Some(DropBlock::Hierarchy(
            HierarchyDropViolation::PrefabBoundary { root },
        ));
    }
    hierarchy_drop_violation(world, drag_entities, new_parent).map(DropBlock::Hierarchy)
}

fn expand_parent_after_drop(world: &mut World, parent: Entity) {
    let path = entity_hierarchy_path(world, parent);
    {
        let mut state = world.resource_mut::<HierarchyPanelState>();
        if state.expanded.insert(parent) {
            state.mark_dirty();
        }
    }

    let mut prefs = world.resource_mut::<UserPreferences>();
    if !prefs
        .hierarchy_expanded_paths
        .iter()
        .any(|entry| entry == &path)
    {
        prefs.hierarchy_expanded_paths.push(path);
    }
}

fn subtree_matches_filter(
    world: &World,
    entity: Entity,
    filter_lower: &str,
    sort_mode: HierarchySort,
) -> bool {
    if filter_lower.is_empty() {
        return true;
    }
    if entity_matches_filter(world, entity, filter_lower) {
        return true;
    }
    let mut children = authored_children(world, entity);
    sort_siblings(world, &mut children, sort_mode);
    children
        .iter()
        .any(|child| subtree_matches_filter(world, *child, filter_lower, sort_mode))
}

fn runtime_roots(world: &mut World) -> Vec<Entity> {
    use shared::ProjectSun;
    use sim::{Player, PlayerCamera};

    let mut query = world.query_filtered::<
        (Entity, Option<&ChildOf>),
        Or<(With<Player>, With<PlayerCamera>, With<ProjectSun>)>,
    >();
    query
        .iter(world)
        .filter_map(|(entity, parent)| {
            if parent
                .map(|parent| is_runtime_hierarchy_entity(world, parent.parent()))
                .unwrap_or(false)
            {
                None
            } else {
                Some(entity)
            }
        })
        .collect()
}

pub(crate) fn is_hierarchy_generated_child(world: &World, entity: Entity) -> bool {
    use shared::LevelObject;
    world.get_entity(entity).is_ok_and(|entity_ref| {
        !entity_ref.contains::<crate::infra::EditorOnly>()
            && !entity_ref.contains::<LevelObject>()
            && !entity_ref.contains::<crate::visualizers::EditorVisualizerProxy>()
            && (entity_ref.contains::<Transform>() || entity_ref.contains::<GlobalTransform>())
            && (entity_ref.contains::<Name>()
                || entity_ref.contains::<Mesh3d>()
                || entity_ref.contains::<WorldAssetRoot>())
    })
}

pub(crate) fn is_runtime_hierarchy_entity(world: &World, entity: Entity) -> bool {
    use shared::ProjectSun;
    use sim::{Player, PlayerCamera};

    world.get_entity(entity).is_ok_and(|entity_ref| {
        !entity_ref.contains::<crate::infra::EditorOnly>()
            && (entity_ref.contains::<Player>()
                || entity_ref.contains::<PlayerCamera>()
                || entity_ref.contains::<ProjectSun>())
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use shared::LevelObject;

    #[test]
    fn drag_selection_keeps_only_top_level_actors() {
        let mut world = World::new();
        let parent = world.spawn(LevelObject).id();
        let child = world.spawn((LevelObject, ChildOf(parent))).id();
        let peer = world.spawn(LevelObject).id();

        let normalized = top_level_drag_entities(&world, &[parent, child, peer]);

        assert_eq!(normalized, vec![parent, peer]);
    }

    #[test]
    fn drag_selection_excludes_locked_actors_before_subtree_normalization() {
        let mut world = World::new();
        let parent = world.spawn(LevelObject).id();
        let child = world.spawn((LevelObject, ChildOf(parent))).id();
        let peer = world.spawn(LevelObject).id();
        let locked = std::collections::HashSet::from([parent]);

        let normalized = draggable_selection(&world, &[parent, child, peer], &locked);

        assert_eq!(normalized, vec![child, peer]);
    }

    #[test]
    fn locked_drop_target_reports_specific_block() {
        let mut world = World::new();
        let actor = world.spawn(LevelObject).id();
        let target = world.spawn(LevelObject).id();
        let locked = std::collections::HashSet::from([target]);

        assert_eq!(
            drop_block_for_target(&world, &locked, &[actor], Some(target), target),
            Some(DropBlock::LockedTarget(target))
        );
    }

    #[test]
    fn deep_filtered_tree_keeps_matching_descendant_discoverable() {
        let mut world = World::new();
        let root = world.spawn((LevelObject, Name::new("Root"))).id();
        let mut parent = root;
        for depth in 0..64 {
            parent = world
                .spawn((
                    LevelObject,
                    Name::new(if depth == 63 { "Needle" } else { "Branch" }),
                    ChildOf(parent),
                ))
                .id();
        }

        assert!(subtree_matches_filter(
            &world,
            root,
            "needle",
            HierarchySort::Manual
        ));
        assert!(!subtree_matches_filter(
            &world,
            root,
            "missing",
            HierarchySort::Manual
        ));
    }
}
