//! Inspector extensions for authoring components.

use std::collections::{HashMap, HashSet};
use std::path::Path;

use bevy::prelude::*;
use bevy_egui::egui;
use egui_phosphor_icons::{icons, Icon};
use shared::{
    authoring_component_active,
    brush_math::{validate_brush, BrushDiagnosticSeverity},
    infer_actor_kind, ActorId, ActorKind, AnimationControllerDesc, AudioListenerDesc,
    AudioSourceDesc, AuthoringComponentStates, AuthoringLightKind, AuthoringRigidBody, BrushDesc,
    BrushKind, ColliderDesc, ColliderShapeDesc, ColorDesc, ComponentInstanceId, EditorAssetRef,
    InspectorOrder, LevelObject, LightDesc, MaterialDesc, MaterialParameter,
    MaterialParameterValue, MaterialRef, MaterialShaderKind, NavigationArea, NavigationBounds,
    NavigationLink, NavigationObstacle, ObjectiveMarker, PhysicsBody, PlayerSpawn,
    PostProcessVolumeDesc, PrefabInstance, Primitive, PrimitiveShape, ProjectSun, RigidBodyDesc,
    SkinnedMeshRenderer, StaticMeshRenderer, StaticMeshRendererEntry, TeamSpawn, TriggerVolume,
    WeaponSpawn, AUTHORING_DIRECTIONAL_LUX_MAX, AUTHORING_POINT_SPOT_LUMENS_MAX,
    COMPONENT_ANIMATION_CONTROLLER_DESC, COMPONENT_AUDIO_LISTENER_DESC,
    COMPONENT_AUDIO_SOURCE_DESC, COMPONENT_BRUSH_DESC, COMPONENT_COLLIDER_DESC,
    COMPONENT_LIGHT_DESC, COMPONENT_MATERIAL_DESC, COMPONENT_NAVIGATION_AREA,
    COMPONENT_NAVIGATION_BOUNDS, COMPONENT_NAVIGATION_LINK, COMPONENT_NAVIGATION_OBSTACLE,
    COMPONENT_OBJECTIVE_MARKER, COMPONENT_PHYSICS_BODY, COMPONENT_PLAYER_SPAWN,
    COMPONENT_POST_PROCESS_VOLUME, COMPONENT_PREFAB_INSTANCE, COMPONENT_PRIMITIVE,
    COMPONENT_PROJECT_SUN, COMPONENT_RIGID_BODY_DESC, COMPONENT_SKINNED_MESH_RENDERER,
    COMPONENT_STATIC_MESH_RENDERER, COMPONENT_TEAM_SPAWN, COMPONENT_TRIGGER_VOLUME,
    COMPONENT_WEAPON_SPAWN,
};

use crate::history::{
    material_eq, reflected_component_transaction, set_animation_controller_with_history,
    set_audio_listener_with_history, set_audio_source_with_history, set_brush_with_history,
    set_collider_with_history, set_inspector_order_with_history, set_light_with_history,
    set_material_with_history, set_physics_with_history, set_post_process_volume_with_history,
    set_primitive_with_history, set_rigid_body_with_history, set_static_mesh_renderer_with_history,
    EditorEntitySnapshot,
};
use crate::selection::SelectedEntity;
use crate::ui::theme::{
    panel_heading, BORDER, ELEVATED_BG, PANEL_BG_DARK, SELECTION_BG_MUTED, TEXT_DIM, TEXT_MUTED,
    WIDGET_BG,
};
use crate::ui::widgets::{icon_button_small, phosphor_icon, phosphor_icon_text};
use crate::viewport::brush_edit::{BrushElementKey, BrushElementSelection};

use super::component_registry::{
    EditorComponentCategory, EditorComponentDescriptor, EditorComponentRegistry,
};
use super::dock_tabs::open_and_focus_tab;
use super::helpers::create_scene_sun_override_from_project_settings;
use super::{EditorTab, UiState};
use crate::assets::asset_db::{find_asset_by_path, AssetRegistry};
use crate::assets::static_mesh::{
    load_static_mesh_manifest, material_id_from_label, part_id_from_label,
};
use crate::assets::thumbnails::ThumbnailStudio;
use crate::assets::{
    asset_cache_key, AssetSelection, AssetSubAssetKind, AssetThumbnailCache, EditorAsset,
    EditorAssetKind, EditorAssets,
};

const COMPACT_INSPECTOR_WIDTH: f32 = 360.0;
const ASSET_SELECTOR_LABEL_WIDTH: f32 = 72.0;
const ASSET_SELECTOR_HEIGHT: f32 = 48.0;
const MIN_INLINE_CONTROL_WIDTH: f32 = 120.0;
const TEXT_FIELD_MAX_WIDTH: f32 = 320.0;
const PROPERTY_LABEL_WIDTH: f32 = 136.0;
pub(crate) const COMPONENT_TRANSFORM: &str = "bevy_transform::components::Transform";

fn fit_width(ui: &egui::Ui, min: f32, max: f32) -> f32 {
    let available = ui.available_width().max(1.0);
    available.min(max).max(min.min(available))
}

fn exact_region<R>(
    ui: &mut egui::Ui,
    size: egui::Vec2,
    layout: egui::Layout,
    add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
    let (rect, _response) = ui.allocate_exact_size(size, egui::Sense::hover());
    let mut child = ui.new_child(egui::UiBuilder::new().max_rect(rect).layout(layout));
    add_contents(&mut child)
}

#[derive(Debug, Clone)]
struct AssetRefCandidate {
    reference: EditorAssetRef,
    label: String,
    detail: String,
    selection: AssetSelection,
    folder_path: String,
    texture_id: Option<egui::TextureId>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AssetRefCandidateKind {
    Mesh,
    Material,
    Texture,
}

#[derive(Default)]
struct AssetSelectorResponse {
    selected: Option<EditorAssetRef>,
    clear: bool,
    locate: bool,
    accepted_drop: bool,
}

#[derive(Debug, Clone)]
struct TextureAssetCandidate {
    label: String,
    path: String,
    folder_path: String,
    selection: AssetSelection,
    texture_id: Option<egui::TextureId>,
}

#[derive(Resource, Default, Debug, Clone)]
pub(crate) struct InspectorClipboard {
    component: Option<CopiedComponent>,
}

#[derive(Resource, Default, Debug, Clone)]
pub(crate) struct InspectorPanelState {
    add_component_search: String,
    add_component_open: bool,
    add_component_focus_search: bool,
    add_component_scroll_selected: bool,
    add_component_selected_index: usize,
    add_component_target: Option<Entity>,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum AddComponentShelfDirection {
    Up,
    Down,
}

#[derive(Debug, Clone)]
enum CopiedComponent {
    Reflected(crate::history::ReflectedComponentValue),
    AnimationControllerDesc(AnimationControllerDesc),
    Primitive(Primitive),
    BrushDesc(BrushDesc),
    StaticMeshRenderer(StaticMeshRenderer),
    MaterialDesc(MaterialDesc),
    LightDesc(LightDesc),
    AudioSourceDesc(AudioSourceDesc),
    AudioListenerDesc(AudioListenerDesc),
    RigidBodyDesc(RigidBodyDesc),
    ColliderDesc(ColliderDesc),
    PhysicsBody(PhysicsBody),
    WeaponSpawn(WeaponSpawn),
    TriggerVolume(TriggerVolume),
    TeamSpawn(TeamSpawn),
    ObjectiveMarker(ObjectiveMarker),
    PostProcessVolume(PostProcessVolumeDesc),
    PrefabInstance(PrefabInstance),
    NavigationBounds(NavigationBounds),
    NavigationObstacle(NavigationObstacle),
    NavigationArea(NavigationArea),
    NavigationLink(NavigationLink),
}

impl CopiedComponent {
    fn type_name(&self) -> &str {
        match self {
            Self::Reflected(value) => &value.type_path,
            Self::AnimationControllerDesc(_) => COMPONENT_ANIMATION_CONTROLLER_DESC,
            Self::Primitive(_) => COMPONENT_PRIMITIVE,
            Self::BrushDesc(_) => COMPONENT_BRUSH_DESC,
            Self::StaticMeshRenderer(_) => COMPONENT_STATIC_MESH_RENDERER,
            Self::MaterialDesc(_) => COMPONENT_MATERIAL_DESC,
            Self::LightDesc(_) => COMPONENT_LIGHT_DESC,
            Self::AudioSourceDesc(_) => COMPONENT_AUDIO_SOURCE_DESC,
            Self::AudioListenerDesc(_) => COMPONENT_AUDIO_LISTENER_DESC,
            Self::RigidBodyDesc(_) => COMPONENT_RIGID_BODY_DESC,
            Self::ColliderDesc(_) => COMPONENT_COLLIDER_DESC,
            Self::PhysicsBody(_) => COMPONENT_PHYSICS_BODY,
            Self::WeaponSpawn(_) => COMPONENT_WEAPON_SPAWN,
            Self::TriggerVolume(_) => COMPONENT_TRIGGER_VOLUME,
            Self::TeamSpawn(_) => COMPONENT_TEAM_SPAWN,
            Self::ObjectiveMarker(_) => COMPONENT_OBJECTIVE_MARKER,
            Self::PostProcessVolume(_) => COMPONENT_POST_PROCESS_VOLUME,
            Self::PrefabInstance(_) => COMPONENT_PREFAB_INSTANCE,
            Self::NavigationBounds(_) => COMPONENT_NAVIGATION_BOUNDS,
            Self::NavigationObstacle(_) => COMPONENT_NAVIGATION_OBSTACLE,
            Self::NavigationArea(_) => COMPONENT_NAVIGATION_AREA,
            Self::NavigationLink(_) => COMPONENT_NAVIGATION_LINK,
        }
    }
}

#[derive(Debug, Clone, Copy)]
pub(crate) struct ComponentCardOptions {
    pub type_name: &'static str,
    pub title: &'static str,
    pub icon: Icon,
    pub active_toggle: bool,
    pub removable: bool,
    pub reorderable: bool,
    pub resettable: bool,
    pub copyable: bool,
}

impl ComponentCardOptions {
    pub(crate) fn removable(type_name: &'static str, title: &'static str, icon: Icon) -> Self {
        Self {
            type_name,
            title,
            icon,
            active_toggle: true,
            removable: true,
            reorderable: true,
            resettable: true,
            copyable: true,
        }
    }

    pub(crate) fn fixed(type_name: &'static str, title: &'static str, icon: Icon) -> Self {
        Self {
            type_name,
            title,
            icon,
            active_toggle: false,
            removable: false,
            reorderable: false,
            resettable: true,
            copyable: false,
        }
    }
}

#[derive(Debug, Clone)]
pub(crate) struct ComponentCardContext {
    options: ComponentCardOptions,
    active: bool,
    collapsed: bool,
    pasteable: bool,
    can_move_up: bool,
    can_move_down: bool,
}

#[derive(Debug, Clone, Default)]
pub(crate) struct ComponentCardResponse {
    pub(crate) type_name: &'static str,
    pub(crate) collapsed: Option<bool>,
    pub(crate) active: Option<bool>,
    pub(crate) reset: bool,
    pub(crate) copy: bool,
    pub(crate) paste: bool,
    pub(crate) move_up: bool,
    pub(crate) move_down: bool,
    pub(crate) remove: bool,
}

pub(crate) fn component_card_context(
    world: &World,
    entity: Entity,
    options: ComponentCardOptions,
) -> ComponentCardContext {
    let present = present_component_type_names(world, entity);
    let order = world.get::<InspectorOrder>(entity);
    let registry = world.resource::<EditorComponentRegistry>();
    let state_key = registry.stable_id_for_type(options.type_name);
    let active = authoring_component_active(
        world.get::<AuthoringComponentStates>(entity),
        order,
        state_key,
    );
    let collapsed = world
        .get_resource::<UiState>()
        .map(|state| {
            state
                .inspector_collapsed_components
                .contains(&component_card_key(world, entity, options.type_name))
        })
        .unwrap_or(false);
    let pasteable = options.copyable
        && world
            .get_resource::<InspectorClipboard>()
            .and_then(|clipboard| clipboard.component.as_ref())
            .is_some_and(|component| component.type_name() == options.type_name);
    let ordered = order
        .map(|order| registry.ordered_components(order, &present))
        .unwrap_or_else(|| present.clone());
    let order_index = ordered
        .iter()
        .position(|type_name| *type_name == options.type_name);

    ComponentCardContext {
        options,
        active,
        collapsed,
        pasteable,
        can_move_up: options.reorderable && order_index.is_some_and(|index| index > 0),
        can_move_down: options.reorderable
            && order_index.is_some_and(|index| index + 1 < ordered.len()),
    }
}

pub(crate) fn apply_component_card_response(
    world: &mut World,
    entity: Entity,
    response: ComponentCardResponse,
) {
    let type_name = response_type_name(&response).unwrap_or_default();
    if type_name.is_empty() {
        return;
    }
    apply_component_card_response_for_type(world, entity, type_name, response);
}

fn apply_component_card_response_for_type(
    world: &mut World,
    entity: Entity,
    type_name: &'static str,
    response: ComponentCardResponse,
) {
    if let Some(collapsed) = response.collapsed {
        let key = component_card_key(world, entity, type_name);
        if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
            if collapsed {
                ui_state.inspector_collapsed_components.insert(key);
            } else {
                ui_state.inspector_collapsed_components.remove(&key);
            }
        }
    }

    if let Some(active) = response.active {
        let mut states = world
            .get::<AuthoringComponentStates>(entity)
            .cloned()
            .unwrap_or_else(|| AuthoringComponentStates {
                states: world
                    .get::<InspectorOrder>(entity)
                    .map(|order| order.component_states.clone())
                    .unwrap_or_default(),
            });
        let component_id = world
            .resource::<EditorComponentRegistry>()
            .stable_id_for_type(type_name);
        states.set_component_active(component_id, active);
        let _ = crate::history::reflected_component_transaction(
            world,
            entity,
            "Set Component Active",
            "editor.component_states",
            "shared::components::AuthoringComponentStates",
            move |world, entity| {
                world.entity_mut(entity).insert(states);
                Ok(())
            },
        );
    }

    if response.move_up || response.move_down {
        let mut order = world
            .get::<InspectorOrder>(entity)
            .cloned()
            .unwrap_or_default();
        let present = present_component_type_names(world, entity);
        let offset = if response.move_up { -1 } else { 1 };
        let moved = world
            .resource::<EditorComponentRegistry>()
            .move_component(&mut order, type_name, offset, &present);
        if moved {
            set_inspector_order_with_history(world, entity, order);
        }
    }

    if response.copy {
        copy_component(world, entity, type_name);
    }
    if response.paste {
        paste_component(world, entity, type_name);
    }
    if response.reset {
        reset_component(world, entity, type_name);
    }
    if response.remove {
        remove_registered_component(world, entity, type_name);
    }
}

fn response_type_name(_response: &ComponentCardResponse) -> Option<&'static str> {
    Some(_response.type_name)
}

fn component_card_key(world: &World, entity: Entity, type_name: &str) -> String {
    let actor_key = world
        .get::<shared::ActorId>(entity)
        .map(|id| id.0.as_str())
        .filter(|id| !id.trim().is_empty())
        .map(str::to_string)
        .unwrap_or_else(|| format!("{entity:?}"));
    format!("{actor_key}::{type_name}")
}

fn present_component_type_names(world: &World, entity: Entity) -> Vec<&'static str> {
    if let Some(registry) = world.get_resource::<EditorComponentRegistry>() {
        return registry
            .descriptors
            .iter()
            .filter(|descriptor| {
                !descriptor.hidden
                    && registry.component_present(world, entity, descriptor.type_name)
            })
            .map(|descriptor| descriptor.type_name)
            .collect();
    }
    let mut present = Vec::new();
    if world.get::<AnimationControllerDesc>(entity).is_some() {
        present.push(COMPONENT_ANIMATION_CONTROLLER_DESC);
    }
    if world.get::<StaticMeshRenderer>(entity).is_some() {
        present.push(COMPONENT_STATIC_MESH_RENDERER);
    }
    if world.get::<SkinnedMeshRenderer>(entity).is_some() {
        present.push(COMPONENT_SKINNED_MESH_RENDERER);
    }
    if world.get::<Primitive>(entity).is_some() {
        present.push(COMPONENT_PRIMITIVE);
    }
    if world.get::<BrushDesc>(entity).is_some() {
        present.push(COMPONENT_BRUSH_DESC);
    }
    if world.get::<MaterialDesc>(entity).is_some() || world.get::<Primitive>(entity).is_some() {
        present.push(COMPONENT_MATERIAL_DESC);
    }
    if world.get::<LightDesc>(entity).is_some() {
        present.push(COMPONENT_LIGHT_DESC);
    }
    if world.get::<AudioSourceDesc>(entity).is_some() {
        present.push(COMPONENT_AUDIO_SOURCE_DESC);
    }
    if world.get::<AudioListenerDesc>(entity).is_some() {
        present.push(COMPONENT_AUDIO_LISTENER_DESC);
    }
    if world.get::<RigidBodyDesc>(entity).is_some() {
        present.push(COMPONENT_RIGID_BODY_DESC);
    }
    if world.get::<ColliderDesc>(entity).is_some() {
        present.push(COMPONENT_COLLIDER_DESC);
    }
    if world.get::<PhysicsBody>(entity).is_some() {
        present.push(COMPONENT_PHYSICS_BODY);
    }
    if world.get::<PlayerSpawn>(entity).is_some() {
        present.push(COMPONENT_PLAYER_SPAWN);
    }
    if world.get::<WeaponSpawn>(entity).is_some() {
        present.push(COMPONENT_WEAPON_SPAWN);
    }
    if world.get::<TriggerVolume>(entity).is_some() {
        present.push(COMPONENT_TRIGGER_VOLUME);
    }
    if world.get::<TeamSpawn>(entity).is_some() {
        present.push(COMPONENT_TEAM_SPAWN);
    }
    if world.get::<ObjectiveMarker>(entity).is_some() {
        present.push(COMPONENT_OBJECTIVE_MARKER);
    }
    if world.get::<PrefabInstance>(entity).is_some() {
        present.push(COMPONENT_PREFAB_INSTANCE);
    }
    if world.get::<PostProcessVolumeDesc>(entity).is_some() {
        present.push(COMPONENT_POST_PROCESS_VOLUME);
    }
    if world.get::<ProjectSun>(entity).is_some() {
        present.push(COMPONENT_PROJECT_SUN);
    }
    if world.get::<NavigationBounds>(entity).is_some() {
        present.push(COMPONENT_NAVIGATION_BOUNDS);
    }
    if world.get::<NavigationObstacle>(entity).is_some() {
        present.push(COMPONENT_NAVIGATION_OBSTACLE);
    }
    if world.get::<NavigationArea>(entity).is_some() {
        present.push(COMPONENT_NAVIGATION_AREA);
    }
    if world.get::<NavigationLink>(entity).is_some() {
        present.push(COMPONENT_NAVIGATION_LINK);
    }
    present
}

fn copy_component(world: &mut World, entity: Entity, type_name: &str) {
    let descriptor = world
        .resource::<EditorComponentRegistry>()
        .by_type_name(type_name)
        .cloned();
    if let Some(descriptor) = descriptor {
        if let Ok(Some(value)) = crate::history::capture_reflected_component(
            world,
            entity,
            descriptor.id,
            descriptor.type_name,
        ) {
            world.resource_mut::<InspectorClipboard>().component =
                Some(CopiedComponent::Reflected(value));
            return;
        }
    }
    let component = match type_name {
        COMPONENT_ANIMATION_CONTROLLER_DESC => world
            .get::<AnimationControllerDesc>(entity)
            .cloned()
            .map(CopiedComponent::AnimationControllerDesc),
        COMPONENT_PRIMITIVE => world
            .get::<Primitive>(entity)
            .cloned()
            .map(CopiedComponent::Primitive),
        COMPONENT_BRUSH_DESC => world
            .get::<BrushDesc>(entity)
            .cloned()
            .map(CopiedComponent::BrushDesc),
        COMPONENT_STATIC_MESH_RENDERER => world
            .get::<StaticMeshRenderer>(entity)
            .cloned()
            .map(CopiedComponent::StaticMeshRenderer),
        COMPONENT_MATERIAL_DESC => world
            .get::<MaterialDesc>(entity)
            .cloned()
            .map(CopiedComponent::MaterialDesc),
        COMPONENT_LIGHT_DESC => world
            .get::<LightDesc>(entity)
            .cloned()
            .map(CopiedComponent::LightDesc),
        COMPONENT_AUDIO_SOURCE_DESC => world
            .get::<AudioSourceDesc>(entity)
            .cloned()
            .map(CopiedComponent::AudioSourceDesc),
        COMPONENT_AUDIO_LISTENER_DESC => world
            .get::<AudioListenerDesc>(entity)
            .copied()
            .map(CopiedComponent::AudioListenerDesc),
        COMPONENT_RIGID_BODY_DESC => world
            .get::<RigidBodyDesc>(entity)
            .copied()
            .map(CopiedComponent::RigidBodyDesc),
        COMPONENT_COLLIDER_DESC => world
            .get::<ColliderDesc>(entity)
            .cloned()
            .map(CopiedComponent::ColliderDesc),
        COMPONENT_PHYSICS_BODY => world
            .get::<PhysicsBody>(entity)
            .cloned()
            .map(CopiedComponent::PhysicsBody),
        COMPONENT_WEAPON_SPAWN => world
            .get::<WeaponSpawn>(entity)
            .cloned()
            .map(CopiedComponent::WeaponSpawn),
        COMPONENT_TRIGGER_VOLUME => world
            .get::<TriggerVolume>(entity)
            .cloned()
            .map(CopiedComponent::TriggerVolume),
        COMPONENT_TEAM_SPAWN => world
            .get::<TeamSpawn>(entity)
            .cloned()
            .map(CopiedComponent::TeamSpawn),
        COMPONENT_OBJECTIVE_MARKER => world
            .get::<ObjectiveMarker>(entity)
            .cloned()
            .map(CopiedComponent::ObjectiveMarker),
        COMPONENT_POST_PROCESS_VOLUME => world
            .get::<PostProcessVolumeDesc>(entity)
            .cloned()
            .map(CopiedComponent::PostProcessVolume),
        COMPONENT_PREFAB_INSTANCE => world
            .get::<PrefabInstance>(entity)
            .cloned()
            .map(CopiedComponent::PrefabInstance),
        COMPONENT_NAVIGATION_BOUNDS => world
            .get::<NavigationBounds>(entity)
            .cloned()
            .map(CopiedComponent::NavigationBounds),
        COMPONENT_NAVIGATION_OBSTACLE => world
            .get::<NavigationObstacle>(entity)
            .cloned()
            .map(CopiedComponent::NavigationObstacle),
        COMPONENT_NAVIGATION_AREA => world
            .get::<NavigationArea>(entity)
            .cloned()
            .map(CopiedComponent::NavigationArea),
        COMPONENT_NAVIGATION_LINK => world
            .get::<NavigationLink>(entity)
            .cloned()
            .map(CopiedComponent::NavigationLink),
        _ => None,
    };
    if let Some(component) = component {
        world.resource_mut::<InspectorClipboard>().component = Some(component);
    }
}

fn paste_component(world: &mut World, entity: Entity, type_name: &str) {
    let component = world
        .get_resource::<InspectorClipboard>()
        .and_then(|clipboard| clipboard.component.clone());
    if let Some(CopiedComponent::Reflected(value)) = component.as_ref() {
        if value.type_path == type_name {
            let value = value.clone();
            let component_id = value.component_id.clone();
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Paste Component",
                &component_id,
                type_name,
                move |world, entity| {
                    crate::history::apply_reflected_component(
                        world,
                        entity,
                        type_name,
                        Some(&value),
                    )
                },
            );
        }
        return;
    }
    match component {
        Some(CopiedComponent::Reflected(_)) => {}
        Some(CopiedComponent::AnimationControllerDesc(value))
            if type_name == COMPONENT_ANIMATION_CONTROLLER_DESC =>
        {
            set_animation_controller_with_history(world, entity, value);
        }
        Some(CopiedComponent::Primitive(value)) if type_name == COMPONENT_PRIMITIVE => {
            set_primitive_with_history(world, entity, value);
        }
        Some(CopiedComponent::BrushDesc(value)) if type_name == COMPONENT_BRUSH_DESC => {
            set_brush_with_history(world, entity, value);
        }
        Some(CopiedComponent::StaticMeshRenderer(value))
            if type_name == COMPONENT_STATIC_MESH_RENDERER =>
        {
            set_static_mesh_renderer_with_history(world, entity, value);
        }
        Some(CopiedComponent::MaterialDesc(value)) if type_name == COMPONENT_MATERIAL_DESC => {
            set_material_with_history(world, entity, value);
        }
        Some(CopiedComponent::LightDesc(value)) if type_name == COMPONENT_LIGHT_DESC => {
            set_light_with_history(world, entity, value);
        }
        Some(CopiedComponent::AudioSourceDesc(value))
            if type_name == COMPONENT_AUDIO_SOURCE_DESC =>
        {
            set_audio_source_with_history(world, entity, value);
        }
        Some(CopiedComponent::AudioListenerDesc(value))
            if type_name == COMPONENT_AUDIO_LISTENER_DESC =>
        {
            set_audio_listener_with_history(world, entity, value);
        }
        Some(CopiedComponent::RigidBodyDesc(value)) if type_name == COMPONENT_RIGID_BODY_DESC => {
            set_rigid_body_with_history(world, entity, value);
        }
        Some(CopiedComponent::ColliderDesc(value)) if type_name == COMPONENT_COLLIDER_DESC => {
            set_collider_with_history(world, entity, value);
        }
        Some(CopiedComponent::PhysicsBody(value)) if type_name == COMPONENT_PHYSICS_BODY => {
            set_physics_with_history(world, entity, value);
        }
        Some(CopiedComponent::PostProcessVolume(value))
            if type_name == COMPONENT_POST_PROCESS_VOLUME =>
        {
            set_post_process_volume_with_history(world, entity, value);
        }
        Some(CopiedComponent::WeaponSpawn(value)) if type_name == COMPONENT_WEAPON_SPAWN => {
            insert_direct_component(world, entity, value);
        }
        Some(CopiedComponent::TriggerVolume(value)) if type_name == COMPONENT_TRIGGER_VOLUME => {
            insert_direct_component(world, entity, value);
        }
        Some(CopiedComponent::TeamSpawn(value)) if type_name == COMPONENT_TEAM_SPAWN => {
            insert_direct_component(world, entity, value);
        }
        Some(CopiedComponent::ObjectiveMarker(value))
            if type_name == COMPONENT_OBJECTIVE_MARKER =>
        {
            insert_direct_component(world, entity, value);
        }
        Some(CopiedComponent::PrefabInstance(value)) if type_name == COMPONENT_PREFAB_INSTANCE => {
            insert_direct_component(world, entity, value);
        }
        Some(CopiedComponent::NavigationBounds(mut value))
            if type_name == COMPONENT_NAVIGATION_BOUNDS =>
        {
            value.artifact_path = world
                .get::<NavigationBounds>(entity)
                .map(|bounds| bounds.artifact_path.clone())
                .unwrap_or_else(|| navigation_bounds_for_entity(world, entity).artifact_path);
            crate::history::set_navigation_with_history(
                world,
                entity,
                crate::history::NavigationComponentState {
                    bounds: Some(value),
                    ..Default::default()
                },
            );
        }
        Some(CopiedComponent::NavigationObstacle(value))
            if type_name == COMPONENT_NAVIGATION_OBSTACLE =>
        {
            crate::history::set_navigation_with_history(
                world,
                entity,
                crate::history::NavigationComponentState {
                    obstacle: Some(value),
                    ..Default::default()
                },
            );
        }
        Some(CopiedComponent::NavigationArea(value)) if type_name == COMPONENT_NAVIGATION_AREA => {
            crate::history::set_navigation_with_history(
                world,
                entity,
                crate::history::NavigationComponentState {
                    area: Some(value),
                    ..Default::default()
                },
            );
        }
        Some(CopiedComponent::NavigationLink(value)) if type_name == COMPONENT_NAVIGATION_LINK => {
            crate::history::set_navigation_with_history(
                world,
                entity,
                crate::history::NavigationComponentState {
                    link: Some(value),
                    ..Default::default()
                },
            );
        }
        _ => {}
    }
}

fn reset_component(world: &mut World, entity: Entity, type_name: &str) {
    let descriptor = world
        .resource::<EditorComponentRegistry>()
        .by_type_name(type_name)
        .cloned();
    if let Some(descriptor) = descriptor {
        let component_id = descriptor.id;
        let type_path = descriptor.type_name;
        if type_path == COMPONENT_ANIMATION_CONTROLLER_DESC {
            let controller =
                crate::ui::animation_inspector::default_controller_for_actor(world, entity);
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Reset Component",
                component_id,
                type_path,
                move |world, entity| {
                    world.entity_mut(entity).insert(controller);
                    Ok(())
                },
            );
        } else if type_path == COMPONENT_NAVIGATION_BOUNDS {
            let bounds = reset_navigation_bounds_for_entity(world, entity);
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Reset Component",
                component_id,
                type_path,
                move |world, entity| {
                    world.entity_mut(entity).insert(bounds);
                    Ok(())
                },
            );
        } else {
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Reset Component",
                component_id,
                type_path,
                move |world, entity| {
                    crate::history::apply_reflected_default(world, entity, type_path)
                },
            );
        }
        return;
    }
    match type_name {
        COMPONENT_ANIMATION_CONTROLLER_DESC => {
            let controller =
                crate::ui::animation_inspector::default_controller_for_actor(world, entity);
            set_animation_controller_with_history(world, entity, controller);
        }
        COMPONENT_PRIMITIVE => set_primitive_with_history(world, entity, Primitive::default()),
        COMPONENT_BRUSH_DESC => set_brush_with_history(world, entity, BrushDesc::default()),
        COMPONENT_STATIC_MESH_RENDERER => {
            set_static_mesh_renderer_with_history(world, entity, StaticMeshRenderer::default());
        }
        COMPONENT_MATERIAL_DESC => {
            set_material_with_history(world, entity, MaterialDesc::default())
        }
        COMPONENT_LIGHT_DESC => set_light_with_history(world, entity, LightDesc::default()),
        COMPONENT_AUDIO_SOURCE_DESC => {
            set_audio_source_with_history(world, entity, AudioSourceDesc::default())
        }
        COMPONENT_AUDIO_LISTENER_DESC => {
            set_audio_listener_with_history(world, entity, AudioListenerDesc::default())
        }
        COMPONENT_RIGID_BODY_DESC => {
            set_rigid_body_with_history(world, entity, RigidBodyDesc::default());
        }
        COMPONENT_COLLIDER_DESC => {
            set_collider_with_history(world, entity, ColliderDesc::default())
        }
        COMPONENT_PHYSICS_BODY => set_physics_with_history(world, entity, PhysicsBody::default()),
        COMPONENT_POST_PROCESS_VOLUME => {
            set_post_process_volume_with_history(world, entity, PostProcessVolumeDesc::default());
        }
        COMPONENT_NAVIGATION_BOUNDS => crate::history::set_navigation_with_history(
            world,
            entity,
            crate::history::NavigationComponentState {
                bounds: Some(reset_navigation_bounds_for_entity(world, entity)),
                ..Default::default()
            },
        ),
        COMPONENT_NAVIGATION_OBSTACLE => crate::history::set_navigation_with_history(
            world,
            entity,
            crate::history::NavigationComponentState {
                obstacle: Some(NavigationObstacle::default()),
                ..Default::default()
            },
        ),
        COMPONENT_NAVIGATION_AREA => crate::history::set_navigation_with_history(
            world,
            entity,
            crate::history::NavigationComponentState {
                area: Some(NavigationArea::default()),
                ..Default::default()
            },
        ),
        COMPONENT_NAVIGATION_LINK => crate::history::set_navigation_with_history(
            world,
            entity,
            crate::history::NavigationComponentState {
                link: Some(NavigationLink::default()),
                ..Default::default()
            },
        ),
        COMPONENT_WEAPON_SPAWN => insert_direct_component(
            world,
            entity,
            WeaponSpawn {
                weapon_id: "rifle".into(),
            },
        ),
        COMPONENT_TRIGGER_VOLUME => {
            insert_direct_component(world, entity, TriggerVolume::default())
        }
        COMPONENT_TEAM_SPAWN => insert_direct_component(world, entity, TeamSpawn { team_id: 0 }),
        COMPONENT_OBJECTIVE_MARKER => insert_direct_component(
            world,
            entity,
            ObjectiveMarker {
                objective_id: "objective".into(),
            },
        ),
        _ => {}
    }
}

fn insert_direct_component<T: Component>(world: &mut World, entity: Entity, component: T) {
    if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
        entity_mut.insert(component);
    }
    if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
        scene_io.mark_dirty();
    }
}

fn remove_registered_component(world: &mut World, entity: Entity, type_name: &str) {
    let descriptor = world
        .resource::<EditorComponentRegistry>()
        .by_type_name(type_name)
        .cloned();
    if let Some(descriptor) = descriptor {
        if !descriptor.removable {
            return;
        }
        let dependents = world
            .resource::<EditorComponentRegistry>()
            .present_dependents(world, entity, descriptor.id)
            .iter()
            .map(|dependent| dependent.display_name)
            .collect::<Vec<_>>();
        if !dependents.is_empty() {
            world
                .resource_mut::<crate::scene_io::SceneIo>()
                .set_status(format!(
                    "Remove blocked: required by {}. Remove dependent components first.",
                    dependents.join(", ")
                ));
            return;
        }
        let result = crate::history::reflected_component_transaction(
            world,
            entity,
            "Remove Component",
            descriptor.id,
            descriptor.type_name,
            |world, entity| {
                crate::history::apply_reflected_component(world, entity, descriptor.type_name, None)
            },
        );
        if result.is_ok() && type_name == COMPONENT_ANIMATION_CONTROLLER_DESC {
            crate::ui::animation_inspector::stop_preview_if_actor(world, entity);
        }
        return;
    }
    let Some(before) = crate::history::snapshot_entity(world, entity) else {
        return;
    };
    let removed_dedicated_audio_kind = matches!(
        (type_name, before.actor_kind),
        (COMPONENT_AUDIO_SOURCE_DESC, ActorKind::AudioSource)
            | (COMPONENT_AUDIO_LISTENER_DESC, ActorKind::AudioListener)
    );
    let removed_dedicated_navigation_kind = before.actor_kind == ActorKind::Navigation
        && matches!(
            type_name,
            COMPONENT_NAVIGATION_BOUNDS
                | COMPONENT_NAVIGATION_OBSTACLE
                | COMPONENT_NAVIGATION_AREA
                | COMPONENT_NAVIGATION_LINK
        );
    let removed_dedicated_skinned_kind =
        type_name == COMPONENT_SKINNED_MESH_RENDERER && before.actor_kind == ActorKind::SkinnedMesh;
    let removed = if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
        match type_name {
            COMPONENT_ANIMATION_CONTROLLER_DESC if before.animation_controller.is_some() => {
                entity_mut.remove::<AnimationControllerDesc>();
                true
            }
            COMPONENT_PRIMITIVE if before.primitive.is_some() => {
                entity_mut.remove::<Primitive>();
                true
            }
            COMPONENT_BRUSH_DESC if before.brush.is_some() => {
                entity_mut.remove::<BrushDesc>();
                true
            }
            COMPONENT_STATIC_MESH_RENDERER if before.static_mesh_renderer.is_some() => {
                entity_mut.remove::<StaticMeshRenderer>();
                true
            }
            COMPONENT_SKINNED_MESH_RENDERER if before.skinned_mesh_renderer.is_some() => {
                entity_mut.remove::<SkinnedMeshRenderer>();
                true
            }
            COMPONENT_MATERIAL_DESC if before.material.is_some() => {
                entity_mut.remove::<MaterialDesc>();
                true
            }
            COMPONENT_LIGHT_DESC if before.light.is_some() => {
                entity_mut.remove::<LightDesc>();
                true
            }
            COMPONENT_AUDIO_SOURCE_DESC if before.audio_source.is_some() => {
                entity_mut.remove::<AudioSourceDesc>();
                true
            }
            COMPONENT_AUDIO_LISTENER_DESC if before.audio_listener.is_some() => {
                entity_mut.remove::<AudioListenerDesc>();
                true
            }
            COMPONENT_RIGID_BODY_DESC if before.rigid_body.is_some() => {
                entity_mut.remove::<RigidBodyDesc>();
                true
            }
            COMPONENT_COLLIDER_DESC if before.collider.is_some() => {
                entity_mut.remove::<ColliderDesc>();
                true
            }
            COMPONENT_PHYSICS_BODY if before.physics.is_some() => {
                entity_mut.remove::<PhysicsBody>();
                true
            }
            COMPONENT_PLAYER_SPAWN if before.player_spawn => {
                entity_mut.remove::<PlayerSpawn>();
                true
            }
            COMPONENT_WEAPON_SPAWN if before.weapon_spawn.is_some() => {
                entity_mut.remove::<WeaponSpawn>();
                true
            }
            COMPONENT_TRIGGER_VOLUME if before.trigger_volume.is_some() => {
                entity_mut.remove::<TriggerVolume>();
                true
            }
            COMPONENT_TEAM_SPAWN if before.team_spawn.is_some() => {
                entity_mut.remove::<TeamSpawn>();
                true
            }
            COMPONENT_OBJECTIVE_MARKER if before.objective.is_some() => {
                entity_mut.remove::<ObjectiveMarker>();
                true
            }
            COMPONENT_PREFAB_INSTANCE if before.prefab_instance.is_some() => {
                entity_mut.remove::<PrefabInstance>();
                true
            }
            COMPONENT_POST_PROCESS_VOLUME if before.post_process_volume.is_some() => {
                entity_mut.remove::<PostProcessVolumeDesc>();
                true
            }
            COMPONENT_NAVIGATION_BOUNDS if before.navigation_bounds.is_some() => {
                entity_mut.remove::<NavigationBounds>();
                true
            }
            COMPONENT_NAVIGATION_OBSTACLE if before.navigation_obstacle.is_some() => {
                entity_mut.remove::<NavigationObstacle>();
                true
            }
            COMPONENT_NAVIGATION_AREA if before.navigation_area.is_some() => {
                entity_mut.remove::<NavigationArea>();
                true
            }
            COMPONENT_NAVIGATION_LINK if before.navigation_link.is_some() => {
                entity_mut.remove::<NavigationLink>();
                true
            }
            _ => false,
        }
    } else {
        false
    };
    if !removed {
        return;
    }
    if type_name == COMPONENT_ANIMATION_CONTROLLER_DESC {
        crate::ui::animation_inspector::stop_preview_if_actor(world, entity);
    }
    if removed_dedicated_audio_kind
        || removed_dedicated_navigation_kind
        || removed_dedicated_skinned_kind
    {
        let fallback = world
            .get_entity(entity)
            .ok()
            .and_then(infer_actor_kind)
            .unwrap_or(ActorKind::Empty);
        if let Ok(mut entity_mut) = world.get_entity_mut(entity) {
            entity_mut.insert(fallback);
        }
    }
    if let Some(after) = crate::history::snapshot_entity(world, entity) {
        let snapshot = diff_added(&after, &before);
        crate::history::push_command(
            world,
            crate::history::EditorCommand::RemoveComponent { entity, snapshot },
        );
    }
}

pub fn authoring_inspector_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    if !world
        .get_entity(entity)
        .is_ok_and(|entity_ref| entity_ref.contains::<LevelObject>())
    {
        return;
    }

    let present = present_component_type_names(world, entity);
    let ordered = if let Some(order) = world.get::<InspectorOrder>(entity) {
        world
            .resource::<EditorComponentRegistry>()
            .ordered_components(order, &present)
    } else {
        present
    };
    for type_name in ordered {
        draw_authoring_component_by_type(world, ui, entity, type_name);
    }
}

fn draw_authoring_component_by_type(
    world: &mut World,
    ui: &mut egui::Ui,
    entity: Entity,
    type_name: &'static str,
) {
    if let Some(inspector) = world
        .resource::<EditorComponentRegistry>()
        .inspector(type_name)
    {
        inspector(world, ui, entity);
        return;
    }
    match type_name {
        COMPONENT_ANIMATION_CONTROLLER_DESC => {
            crate::ui::animation_inspector::animation_controller_inspector_ui(world, ui, entity);
        }
        COMPONENT_STATIC_MESH_RENDERER => static_mesh_renderer_ui(world, ui, entity),
        COMPONENT_SKINNED_MESH_RENDERER => skinned_mesh_renderer_ui(world, ui, entity),
        COMPONENT_BRUSH_DESC => brush_editor_ui(world, ui, entity),
        COMPONENT_PRIMITIVE => primitive_editor_ui(world, ui, entity),
        COMPONENT_MATERIAL_DESC => material_editor_ui(world, ui, entity),
        COMPONENT_LIGHT_DESC => light_editor_ui(world, ui, entity),
        COMPONENT_AUDIO_SOURCE_DESC => {
            crate::ui::audio_inspector::audio_source_inspector_ui(world, ui, entity);
        }
        COMPONENT_AUDIO_LISTENER_DESC => {
            crate::ui::audio_inspector::audio_listener_inspector_ui(world, ui, entity);
        }
        COMPONENT_RIGID_BODY_DESC => rigid_body_editor_ui(world, ui, entity),
        COMPONENT_COLLIDER_DESC => collider_editor_ui(world, ui, entity),
        COMPONENT_PHYSICS_BODY => physics_editor_ui(world, ui, entity),
        COMPONENT_PLAYER_SPAWN => player_spawn_ui(world, ui, entity),
        COMPONENT_WEAPON_SPAWN => weapon_spawn_ui(world, ui, entity),
        COMPONENT_TRIGGER_VOLUME => trigger_volume_ui(world, ui, entity),
        COMPONENT_TEAM_SPAWN => team_spawn_ui(world, ui, entity),
        COMPONENT_OBJECTIVE_MARKER => objective_ui(world, ui, entity),
        COMPONENT_PREFAB_INSTANCE => prefab_instance_ui(world, ui, entity),
        COMPONENT_POST_PROCESS_VOLUME => {
            crate::ui::post_process_volume_ui::post_process_volume_inspector_ui(world, ui, entity);
        }
        COMPONENT_PROJECT_SUN => project_sun_ui(world, ui, entity),
        COMPONENT_NAVIGATION_BOUNDS => {
            super::navigation_inspector::navigation_bounds_inspector_ui(world, ui, entity)
        }
        COMPONENT_NAVIGATION_OBSTACLE => {
            super::navigation_inspector::navigation_obstacle_inspector_ui(world, ui, entity)
        }
        COMPONENT_NAVIGATION_AREA => {
            super::navigation_inspector::navigation_area_inspector_ui(world, ui, entity)
        }
        COMPONENT_NAVIGATION_LINK => {
            super::navigation_inspector::navigation_link_inspector_ui(world, ui, entity)
        }
        _ => {}
    }
}

pub(crate) fn add_component_footer(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    ui.add_space(4.0);
    let button_response = egui::Frame::new()
        .fill(WIDGET_BG)
        .stroke(egui::Stroke::new(1.0, BORDER))
        .corner_radius(egui::CornerRadius::same(4))
        .inner_margin(egui::Margin::symmetric(8, 6))
        .show(ui, |ui| {
            let open_for_entity = add_component_picker_open_for(world, entity);
            let label = if open_for_entity {
                "Close Add Component"
            } else {
                "+ Add Component"
            };
            if ui.button(label).clicked() {
                if let Some(mut state) = world.get_resource_mut::<InspectorPanelState>() {
                    if open_for_entity {
                        state.add_component_open = false;
                        state.add_component_target = None;
                    } else {
                        state.add_component_open = true;
                        state.add_component_focus_search = true;
                        state.add_component_scroll_selected = true;
                        state.add_component_selected_index = 0;
                        state.add_component_target = Some(entity);
                        state.add_component_search.clear();
                    }
                }
            }
        });

    if add_component_picker_open_for(world, entity) {
        add_component_picker_shelf(world, ui, entity, button_response.response.rect);
    }
}

fn add_component_picker_open_for(world: &World, entity: Entity) -> bool {
    world
        .get_resource::<InspectorPanelState>()
        .is_some_and(|state| state.add_component_open && state.add_component_target == Some(entity))
}

fn shelf_list_max_height(visible_space: f32) -> f32 {
    (visible_space - 108.0).clamp(56.0, 320.0)
}

fn add_component_picker_shelf(
    world: &mut World,
    ui: &mut egui::Ui,
    target: Entity,
    anchor: egui::Rect,
) {
    if world.get_entity(target).is_err() {
        if let Some(mut state) = world.get_resource_mut::<InspectorPanelState>() {
            state.add_component_open = false;
            state.add_component_target = None;
        }
        return;
    }

    let descriptors = world
        .resource::<EditorComponentRegistry>()
        .descriptors
        .clone();

    let visible = ui.clip_rect().intersect(ui.ctx().content_rect());
    let space_above = (anchor.min.y - visible.top()).max(0.0);
    let space_below = (visible.bottom() - anchor.max.y).max(0.0);
    let direction = if space_below >= space_above {
        AddComponentShelfDirection::Down
    } else {
        AddComponentShelfDirection::Up
    };
    let visible_space = match direction {
        AddComponentShelfDirection::Up => space_above,
        AddComponentShelfDirection::Down => space_below,
    };
    let list_max_height = shelf_list_max_height(visible_space);
    let shelf_height = (list_max_height + 108.0).min((visible_space - 4.0).max(96.0));
    let x = anchor
        .min
        .x
        .clamp(visible.left(), visible.right() - anchor.width());
    let y = match direction {
        AddComponentShelfDirection::Up => anchor.min.y - shelf_height - 4.0,
        AddComponentShelfDirection::Down => anchor.max.y + 4.0,
    }
    .clamp(
        visible.top(),
        (visible.bottom() - shelf_height).max(visible.top()),
    );
    let shelf_width = anchor.width().max(260.0).min(visible.width());

    egui::Area::new(egui::Id::new(("add_component_shelf", target)))
        .order(egui::Order::Foreground)
        .fixed_pos(egui::pos2(x, y))
        .show(ui.ctx(), |ui| {
            ui.set_width(shelf_width);
            add_component_picker_shelf_contents(world, ui, target, &descriptors, list_max_height);
        });
}

fn add_component_picker_shelf_contents(
    world: &mut World,
    ui: &mut egui::Ui,
    target: Entity,
    descriptors: &[EditorComponentDescriptor],
    list_max_height: f32,
) {
    egui::Frame::new()
        .fill(PANEL_BG_DARK)
        .stroke(egui::Stroke::new(1.0, BORDER))
        .corner_radius(egui::CornerRadius::same(4))
        .inner_margin(egui::Margin::symmetric(8, 8))
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                ui.label(panel_heading("Add Component"));
                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    if icon_button_small(ui, icons::X, "Close Add Component").clicked() {
                        let mut state = world.resource_mut::<InspectorPanelState>();
                        state.add_component_open = false;
                        state.add_component_target = None;
                    }
                });
            });
            ui.add_space(4.0);

            let mut search_input = world
                .resource::<InspectorPanelState>()
                .add_component_search
                .clone();
            let search_response = ui.add(
                egui::TextEdit::singleline(&mut search_input)
                    .hint_text("Search components...")
                    .desired_width(f32::INFINITY),
            );
            if search_response.changed() {
                let mut state = world.resource_mut::<InspectorPanelState>();
                state.add_component_search = search_input.clone();
                state.add_component_scroll_selected = true;
                state.add_component_selected_index = 0;
            }
            if world
                .resource::<InspectorPanelState>()
                .add_component_focus_search
            {
                search_response.request_focus();
                world
                    .resource_mut::<InspectorPanelState>()
                    .add_component_focus_search = false;
            }

            let search = search_input.to_lowercase();
            let filtered = filtered_component_descriptors(descriptors, &search);
            {
                let mut state = world.resource_mut::<InspectorPanelState>();
                if !filtered.is_empty() {
                    state.add_component_selected_index =
                        state.add_component_selected_index.min(filtered.len() - 1);
                } else {
                    state.add_component_selected_index = 0;
                }
            }

            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
                && !filtered.is_empty()
            {
                let mut state = world.resource_mut::<InspectorPanelState>();
                state.add_component_selected_index =
                    (state.add_component_selected_index + 1) % filtered.len();
                state.add_component_scroll_selected = true;
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
                && !filtered.is_empty()
            {
                let mut state = world.resource_mut::<InspectorPanelState>();
                state.add_component_selected_index = if state.add_component_selected_index == 0 {
                    filtered.len() - 1
                } else {
                    state.add_component_selected_index - 1
                };
                state.add_component_scroll_selected = true;
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
                let mut state = world.resource_mut::<InspectorPanelState>();
                state.add_component_open = false;
                state.add_component_target = None;
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
                let selected_index = world
                    .resource::<InspectorPanelState>()
                    .add_component_selected_index;
                if let Some(descriptor) = filtered.get(selected_index) {
                    let add_state = component_add_state(world, target, descriptor, descriptors);
                    if add_state.addable {
                        insert_registered_component(world, target, descriptor.type_name);
                        let mut state = world.resource_mut::<InspectorPanelState>();
                        state.add_component_open = false;
                        state.add_component_target = None;
                    }
                }
            }

            ui.separator();
            if filtered.is_empty() {
                ui.label(egui::RichText::new("No matching components").color(TEXT_DIM));
                return;
            }

            egui::ScrollArea::vertical()
                .max_height(list_max_height)
                .show(ui, |ui| {
                    let (selected_index, scroll_selected) = {
                        let state = world.resource::<InspectorPanelState>();
                        (
                            state.add_component_selected_index,
                            state.add_component_scroll_selected,
                        )
                    };
                    let mut last_category = None;
                    for (index, descriptor) in filtered.iter().enumerate() {
                        if last_category != Some(descriptor.category) {
                            if last_category.is_some() {
                                ui.separator();
                            }
                            ui.label(panel_heading(component_category_label(descriptor.category)));
                            last_category = Some(descriptor.category);
                        }

                        let add_state = component_add_state(world, target, descriptor, descriptors);
                        let selected = index == selected_index;
                        let row = ui
                            .horizontal(|ui| {
                                ui.label(phosphor_icon_text(descriptor.icon, 14.0).color(TEXT_DIM));
                                ui.add_enabled(
                                    add_state.addable,
                                    egui::Button::selectable(selected, descriptor.display_name),
                                )
                            })
                            .inner
                            .on_hover_text(component_hover_text(descriptor, &add_state));
                        if selected && scroll_selected {
                            row.scroll_to_me(Some(egui::Align::Center));
                        }
                        if row.clicked() {
                            world
                                .resource_mut::<InspectorPanelState>()
                                .add_component_selected_index = index;
                            if add_state.addable {
                                insert_registered_component(world, target, descriptor.type_name);
                                let mut state = world.resource_mut::<InspectorPanelState>();
                                state.add_component_open = false;
                                state.add_component_target = None;
                            }
                        }
                    }
                });
            world
                .resource_mut::<InspectorPanelState>()
                .add_component_scroll_selected = false;
        });
}

fn filtered_component_descriptors<'a>(
    descriptors: &'a [EditorComponentDescriptor],
    search: &str,
) -> Vec<&'a EditorComponentDescriptor> {
    [
        EditorComponentCategory::Authoring,
        EditorComponentCategory::Rendering,
        EditorComponentCategory::Animation,
        EditorComponentCategory::Audio,
        EditorComponentCategory::Navigation,
        EditorComponentCategory::Physics,
        EditorComponentCategory::Gameplay,
        EditorComponentCategory::Volumes,
    ]
    .into_iter()
    .flat_map(|category| {
        descriptors.iter().filter(move |descriptor| {
            descriptor.addable
                && !descriptor.hidden
                && descriptor.category == category
                && descriptor_matches_search(descriptor, search)
        })
    })
    .collect()
}

fn component_category_label(category: EditorComponentCategory) -> &'static str {
    match category {
        EditorComponentCategory::Authoring => "Authoring",
        EditorComponentCategory::Rendering => "Rendering",
        EditorComponentCategory::Animation => "Animation",
        EditorComponentCategory::Audio => "Audio",
        EditorComponentCategory::Navigation => "Navigation",
        EditorComponentCategory::Physics => "Physics",
        EditorComponentCategory::Gameplay => "Gameplay",
        EditorComponentCategory::Volumes => "Volumes",
        EditorComponentCategory::Editor => "Editor",
    }
}

struct ComponentAddState {
    addable: bool,
    reason: Option<String>,
    required: Vec<&'static str>,
    recommended: Vec<&'static str>,
    conflicts: Vec<&'static str>,
}

fn descriptor_matches_search(descriptor: &EditorComponentDescriptor, search: &str) -> bool {
    search.trim().is_empty()
        || descriptor.display_name.to_lowercase().contains(search)
        || descriptor.type_name.to_lowercase().contains(search)
        || descriptor.description.to_lowercase().contains(search)
        || descriptor
            .search_terms
            .iter()
            .any(|term| term.to_lowercase().contains(search))
}

fn component_add_state(
    world: &World,
    entity: Entity,
    descriptor: &EditorComponentDescriptor,
    descriptors: &[EditorComponentDescriptor],
) -> ComponentAddState {
    let duplicate = component_present(world, entity, descriptor.type_name);
    let conflicts = descriptor
        .conflicts_with
        .iter()
        .copied()
        .filter(|type_name| component_present(world, entity, type_name))
        .collect::<Vec<_>>();
    let recommended = descriptor
        .recommended
        .iter()
        .copied()
        .filter(|type_name| !component_present(world, entity, type_name))
        .collect::<Vec<_>>();
    let registry = world.resource::<EditorComponentRegistry>();
    let required = registry
        .required_component_ids(descriptor)
        .iter()
        .filter_map(|id| registry.by_id(id))
        .filter(|required| !registry.component_present(world, entity, required.type_name))
        .map(|required| required.display_name)
        .collect::<Vec<_>>();
    let reason = if duplicate {
        Some("Already present on this actor.".to_string())
    } else if !required.is_empty() {
        Some(format!("Requires {}.", required.join(", ")))
    } else if !conflicts.is_empty() {
        Some(format!(
            "Conflicts with {}.",
            conflicts
                .iter()
                .map(|type_name| component_display_name(descriptors, type_name))
                .collect::<Vec<_>>()
                .join(", ")
        ))
    } else {
        None
    };
    ComponentAddState {
        addable: reason.is_none(),
        reason,
        required,
        recommended,
        conflicts,
    }
}

fn component_hover_text(
    descriptor: &EditorComponentDescriptor,
    state: &ComponentAddState,
) -> String {
    let mut lines = vec![
        descriptor.description.to_string(),
        format!("Type: {}", descriptor.type_name),
        format!("Hydration: {}", descriptor.hydration_effect),
        format!(
            "Inspector: removable={} reorderable={}",
            descriptor.removable, descriptor.reorderable
        ),
    ];
    if let Some(reason) = &state.reason {
        lines.push(format!("Unavailable: {reason}"));
    }
    if !state.required.is_empty() {
        lines.push(format!("Requires: {}", state.required.join(", ")));
    }
    if !state.recommended.is_empty() {
        lines.push(format!(
            "Recommended with: {}",
            state.recommended.join(", ")
        ));
    }
    if !state.conflicts.is_empty() {
        lines.push(format!("Conflicts: {}", state.conflicts.join(", ")));
    }
    lines.push(format!("Search: {}", descriptor.search_terms.join(", ")));
    lines.join("\n")
}

fn component_display_name(
    descriptors: &[EditorComponentDescriptor],
    type_name: &str,
) -> &'static str {
    descriptors
        .iter()
        .find(|descriptor| descriptor.type_name == type_name)
        .map(|descriptor| descriptor.display_name)
        .unwrap_or("component")
}

fn component_present(world: &World, entity: Entity, type_name: &str) -> bool {
    if let Some(registry) = world.get_resource::<EditorComponentRegistry>() {
        if registry.by_type_name(type_name).is_some() {
            return registry.component_present(world, entity, type_name);
        }
    }
    match type_name {
        COMPONENT_ANIMATION_CONTROLLER_DESC => {
            world.get::<AnimationControllerDesc>(entity).is_some()
        }
        "shared::components::Primitive" => world.get::<Primitive>(entity).is_some(),
        "shared::components::BrushDesc" => world.get::<BrushDesc>(entity).is_some(),
        "shared::components::StaticMeshRenderer" => {
            world.get::<StaticMeshRenderer>(entity).is_some()
        }
        COMPONENT_SKINNED_MESH_RENDERER => world.get::<SkinnedMeshRenderer>(entity).is_some(),
        "shared::components::MaterialDesc" => world.get::<MaterialDesc>(entity).is_some(),
        "shared::components::LightDesc" => world.get::<LightDesc>(entity).is_some(),
        "shared::components::AudioSourceDesc" => world.get::<AudioSourceDesc>(entity).is_some(),
        "shared::components::AudioListenerDesc" => world.get::<AudioListenerDesc>(entity).is_some(),
        "shared::components::RigidBodyDesc" => world.get::<RigidBodyDesc>(entity).is_some(),
        "shared::components::ColliderDesc" => world.get::<ColliderDesc>(entity).is_some(),
        "shared::components::PlayerSpawn" => world.get::<PlayerSpawn>(entity).is_some(),
        "shared::components::WeaponSpawn" => world.get::<WeaponSpawn>(entity).is_some(),
        "shared::components::TriggerVolume" => world.get::<TriggerVolume>(entity).is_some(),
        "shared::components::TeamSpawn" => world.get::<TeamSpawn>(entity).is_some(),
        "shared::components::ObjectiveMarker" => world.get::<ObjectiveMarker>(entity).is_some(),
        "shared::components::PostProcessVolumeDesc" => {
            world.get::<PostProcessVolumeDesc>(entity).is_some()
        }
        COMPONENT_NAVIGATION_BOUNDS => world.get::<NavigationBounds>(entity).is_some(),
        COMPONENT_NAVIGATION_OBSTACLE => world.get::<NavigationObstacle>(entity).is_some(),
        COMPONENT_NAVIGATION_AREA => world.get::<NavigationArea>(entity).is_some(),
        COMPONENT_NAVIGATION_LINK => world.get::<NavigationLink>(entity).is_some(),
        _ => false,
    }
}

fn insert_registered_component(world: &mut World, entity: Entity, type_name: &str) {
    let descriptor = world
        .resource::<EditorComponentRegistry>()
        .by_type_name(type_name)
        .cloned();
    if let Some(descriptor) = descriptor {
        if !descriptor.addable || component_present(world, entity, descriptor.type_name) {
            return;
        }
        if descriptor.type_name == COMPONENT_ANIMATION_CONTROLLER_DESC {
            let controller =
                crate::ui::animation_inspector::default_controller_for_actor(world, entity);
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Add Component",
                descriptor.id,
                descriptor.type_name,
                move |world, entity| {
                    world.entity_mut(entity).insert(controller);
                    Ok(())
                },
            );
        } else if descriptor.type_name == COMPONENT_NAVIGATION_BOUNDS {
            let bounds = navigation_bounds_for_entity(world, entity);
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Add Component",
                descriptor.id,
                descriptor.type_name,
                move |world, entity| {
                    world.entity_mut(entity).insert(bounds);
                    Ok(())
                },
            );
        } else {
            let _ = crate::history::reflected_component_transaction(
                world,
                entity,
                "Add Component",
                descriptor.id,
                descriptor.type_name,
                move |world, entity| {
                    crate::history::apply_reflected_default(world, entity, descriptor.type_name)
                },
            );
        }
        return;
    }
    match type_name {
        COMPONENT_ANIMATION_CONTROLLER_DESC => {
            let controller =
                crate::ui::animation_inspector::default_controller_for_actor(world, entity);
            insert_component(world, entity, move |world, e| {
                world.entity_mut(e).insert(controller);
            });
        }
        "shared::components::Primitive" => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((shared::ActorKind::StaticMesh, Primitive::cuboid(Vec3::ONE)));
        }),
        "shared::components::BrushDesc" => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((shared::ActorKind::Brush, BrushDesc::default()));
        }),
        "shared::components::StaticMeshRenderer" => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((shared::ActorKind::StaticMesh, StaticMeshRenderer::default()));
        }),
        "shared::components::MaterialDesc" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(MaterialDesc::default());
        }),
        "shared::components::LightDesc" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(LightDesc::default());
        }),
        "shared::components::AudioSourceDesc" => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((shared::ActorKind::AudioSource, AudioSourceDesc::default()));
        }),
        "shared::components::AudioListenerDesc" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert((
                shared::ActorKind::AudioListener,
                AudioListenerDesc::default(),
            ));
        }),
        "shared::components::RigidBodyDesc" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(RigidBodyDesc::default());
        }),
        "shared::components::ColliderDesc" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(ColliderDesc::default());
        }),
        "shared::components::PlayerSpawn" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(PlayerSpawn);
        }),
        "shared::components::WeaponSpawn" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(WeaponSpawn {
                weapon_id: "rifle".into(),
            });
        }),
        "shared::components::TriggerVolume" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(TriggerVolume::default());
        }),
        "shared::components::TeamSpawn" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(TeamSpawn { team_id: 0 });
        }),
        "shared::components::ObjectiveMarker" => insert_component(world, entity, |world, e| {
            world.entity_mut(e).insert(ObjectiveMarker {
                objective_id: "objective".into(),
            });
        }),
        COMPONENT_NAVIGATION_BOUNDS => insert_component(world, entity, |world, e| {
            let bounds = navigation_bounds_for_entity(world, e);
            world.entity_mut(e).insert((ActorKind::Navigation, bounds));
        }),
        COMPONENT_NAVIGATION_OBSTACLE => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((ActorKind::Navigation, NavigationObstacle::default()));
        }),
        COMPONENT_NAVIGATION_AREA => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((ActorKind::Navigation, NavigationArea::default()));
        }),
        COMPONENT_NAVIGATION_LINK => insert_component(world, entity, |world, e| {
            world
                .entity_mut(e)
                .insert((ActorKind::Navigation, NavigationLink::default()));
        }),
        "shared::components::PostProcessVolumeDesc" => {
            insert_component(world, entity, |world, e| {
                world.entity_mut(e).insert(PostProcessVolumeDesc::default());
            })
        }
        _ => {}
    }
}

fn navigation_bounds_for_entity(world: &World, entity: Entity) -> NavigationBounds {
    world
        .get::<ActorId>(entity)
        .map(|actor_id| NavigationBounds::for_actor(&actor_id.0))
        .unwrap_or_else(|| NavigationBounds::for_actor(&uuid::Uuid::new_v4().to_string()))
}

fn reset_navigation_bounds_for_entity(world: &World, entity: Entity) -> NavigationBounds {
    let mut bounds = navigation_bounds_for_entity(world, entity);
    if let Some(current) = world.get::<NavigationBounds>(entity) {
        bounds.artifact_path.clone_from(&current.artifact_path);
    }
    bounds
}

fn insert_component(world: &mut World, entity: Entity, insert: impl FnOnce(&mut World, Entity)) {
    let before = crate::history::snapshot_entity(world, entity);
    let old_kind = before.as_ref().map(|s| s.actor_kind);
    insert(world, entity);
    let after = crate::history::snapshot_entity(world, entity);
    if let (Some(before), Some(after)) = (before, after) {
        if let Some(old) = old_kind {
            if old != after.actor_kind {
                crate::history::set_actor_kind_with_history(world, entity, old, after.actor_kind);
            }
        }
        crate::history::push_command(
            world,
            crate::history::EditorCommand::AddComponent {
                entity,
                snapshot: diff_added(&before, &after),
            },
        );
    }
}

fn diff_added(before: &EditorEntitySnapshot, after: &EditorEntitySnapshot) -> EditorEntitySnapshot {
    EditorEntitySnapshot {
        actor_id: None,
        actor_kind: after.actor_kind,
        actor_name: None,
        name: None,
        transform: after.transform,
        primitive: after
            .primitive
            .clone()
            .filter(|_| before.primitive.is_none()),
        brush: after.brush.clone().filter(|_| before.brush.is_none()),
        static_mesh_renderer: after
            .static_mesh_renderer
            .clone()
            .filter(|_| before.static_mesh_renderer.is_none()),
        skinned_mesh_renderer: after
            .skinned_mesh_renderer
            .clone()
            .filter(|_| before.skinned_mesh_renderer.is_none()),
        material: after.material.clone().filter(|_| before.material.is_none()),
        material_override: after
            .material_override
            .clone()
            .filter(|_| before.material_override.is_none()),
        rigid_body: after.rigid_body.filter(|_| before.rigid_body.is_none()),
        collider: after.collider.clone().filter(|_| before.collider.is_none()),
        physics: after.physics.clone().filter(|_| before.physics.is_none()),
        light: after.light.clone().filter(|_| before.light.is_none()),
        animation_controller: after
            .animation_controller
            .clone()
            .filter(|_| before.animation_controller.is_none()),
        audio_source: after
            .audio_source
            .clone()
            .filter(|_| before.audio_source.is_none()),
        audio_listener: after
            .audio_listener
            .filter(|_| before.audio_listener.is_none()),
        player_spawn: after.player_spawn && !before.player_spawn,
        model: after.model.clone().filter(|_| before.model.is_none()),
        prefab: after.prefab.clone().filter(|_| before.prefab.is_none()),
        prefab_instance: after
            .prefab_instance
            .clone()
            .filter(|_| before.prefab_instance.is_none()),
        weapon_spawn: after
            .weapon_spawn
            .clone()
            .filter(|_| before.weapon_spawn.is_none()),
        trigger_volume: after
            .trigger_volume
            .clone()
            .filter(|_| before.trigger_volume.is_none()),
        post_process_volume: after
            .post_process_volume
            .clone()
            .filter(|_| before.post_process_volume.is_none()),
        team_spawn: after
            .team_spawn
            .clone()
            .filter(|_| before.team_spawn.is_none()),
        objective: after
            .objective
            .clone()
            .filter(|_| before.objective.is_none()),
        navigation_bounds: after
            .navigation_bounds
            .clone()
            .filter(|_| before.navigation_bounds.is_none()),
        navigation_obstacle: after
            .navigation_obstacle
            .clone()
            .filter(|_| before.navigation_obstacle.is_none()),
        navigation_area: after
            .navigation_area
            .clone()
            .filter(|_| before.navigation_area.is_none()),
        navigation_link: after
            .navigation_link
            .clone()
            .filter(|_| before.navigation_link.is_none()),
        hierarchy_sibling_index: after.hierarchy_sibling_index,
        editor_visibility: after.editor_visibility,
        inspector_order: None,
        component_states: None,
        children: Vec::new(),
    }
}

fn static_mesh_asset_ref_candidates(
    world: &mut World,
    kind: AssetRefCandidateKind,
) -> Vec<AssetRefCandidate> {
    let thumbnail_by_path = model_thumbnail_texture_by_path(world);
    let Some(registry) = world.get_resource::<AssetRegistry>() else {
        return Vec::new();
    };
    let catalog = world.get_resource::<EditorAssets>();
    let mut seen = HashSet::new();
    let mut candidates = Vec::new();

    for record in &registry.records {
        let Some(manifest_path) = record.import_settings.static_mesh_manifest_path.as_deref()
        else {
            continue;
        };
        let Ok(manifest) = load_static_mesh_manifest(manifest_path) else {
            continue;
        };
        let selection = AssetSelection::File(manifest.source.path.clone());
        let texture_id = thumbnail_by_path.get(&manifest.source.path).copied();
        let folder_path = catalog
            .and_then(|assets| {
                assets
                    .assets
                    .iter()
                    .find(|asset| asset.path.as_deref() == Some(manifest.source.path.as_str()))
                    .map(|asset| asset.folder_path.clone())
            })
            .unwrap_or_else(|| fallback_asset_folder(&manifest.source.path));

        for part in &manifest.parts {
            let source_material_ref = source_material_ref_for_part(&manifest.asset_id, part);
            let candidate = match kind {
                AssetRefCandidateKind::Mesh => {
                    let sub_asset_id = if part.id.trim().is_empty() {
                        part_id_from_label(&part.mesh_label)
                    } else {
                        part.id.clone()
                    };
                    AssetRefCandidate {
                        reference: EditorAssetRef::new(
                            manifest.asset_id.clone(),
                            sub_asset_id,
                            part.name.clone(),
                        ),
                        label: part.name.clone(),
                        detail: format!("{} | {}", manifest.label, manifest.source.path),
                        selection: selection.clone(),
                        folder_path: folder_path.clone(),
                        texture_id,
                    }
                }
                AssetRefCandidateKind::Material => {
                    let Some(material_ref) = source_material_ref else {
                        continue;
                    };
                    AssetRefCandidate {
                        reference: material_ref,
                        label: part.material_slot_name.clone(),
                        detail: format!("{} | {}", manifest.label, manifest.source.path),
                        selection: selection.clone(),
                        folder_path: folder_path.clone(),
                        texture_id: None,
                    }
                }
                AssetRefCandidateKind::Texture => continue,
            };

            let key = (
                candidate.reference.asset_id.clone(),
                candidate.reference.sub_asset_id.clone(),
            );
            if seen.insert(key) {
                candidates.push(candidate);
            }
        }
    }

    candidates.sort_by(|a, b| a.detail.cmp(&b.detail).then(a.label.cmp(&b.label)));
    candidates
}

fn texture_asset_candidates(world: &World) -> Vec<TextureAssetCandidate> {
    let Some(catalog) = world.get_resource::<EditorAssets>() else {
        return Vec::new();
    };
    let snapshot = world
        .get_resource::<AssetThumbnailCache>()
        .map(AssetThumbnailCache::snapshot);
    let mut candidates: Vec<_> = catalog
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
        .filter_map(|asset| {
            let path = asset.path.clone()?;
            let texture_id = snapshot
                .as_ref()
                .and_then(|snapshot| snapshot.texture_for(asset));
            Some(TextureAssetCandidate {
                label: asset.label.clone(),
                path: path.clone(),
                folder_path: asset.folder_path.clone(),
                selection: AssetSelection::File(path),
                texture_id,
            })
        })
        .collect();
    candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.path.cmp(&b.path)));
    candidates
}

fn texture_asset_ref_candidates(world: &World) -> Vec<AssetRefCandidate> {
    let Some(catalog) = world.get_resource::<EditorAssets>() else {
        return Vec::new();
    };
    let Some(registry) = world.get_resource::<AssetRegistry>() else {
        return Vec::new();
    };
    let snapshot = world
        .get_resource::<AssetThumbnailCache>()
        .map(AssetThumbnailCache::snapshot);
    let mut candidates: Vec<_> = catalog
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
        .filter_map(|asset| {
            let path = asset.path.as_deref()?;
            let record = find_asset_by_path(registry, path)?;
            Some(AssetRefCandidate {
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    "texture:source",
                    asset.label.clone(),
                )
                .with_source_path(path),
                label: asset.label.clone(),
                detail: path.to_string(),
                selection: AssetSelection::File(path.to_string()),
                folder_path: asset.folder_path.clone(),
                texture_id: snapshot
                    .as_ref()
                    .and_then(|snapshot| snapshot.texture_for(asset)),
            })
        })
        .collect();
    candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.detail.cmp(&b.detail)));
    candidates
}

fn brush_face_material_ref_candidates(world: &mut World) -> Vec<AssetRefCandidate> {
    let mut candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Material);
    let Some(catalog) = world.get_resource::<EditorAssets>() else {
        return candidates;
    };
    let Some(registry) = world.get_resource::<AssetRegistry>() else {
        return candidates;
    };
    let mut seen: HashSet<_> = candidates
        .iter()
        .map(|candidate| {
            (
                candidate.reference.asset_id.clone(),
                candidate.reference.sub_asset_id.clone(),
            )
        })
        .collect();
    for asset in catalog
        .assets
        .iter()
        .filter(|asset| matches!(asset.kind, EditorAssetKind::Material))
    {
        let Some(path) = asset.path.as_deref() else {
            continue;
        };
        let Some(record) = find_asset_by_path(registry, path) else {
            continue;
        };
        let sub_asset_id = if shared::MaterialInstanceAsset::load_from_path(path).is_ok() {
            "material:instance"
        } else {
            "material:source"
        };
        let reference = EditorAssetRef::new(record.id.as_string(), sub_asset_id, &asset.label)
            .with_source_path(path);
        let key = (reference.asset_id.clone(), reference.sub_asset_id.clone());
        if !seen.insert(key) {
            continue;
        }
        candidates.push(AssetRefCandidate {
            reference,
            label: asset.label.clone(),
            detail: path.to_string(),
            selection: AssetSelection::File(path.to_string()),
            folder_path: asset.folder_path.clone(),
            texture_id: None,
        });
    }
    candidates.sort_by(|a, b| a.label.cmp(&b.label).then(a.detail.cmp(&b.detail)));
    candidates
}

fn asset_ref_candidate_from_selection(
    world: &World,
    selection: &AssetSelection,
    kind: AssetRefCandidateKind,
) -> Option<AssetRefCandidate> {
    let registry = world.get_resource::<AssetRegistry>()?;
    match (kind, selection) {
        (AssetRefCandidateKind::Material, AssetSelection::File(path)) => {
            let asset = world
                .get_resource::<EditorAssets>()?
                .assets
                .iter()
                .find(|asset| {
                    matches!(asset.kind, EditorAssetKind::Material)
                        && asset.path.as_deref() == Some(path.as_str())
                })?;
            let record = find_asset_by_path(registry, path)?;
            let sub_asset_id = if shared::MaterialInstanceAsset::load_from_path(path).is_ok() {
                "material:instance"
            } else {
                "material:source"
            };
            Some(AssetRefCandidate {
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    sub_asset_id,
                    asset.label.clone(),
                )
                .with_source_path(path),
                label: asset.label.clone(),
                detail: path.clone(),
                selection: selection.clone(),
                folder_path: asset.folder_path.clone(),
                texture_id: None,
            })
        }
        (AssetRefCandidateKind::Texture, AssetSelection::File(path)) => {
            let asset = world
                .get_resource::<EditorAssets>()?
                .assets
                .iter()
                .find(|asset| {
                    matches!(asset.kind, EditorAssetKind::Texture)
                        && asset.path.as_deref() == Some(path.as_str())
                })?;
            let record = find_asset_by_path(registry, path)?;
            Some(AssetRefCandidate {
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    "texture:source",
                    asset.label.clone(),
                )
                .with_source_path(path),
                label: asset.label.clone(),
                detail: path.clone(),
                selection: selection.clone(),
                folder_path: asset.folder_path.clone(),
                texture_id: None,
            })
        }
        (
            AssetRefCandidateKind::Material,
            AssetSelection::SubAsset {
                parent_path,
                sub_asset_id,
                label,
                kind: AssetSubAssetKind::Material,
                ..
            },
        ) => {
            let record = find_asset_by_path(registry, parent_path)?;
            Some(AssetRefCandidate {
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    sub_asset_id.clone(),
                    label.clone(),
                ),
                label: label.clone(),
                detail: parent_path.clone(),
                selection: selection.clone(),
                folder_path: fallback_asset_folder(parent_path),
                texture_id: None,
            })
        }
        (
            AssetRefCandidateKind::Texture,
            AssetSelection::SubAsset {
                parent_path,
                sub_asset_id,
                label,
                kind: AssetSubAssetKind::Texture,
                source_path,
            },
        ) => {
            let record = find_asset_by_path(registry, parent_path)?;
            Some(AssetRefCandidate {
                reference: EditorAssetRef::new(
                    record.id.as_string(),
                    sub_asset_id.clone(),
                    label.clone(),
                )
                .with_source_path(source_path.clone().unwrap_or_else(|| parent_path.clone())),
                label: label.clone(),
                detail: source_path.clone().unwrap_or_else(|| parent_path.clone()),
                selection: selection.clone(),
                folder_path: fallback_asset_folder(parent_path),
                texture_id: None,
            })
        }
        _ => None,
    }
}

fn request_texture_asset_thumbnails(world: &mut World) {
    let requests: Vec<_> = world
        .get_resource::<EditorAssets>()
        .map(|assets| {
            assets
                .assets
                .iter()
                .filter(|asset| matches!(asset.kind, EditorAssetKind::Texture))
                .filter_map(|asset| Some((asset_cache_key(asset), asset.path.clone()?)))
                .collect()
        })
        .unwrap_or_default();
    if requests.is_empty() || world.get_resource::<AssetThumbnailCache>().is_none() {
        return;
    }
    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|_world, mut cache: Mut<AssetThumbnailCache>| {
        for (key, path) in requests {
            cache.request_texture(key, path, &asset_server);
        }
    });
}

fn texture_path_from_selection(
    world: &World,
    selection: &AssetSelection,
) -> Option<TextureAssetCandidate> {
    match selection {
        AssetSelection::File(path) => {
            let asset = world
                .get_resource::<EditorAssets>()?
                .assets
                .iter()
                .find(|asset| {
                    matches!(asset.kind, EditorAssetKind::Texture)
                        && asset.path.as_deref() == Some(path.as_str())
                })?;
            Some(TextureAssetCandidate {
                label: asset.label.clone(),
                path: path.clone(),
                folder_path: asset.folder_path.clone(),
                selection: selection.clone(),
                texture_id: None,
            })
        }
        AssetSelection::SubAsset {
            label,
            kind: AssetSubAssetKind::Texture,
            source_path: Some(path),
            ..
        } => {
            if let Some(asset) = world.get_resource::<EditorAssets>().and_then(|assets| {
                assets
                    .assets
                    .iter()
                    .find(|asset| asset.path.as_deref() == Some(path.as_str()))
            }) {
                return Some(TextureAssetCandidate {
                    label: asset.label.clone(),
                    path: path.clone(),
                    folder_path: asset.folder_path.clone(),
                    selection: selection.clone(),
                    texture_id: None,
                });
            }
            Some(TextureAssetCandidate {
                label: label.clone(),
                path: path.clone(),
                folder_path: fallback_asset_folder(path),
                selection: selection.clone(),
                texture_id: None,
            })
        }
        _ => None,
    }
}

fn source_material_ref_for_part(
    asset_id: &str,
    part: &crate::assets::static_mesh::StaticMeshPart,
) -> Option<EditorAssetRef> {
    let material_label = part.material_label.as_deref()?;
    let sub_asset_id = part
        .material_id
        .clone()
        .filter(|id| !id.trim().is_empty())
        .unwrap_or_else(|| material_id_from_label(material_label));
    Some(EditorAssetRef::new(
        asset_id.to_string(),
        sub_asset_id,
        part.material_slot_name.clone(),
    ))
}

fn model_thumbnail_texture_by_path(world: &mut World) -> HashMap<String, egui::TextureId> {
    let model_assets: Vec<EditorAsset> = world
        .get_resource::<EditorAssets>()
        .map(|assets| {
            assets
                .assets
                .iter()
                .filter(|asset| matches!(asset.kind, EditorAssetKind::Model))
                .cloned()
                .collect()
        })
        .unwrap_or_default();
    if model_assets.is_empty() {
        return HashMap::new();
    }
    if world.get_resource::<AssetThumbnailCache>().is_none() {
        return HashMap::new();
    }

    let asset_server = world.resource::<AssetServer>().clone();
    world.resource_scope(|world, mut cache: Mut<AssetThumbnailCache>| {
        if let Some(mut studio) = world.get_resource_mut::<ThumbnailStudio>() {
            for asset in &model_assets {
                let Some(path) = asset.path.as_ref() else {
                    continue;
                };
                cache.request_model(
                    asset_cache_key(asset),
                    path.clone(),
                    &asset_server,
                    &mut studio,
                );
            }
        }
    });

    let snapshot = world.resource::<AssetThumbnailCache>().snapshot();
    model_assets
        .iter()
        .filter_map(|asset| {
            Some((
                asset.path.clone()?,
                snapshot.texture_ids.get(&asset_cache_key(asset)).copied()?,
            ))
        })
        .collect()
}

fn fallback_asset_folder(path: &str) -> String {
    Path::new(path)
        .parent()
        .and_then(|parent| parent.to_str())
        .filter(|folder| !folder.trim().is_empty())
        .unwrap_or(crate::assets::ASSETS_ROOT)
        .replace('\\', "/")
}

fn locate_asset_ref(
    world: &mut World,
    asset: Option<&EditorAssetRef>,
    candidates: &[AssetRefCandidate],
) {
    let Some(asset) = asset.filter(|asset| asset.is_resolved()) else {
        return;
    };
    let Some(candidate) = candidates.iter().find(|candidate| {
        candidate.reference.asset_id == asset.asset_id
            && candidate.reference.sub_asset_id == asset.sub_asset_id
    }) else {
        if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
            scene_io.status = format!("Could not locate imported asset {}", asset.label);
        }
        return;
    };

    if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
        assets.current_folder = candidate.folder_path.clone();
        assets.select(candidate.selection.clone());
    }
    if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
        let panel_nodes = ui_state.panel_nodes;
        open_and_focus_tab(
            &mut ui_state.dock_state,
            EditorTab::AssetBrowser,
            &panel_nodes,
        );
    }
}

fn skinned_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let material_candidates = brush_face_material_ref_candidates(world);
    let Some(mut renderer) = world.get::<SkinnedMeshRenderer>(entity).cloned() else {
        return;
    };
    let original = renderer.clone();
    let mut changed = false;
    let mut options = ComponentCardOptions::removable(
        COMPONENT_SKINNED_MESH_RENDERER,
        "Skinned Mesh Renderer",
        icons::PERSON_SIMPLE_RUN,
    );
    options.active_toggle = false;
    options.removable = world.get::<AnimationControllerDesc>(entity).is_none();
    options.resettable = false;
    options.copyable = false;
    let context = component_card_context(world, entity, options);
    let response = component_card(ui, &context, |ui| {
        property_row(ui, "Source", |ui| {
            ui.add(
                egui::Label::new(if renderer.path.trim().is_empty() {
                    "Unassigned"
                } else {
                    renderer.path.as_str()
                })
                .truncate(),
            );
        });
        property_row(ui, "Scene", |ui| {
            ui.label(renderer.scene_index.to_string());
        });
        property_row(ui, "Asset ID", |ui| {
            ui.add(
                egui::Label::new(if renderer.asset_id.trim().is_empty() {
                    "Legacy/path-only"
                } else {
                    renderer.asset_id.as_str()
                })
                .truncate(),
            );
        });
        ui.label(
            egui::RichText::new(
                "Preserves the imported skeleton hierarchy and Bevy skinned-mesh bindings.",
            )
            .small()
            .color(TEXT_DIM),
        );
        ui.add_space(8.0);
        ui.label(egui::RichText::new("Material slots").strong());
        if renderer.materials.slots.is_empty() {
            ui.label(
                egui::RichText::new("No imported slots; reimport the source model")
                    .small()
                    .color(TEXT_DIM),
            );
        }
        for slot in &mut renderer.materials.slots {
            slot_card(ui, |ui| {
                ui.label(egui::RichText::new(&slot.name).strong());
                let response = asset_selector_row(
                    ui,
                    "Material",
                    icons::PALETTE,
                    slot.material.as_ref().map(|reference| &reference.0),
                    slot.source_material.as_ref().map(|reference| &reference.0),
                    true,
                    &material_candidates,
                    None,
                );
                if let Some(selected) = response.selected {
                    slot.material = Some(MaterialRef::new(selected));
                    changed = true;
                }
                if response.clear {
                    slot.material = None;
                    changed = true;
                }
                if response.locate {
                    let reference = slot
                        .material
                        .as_ref()
                        .or(slot.source_material.as_ref())
                        .map(|reference| &reference.0);
                    locate_asset_ref(world, reference, &material_candidates);
                }
                ui.label(
                    egui::RichText::new(format!("ID: {}", slot.id.0))
                        .small()
                        .color(TEXT_DIM),
                );
            });
        }
        for orphan in &renderer.materials.orphaned_assignments {
            ui.label(
                egui::RichText::new(format!(
                    "Orphaned: {} ({})",
                    orphan.last_known_name, orphan.slot_id.0
                ))
                .small()
                .color(egui::Color32::YELLOW),
            );
        }
        if world.get::<AnimationControllerDesc>(entity).is_some() {
            ui.label(
                egui::RichText::new(
                    "Remove the Animation Controller before removing its renderer.",
                )
                .small()
                .color(TEXT_MUTED),
            );
        }
    });
    apply_component_card_response(world, entity, response);
    if changed && renderer != original {
        let result = reflected_component_transaction(
            world,
            entity,
            "Assign Skinned Material Slot",
            shared::AUTHORING_COMPONENT_SKINNED_MESH_RENDERER,
            COMPONENT_SKINNED_MESH_RENDERER,
            move |world, entity| {
                world.entity_mut(entity).insert(renderer);
                Ok(())
            },
        );
        if let Err(error) = result {
            world.resource_mut::<crate::scene_io::SceneIo>().status = error;
        }
    }
}

fn static_mesh_renderer_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh);
    let material_candidates = brush_face_material_ref_candidates(world);
    let Some(mut renderer) = world.get::<StaticMeshRenderer>(entity).cloned() else {
        return;
    };
    let original = renderer.clone();
    let mut changed = false;
    let mut remove_entry = None;
    for index in 0..renderer.slots.len() {
        ensure_slot_id(&mut renderer.slots[index], index);
        let part = &renderer.slots[index];
        if renderer.materials.slot(&part.material_slot_id).is_none() {
            renderer.materials.slots.push(shared::RendererMaterialSlot {
                id: part.material_slot_id.clone(),
                name: part.name.clone(),
                source_material: part.material.clone().map(MaterialRef::new),
                material: None,
            });
        }
    }

    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_STATIC_MESH_RENDERER,
            "Static Mesh Renderer",
            icons::CUBE,
        ),
    );
    let card_response = component_card(ui, &card, |ui| {
        if renderer.slots.is_empty() {
            ui.label(egui::RichText::new("No renderer slots").color(TEXT_DIM));
        }

        for (index, entry) in renderer.slots.iter_mut().enumerate() {
            ensure_slot_id(entry, index);
            let material_slot_name = renderer
                .materials
                .slot(&entry.material_slot_id)
                .map(|slot| slot.name.clone())
                .unwrap_or_else(|| "Missing slot".into());
            let thumbnail = thumbnail_for_mesh(&entry.mesh, &mesh_candidates);

            slot_card(ui, |ui| {
                slot_header(ui, index, &entry.name, |ui| {
                    status_dot(ui, egui::Color32::from_rgb(74, 181, 104), "Slot active");
                    if icon_button_small(ui, icons::TRASH, "Remove slot").clicked() {
                        remove_entry = Some(index);
                    }
                    ui.label(phosphor_icon(icons::DOTS_THREE_VERTICAL, 16.0).color(TEXT_DIM));
                });
                ui.add_space(6.0);

                let mut draw_fields = |ui: &mut egui::Ui, entry: &mut StaticMeshRendererEntry| {
                    property_row(ui, "Name", |ui| {
                        changed |= ui
                            .add_sized(
                                [text_field_width(ui), 20.0],
                                egui::TextEdit::singleline(&mut entry.name),
                            )
                            .changed();
                    });
                    let mesh_response = asset_selector_row(
                        ui,
                        "Mesh",
                        icons::CUBE,
                        Some(&entry.mesh),
                        None,
                        false,
                        &mesh_candidates,
                        None,
                    );
                    if let Some(selected) = mesh_response.selected {
                        entry.mesh = selected;
                        changed = true;
                    }
                    if mesh_response.locate {
                        locate_asset_ref(world, Some(&entry.mesh), &mesh_candidates);
                    }

                    property_row(ui, "Material slot", |ui| {
                        ui.label(&material_slot_name);
                    });
                    ui.horizontal_wrapped(|ui| {
                        changed |= ui.checkbox(&mut entry.visible, "Visible").changed();
                        changed |= ui
                            .checkbox(&mut entry.cast_shadows, "Cast shadows")
                            .changed();
                        changed |= ui
                            .checkbox(&mut entry.receive_shadows, "Receive shadows")
                            .changed();
                    });
                };

                if ui.available_width() < 430.0 {
                    slot_thumbnail(ui, thumbnail);
                    ui.add_space(6.0);
                    draw_fields(ui, entry);
                } else {
                    ui.horizontal(|ui| {
                        slot_thumbnail(ui, thumbnail);
                        ui.add_space(10.0);
                        ui.vertical(|ui| {
                            ui.set_max_width(ui.available_width());
                            draw_fields(ui, entry);
                        });
                    });
                }
            });
        }

        ui.add_space(8.0);
        ui.label(egui::RichText::new("Material slots").strong());
        for slot in &mut renderer.materials.slots {
            slot_card(ui, |ui| {
                ui.label(egui::RichText::new(&slot.name).strong());
                let response = asset_selector_row(
                    ui,
                    "Material",
                    icons::PALETTE,
                    slot.material.as_ref().map(|reference| &reference.0),
                    slot.source_material.as_ref().map(|reference| &reference.0),
                    true,
                    &material_candidates,
                    None,
                );
                if let Some(selected) = response.selected {
                    slot.material = Some(MaterialRef::new(selected));
                    changed = true;
                }
                if response.clear {
                    slot.material = None;
                    changed = true;
                }
                if response.locate {
                    let reference = slot
                        .material
                        .as_ref()
                        .or(slot.source_material.as_ref())
                        .map(|reference| &reference.0);
                    locate_asset_ref(world, reference, &material_candidates);
                }
                ui.label(
                    egui::RichText::new(format!("ID: {}", slot.id.0))
                        .small()
                        .color(TEXT_DIM),
                );
            });
        }

        let add_slot = ui
            .add_sized(
                [ui.available_width().max(1.0), 28.0],
                egui::Button::new("+  Add Slot").fill(ELEVATED_BG.linear_multiply(0.45)),
            )
            .clicked();
        if add_slot {
            let index = renderer.slots.len();
            let entry = StaticMeshRendererEntry {
                id: ComponentInstanceId::new(format!("slot:{index}")),
                material_slot_id: ComponentInstanceId::new(format!("slot:manual:{index}")),
                ..Default::default()
            };
            renderer.materials.slots.push(shared::RendererMaterialSlot {
                id: entry.material_slot_id.clone(),
                name: format!("Material {index}"),
                source_material: None,
                material: None,
            });
            renderer.slots.push(entry);
            changed = true;
        }
    });
    apply_component_card_response(world, entity, card_response);

    if let Some(index) = remove_entry {
        let removed = renderer.slots.remove(index);
        if let Some(slot_index) = renderer
            .materials
            .slots
            .iter()
            .position(|slot| slot.id == removed.material_slot_id)
        {
            let slot = renderer.materials.slots.remove(slot_index);
            if let Some(material) = slot.material {
                renderer
                    .materials
                    .orphaned_assignments
                    .push(shared::OrphanedMaterialAssignment {
                        slot_id: slot.id,
                        last_known_name: slot.name,
                        material,
                    });
            }
        }
        changed = true;
    }
    if changed && renderer != original {
        set_static_mesh_renderer_with_history(world, entity, renderer);
    }
}

fn ensure_slot_id(entry: &mut StaticMeshRendererEntry, index: usize) {
    if entry.id.is_empty() {
        entry.id = ComponentInstanceId::new(format!("slot:{index}"));
    }
    if entry.material_slot_id.is_empty() {
        entry.material_slot_id = ComponentInstanceId::new(format!("slot:{}", entry.id.0));
    }
}

fn slot_card(ui: &mut egui::Ui, add_contents: impl FnOnce(&mut egui::Ui)) {
    egui::Frame::new()
        .fill(WIDGET_BG.linear_multiply(0.82))
        .stroke(egui::Stroke::new(1.0, BORDER))
        .corner_radius(egui::CornerRadius::same(4))
        .inner_margin(egui::Margin::symmetric(8, 8))
        .show(ui, |ui| {
            ui.set_max_width(ui.available_width());
            add_contents(ui);
        });
    ui.add_space(4.0);
}

fn slot_header(
    ui: &mut egui::Ui,
    index: usize,
    slot_name: &str,
    add_actions: impl FnOnce(&mut egui::Ui),
) {
    ui.horizontal(|ui| {
        ui.label(phosphor_icon(icons::CARET_DOWN, 13.0).color(TEXT_DIM));
        ui.label(format!("Slot {index}"));
        let badge = if slot_name.trim().is_empty() {
            "Mesh"
        } else {
            slot_name.trim()
        };
        ui.add(
            egui::Button::new(
                egui::RichText::new(badge).color(egui::Color32::from_rgb(153, 202, 255)),
            )
            .fill(SELECTION_BG_MUTED)
            .stroke(egui::Stroke::new(1.0, SELECTION_BG_MUTED))
            .corner_radius(4.0),
        );
        ui.with_layout(
            egui::Layout::right_to_left(egui::Align::Center),
            add_actions,
        );
    });
}

fn status_dot(ui: &mut egui::Ui, color: egui::Color32, tooltip: &str) {
    let (rect, response) = ui.allocate_exact_size(egui::vec2(18.0, 20.0), egui::Sense::hover());
    ui.painter().circle_filled(rect.center(), 4.0, color);
    response.on_hover_text(tooltip);
}

fn status_dot_button(ui: &mut egui::Ui, color: egui::Color32, tooltip: &str) -> egui::Response {
    let (rect, response) = ui.allocate_exact_size(egui::vec2(18.0, 20.0), egui::Sense::click());
    let color = if response.hovered() {
        color.linear_multiply(1.2)
    } else {
        color
    };
    ui.painter().circle_filled(rect.center(), 4.0, color);
    response.on_hover_text(tooltip)
}

fn slot_thumbnail(ui: &mut egui::Ui, texture_id: Option<egui::TextureId>) {
    let size = 86.0_f32.min(ui.available_width().max(1.0));
    let (rect, _response) = ui.allocate_exact_size(egui::vec2(size, size), egui::Sense::hover());
    ui.painter().rect(
        rect,
        4.0,
        PANEL_BG_DARK,
        egui::Stroke::new(1.0, BORDER),
        egui::StrokeKind::Inside,
    );
    if let Some(texture_id) = texture_id {
        let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
        ui.painter()
            .image(texture_id, rect.shrink(6.0), uv, egui::Color32::WHITE);
    } else {
        ui.painter().text(
            rect.center(),
            egui::Align2::CENTER_CENTER,
            icons::CUBE.as_str(),
            egui::FontId::new(28.0, egui::FontFamily::Name("phosphor-regular".into())),
            TEXT_MUTED,
        );
    }
}

fn thumbnail_for_mesh(
    mesh: &EditorAssetRef,
    candidates: &[AssetRefCandidate],
) -> Option<egui::TextureId> {
    candidates
        .iter()
        .find(|candidate| {
            candidate.reference.asset_id == mesh.asset_id
                && candidate.reference.sub_asset_id == mesh.sub_asset_id
        })
        .and_then(|candidate| candidate.texture_id)
}

#[expect(
    clippy::too_many_arguments,
    reason = "asset selector rows keep immediate-mode UI inputs explicit"
)]
fn asset_selector_row(
    ui: &mut egui::Ui,
    label: &str,
    icon: Icon,
    asset: Option<&EditorAssetRef>,
    inherited_asset: Option<&EditorAssetRef>,
    clearable: bool,
    candidates: &[AssetRefCandidate],
    drop_candidate: Option<&AssetRefCandidate>,
) -> AssetSelectorResponse {
    let mut response = AssetSelectorResponse::default();
    if ui.available_width() < COMPACT_INSPECTOR_WIDTH {
        ui.vertical(|ui| {
            ui.label(label);
            let control_width = ui.available_width().max(1.0);
            asset_selector_control(
                ui,
                icon,
                asset,
                inherited_asset,
                clearable,
                control_width,
                candidates,
                drop_candidate,
                &mut response,
            );
        });
    } else {
        ui.horizontal(|ui| {
            let row_width = ui.available_width().max(1.0);
            let label_width = ASSET_SELECTOR_LABEL_WIDTH.min(row_width);
            let control_width = (row_width - label_width - ui.spacing().item_spacing.x).max(1.0);
            ui.add_sized([label_width, 20.0], egui::Label::new(label));
            asset_selector_control(
                ui,
                icon,
                asset,
                inherited_asset,
                clearable,
                control_width,
                candidates,
                drop_candidate,
                &mut response,
            );
        });
    }
    response
}

#[expect(
    clippy::too_many_arguments,
    reason = "asset selector controls keep immediate-mode UI inputs explicit"
)]
fn asset_selector_control(
    ui: &mut egui::Ui,
    icon: Icon,
    asset: Option<&EditorAssetRef>,
    inherited_asset: Option<&EditorAssetRef>,
    clearable: bool,
    control_width: f32,
    candidates: &[AssetRefCandidate],
    drop_candidate: Option<&AssetRefCandidate>,
    response: &mut AssetSelectorResponse,
) {
    let display_asset = asset.or(inherited_asset);
    let inherited = asset.is_none() && inherited_asset.is_some();
    let selector_width = control_width.min(ui.available_width()).max(1.0);
    exact_region(
        ui,
        egui::vec2(selector_width, ASSET_SELECTOR_HEIGHT),
        egui::Layout::top_down(egui::Align::Min),
        |ui| {
            ui.set_clip_rect(ui.max_rect());
            let rect = ui.max_rect();
            let valid_drag = drop_candidate.is_some();
            let drop_hovered = valid_drag && ui.rect_contains_pointer(rect);
            let stroke = if drop_hovered {
                egui::Stroke::new(2.0, egui::Color32::from_rgb(125, 198, 255))
            } else if valid_drag {
                egui::Stroke::new(1.0, egui::Color32::from_rgb(58, 88, 122))
            } else {
                egui::Stroke::new(1.0, BORDER)
            };
            let fill = if drop_hovered {
                egui::Color32::from_rgb(29, 57, 86)
            } else {
                WIDGET_BG.linear_multiply(0.75)
            };
            let inner_width = (selector_width - 18.0).max(1.0);
            egui::Frame::new()
                .fill(fill)
                .stroke(stroke)
                .corner_radius(egui::CornerRadius::same(4))
                .inner_margin(egui::Margin::symmetric(8, 6))
                .show(ui, |ui| {
                    ui.set_width(inner_width);
                    ui.horizontal(|ui| {
                        ui.add_sized(
                            [18.0, 20.0],
                            egui::Label::new(phosphor_icon(icon, 14.0).color(TEXT_DIM)),
                        );
                        let action_width = if clearable { 78.0 } else { 52.0 };
                        let text_width = (ui.available_width() - action_width)
                            .max(1.0)
                            .min(ui.available_width().max(1.0));
                        ui.vertical(|ui| {
                            ui.set_max_width(text_width);
                            let name = display_asset
                                .map(|asset| asset.label.as_str())
                                .filter(|label| !label.trim().is_empty())
                                .unwrap_or("(none)");
                            ui.add(egui::Label::new(name).truncate());
                            let id = display_asset
                                .map(|asset| {
                                    if inherited {
                                        format!("Source default | {}", asset.sub_asset_id)
                                    } else {
                                        asset.sub_asset_id.clone()
                                    }
                                })
                                .filter(|id| !id.trim().is_empty())
                                .unwrap_or_else(|| "No imported asset selected".to_string());
                            ui.add(
                                egui::Label::new(egui::RichText::new(id).color(TEXT_DIM))
                                    .truncate(),
                            );
                        });
                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                            if clearable {
                                let clear = ui.add_enabled(
                                    asset.is_some(),
                                    egui::Button::new(phosphor_icon(icons::X, 16.0))
                                        .frame(false)
                                        .min_size(egui::vec2(22.0, 22.0)),
                                );
                                if clear.on_hover_text("Clear actor override").clicked() {
                                    response.clear = true;
                                }
                            }
                            let locate = ui.add_enabled(
                                display_asset.is_some_and(EditorAssetRef::is_resolved),
                                egui::Button::new(phosphor_icon(icons::CROSSHAIR, 16.0))
                                    .frame(false)
                                    .min_size(egui::vec2(22.0, 22.0)),
                            );
                            if locate.on_hover_text("Locate in content browser").clicked() {
                                response.locate = true;
                            }
                            if candidates.is_empty() {
                                ui.add_enabled(
                                    false,
                                    egui::Button::new(phosphor_icon(icons::FOLDER_OPEN, 16.0))
                                        .frame(false)
                                        .min_size(egui::vec2(22.0, 22.0)),
                                )
                                .on_hover_text("No imported assets available");
                            } else {
                                let menu =
                                    ui.menu_button(phosphor_icon(icons::FOLDER_OPEN, 16.0), |ui| {
                                        ui.set_min_width(220.0);
                                        for candidate in candidates {
                                            let selected = display_asset
                                                .is_some_and(|asset| asset == &candidate.reference);
                                            let clicked = ui
                                                .selectable_label(
                                                    selected,
                                                    candidate.label.as_str(),
                                                )
                                                .on_hover_text(candidate.detail.as_str())
                                                .clicked();
                                            if clicked {
                                                response.selected =
                                                    Some(candidate.reference.clone());
                                                ui.close();
                                            }
                                        }
                                    });
                                menu.response.on_hover_text("Browse assets");
                            }
                        });
                    });
                });
            if drop_hovered && ui.input(|input| input.pointer.any_released()) {
                if let Some(candidate) = drop_candidate {
                    response.selected = Some(candidate.reference.clone());
                    response.accepted_drop = true;
                }
            }
        },
    );
}

fn material_shader_ui(ui: &mut egui::Ui, material: &mut MaterialDesc) -> bool {
    let mut changed = false;
    property_row(ui, "Shader", |ui| {
        egui::ComboBox::from_id_salt("material_shader_kind")
            .selected_text(match material.shader.kind {
                MaterialShaderKind::StandardLit => "Standard Lit",
                MaterialShaderKind::Unlit => "Unlit",
                MaterialShaderKind::Custom => "Custom",
            })
            .show_ui(ui, |ui| {
                changed |= ui
                    .selectable_value(
                        &mut material.shader.kind,
                        MaterialShaderKind::StandardLit,
                        "Standard Lit",
                    )
                    .changed();
                changed |= ui
                    .selectable_value(
                        &mut material.shader.kind,
                        MaterialShaderKind::Unlit,
                        "Unlit",
                    )
                    .changed();
                changed |= ui
                    .selectable_value(
                        &mut material.shader.kind,
                        MaterialShaderKind::Custom,
                        "Custom",
                    )
                    .changed();
            });
    });
    if matches!(material.shader.kind, MaterialShaderKind::Custom) {
        changed |= option_string_ui(ui, "Shader schema", &mut material.shader.schema_path);
        changed |= option_string_ui(ui, "WGSL shader", &mut material.shader.shader_path);
    }
    if !material.parameters.is_empty() {
        ui.label(panel_heading("Shader Parameters"));
        for parameter in &mut material.parameters {
            changed |= material_parameter_ui(ui, parameter);
        }
    }
    changed
}

fn material_parameter_ui(ui: &mut egui::Ui, parameter: &mut MaterialParameter) -> bool {
    ui.horizontal_wrapped(|ui| {
        ui.label(&parameter.name);
        match &mut parameter.value {
            MaterialParameterValue::Bool(value) => ui.checkbox(value, "").changed(),
            MaterialParameterValue::Float(value) => ui
                .add_sized(
                    [fit_width(ui, 64.0, 120.0), 20.0],
                    egui::DragValue::new(value)
                        .speed(0.01)
                        .min_decimals(2)
                        .max_decimals(4),
                )
                .changed(),
            MaterialParameterValue::Vec2(value) => {
                let mut changed = false;
                changed |= ui
                    .add_sized(
                        [fit_width(ui, 52.0, 88.0), 20.0],
                        egui::DragValue::new(&mut value.x).speed(0.01),
                    )
                    .changed();
                changed |= ui
                    .add_sized(
                        [fit_width(ui, 52.0, 88.0), 20.0],
                        egui::DragValue::new(&mut value.y).speed(0.01),
                    )
                    .changed();
                changed
            }
            MaterialParameterValue::Vec3(value) => {
                let mut changed = false;
                changed |= ui
                    .add_sized(
                        [fit_width(ui, 52.0, 88.0), 20.0],
                        egui::DragValue::new(&mut value.x).speed(0.01),
                    )
                    .changed();
                changed |= ui
                    .add_sized(
                        [fit_width(ui, 52.0, 88.0), 20.0],
                        egui::DragValue::new(&mut value.y).speed(0.01),
                    )
                    .changed();
                changed |= ui
                    .add_sized(
                        [fit_width(ui, 52.0, 88.0), 20.0],
                        egui::DragValue::new(&mut value.z).speed(0.01),
                    )
                    .changed();
                changed
            }
            MaterialParameterValue::Color(value) => {
                let mut rgba = [value.r, value.g, value.b, value.a];
                let changed = ui.color_edit_button_rgba_unmultiplied(&mut rgba).changed();
                if changed {
                    *value = ColorDesc {
                        r: rgba[0],
                        g: rgba[1],
                        b: rgba[2],
                        a: rgba[3],
                    };
                }
                changed
            }
            MaterialParameterValue::Enum(value) => ui
                .add_sized(
                    [
                        fit_width(ui, MIN_INLINE_CONTROL_WIDTH, TEXT_FIELD_MAX_WIDTH),
                        20.0,
                    ],
                    egui::TextEdit::singleline(value),
                )
                .changed(),
        }
    })
    .inner
}

pub(crate) fn component_card(
    ui: &mut egui::Ui,
    context: &ComponentCardContext,
    add_contents: impl FnOnce(&mut egui::Ui),
) -> ComponentCardResponse {
    let mut response = ComponentCardResponse {
        type_name: context.options.type_name,
        ..Default::default()
    };
    egui::Frame::new()
        .fill(WIDGET_BG)
        .stroke(egui::Stroke::new(1.0, BORDER))
        .corner_radius(egui::CornerRadius::same(4))
        .inner_margin(egui::Margin::symmetric(8, 8))
        .show(ui, |ui| {
            ui.horizontal(|ui| {
                let caret = if context.collapsed {
                    icons::CARET_RIGHT
                } else {
                    icons::CARET_DOWN
                };
                if ui
                    .add(
                        egui::Button::new(phosphor_icon(caret, 13.0).color(TEXT_DIM))
                            .frame(false)
                            .min_size(egui::vec2(20.0, 20.0)),
                    )
                    .on_hover_text(if context.collapsed {
                        "Expand component"
                    } else {
                        "Collapse component"
                    })
                    .clicked()
                {
                    response.collapsed = Some(!context.collapsed);
                }
                ui.label(phosphor_icon(context.options.icon, 16.0).color(TEXT_DIM));
                ui.label(panel_heading(context.options.title));
                ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
                    let menu = ui.menu_button(
                        phosphor_icon(icons::DOTS_THREE_VERTICAL, 16.0).color(TEXT_DIM),
                        |ui| {
                            ui.set_min_width(160.0);
                            if ui
                                .add_enabled(context.options.resettable, egui::Button::new("Reset"))
                                .clicked()
                            {
                                response.reset = true;
                                ui.close();
                            }
                            if ui
                                .add_enabled(
                                    context.options.copyable,
                                    egui::Button::new("Copy Values"),
                                )
                                .clicked()
                            {
                                response.copy = true;
                                ui.close();
                            }
                            if ui
                                .add_enabled(context.pasteable, egui::Button::new("Paste Values"))
                                .clicked()
                            {
                                response.paste = true;
                                ui.close();
                            }
                            ui.separator();
                            if ui
                                .add_enabled(context.can_move_up, egui::Button::new("Move Up"))
                                .clicked()
                            {
                                response.move_up = true;
                                ui.close();
                            }
                            if ui
                                .add_enabled(context.can_move_down, egui::Button::new("Move Down"))
                                .clicked()
                            {
                                response.move_down = true;
                                ui.close();
                            }
                            ui.separator();
                            ui.add_enabled(false, egui::Button::new("Open Documentation"));
                            if ui
                                .add_enabled(
                                    context.options.removable,
                                    egui::Button::new(
                                        egui::RichText::new("Remove")
                                            .color(egui::Color32::from_rgb(255, 137, 129)),
                                    ),
                                )
                                .clicked()
                            {
                                response.remove = true;
                                ui.close();
                            }
                        },
                    );
                    menu.response.on_hover_text("Component actions");
                    if context.options.active_toggle {
                        if status_dot_button(
                            ui,
                            if context.active {
                                egui::Color32::from_rgb(74, 181, 104)
                            } else {
                                TEXT_MUTED
                            },
                            if context.active {
                                "Component active. Click to disable."
                            } else {
                                "Component disabled. Click to enable."
                            },
                        )
                        .clicked()
                        {
                            response.active = Some(!context.active);
                        }
                    } else {
                        status_dot(
                            ui,
                            egui::Color32::from_rgb(74, 181, 104),
                            "Component active",
                        );
                    }
                });
            });
            if !context.collapsed {
                ui.add_space(4.0);
                egui::Frame::new()
                    .fill(ELEVATED_BG.linear_multiply(0.35))
                    .corner_radius(egui::CornerRadius::same(4))
                    .inner_margin(egui::Margin::symmetric(8, 8))
                    .show(ui, |ui| {
                        ui.set_clip_rect(ui.max_rect());
                        ui.set_max_width(ui.available_width());
                        add_contents(ui);
                    });
            }
        });
    ui.add_space(6.0);
    response
}

pub(crate) fn property_row<R>(
    ui: &mut egui::Ui,
    label: &str,
    add_contents: impl FnOnce(&mut egui::Ui) -> R,
) -> R {
    if ui.available_width() < COMPACT_INSPECTOR_WIDTH {
        ui.vertical(|ui| {
            ui.label(label);
            add_contents(ui)
        })
        .inner
    } else {
        ui.horizontal(|ui| {
            let label_width = PROPERTY_LABEL_WIDTH.min(ui.available_width().max(1.0));
            ui.add_sized([label_width, 20.0], egui::Label::new(label));
            add_contents(ui)
        })
        .inner
    }
}

pub(crate) fn text_field_width(ui: &egui::Ui) -> f32 {
    fit_width(ui, MIN_INLINE_CONTROL_WIDTH, TEXT_FIELD_MAX_WIDTH).min(ui.available_width().max(1.0))
}

pub fn material_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let mut material = world
        .get::<MaterialDesc>(entity)
        .cloned()
        .unwrap_or_default();
    request_texture_asset_thumbnails(world);
    let texture_candidates = texture_asset_candidates(world);
    let original = material.clone();
    let mut changed = false;
    let mut options = ComponentCardOptions::removable(
        COMPONENT_MATERIAL_DESC,
        "Authoring Material",
        icons::PALETTE,
    );
    options.removable = world.get::<MaterialDesc>(entity).is_some();
    options.copyable = options.removable;
    let card = component_card_context(world, entity, options);
    let card_response = component_card(ui, &card, |ui| {
        changed |= material_shader_ui(ui, &mut material);
        let mut color = [
            material.base_color.r,
            material.base_color.g,
            material.base_color.b,
            material.base_color.a,
        ];
        property_row(ui, "Base color", |ui| {
            if ui.color_edit_button_rgba_unmultiplied(&mut color).changed() {
                material.base_color = ColorDesc {
                    r: color[0],
                    g: color[1],
                    b: color[2],
                    a: color[3],
                };
                changed = true;
            }
        });
        property_row(ui, "Metallic", |ui| {
            changed |= ui
                .add(egui::Slider::new(&mut material.metallic, 0.0..=1.0))
                .changed();
        });
        property_row(ui, "Roughness", |ui| {
            changed |= ui
                .add(egui::Slider::new(&mut material.roughness, 0.0..=1.0))
                .changed();
        });

        let mut emissive_color = [
            material.emissive_color.r,
            material.emissive_color.g,
            material.emissive_color.b,
            material.emissive_color.a,
        ];
        property_row(ui, "Emissive", |ui| {
            if ui
                .color_edit_button_rgba_unmultiplied(&mut emissive_color)
                .changed()
            {
                material.emissive_color = ColorDesc {
                    r: emissive_color[0],
                    g: emissive_color[1],
                    b: emissive_color[2],
                    a: emissive_color[3],
                };
                changed = true;
            }
        });
        property_row(ui, "Emissive nits", |ui| {
            changed |= ui
                .add(egui::Slider::new(
                    &mut material.emissive_intensity,
                    0.0..=20_000.0,
                ))
                .changed();
        });

        changed |= texture_asset_picker_ui(
            world,
            ui,
            "Base color texture",
            &mut material.base_color_texture,
            &texture_candidates,
        );
        changed |= texture_asset_picker_ui(
            world,
            ui,
            "Emissive texture",
            &mut material.emissive_texture,
            &texture_candidates,
        );
        changed |= texture_asset_picker_ui(
            world,
            ui,
            "Normal map",
            &mut material.normal_map_texture,
            &texture_candidates,
        );
        changed |= texture_asset_picker_ui(
            world,
            ui,
            "Metallic/roughness texture",
            &mut material.metallic_roughness_texture,
            &texture_candidates,
        );

        if crate::assets::materials::material_asset_picker_ui(
            world,
            ui,
            entity,
            &mut material,
            &original,
        ) {
            changed = true;
        }
    });
    apply_component_card_response(world, entity, card_response);

    if changed && !material_eq(&original, &material) {
        set_material_with_history(world, entity, material);
    }
}

fn brush_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let material_candidates = brush_face_material_ref_candidates(world);
    request_texture_asset_thumbnails(world);
    let texture_candidates = texture_asset_ref_candidates(world);
    let Some(mut brush) = world.get::<BrushDesc>(entity).cloned() else {
        return;
    };
    let original = brush.clone();
    let mut changed = false;
    let mut reset_cube = false;
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_BRUSH_DESC, "Brush", icons::CUBE),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Kind", |ui| {
            egui::ComboBox::from_id_salt("brush_kind")
                .selected_text(match brush.kind {
                    BrushKind::Additive => "Additive",
                    BrushKind::SubtractiveMarker => "Subtractive Marker",
                })
                .show_ui(ui, |ui| {
                    changed |= ui
                        .selectable_value(&mut brush.kind, BrushKind::Additive, "Additive")
                        .changed();
                    changed |= ui
                        .selectable_value(
                            &mut brush.kind,
                            BrushKind::SubtractiveMarker,
                            "Subtractive Marker",
                        )
                        .changed();
                });
        });
        property_row(ui, "Faces", |ui| {
            ui.label(format!("{}", brush.faces.len()));
        });
        property_row(ui, "Shadows", |ui| {
            ui.horizontal_wrapped(|ui| {
                changed |= ui.checkbox(&mut brush.cast_shadows, "Cast").changed();
                changed |= ui.checkbox(&mut brush.receive_shadows, "Receive").changed();
            });
        });
        brush_validation_ui(ui, &brush);
        changed |= selected_brush_face_controls(
            world,
            ui,
            entity,
            &mut brush,
            &material_candidates,
            &texture_candidates,
        );
        if ui.button("Reset Cube Brush").clicked() {
            reset_cube = true;
        }
    });
    apply_component_card_response(world, entity, card_response);

    if reset_cube {
        brush = BrushDesc::default();
        changed = true;
    }
    if changed && brush != original {
        set_brush_with_history(world, entity, brush);
    }
}

fn brush_validation_ui(ui: &mut egui::Ui, brush: &BrushDesc) {
    let report = validate_brush(brush);
    if report.diagnostics.is_empty() {
        return;
    }

    ui.add_space(6.0);
    egui::Frame::new()
        .fill(egui::Color32::from_rgb(31, 32, 36))
        .stroke(egui::Stroke::new(1.0, BORDER))
        .inner_margin(egui::Margin::symmetric(8, 6))
        .show(ui, |ui| {
            let has_errors = !report.is_valid();
            let title_color = if has_errors {
                egui::Color32::from_rgb(255, 137, 129)
            } else {
                egui::Color32::from_rgb(255, 190, 110)
            };
            ui.horizontal(|ui| {
                let icon = if has_errors {
                    icons::WARNING
                } else {
                    icons::INFO
                };
                ui.label(phosphor_icon(icon, 14.0).color(title_color));
                ui.label(
                    egui::RichText::new(if has_errors {
                        "Brush geometry needs repair"
                    } else {
                        "Brush diagnostics"
                    })
                    .color(title_color),
                );
            });
            for diagnostic in report.diagnostics.iter().take(4) {
                let color = match diagnostic.severity {
                    BrushDiagnosticSeverity::Error => egui::Color32::from_rgb(255, 137, 129),
                    BrushDiagnosticSeverity::Warning => egui::Color32::from_rgb(255, 190, 110),
                };
                let face = diagnostic
                    .face
                    .as_ref()
                    .filter(|face| !face.is_empty())
                    .map(|face| format!("{}: ", face.0))
                    .unwrap_or_default();
                ui.label(egui::RichText::new(format!("{face}{}", diagnostic.message)).color(color));
            }
            let hidden_count = report.diagnostics.len().saturating_sub(4);
            if hidden_count > 0 {
                ui.label(egui::RichText::new(format!("+{hidden_count} more")).color(TEXT_MUTED));
            }
        });
}

fn selected_brush_face_controls(
    world: &mut World,
    ui: &mut egui::Ui,
    entity: Entity,
    brush: &mut BrushDesc,
    material_candidates: &[AssetRefCandidate],
    texture_candidates: &[AssetRefCandidate],
) -> bool {
    let Some(selection) = world.get_resource::<BrushElementSelection>() else {
        return false;
    };
    if selection.brush != Some(entity) {
        return false;
    }
    let selected_faces: Vec<_> = selection
        .elements
        .iter()
        .filter_map(|element| match element {
            BrushElementKey::Face { face } => Some(face.clone()),
            _ => None,
        })
        .collect();
    if selected_faces.is_empty() {
        return false;
    }

    let dragging_selection = world
        .get_resource::<EditorAssets>()
        .and_then(|assets| assets.dragging_selection().cloned());
    let material_drop_candidate = dragging_selection.as_ref().and_then(|selection| {
        asset_ref_candidate_from_selection(world, selection, AssetRefCandidateKind::Material)
    });
    let texture_drop_candidate = dragging_selection.as_ref().and_then(|selection| {
        asset_ref_candidate_from_selection(world, selection, AssetRefCandidateKind::Texture)
    });
    let mut changed = false;
    ui.add_space(6.0);
    ui.separator();
    ui.label(
        egui::RichText::new(format!("{} selected face(s)", selected_faces.len())).color(TEXT_DIM),
    );

    for face_id in selected_faces {
        let Some(face) = brush.faces.iter_mut().find(|face| face.id == face_id) else {
            continue;
        };
        let label = if face.id.0.trim().is_empty() {
            "Face".to_string()
        } else {
            face.id.0.clone()
        };
        egui::CollapsingHeader::new(label)
            .default_open(true)
            .show(ui, |ui| {
                property_row(ui, "UV offset", |ui| {
                    changed |= ui
                        .add(egui::DragValue::new(&mut face.uv_offset.x).speed(0.05))
                        .changed();
                    changed |= ui
                        .add(egui::DragValue::new(&mut face.uv_offset.y).speed(0.05))
                        .changed();
                });
                property_row(ui, "UV scale", |ui| {
                    changed |= ui
                        .add(egui::DragValue::new(&mut face.uv_scale.x).speed(0.05))
                        .changed();
                    changed |= ui
                        .add(egui::DragValue::new(&mut face.uv_scale.y).speed(0.05))
                        .changed();
                });
                property_row(ui, "UV rotation", |ui| {
                    changed |= ui
                        .add(egui::DragValue::new(&mut face.uv_rotation).speed(1.0))
                        .changed();
                });
                let material_response = asset_selector_row(
                    ui,
                    "Material",
                    icons::PALETTE,
                    face.material.as_ref(),
                    None,
                    true,
                    material_candidates,
                    material_drop_candidate.as_ref(),
                );
                if let Some(selected) = material_response.selected {
                    face.material = Some(selected);
                    changed = true;
                }
                if material_response.clear {
                    face.material = None;
                    changed = true;
                }
                if material_response.locate {
                    locate_asset_ref(world, face.material.as_ref(), material_candidates);
                }
                if material_response.accepted_drop {
                    clear_asset_drag(world);
                }

                let texture_response = asset_selector_row(
                    ui,
                    "Texture",
                    icons::IMAGE,
                    face.texture.as_ref(),
                    None,
                    true,
                    texture_candidates,
                    texture_drop_candidate.as_ref(),
                );
                if let Some(selected) = texture_response.selected {
                    face.texture = Some(selected);
                    changed = true;
                }
                if texture_response.clear {
                    face.texture = None;
                    changed = true;
                }
                if texture_response.locate {
                    locate_asset_ref(world, face.texture.as_ref(), texture_candidates);
                }
                if texture_response.accepted_drop {
                    clear_asset_drag(world);
                }
            });
    }

    changed
}

fn clear_asset_drag(world: &mut World) {
    if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
        assets.clear_drag();
    }
}

fn primitive_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut primitive) = world.get::<Primitive>(entity).cloned() else {
        return;
    };
    let _original = primitive.clone();
    let mut changed = false;
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_PRIMITIVE, "Primitive", icons::CUBE),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Shape", |ui| {
            ui.horizontal_wrapped(|ui| {
                for (label, shape) in [
                    ("Box", PrimitiveShape::Box),
                    ("Sphere", PrimitiveShape::Sphere),
                    ("Ramp", PrimitiveShape::Ramp),
                ] {
                    changed |= ui
                        .selectable_value(&mut primitive.shape, shape, label)
                        .changed();
                }
            });
        });
        property_row(ui, "Size X", |ui| {
            changed |= ui
                .add(egui::Slider::new(&mut primitive.size.x, 0.1..=20.0))
                .changed();
        });
        property_row(ui, "Size Y", |ui| {
            changed |= ui
                .add(egui::Slider::new(&mut primitive.size.y, 0.1..=20.0))
                .changed();
        });
        property_row(ui, "Size Z", |ui| {
            changed |= ui
                .add(egui::Slider::new(&mut primitive.size.z, 0.1..=20.0))
                .changed();
        });
    });
    apply_component_card_response(world, entity, card_response);
    if changed {
        set_primitive_with_history(world, entity, primitive);
    }
}

fn light_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut light) = world.get::<LightDesc>(entity).cloned() else {
        return;
    };
    let _original = light.clone();
    let mut changed = false;
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_LIGHT_DESC, "Light", icons::LIGHTBULB),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Kind", |ui| {
            ui.horizontal_wrapped(|ui| {
                for (label, kind) in [
                    ("Point", AuthoringLightKind::Point),
                    ("Spot", AuthoringLightKind::Spot),
                    ("Directional", AuthoringLightKind::Directional),
                ] {
                    if ui.selectable_label(light.kind == kind, label).clicked() {
                        let color = light.color;
                        light = LightDesc {
                            color,
                            ..LightDesc::for_kind(kind)
                        };
                        changed = true;
                    }
                }
            });
        });
        let solari_disabled = solari_disables_local_light(world, &light);
        if solari_disabled {
            ui.colored_label(
                egui::Color32::from_rgb(255, 180, 100),
                "Point and spot lights are disabled while Solari is active. Use a directional light or emissive material, or switch GI to Forward.",
            );
        }
        ui.add_enabled_ui(!solari_disabled, |ui| {
            let mut color = [light.color.r, light.color.g, light.color.b, light.color.a];
            property_row(ui, "Color", |ui| {
                if ui.color_edit_button_rgba_unmultiplied(&mut color).changed() {
                    light.color = ColorDesc {
                        r: color[0],
                        g: color[1],
                        b: color[2],
                        a: color[3],
                    };
                    changed = true;
                }
            });
            let intensity_label = match light.kind {
                AuthoringLightKind::Directional => "Intensity (lux)",
                _ => "Intensity (lumens)",
            };
            let intensity_max = match light.kind {
                AuthoringLightKind::Directional => AUTHORING_DIRECTIONAL_LUX_MAX,
                _ => AUTHORING_POINT_SPOT_LUMENS_MAX,
            };
            property_row(ui, intensity_label, |ui| {
                ui.horizontal_wrapped(|ui| {
                    if ui
                        .add(
                            egui::Slider::new(&mut light.intensity, 0.0..=intensity_max)
                                .show_value(false),
                        )
                        .changed()
                    {
                        changed = true;
                    }
                    if ui
                        .add_sized(
                            [fit_width(ui, 72.0, 120.0), 20.0],
                            egui::DragValue::new(&mut light.intensity)
                                .range(0.0..=intensity_max)
                                .speed(intensity_max * 0.001),
                        )
                        .changed()
                    {
                        changed = true;
                    }
                });
            });
            if matches!(
                light.kind,
                AuthoringLightKind::Point | AuthoringLightKind::Spot
            ) {
                property_row(ui, "Range (m)", |ui| {
                    changed |= ui
                        .add(egui::Slider::new(&mut light.range, 0.0..=100.0))
                        .changed();
                });
            }
            if matches!(light.kind, AuthoringLightKind::Spot) {
                property_row(ui, "Inner angle", |ui| {
                    changed |= ui
                        .add(egui::Slider::new(&mut light.inner_angle_deg, 1.0..=80.0))
                        .changed();
                });
                property_row(ui, "Outer angle", |ui| {
                    changed |= ui
                        .add(egui::Slider::new(&mut light.outer_angle_deg, 1.0..=90.0))
                        .changed();
                });
            }
            property_row(ui, "Shadows", |ui| {
                changed |= ui.checkbox(&mut light.shadows, "Cast shadows").changed();
            });
            if matches!(light.kind, AuthoringLightKind::Directional) {
                ui.small("Controls project sun while this directional exists.");
            }
        });
    });
    apply_component_card_response(world, entity, card_response);
    if changed {
        set_light_with_history(world, entity, light);
    }
}

fn solari_disables_local_light(world: &World, light: &LightDesc) -> bool {
    matches!(
        light.kind,
        AuthoringLightKind::Point | AuthoringLightKind::Spot
    ) && world
        .get_resource::<settings::ActiveCameraRenderProfile>()
        .is_some_and(|profile| profile.gi_path == settings::GiPath::SolariDeferred)
        && world
            .get_resource::<settings::RenderingCapabilities>()
            .is_some_and(|caps| caps.rt_supported)
}

fn rigid_body_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut body) = world.get::<RigidBodyDesc>(entity).copied() else {
        return;
    };
    let original = body;
    let mut changed = false;
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_RIGID_BODY_DESC, "Rigid Body", icons::SPHERE),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Body", |ui| {
            ui.horizontal_wrapped(|ui| {
                for (label, kind) in [
                    ("Static", AuthoringRigidBody::Static),
                    ("Kinematic", AuthoringRigidBody::Kinematic),
                    ("Dynamic", AuthoringRigidBody::Dynamic),
                ] {
                    changed |= ui.selectable_value(&mut body.body, kind, label).changed();
                }
            });
        });
    });
    apply_component_card_response(world, entity, card_response);
    if changed && body != original {
        set_rigid_body_with_history(world, entity, body);
    }
}

fn collider_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let mesh_candidates = static_mesh_asset_ref_candidates(world, AssetRefCandidateKind::Mesh);
    let Some(mut collider) = world.get::<ColliderDesc>(entity).cloned() else {
        return;
    };
    let original = collider.clone();
    let mut changed = false;
    let renderer_meshes: Vec<EditorAssetRef> = world
        .get::<StaticMeshRenderer>(entity)
        .map(|renderer| {
            renderer
                .slots
                .iter()
                .map(|slot| slot.mesh.clone())
                .collect()
        })
        .unwrap_or_default();

    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_COLLIDER_DESC, "Collider", icons::SELECTION),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Mode", |ui| {
            changed |= ui.checkbox(&mut collider.enabled, "Enabled").changed();
            changed |= ui.checkbox(&mut collider.is_trigger, "Trigger").changed();
        });
        property_row(ui, "Shape", |ui| {
            let mut shape_kind = collider_shape_kind(&collider.shape);
            egui::ComboBox::from_id_salt("collider_shape_kind")
                .selected_text(shape_kind)
                .show_ui(ui, |ui| {
                    for label in ["Box", "Sphere", "Capsule", "Static Mesh"] {
                        if ui.selectable_label(shape_kind == label, label).clicked() {
                            shape_kind = label;
                        }
                    }
                });
            if shape_kind != collider_shape_kind(&collider.shape) {
                collider.shape = match shape_kind {
                    "Sphere" => ColliderShapeDesc::Sphere { radius: 0.5 },
                    "Capsule" => ColliderShapeDesc::Capsule {
                        radius: 0.5,
                        height: 1.0,
                    },
                    "Static Mesh" => ColliderShapeDesc::static_mesh(renderer_meshes.clone()),
                    _ => ColliderShapeDesc::default(),
                };
                changed = true;
            }
        });

        match &mut collider.shape {
            ColliderShapeDesc::Cuboid {
                x_length,
                y_length,
                z_length,
            } => {
                changed |= dimension_drag(ui, "X", x_length);
                changed |= dimension_drag(ui, "Y", y_length);
                changed |= dimension_drag(ui, "Z", z_length);
            }
            ColliderShapeDesc::Sphere { radius } => {
                changed |= dimension_drag(ui, "Radius", radius);
            }
            ColliderShapeDesc::Capsule { radius, height } => {
                changed |= dimension_drag(ui, "Radius", radius);
                changed |= dimension_drag(ui, "Height", height);
            }
            ColliderShapeDesc::StaticMesh { meshes, .. } => {
                if meshes.is_empty() {
                    ui.label(egui::RichText::new("No mesh collider sources").color(TEXT_DIM));
                }
                for mesh in meshes.iter_mut() {
                    let mesh_response = asset_selector_row(
                        ui,
                        "Mesh",
                        icons::CUBE,
                        Some(mesh),
                        None,
                        false,
                        &mesh_candidates,
                        None,
                    );
                    if let Some(selected) = mesh_response.selected {
                        *mesh = selected;
                        changed = true;
                    }
                    if mesh_response.locate {
                        locate_asset_ref(world, Some(mesh), &mesh_candidates);
                    }
                }
                if !renderer_meshes.is_empty() && ui.button("Use renderer meshes").clicked() {
                    *meshes = renderer_meshes.clone();
                    changed = true;
                }
            }
        }
    });
    apply_component_card_response(world, entity, card_response);

    if changed && collider != original {
        set_collider_with_history(world, entity, collider);
    }
}

fn collider_shape_kind(shape: &ColliderShapeDesc) -> &'static str {
    match shape {
        ColliderShapeDesc::Cuboid { .. } => "Box",
        ColliderShapeDesc::Sphere { .. } => "Sphere",
        ColliderShapeDesc::Capsule { .. } => "Capsule",
        ColliderShapeDesc::StaticMesh { .. } => "Static Mesh",
    }
}

fn dimension_drag(ui: &mut egui::Ui, label: &str, value: &mut f32) -> bool {
    property_row(ui, label, |ui| {
        ui.add_sized(
            [fit_width(ui, 72.0, 120.0), 20.0],
            egui::DragValue::new(value)
                .range(0.0..=10_000.0)
                .speed(0.05)
                .min_decimals(2)
                .max_decimals(3),
        )
        .changed()
    })
}

fn physics_editor_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut body) = world.get::<PhysicsBody>(entity).cloned() else {
        return;
    };
    let _original = body.clone();
    let mut changed = false;
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_PHYSICS_BODY, "Physics", icons::SPHERE),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Body", |ui| {
            ui.horizontal_wrapped(|ui| {
                for (label, kind) in [
                    ("Static", AuthoringRigidBody::Static),
                    ("Kinematic", AuthoringRigidBody::Kinematic),
                    ("Dynamic", AuthoringRigidBody::Dynamic),
                ] {
                    if ui.selectable_label(body.body == kind, label).clicked() {
                        body.body = kind;
                        changed = true;
                    }
                }
            });
        });
    });
    apply_component_card_response(world, entity, card_response);
    if changed {
        set_physics_with_history(world, entity, body);
    }
}

fn player_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    if world.get::<PlayerSpawn>(entity).is_none() {
        return;
    }
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_PLAYER_SPAWN,
            "Player Spawn",
            icons::PERSON_SIMPLE_RUN,
        ),
    );
    let card_response = component_card(ui, &card, |ui| {
        ui.label(
            egui::RichText::new("Uses this actor transform as a player start.").color(TEXT_DIM),
        );
    });
    apply_component_card_response(world, entity, card_response);
}

fn weapon_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut spawn) = world.get::<WeaponSpawn>(entity).cloned() else {
        return;
    };
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_WEAPON_SPAWN, "Weapon Spawn", icons::CROSSHAIR),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Weapon ID", |ui| {
            ui.add_sized(
                [text_field_width(ui), 20.0],
                egui::TextEdit::singleline(&mut spawn.weapon_id),
            );
        });
    });
    apply_component_card_response(world, entity, card_response);
    if let Ok(mut e) = world.get_entity_mut(entity) {
        e.insert(spawn);
    }
}

fn trigger_volume_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut trigger) = world.get::<TriggerVolume>(entity).cloned() else {
        return;
    };
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(
            COMPONENT_TRIGGER_VOLUME,
            "Trigger Volume",
            icons::SELECTION,
        ),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Event", |ui| {
            ui.add_sized(
                [text_field_width(ui), 20.0],
                egui::TextEdit::singleline(&mut trigger.event_name),
            );
        });
        property_row(ui, "Half X", |ui| {
            ui.add(egui::Slider::new(&mut trigger.half_extents.x, 0.1..=50.0));
        });
        property_row(ui, "Half Y", |ui| {
            ui.add(egui::Slider::new(&mut trigger.half_extents.y, 0.1..=50.0));
        });
        property_row(ui, "Half Z", |ui| {
            ui.add(egui::Slider::new(&mut trigger.half_extents.z, 0.1..=50.0));
        });
    });
    apply_component_card_response(world, entity, card_response);
    if let Ok(mut e) = world.get_entity_mut(entity) {
        e.insert(trigger);
    }
}

fn team_spawn_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut spawn) = world.get::<TeamSpawn>(entity).cloned() else {
        return;
    };
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_TEAM_SPAWN, "Team Spawn", icons::FLAG),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Team", |ui| {
            ui.add(egui::Slider::new(&mut spawn.team_id, 0..=8));
        });
    });
    apply_component_card_response(world, entity, card_response);
    if let Ok(mut e) = world.get_entity_mut(entity) {
        e.insert(spawn);
    }
}

fn objective_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    let Some(mut marker) = world.get::<ObjectiveMarker>(entity).cloned() else {
        return;
    };
    let card = component_card_context(
        world,
        entity,
        ComponentCardOptions::removable(COMPONENT_OBJECTIVE_MARKER, "Objective", icons::TARGET),
    );
    let card_response = component_card(ui, &card, |ui| {
        property_row(ui, "Objective ID", |ui| {
            ui.add_sized(
                [text_field_width(ui), 20.0],
                egui::TextEdit::singleline(&mut marker.objective_id),
            );
        });
    });
    apply_component_card_response(world, entity, card_response);
    if let Ok(mut e) = world.get_entity_mut(entity) {
        e.insert(marker);
    }
}

fn prefab_instance_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    crate::assets::prefab_overrides::prefab_instance_inspector_ui(world, ui, entity);
}

pub fn project_sun_ui(world: &mut World, ui: &mut egui::Ui, entity: Entity) {
    if world.get::<ProjectSun>(entity).is_none() {
        return;
    }

    let rendering = world
        .resource::<settings::ProjectSettings>()
        .rendering
        .clone();
    let scene_override_active = has_scene_sun_override(world);
    let mut create_override = false;
    let mut options = ComponentCardOptions::fixed(COMPONENT_PROJECT_SUN, "Project Sun", icons::SUN);
    options.resettable = false;
    let card = component_card_context(world, entity, options);
    let card_response = component_card(ui, &card, |ui| {
        ui.label("Runtime default world lighting from assets/project.ron.");
        property_row(ui, "Illuminance", |ui| {
            ui.small(format!("{:.0} lux", rendering.sun_illuminance));
        });
        property_row(ui, "Ambient", |ui| {
            ui.small(format!(
                "{:.2}, {:.2}, {:.2} @ {:.1}",
                rendering.ambient_color[0],
                rendering.ambient_color[1],
                rendering.ambient_color[2],
                rendering.ambient_brightness
            ));
        });
        property_row(ui, "Shadows", |ui| {
            ui.small(format!(
                "{} cascades, max {:.0}m",
                rendering.shadow_cascades, rendering.shadow_max_distance
            ));
        });
        if scene_override_active {
            ui.label(egui::RichText::new("Scene sun override is active.").weak());
        } else if ui.button("Create Scene Sun Override").clicked() {
            create_override = true;
        }
    });
    apply_component_card_response(world, entity, card_response);

    if create_override {
        let sun = create_scene_sun_override_from_project_settings(world);
        world.resource_mut::<SelectedEntity>().0 = Some(sun);
        if let Some(mut ui_state) = world.get_resource_mut::<crate::ui::UiState>() {
            ui_state.selected_entities.select_replace(sun);
        }
    }
}

fn has_scene_sun_override(world: &mut World) -> bool {
    world
        .query_filtered::<(
            &shared::LightDesc,
            Option<&AuthoringComponentStates>,
            Option<&InspectorOrder>,
        ), With<LevelObject>>()
        .iter(world)
        .any(|(light, states, legacy_order)| {
            authoring_component_active(states, legacy_order, COMPONENT_LIGHT_DESC)
                && matches!(light.kind, shared::AuthoringLightKind::Directional)
        })
}

fn texture_asset_picker_ui(
    world: &mut World,
    ui: &mut egui::Ui,
    label: &str,
    value: &mut Option<String>,
    candidates: &[TextureAssetCandidate],
) -> bool {
    let current_path = value.clone();
    let dragging_selection = world
        .get_resource::<EditorAssets>()
        .and_then(|assets| assets.dragging_selection().cloned());
    let drop_candidate = dragging_selection
        .as_ref()
        .and_then(|selection| texture_path_from_selection(world, selection));
    let mut selected_path: Option<String> = None;
    let mut clear = false;
    let mut locate = false;
    let mut accepted_drop = false;
    let current_candidate = current_path.as_ref().and_then(|path| {
        candidates
            .iter()
            .find(|candidate| candidate.path == *path)
            .cloned()
    });

    property_row(ui, label, |ui| {
        let control_width = ui
            .available_width()
            .max(MIN_INLINE_CONTROL_WIDTH.min(ui.available_width().max(1.0)));
        let row_height = 30.0;
        let (rect, _response) =
            ui.allocate_exact_size(egui::vec2(control_width, row_height), egui::Sense::hover());
        let valid_drag = drop_candidate.is_some();
        let row_hovered = ui.rect_contains_pointer(rect);
        let drop_hovered = valid_drag && row_hovered;
        let stroke = if drop_hovered {
            egui::Stroke::new(2.0, egui::Color32::from_rgb(125, 198, 255))
        } else if valid_drag {
            egui::Stroke::new(1.0, egui::Color32::from_rgb(58, 88, 122))
        } else if row_hovered {
            egui::Stroke::new(1.0, egui::Color32::from_rgb(92, 102, 118))
        } else {
            egui::Stroke::new(1.0, BORDER)
        };
        let fill = if drop_hovered {
            egui::Color32::from_rgb(29, 57, 86)
        } else if valid_drag {
            WIDGET_BG.linear_multiply(0.88)
        } else if row_hovered {
            WIDGET_BG.linear_multiply(1.05)
        } else {
            WIDGET_BG.linear_multiply(0.75)
        };
        ui.painter()
            .rect(rect, 4.0, fill, stroke, egui::StrokeKind::Inside);
        if drop_hovered {
            let badge_rect = egui::Rect::from_min_size(
                rect.right_top() + egui::vec2(-82.0, 4.0),
                egui::vec2(74.0, 16.0),
            );
            ui.painter().rect(
                badge_rect,
                3.0,
                egui::Color32::from_rgb(35, 95, 155),
                egui::Stroke::NONE,
                egui::StrokeKind::Inside,
            );
            ui.painter().text(
                badge_rect.center(),
                egui::Align2::CENTER_CENTER,
                "Drop texture",
                egui::FontId::new(10.0, egui::FontFamily::Proportional),
                egui::Color32::from_rgb(225, 241, 255),
            );
        }

        let mut child = ui.new_child(
            egui::UiBuilder::new()
                .max_rect(rect.shrink2(egui::vec2(6.0, 4.0)))
                .layout(egui::Layout::left_to_right(egui::Align::Center)),
        );
        child.set_clip_rect(rect);
        let preview_size = 22.0;
        let (preview_rect, _preview_response) =
            child.allocate_exact_size(egui::vec2(preview_size, preview_size), egui::Sense::hover());
        child.painter().rect(
            preview_rect,
            3.0,
            PANEL_BG_DARK,
            egui::Stroke::new(1.0, BORDER),
            egui::StrokeKind::Inside,
        );
        if let Some(texture_id) = current_candidate
            .as_ref()
            .and_then(|candidate| candidate.texture_id)
        {
            let uv = egui::Rect::from_min_max(egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0));
            child.painter().image(
                texture_id,
                preview_rect.shrink(1.0),
                uv,
                egui::Color32::WHITE,
            );
        } else {
            child.painter().text(
                preview_rect.center(),
                egui::Align2::CENTER_CENTER,
                icons::IMAGE.as_str(),
                egui::FontId::new(12.0, egui::FontFamily::Name("phosphor-regular".into())),
                TEXT_DIM,
            );
        }

        let action_width = 92.0;
        let text_width = (child.available_width() - action_width).max(1.0);
        child.allocate_ui_with_layout(
            egui::vec2(text_width, 22.0),
            egui::Layout::left_to_right(egui::Align::Center),
            |ui| {
                let display_label = current_candidate
                    .as_ref()
                    .map(|candidate| candidate.label.as_str())
                    .or(current_path.as_deref())
                    .unwrap_or("(none)");
                ui.add_sized(
                    [text_width, 20.0],
                    egui::Label::new(egui::RichText::new(display_label).color(
                        if current_path.is_some() {
                            TEXT_DIM.linear_multiply(1.45)
                        } else {
                            TEXT_DIM
                        },
                    ))
                    .truncate(),
                );
            },
        );

        child.allocate_ui_with_layout(
            egui::vec2(action_width, 28.0),
            egui::Layout::right_to_left(egui::Align::Center),
            |ui| {
                let clear_response = ui.add_enabled(
                    current_path.is_some(),
                    egui::Button::new(phosphor_icon(icons::X, 16.0))
                        .frame(false)
                        .min_size(egui::vec2(22.0, 22.0)),
                );
                if clear_response.on_hover_text("Clear texture").clicked() {
                    clear = true;
                }
                let locate_response = ui.add_enabled(
                    current_path.is_some(),
                    egui::Button::new(phosphor_icon(icons::CROSSHAIR, 16.0))
                        .frame(false)
                        .min_size(egui::vec2(22.0, 22.0)),
                );
                if locate_response
                    .on_hover_text("Locate in content browser")
                    .clicked()
                {
                    locate = true;
                }
                if candidates.is_empty() {
                    ui.add_enabled(
                        false,
                        egui::Button::new(phosphor_icon(icons::FOLDER_OPEN, 16.0))
                            .frame(false)
                            .min_size(egui::vec2(22.0, 22.0)),
                    )
                    .on_hover_text("No texture assets available");
                } else {
                    let menu = ui.menu_button(phosphor_icon(icons::FOLDER_OPEN, 16.0), |ui| {
                        ui.set_min_width(240.0);
                        for candidate in candidates {
                            let selected = current_path.as_deref() == Some(candidate.path.as_str());
                            if ui
                                .selectable_label(selected, candidate.label.as_str())
                                .on_hover_text(candidate.path.as_str())
                                .clicked()
                            {
                                selected_path = Some(candidate.path.clone());
                                ui.close();
                            }
                        }
                    });
                    menu.response.on_hover_text("Browse textures");
                }
            },
        );

        if drop_hovered && ui.input(|input| input.pointer.any_released()) {
            if let Some(candidate) = drop_candidate.as_ref() {
                selected_path = Some(candidate.path.clone());
                accepted_drop = true;
            }
        }
    });

    if locate {
        locate_texture_asset(world, current_path.as_deref(), candidates);
    }
    if accepted_drop {
        if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
            assets.clear_drag();
        }
    }
    if clear {
        *value = None;
        return current_path.is_some();
    }
    if let Some(path) = selected_path {
        let changed = current_path.as_deref() != Some(path.as_str());
        *value = Some(path);
        return changed;
    }

    false
}

fn locate_texture_asset(
    world: &mut World,
    path: Option<&str>,
    candidates: &[TextureAssetCandidate],
) {
    let Some(path) = path else {
        return;
    };
    let Some(candidate) = candidates.iter().find(|candidate| candidate.path == path) else {
        if let Some(mut scene_io) = world.get_resource_mut::<crate::scene_io::SceneIo>() {
            scene_io.status = format!("Could not locate texture {path}");
        }
        return;
    };

    if let Some(mut assets) = world.get_resource_mut::<EditorAssets>() {
        assets.current_folder = candidate.folder_path.clone();
        assets.select(candidate.selection.clone());
    }
    if let Some(mut ui_state) = world.get_resource_mut::<UiState>() {
        let panel_nodes = ui_state.panel_nodes;
        open_and_focus_tab(
            &mut ui_state.dock_state,
            EditorTab::AssetBrowser,
            &panel_nodes,
        );
    }
}

fn option_string_ui(ui: &mut egui::Ui, label: &str, value: &mut Option<String>) -> bool {
    let mut text = value.clone().unwrap_or_default();
    let before = text.clone();
    property_row(ui, label, |ui| {
        ui.horizontal_wrapped(|ui| {
            let clear_width = 52.0;
            let field_width = (ui.available_width() - clear_width - ui.spacing().item_spacing.x)
                .max(MIN_INLINE_CONTROL_WIDTH.min(ui.available_width().max(1.0)))
                .min(TEXT_FIELD_MAX_WIDTH)
                .min(ui.available_width().max(1.0));
            ui.add_sized([field_width, 20.0], egui::TextEdit::singleline(&mut text));
            if ui.button("Clear").clicked() {
                text.clear();
            }
        });
    });
    let changed = before != text;
    *value = if text.trim().is_empty() {
        None
    } else {
        Some(text)
    };
    changed
}
