use crate::ecs::EditorWorld;
use crate::systems::retained_ui::{Action, UiHandles};
use nightshade::prelude::*;
use std::collections::HashMap;

#[derive(Default, Clone)]
pub struct InspectorHandles {
    pub panel: Entity,
    pub header_label: Entity,
    pub clear_button: Entity,
    pub body_root: Entity,
    pub body_entities: Vec<Entity>,
    pub bindings: HashMap<Entity, InspectorBinding>,
    pub editing_state: HashMap<Entity, bool>,
    pub pre_drag_transforms: HashMap<Entity, LocalTransform>,
    pub pre_drag_components:
        HashMap<(Entity, crate::undo::SnapshotKind), crate::undo::ComponentSnapshot>,
    pub pending_picker_edits: HashMap<Entity, PickerEditSession>,
    pub tag_remove_buttons: HashMap<Entity, String>,
    pub tag_input: Option<Entity>,
    pub dirty: bool,
}

#[derive(Clone)]
pub struct PickerEditSession {
    pub target: Entity,
    pub field: InspectorField,
    pub before: crate::undo::ComponentSnapshot,
}

#[derive(Clone, Copy)]
pub struct InspectorBinding {
    pub entity: Entity,
    pub field: InspectorField,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum InspectorField {
    Name,
    TranslationX,
    TranslationY,
    TranslationZ,
    EulerX,
    EulerY,
    EulerZ,
    ScaleX,
    ScaleY,
    ScaleZ,
    Visibility,
    CastsShadow,
    LightIntensity,
    LightCastsShadow,
    LightColor,
    LightRange,
    LightType,
    LightInnerCone,
    LightOuterCone,
    LightShadowBias,
    LightShadowResolution,
    LightShadowDistance,
    LightCookieTexture,
    LightAreaShape,
    LightAreaWidth,
    LightAreaHeight,
    LightAreaRadius,
    LightAreaTwoSided,
    LightAreaEmissiveTexture,
    AmbientLight,
    BloomEnabled,
    BloomIntensity,
    BloomThreshold,
    BloomKnee,
    BloomFilterRadius,
    SsaoEnabled,
    SsaoRadius,
    SsaoIntensity,
    SsaoBias,
    SsaoSampleCount,
    SsaoVisualization,
    SsgiEnabled,
    SsgiRadius,
    SsgiIntensity,
    SsgiMaxSteps,
    SsrEnabled,
    SsrMaxSteps,
    SsrThickness,
    SsrMaxDistance,
    SsrStride,
    SsrFadeStart,
    SsrFadeEnd,
    SsrIntensity,
    FogEnabled,
    FogColor,
    FogStart,
    FogEnd,
    DofEnabled,
    DofFocusDistance,
    DofFocusRange,
    DofMaxBlurRadius,
    DofBokehThreshold,
    DofBokehIntensity,
    DofQuality,
    FxaaEnabled,
    RenderScale,
    IblBlendFactor,
    PbrDebug,
    UnlitMode,
    SelectionOutlineEnabled,
    SelectionOutlineColor,
    Exposure,
    Saturation,
    Contrast,
    Brightness,
    Gamma,
    Particle(ParticleField),
    Decal(DecalField),
    Audio(AudioField),
    RigidBody(RigidBodyField),
    Collider(ColliderField),
    Navmesh(NavmeshField),
    Camera(CameraField),
    Animation(AnimationField),
    Material(MaterialField),
    Text(TextField),
    Misc(MiscField),
    AddComponent(ComponentKind),
    RemoveComponent(ComponentKind),
    CharacterController(CharacterControllerField),
    Instance(InstanceField),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum InstanceField {
    TranslationX(usize),
    TranslationY(usize),
    TranslationZ(usize),
    EulerX(usize),
    EulerY(usize),
    EulerZ(usize),
    ScaleX(usize),
    ScaleY(usize),
    ScaleZ(usize),
    Tint(usize),
    Add,
    Remove(usize),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CharacterControllerField {
    Shape,
    CapsuleHalfHeight,
    CapsuleRadius,
    BallRadius,
    CuboidX,
    CuboidY,
    CuboidZ,
    MaxSpeed,
    Acceleration,
    JumpImpulse,
    Scale,
    CrouchEnabled,
    CrouchSpeedMultiplier,
    SprintSpeedMultiplier,
    StandingHalfHeight,
    CrouchingHalfHeight,
    ConfigOffset,
    MaxSlopeClimb,
    MinSlopeSlide,
    EngineInputEnabled,
    FrictionRate,
    AboveMaxFrictionRate,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ParticleField {
    Enabled,
    EmitterType,
    Shape,
    ShapeRadius,
    ShapeAngle,
    ShapeHeight,
    ShapeBoxX,
    ShapeBoxY,
    ShapeBoxZ,
    PositionX,
    PositionY,
    PositionZ,
    DirectionX,
    DirectionY,
    DirectionZ,
    SpawnRate,
    BurstCount,
    LifetimeMin,
    LifetimeMax,
    VelocityMin,
    VelocityMax,
    VelocitySpread,
    GravityX,
    GravityY,
    GravityZ,
    Drag,
    SizeStart,
    SizeEnd,
    EmissiveStrength,
    OneShot,
    TurbulenceStrength,
    TurbulenceFrequency,
    TextureIndex,
    SizeCurveTime(usize),
    SizeCurveValue(usize),
    AddSizeCurveKey,
    RemoveSizeCurveKey(usize),
    OpacityCurveTime(usize),
    OpacityCurveValue(usize),
    AddOpacityCurveKey,
    RemoveOpacityCurveKey(usize),
    DeathSubEmitter,
    TrailSubEmitter,
    TrailSpawnInterval,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum DecalField {
    EmissiveStrength,
    Color,
    SizeX,
    SizeY,
    Depth,
    NormalThreshold,
    FadeStart,
    FadeEnd,
    Texture,
    EmissiveTexture,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AudioField {
    AudioRef,
    Volume,
    Looping,
    Playing,
    Spatial,
    Bus,
    MinDistance,
    MaxDistance,
    ReverbZoneName(usize),
    ReverbZoneSend(usize),
    AddReverbZone,
    RemoveReverbZone(usize),
    RandomPick,
    RandomClip(usize),
    AddRandomClip,
    RemoveRandomClip(usize),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RigidBodyField {
    BodyType,
    Mass,
    LinvelX,
    LinvelY,
    LinvelZ,
    AngvelX,
    AngvelY,
    AngvelZ,
    Ccd,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ColliderField {
    Shape,
    BallRadius,
    CuboidX,
    CuboidY,
    CuboidZ,
    CapsuleHalfHeight,
    CapsuleRadius,
    CylinderHalfHeight,
    CylinderRadius,
    ConeHalfHeight,
    ConeRadius,
    Friction,
    Restitution,
    Density,
    IsSensor,
    CollisionMemberships,
    CollisionFilter,
    SolverMemberships,
    SolverFilter,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum NavmeshField {
    MovementSpeed,
    ArrivalThreshold,
    PathRecalculationThreshold,
    AgentRadius,
    AgentHeight,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum CameraField {
    ProjectionKind,
    FovDegrees,
    AspectOverride,
    AspectValue,
    NearPlane,
    FarOverride,
    FarValue,
    OrthoX,
    OrthoY,
    OrthoNear,
    OrthoFar,
    SetActive,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum AnimationField {
    Playing,
    Looping,
    Speed,
    PlayAll,
    ClipSelect,
    Time,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MaterialField {
    BaseColor,
    Roughness,
    Metallic,
    EmissiveFactor,
    EmissiveStrength,
    AlphaMode,
    AlphaCutoff,
    Unlit,
    DoubleSided,
    NormalScale,
    OcclusionStrength,
    Ior,
    Transmission,
    Thickness,
    ClearcoatFactor,
    ClearcoatRoughness,
    SheenColor,
    SheenRoughness,
    IridescenceFactor,
    AnisotropyStrength,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum TextField {
    Content,
    FontSize,
    Color,
}

pub struct SceneStructureSnapshot {
    pub layers: Vec<nightshade::ecs::scene::SceneLayerConfig>,
    pub chunks: Vec<nightshade::ecs::scene::SceneChunkConfig>,
    pub entity_layer: Option<nightshade::ecs::scene::LayerId>,
    pub entity_chunk: Option<nightshade::ecs::scene::ChunkId>,
    pub joints: Vec<nightshade::ecs::scene::SceneJointConnection>,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum MiscField {
    RenderLayer,
    CullingMaskBit(u8),
    CameraCullingMaskBit(u8),
    IgnoreParentScale,
    LinesAlwaysOnTop,
    LinesClear,
    SceneLayerAssignment,
    SceneChunkAssignment,
    SceneAddLayer,
    SceneAddChunk,
    SceneRemoveLayer(u32),
    SceneRemoveChunk(u32),
    SceneLayerName(u32),
    SceneLayerOrder(u32),
    SceneLayerEnabled(u32),
    SceneChunkName(u32),
    SceneChunkLoadDistance(u32),
    SceneChunkUnloadDistance(u32),
    SceneChunkBoundsMinX(u32),
    SceneChunkBoundsMinY(u32),
    SceneChunkBoundsMinZ(u32),
    SceneChunkBoundsMaxX(u32),
    SceneChunkBoundsMaxY(u32),
    SceneChunkBoundsMaxZ(u32),
    AddFixedJoint,
    AddRevoluteJoint,
    AddPrismaticJoint,
    AddSphericalJoint,
    AddRopeJoint,
    AddSpringJoint,
    RemoveJoint(usize),
    ToggleJointCollisions(usize),
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum ComponentKind {
    ParticleEmitter,
    Decal,
    AudioSource,
    RigidBody,
    Collider,
    CharacterController,
    NavmeshAgent,
    Camera,
    Text,
    AnimationPlayer,
    Visibility,
    CastsShadow,
    Light,
    RenderLayer,
    CullingMask,
    CameraCullingMask,
    IgnoreParentScale,
    AudioListener,
    CollisionListener,
    PhysicsInterpolation,
    MorphWeights,
    MaterialVariants,
    PanOrbitCamera,
    ThirdPersonCamera,
    CameraEnvironment,
    CameraPostProcess,
    ConstrainedAspect,
    ViewportUpdateMode,
}

impl InspectorField {
    fn is_transform(self) -> bool {
        matches!(
            self,
            Self::TranslationX
                | Self::TranslationY
                | Self::TranslationZ
                | Self::EulerX
                | Self::EulerY
                | Self::EulerZ
                | Self::ScaleX
                | Self::ScaleY
                | Self::ScaleZ
        )
    }

    fn snapshot_kind(self) -> Option<crate::undo::SnapshotKind> {
        use crate::undo::SnapshotKind;
        match self {
            Self::Name => Some(SnapshotKind::Name),
            Self::Visibility => Some(SnapshotKind::Visibility),
            Self::CastsShadow => Some(SnapshotKind::CastsShadow),
            Self::LightIntensity
            | Self::LightRange
            | Self::LightColor
            | Self::LightCastsShadow
            | Self::LightType
            | Self::LightInnerCone
            | Self::LightOuterCone
            | Self::LightShadowBias
            | Self::LightShadowResolution
            | Self::LightShadowDistance
            | Self::LightCookieTexture
            | Self::LightAreaShape
            | Self::LightAreaWidth
            | Self::LightAreaHeight
            | Self::LightAreaRadius
            | Self::LightAreaTwoSided
            | Self::LightAreaEmissiveTexture => Some(SnapshotKind::Light),
            Self::Particle(_) => Some(SnapshotKind::ParticleEmitter),
            Self::Decal(_) => Some(SnapshotKind::Decal),
            Self::Audio(_) => Some(SnapshotKind::AudioSource),
            Self::RigidBody(_) => Some(SnapshotKind::RigidBody),
            Self::Collider(_) => Some(SnapshotKind::Collider),
            Self::Navmesh(_) => Some(SnapshotKind::NavmeshAgent),
            Self::Camera(_) => Some(SnapshotKind::Camera),
            Self::Animation(_) => Some(SnapshotKind::AnimationPlayer),
            Self::Material(_) => Some(SnapshotKind::Material),
            Self::Text(_) => Some(SnapshotKind::Text),
            Self::Misc(MiscField::RenderLayer) => Some(SnapshotKind::RenderLayer),
            Self::Misc(MiscField::CullingMaskBit(_)) => Some(SnapshotKind::CullingMask),
            Self::Misc(MiscField::CameraCullingMaskBit(_)) => Some(SnapshotKind::CameraCullingMask),
            Self::Misc(MiscField::IgnoreParentScale) => Some(SnapshotKind::IgnoreParentScale),
            Self::CharacterController(_) => Some(SnapshotKind::CharacterController),
            Self::AddComponent(kind) | Self::RemoveComponent(kind) => {
                component_kind_snapshot_kind(kind)
            }
            _ => None,
        }
    }
}

fn component_kind_snapshot_kind(kind: ComponentKind) -> Option<crate::undo::SnapshotKind> {
    use crate::undo::SnapshotKind;
    Some(match kind {
        ComponentKind::ParticleEmitter => SnapshotKind::ParticleEmitter,
        ComponentKind::Decal => SnapshotKind::Decal,
        ComponentKind::AudioSource => SnapshotKind::AudioSource,
        ComponentKind::RigidBody => SnapshotKind::RigidBody,
        ComponentKind::Collider => SnapshotKind::Collider,
        ComponentKind::CharacterController => SnapshotKind::CharacterController,
        ComponentKind::NavmeshAgent => SnapshotKind::NavmeshAgent,
        ComponentKind::Camera => SnapshotKind::Camera,
        ComponentKind::Text => SnapshotKind::Text,
        ComponentKind::AnimationPlayer => SnapshotKind::AnimationPlayer,
        ComponentKind::Visibility => SnapshotKind::Visibility,
        ComponentKind::CastsShadow => SnapshotKind::CastsShadow,
        ComponentKind::Light => SnapshotKind::Light,
        ComponentKind::RenderLayer => SnapshotKind::RenderLayer,
        ComponentKind::CullingMask => SnapshotKind::CullingMask,
        ComponentKind::CameraCullingMask => SnapshotKind::CameraCullingMask,
        ComponentKind::IgnoreParentScale => SnapshotKind::IgnoreParentScale,
        ComponentKind::AudioListener
        | ComponentKind::CollisionListener
        | ComponentKind::PhysicsInterpolation
        | ComponentKind::MorphWeights
        | ComponentKind::MaterialVariants
        | ComponentKind::PanOrbitCamera
        | ComponentKind::ThirdPersonCamera
        | ComponentKind::CameraEnvironment
        | ComponentKind::CameraPostProcess
        | ComponentKind::ConstrainedAspect
        | ComponentKind::ViewportUpdateMode => return None,
    })
}

pub fn build(tree: &mut UiTreeBuilder) -> InspectorHandles {
    let panel = tree.add_docked_panel_right("inspector", "Inspector", 320.0);
    let content = super::panel_content(tree, panel);

    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size;
    let text_color = theme.text_color;

    let mut header_label = Entity::default();
    let mut clear_button = Entity::default();
    let mut body_root = Entity::default();

    tree.in_parent(content, |tree| {
        let header_row = tree.add_node().size(100.pct(), (28.0).px()).entity();
        if let Some(node) = tree.world_mut().ui.get_ui_layout_node_mut(header_row) {
            node.flow_layout = Some(FlowLayout {
                direction: FlowDirection::Horizontal,
                padding: 4.0,
                spacing: 4.0,
                alignment: FlowAlignment::Start,
                cross_alignment: FlowAlignment::Center,
                wrap: false,
            });
        }
        tree.in_parent(header_row, |tree| {
            header_label = tree
                .add_node()
                .flow_child(Ab(vec2(0.0, 22.0)))
                .flex_grow(1.0)
                .with_text("World", font)
                .text_left()
                .with_text_overflow(TextOverflow::Ellipsis)
                .color_raw::<UiBase>(text_color)
                .entity();

            clear_button = tree
                .add_node()
                .size((60.0).px(), (22.0).px())
                .with_text("Clear", font * 0.85)
                .text_center()
                .color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
                .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
                .with_interaction()
                .with_cursor_icon(winit::window::CursorIcon::Pointer)
                .entity();
        });

        let scroll = tree.add_scroll_area_fill(0.0, 0.0);
        body_root = widget::<UiScrollAreaData>(tree.world_mut(), scroll)
            .map(|d| d.content_entity)
            .unwrap_or(scroll);
    });

    InspectorHandles {
        panel,
        header_label,
        clear_button,
        body_root,
        body_entities: Vec::new(),
        bindings: HashMap::new(),
        editing_state: HashMap::new(),
        pre_drag_transforms: HashMap::new(),
        pre_drag_components: HashMap::new(),
        pending_picker_edits: HashMap::new(),
        tag_remove_buttons: HashMap::new(),
        tag_input: None,
        dirty: true,
    }
}

pub fn poll(editor_world: &mut EditorWorld, world: &mut World, handles: &UiHandles) {
    let mut clear_selection = false;
    let mut field_changes: Vec<(Entity, InspectorField, FieldValue)> = Vec::new();
    let mut picker_changes: Vec<(Entity, Entity, InspectorField, FieldValue)> = Vec::new();
    let mut drag_ends: Vec<(Entity, InspectorField)> = Vec::new();
    let mut tag_to_add: Option<String> = None;
    let mut tag_to_remove: Option<String> = None;

    for event in ui_events(world) {
        match event {
            UiEvent::ButtonClicked(entity) if *entity == handles.inspector.clear_button => {
                clear_selection = true;
            }
            UiEvent::ButtonClicked(entity) => {
                if let Some(tag) = handles.inspector.tag_remove_buttons.get(entity) {
                    tag_to_remove = Some(tag.clone());
                } else if let Some(binding) = handles.inspector.bindings.get(entity) {
                    field_changes.push((binding.entity, binding.field, FieldValue::Bool(true)));
                }
            }
            UiEvent::DragValueChanged { entity, value } => {
                if let Some(binding) = handles.inspector.bindings.get(entity) {
                    field_changes.push((binding.entity, binding.field, FieldValue::Float(*value)));
                }
            }
            UiEvent::CheckboxChanged { entity, value } => {
                if let Some(binding) = handles.inspector.bindings.get(entity) {
                    field_changes.push((binding.entity, binding.field, FieldValue::Bool(*value)));
                }
            }
            UiEvent::TextInputSubmitted { entity, text } => {
                if Some(*entity) == handles.inspector.tag_input {
                    let trimmed = text.trim();
                    if !trimmed.is_empty() {
                        tag_to_add = Some(trimmed.to_string());
                    }
                } else if let Some(binding) = handles.inspector.bindings.get(entity) {
                    field_changes.push((
                        binding.entity,
                        binding.field,
                        FieldValue::Text(text.clone()),
                    ));
                }
            }
            UiEvent::ColorPickerChanged { entity, color } => {
                if let Some(binding) = handles.inspector.bindings.get(entity) {
                    picker_changes.push((
                        *entity,
                        binding.entity,
                        binding.field,
                        FieldValue::Color(*color),
                    ));
                }
            }
            UiEvent::DropdownChanged {
                entity,
                selected_index,
            } => {
                if let Some(binding) = handles.inspector.bindings.get(entity) {
                    field_changes.push((
                        binding.entity,
                        binding.field,
                        FieldValue::Index(*selected_index),
                    ));
                }
            }
            _ => {}
        }
    }

    for (widget_entity, binding) in &handles.inspector.bindings {
        if let Some(data) = world.ui.get_ui_drag_value(*widget_entity) {
            let was_editing = editor_world
                .resources
                .ui_handles
                .inspector
                .editing_state
                .get(widget_entity)
                .copied()
                .unwrap_or(false);
            if was_editing && !data.editing {
                drag_ends.push((binding.entity, binding.field));
            }
            editor_world
                .resources
                .ui_handles
                .inspector
                .editing_state
                .insert(*widget_entity, data.editing);
        }
    }

    if clear_selection {
        editor_world
            .resources
            .ui_interaction
            .actions
            .push(Action::ClearSelection);
    }

    if let Some(tag) = tag_to_add {
        editor_world
            .resources
            .ui_interaction
            .actions
            .push(Action::AddTagToSelected(tag));
        if let Some(input) = editor_world.resources.ui_handles.inspector.tag_input {
            ui_text_input_set_value(world, input, "");
        }
    }
    if let Some(tag) = tag_to_remove {
        editor_world
            .resources
            .ui_interaction
            .actions
            .push(Action::RemoveTagFromSelected(tag));
    }

    let mut pre_drag_transforms = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .inspector
            .pre_drag_transforms,
    );
    let mut pre_drag_components = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .inspector
            .pre_drag_components,
    );
    let mut pending_picker_edits = std::mem::take(
        &mut editor_world
            .resources
            .ui_handles
            .inspector
            .pending_picker_edits,
    );

    for (entity, field, value) in field_changes {
        if field.is_transform()
            && !pre_drag_transforms.contains_key(&entity)
            && let Some(transform) = world.core.get_local_transform(entity).copied()
        {
            pre_drag_transforms.insert(entity, transform);
        }
        let snapshot_kind = field.snapshot_kind();
        let is_drag_value = matches!(value, FieldValue::Float(_));
        if is_drag_value
            && let Some(kind) = snapshot_kind
            && !pre_drag_components.contains_key(&(entity, kind))
            && let Some(snap) = crate::undo::ComponentSnapshot::capture(world, entity, kind)
        {
            pre_drag_components.insert((entity, kind), snap);
        }
        let before = if is_drag_value {
            None
        } else {
            snapshot_kind
                .and_then(|kind| crate::undo::ComponentSnapshot::capture(world, entity, kind))
        };
        let needs_header_refresh = matches!(field, InspectorField::Name);
        let needs_rebuild = matches!(value, FieldValue::Index(_));
        apply_field_change(editor_world, world, entity, field, value);
        if needs_header_refresh || needs_rebuild {
            editor_world.resources.ui_handles.inspector.dirty = true;
        }
        if let (Some(kind), Some(before)) = (snapshot_kind, before)
            && let Some(after) = crate::undo::ComponentSnapshot::capture(world, entity, kind)
            && before != after
            && let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
        {
            editor_world.resources.undo.push(
                crate::undo::UndoableOperation::ComponentChanged {
                    uuid,
                    old: Box::new(before),
                    new: Box::new(after),
                },
                "Inspector edit",
            );
        }
    }

    let picker_widgets_active: std::collections::HashSet<Entity> =
        picker_changes.iter().map(|(widget, ..)| *widget).collect();

    for (widget_entity, target, field, value) in picker_changes {
        if let Some(kind) = field.snapshot_kind()
            && !pending_picker_edits.contains_key(&widget_entity)
            && let Some(before) = crate::undo::ComponentSnapshot::capture(world, target, kind)
        {
            pending_picker_edits.insert(
                widget_entity,
                PickerEditSession {
                    target,
                    field,
                    before,
                },
            );
        }
        apply_field_change(editor_world, world, target, field, value);
    }

    let stale_pickers: Vec<Entity> = pending_picker_edits
        .keys()
        .copied()
        .filter(|widget| !picker_widgets_active.contains(widget))
        .collect();
    for widget_entity in stale_pickers {
        let Some(session) = pending_picker_edits.remove(&widget_entity) else {
            continue;
        };
        let Some(kind) = session.field.snapshot_kind() else {
            continue;
        };
        let Some(after) = crate::undo::ComponentSnapshot::capture(world, session.target, kind)
        else {
            continue;
        };
        if session.before == after {
            continue;
        }
        let Some(uuid) = editor_world.resources.editor_scene.uuid_for(session.target) else {
            continue;
        };
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::ComponentChanged {
                uuid,
                old: Box::new(session.before),
                new: Box::new(after),
            },
            "Inspector edit",
        );
    }

    for (entity, field) in drag_ends {
        if field.is_transform() {
            if let (Some(start), Some(end)) = (
                pre_drag_transforms.remove(&entity),
                world.core.get_local_transform(entity).copied(),
            ) && start != end
                && let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
            {
                editor_world.resources.undo.push(
                    crate::undo::UndoableOperation::TransformChanged {
                        uuid,
                        old: start,
                        new: end,
                    },
                    "Inspector transform",
                );
                editor_world.resources.ui_handles.inspector.dirty = true;
            }
        } else if let Some(kind) = field.snapshot_kind() {
            let key = (entity, kind);
            if let (Some(before), Some(after)) = (
                pre_drag_components.remove(&key),
                crate::undo::ComponentSnapshot::capture(world, entity, kind),
            ) && before != after
                && let Some(uuid) = editor_world.resources.editor_scene.uuid_for(entity)
            {
                editor_world.resources.undo.push(
                    crate::undo::UndoableOperation::ComponentChanged {
                        uuid,
                        old: Box::new(before),
                        new: Box::new(after),
                    },
                    "Inspector edit",
                );
            }
        }
    }

    editor_world
        .resources
        .ui_handles
        .inspector
        .pre_drag_transforms = pre_drag_transforms;
    editor_world
        .resources
        .ui_handles
        .inspector
        .pre_drag_components = pre_drag_components;
    editor_world
        .resources
        .ui_handles
        .inspector
        .pending_picker_edits = pending_picker_edits;
}

#[derive(Clone)]
enum FieldValue {
    Float(f32),
    Bool(bool),
    Text(String),
    Color(Vec4),
    Index(usize),
}

fn apply_field_change(
    editor_world: &mut EditorWorld,
    world: &mut World,
    entity: Entity,
    field: InspectorField,
    value: FieldValue,
) {
    let mut transform_dirty = false;
    match (field, &value) {
        (InspectorField::Name, FieldValue::Text(text)) => {
            world.core.set_name(entity, Name(text.clone()));
        }
        (
            InspectorField::TranslationX
            | InspectorField::TranslationY
            | InspectorField::TranslationZ,
            FieldValue::Float(v),
        ) => {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                match field {
                    InspectorField::TranslationX => transform.translation.x = *v,
                    InspectorField::TranslationY => transform.translation.y = *v,
                    InspectorField::TranslationZ => transform.translation.z = *v,
                    _ => unreachable!(),
                }
                transform_dirty = true;
            }
        }
        (
            InspectorField::EulerX | InspectorField::EulerY | InspectorField::EulerZ,
            FieldValue::Float(degrees),
        ) => {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                let (mut roll, mut pitch, mut yaw) = quat_to_euler_xyz(&transform.rotation);
                let radians = degrees.to_radians();
                match field {
                    InspectorField::EulerX => roll = radians,
                    InspectorField::EulerY => pitch = radians,
                    InspectorField::EulerZ => yaw = radians,
                    _ => unreachable!(),
                }
                transform.rotation = euler_xyz_to_quat(roll, pitch, yaw);
                transform_dirty = true;
            }
        }
        (
            InspectorField::ScaleX | InspectorField::ScaleY | InspectorField::ScaleZ,
            FieldValue::Float(v),
        ) => {
            if let Some(transform) = world.core.get_local_transform_mut(entity) {
                match field {
                    InspectorField::ScaleX => transform.scale.x = *v,
                    InspectorField::ScaleY => transform.scale.y = *v,
                    InspectorField::ScaleZ => transform.scale.z = *v,
                    _ => unreachable!(),
                }
                transform_dirty = true;
            }
        }
        (InspectorField::Visibility, FieldValue::Bool(visible)) => {
            world
                .core
                .set_visibility(entity, Visibility { visible: *visible });
        }
        (InspectorField::CastsShadow, FieldValue::Bool(value)) => {
            if *value {
                world.core.add_components(entity, CASTS_SHADOW);
                world.core.set_casts_shadow(entity, CastsShadow);
            } else {
                world.core.remove_components(entity, CASTS_SHADOW);
            }
        }
        (InspectorField::LightIntensity, FieldValue::Float(intensity)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.intensity = *intensity;
            }
        }
        (InspectorField::LightRange, FieldValue::Float(range)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.range = *range;
            }
        }
        (InspectorField::LightCastsShadow, FieldValue::Bool(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.cast_shadows = *value;
            }
        }
        (InspectorField::LightColor, FieldValue::Color(color)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.color = Vec3::new(color.x, color.y, color.z);
            }
        }
        (InspectorField::LightType, FieldValue::Index(index)) => {
            use nightshade::ecs::light::components::LightType;
            if let Some(light) = world.core.get_light_mut(entity) {
                light.light_type = match *index {
                    1 => LightType::Point,
                    2 => LightType::Spot,
                    3 => LightType::Area,
                    _ => LightType::Directional,
                };
            }
        }
        (InspectorField::LightInnerCone, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.inner_cone_angle = *value;
            }
        }
        (InspectorField::LightOuterCone, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.outer_cone_angle = *value;
            }
        }
        (InspectorField::LightShadowBias, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.shadow_bias = *value;
            }
        }
        (InspectorField::LightShadowResolution, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.shadow_resolution = (*value).clamp(0.0, 8192.0) as u32;
            }
        }
        (InspectorField::LightShadowDistance, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.shadow_distance = *value;
            }
        }
        (InspectorField::LightCookieTexture, FieldValue::Text(text)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.cookie_texture = if text.is_empty() {
                    None
                } else {
                    Some(text.clone())
                };
            }
        }
        (InspectorField::LightAreaShape, FieldValue::Index(index)) => {
            use nightshade::ecs::light::components::AreaLightShape;
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_shape = match *index {
                    1 => AreaLightShape::Disk,
                    2 => AreaLightShape::Sphere,
                    3 => AreaLightShape::Tube,
                    _ => AreaLightShape::Rectangle,
                };
            }
        }
        (InspectorField::LightAreaWidth, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_width = *value;
            }
        }
        (InspectorField::LightAreaHeight, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_height = *value;
            }
        }
        (InspectorField::LightAreaRadius, FieldValue::Float(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_radius = *value;
            }
        }
        (InspectorField::LightAreaTwoSided, FieldValue::Bool(value)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_two_sided = *value;
            }
        }
        (InspectorField::LightAreaEmissiveTexture, FieldValue::Text(text)) => {
            if let Some(light) = world.core.get_light_mut(entity) {
                light.area_emissive_texture = if text.is_empty() {
                    None
                } else {
                    Some(text.clone())
                };
            }
        }
        (InspectorField::AmbientLight, FieldValue::Color(color)) => {
            world.resources.render_settings.ambient_light = [color.x, color.y, color.z, color.w];
        }
        (InspectorField::BloomEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.bloom_enabled = *value;
        }
        (InspectorField::BloomIntensity, FieldValue::Float(value)) => {
            world.resources.render_settings.bloom_intensity = *value;
        }
        (InspectorField::BloomThreshold, FieldValue::Float(value)) => {
            world.resources.render_settings.bloom_threshold = *value;
        }
        (InspectorField::BloomKnee, FieldValue::Float(value)) => {
            world.resources.render_settings.bloom_knee = *value;
        }
        (InspectorField::BloomFilterRadius, FieldValue::Float(value)) => {
            world.resources.render_settings.bloom_filter_radius = *value;
        }
        (InspectorField::SsaoEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.ssao_enabled = *value;
        }
        (InspectorField::SsaoRadius, FieldValue::Float(value)) => {
            world.resources.render_settings.ssao_radius = *value;
        }
        (InspectorField::SsaoIntensity, FieldValue::Float(value)) => {
            world.resources.render_settings.ssao_intensity = *value;
        }
        (InspectorField::SsaoBias, FieldValue::Float(value)) => {
            world.resources.render_settings.ssao_bias = *value;
        }
        (InspectorField::SsaoSampleCount, FieldValue::Float(value)) => {
            world.resources.render_settings.ssao_sample_count = (*value).max(1.0) as u32;
        }
        (InspectorField::SsaoVisualization, FieldValue::Bool(value)) => {
            world.resources.debug_draw.ssao_visualization = *value;
        }
        (InspectorField::SsgiEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.ssgi_enabled = *value;
        }
        (InspectorField::SsgiRadius, FieldValue::Float(value)) => {
            world.resources.render_settings.ssgi_radius = *value;
        }
        (InspectorField::SsgiIntensity, FieldValue::Float(value)) => {
            world.resources.render_settings.ssgi_intensity = *value;
        }
        (InspectorField::SsgiMaxSteps, FieldValue::Float(value)) => {
            world.resources.render_settings.ssgi_max_steps = (*value).max(1.0) as u32;
        }
        (InspectorField::SsrEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.ssr_enabled = *value;
        }
        (InspectorField::SsrMaxSteps, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_max_steps = (*value).max(1.0) as u32;
        }
        (InspectorField::SsrThickness, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_thickness = *value;
        }
        (InspectorField::SsrMaxDistance, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_max_distance = *value;
        }
        (InspectorField::SsrStride, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_stride = *value;
        }
        (InspectorField::SsrFadeStart, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_fade_start = *value;
        }
        (InspectorField::SsrFadeEnd, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_fade_end = *value;
        }
        (InspectorField::SsrIntensity, FieldValue::Float(value)) => {
            world.resources.render_settings.ssr_intensity = *value;
        }
        (InspectorField::FogEnabled, FieldValue::Bool(value)) => {
            if *value {
                if world.resources.render_settings.fog.is_none() {
                    world.resources.render_settings.fog =
                        Some(nightshade::ecs::graphics::resources::Fog::default());
                }
            } else {
                world.resources.render_settings.fog = None;
            }
            editor_world.resources.ui_handles.inspector.dirty = true;
        }
        (InspectorField::FogColor, FieldValue::Color(color)) => {
            if let Some(fog) = world.resources.render_settings.fog.as_mut() {
                fog.color = [color.x, color.y, color.z];
            }
        }
        (InspectorField::FogStart, FieldValue::Float(value)) => {
            if let Some(fog) = world.resources.render_settings.fog.as_mut() {
                fog.start = *value;
            }
        }
        (InspectorField::FogEnd, FieldValue::Float(value)) => {
            if let Some(fog) = world.resources.render_settings.fog.as_mut() {
                fog.end = *value;
            }
        }
        (InspectorField::DofEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.depth_of_field.enabled = *value;
        }
        (InspectorField::DofFocusDistance, FieldValue::Float(value)) => {
            world
                .resources
                .render_settings
                .depth_of_field
                .focus_distance = *value;
        }
        (InspectorField::DofFocusRange, FieldValue::Float(value)) => {
            world.resources.render_settings.depth_of_field.focus_range = *value;
        }
        (InspectorField::DofMaxBlurRadius, FieldValue::Float(value)) => {
            world
                .resources
                .render_settings
                .depth_of_field
                .max_blur_radius = *value;
        }
        (InspectorField::DofBokehThreshold, FieldValue::Float(value)) => {
            world
                .resources
                .render_settings
                .depth_of_field
                .bokeh_threshold = *value;
        }
        (InspectorField::DofBokehIntensity, FieldValue::Float(value)) => {
            world
                .resources
                .render_settings
                .depth_of_field
                .bokeh_intensity = *value;
        }
        (InspectorField::DofQuality, FieldValue::Index(index)) => {
            use nightshade::ecs::graphics::resources::DepthOfFieldQuality;
            world.resources.render_settings.depth_of_field.quality = match *index {
                0 => DepthOfFieldQuality::Low,
                1 => DepthOfFieldQuality::Medium,
                _ => DepthOfFieldQuality::High,
            };
        }
        (InspectorField::FxaaEnabled, FieldValue::Bool(value)) => {
            world.resources.render_settings.fxaa_enabled = *value;
        }
        (InspectorField::RenderScale, FieldValue::Float(value)) => {
            world.resources.render_settings.render_scale = (*value).clamp(0.25, 4.0);
        }
        (InspectorField::IblBlendFactor, FieldValue::Float(value)) => {
            world.resources.render_settings.ibl_blend_factor = (*value).clamp(0.0, 1.0);
        }
        (InspectorField::PbrDebug, FieldValue::Index(index)) => {
            use nightshade::ecs::graphics::resources::PbrDebugMode;
            world.resources.debug_draw.pbr_debug_mode = PbrDebugMode::ALL
                .get(*index)
                .copied()
                .unwrap_or(PbrDebugMode::None);
        }
        (InspectorField::UnlitMode, FieldValue::Bool(value)) => {
            world.resources.render_settings.unlit_mode = *value;
        }
        (InspectorField::SelectionOutlineEnabled, FieldValue::Bool(value)) => {
            world.resources.debug_draw.selection_outline_enabled = *value;
        }
        (InspectorField::SelectionOutlineColor, FieldValue::Color(color)) => {
            world.resources.debug_draw.selection_outline_color =
                [color.x, color.y, color.z, color.w];
        }
        (InspectorField::Exposure, FieldValue::Float(value)) => {
            world.resources.render_settings.color_grading.exposure = *value;
        }
        (InspectorField::Saturation, FieldValue::Float(value)) => {
            world.resources.render_settings.color_grading.saturation = *value;
        }
        (InspectorField::Contrast, FieldValue::Float(value)) => {
            world.resources.render_settings.color_grading.contrast = *value;
        }
        (InspectorField::Brightness, FieldValue::Float(value)) => {
            world.resources.render_settings.color_grading.brightness = *value;
        }
        (InspectorField::Gamma, FieldValue::Float(value)) => {
            world.resources.render_settings.color_grading.gamma = *value;
        }
        (InspectorField::Particle(field), value) => {
            apply_particle_change(world, entity, field, value);
        }
        (InspectorField::Decal(field), value) => {
            apply_decal_change(world, entity, field, value);
        }
        (InspectorField::Audio(field), value) => {
            apply_audio_change(world, entity, field, value);
        }
        (InspectorField::RigidBody(field), value) => {
            apply_rigid_body_change(world, entity, field, value);
        }
        (InspectorField::Collider(field), value) => {
            apply_collider_change(world, entity, field, value);
        }
        (InspectorField::Navmesh(field), value) => {
            apply_navmesh_change(world, entity, field, value);
        }
        (InspectorField::Camera(field), value) => {
            apply_camera_change(world, entity, field, value);
        }
        (InspectorField::Animation(field), value) => {
            apply_animation_change(world, entity, field, value);
        }
        (InspectorField::Material(field), value) => {
            apply_material_change(world, entity, field, value);
        }
        (InspectorField::Text(field), value) => {
            apply_text_change(world, entity, field, value);
        }
        (InspectorField::Misc(field), value) => {
            if !apply_misc_scene_change(editor_world, entity, field, value) {
                apply_misc_change(world, entity, field, value);
            } else {
                editor_world.resources.ui_handles.inspector.dirty = true;
            }
        }
        (InspectorField::AddComponent(kind), FieldValue::Bool(true)) => {
            attach_default_component(world, entity, kind);
            editor_world.resources.ui_handles.inspector.dirty = true;
        }
        (InspectorField::RemoveComponent(kind), FieldValue::Bool(true)) => {
            remove_component(world, entity, kind);
            editor_world.resources.ui_handles.inspector.dirty = true;
        }
        (InspectorField::CharacterController(field), value) => {
            apply_character_controller_change(world, entity, field, value);
        }
        (InspectorField::Instance(field), value) => {
            let needs_rebuild = apply_instanced_mesh_change(world, entity, field, value);
            if needs_rebuild {
                editor_world.resources.ui_handles.inspector.dirty = true;
            }
        }
        _ => {}
    }

    if transform_dirty {
        mark_local_transform_dirty(world, entity);
    }
    crate::scene_writeback::sync_entity(
        &mut editor_world.resources.project,
        &editor_world.resources.editor_scene,
        world,
        entity,
    );
}

fn quat_to_euler_xyz(quat: &Quat) -> (f32, f32, f32) {
    let sinr = 2.0 * (quat.w * quat.i + quat.j * quat.k);
    let cosr = 1.0 - 2.0 * (quat.i * quat.i + quat.j * quat.j);
    let roll = sinr.atan2(cosr);

    let sinp = 2.0 * (quat.w * quat.j - quat.k * quat.i);
    let pitch = if sinp.abs() >= 1.0 {
        std::f32::consts::FRAC_PI_2.copysign(sinp)
    } else {
        sinp.asin()
    };

    let siny = 2.0 * (quat.w * quat.k + quat.i * quat.j);
    let cosy = 1.0 - 2.0 * (quat.j * quat.j + quat.k * quat.k);
    let yaw = siny.atan2(cosy);

    (roll, pitch, yaw)
}

fn euler_xyz_to_quat(roll: f32, pitch: f32, yaw: f32) -> Quat {
    let cr = (roll * 0.5).cos();
    let sr = (roll * 0.5).sin();
    let cp = (pitch * 0.5).cos();
    let sp = (pitch * 0.5).sin();
    let cy = (yaw * 0.5).cos();
    let sy = (yaw * 0.5).sin();

    Quat::from_parts(
        cr * cp * cy + sr * sp * sy,
        nalgebra_glm::Vec3::new(
            sr * cp * cy - cr * sp * sy,
            cr * sp * cy + sr * cp * sy,
            cr * cp * sy - sr * sp * cy,
        ),
    )
}

pub fn update(editor_world: &mut EditorWorld, world: &mut World) {
    if !editor_world.resources.ui_handles.inspector.dirty {
        return;
    }
    editor_world.resources.ui_handles.inspector.dirty = false;

    let header_label = editor_world.resources.ui_handles.inspector.header_label;
    let body_root = editor_world.resources.ui_handles.inspector.body_root;

    for node in std::mem::take(&mut editor_world.resources.ui_handles.inspector.body_entities) {
        despawn_recursive_immediate(world, node);
    }
    editor_world.resources.ui_handles.inspector.bindings.clear();
    editor_world
        .resources
        .ui_handles
        .inspector
        .editing_state
        .clear();
    editor_world
        .resources
        .ui_handles
        .inspector
        .pre_drag_transforms
        .clear();
    editor_world
        .resources
        .ui_handles
        .inspector
        .pre_drag_components
        .clear();
    editor_world
        .resources
        .ui_handles
        .inspector
        .pending_picker_edits
        .clear();
    editor_world
        .resources
        .ui_handles
        .inspector
        .tag_remove_buttons
        .clear();
    editor_world.resources.ui_handles.inspector.tag_input = None;

    let selected = editor_world.resources.ui.selected_entity;
    let header_text = match selected {
        Some(entity) => world
            .core
            .get_name(entity)
            .map(|n| n.0.clone())
            .filter(|n| !n.is_empty())
            .unwrap_or_else(|| format!("Entity {}", entity.id)),
        None => "World".to_string(),
    };
    ui_set_text(world, header_label, &header_text);

    let scene_snapshot = SceneStructureSnapshot {
        layers: editor_world.resources.project.scene.layers.clone(),
        chunks: editor_world.resources.project.scene.chunks.clone(),
        entity_layer: selected
            .and_then(|entity| editor_world.resources.editor_scene.entity_layer(entity)),
        entity_chunk: selected
            .and_then(|entity| editor_world.resources.editor_scene.entity_chunk(entity)),
        joints: editor_world.resources.project.scene.joints.clone(),
    };

    let mut new_nodes: Vec<Entity> = Vec::new();
    let mut bindings: HashMap<Entity, InspectorBinding> = HashMap::new();
    let mut tag_remove_buttons: HashMap<Entity, String> = HashMap::new();
    let mut tag_input: Option<Entity> = None;
    {
        let mut tree = UiTreeBuilder::new(world);
        let snapshot_ref = &scene_snapshot;
        tree.in_parent(body_root, |tree| match selected {
            Some(entity) => populate_entity_sections(
                tree,
                entity,
                &mut new_nodes,
                &mut bindings,
                &mut tag_remove_buttons,
                &mut tag_input,
                snapshot_ref,
            ),
            None => populate_world_sections(tree, &mut new_nodes, &mut bindings, snapshot_ref),
        });
        tree.finish();
    }
    editor_world.resources.ui_handles.inspector.body_entities = new_nodes;
    editor_world.resources.ui_handles.inspector.bindings = bindings;
    editor_world
        .resources
        .ui_handles
        .inspector
        .tag_remove_buttons = tag_remove_buttons;
    editor_world.resources.ui_handles.inspector.tag_input = tag_input;
}

fn populate_entity_sections(
    tree: &mut UiTreeBuilder,
    entity: Entity,
    new_nodes: &mut Vec<Entity>,
    bindings: &mut HashMap<Entity, InspectorBinding>,
    tag_remove_buttons: &mut HashMap<Entity, String>,
    tag_input: &mut Option<Entity>,
    scene_snapshot: &SceneStructureSnapshot,
) {
    let grid = tree.add_property_grid(120.0);
    new_nodes.push(grid);

    let identity_section = tree.add_property_section(grid, "Identity");
    add_property_value(tree, grid, identity_section, "ID", &entity.id.to_string());
    let current_name = tree
        .world_mut()
        .core
        .get_name(entity)
        .map(|n| n.0.clone())
        .unwrap_or_default();
    let name_input = add_property_text_input(
        tree,
        grid,
        identity_section,
        "Name",
        &current_name,
        "Entity name",
    );
    bind(bindings, entity, InspectorField::Name, name_input);
    let visible = tree
        .world_mut()
        .core
        .get_visibility(entity)
        .map(|v| v.visible)
        .unwrap_or(true);
    let visibility_checkbox =
        add_property_checkbox(tree, grid, identity_section, "Visible", visible);
    bind(
        bindings,
        entity,
        InspectorField::Visibility,
        visibility_checkbox,
    );
    let casts_shadow = tree.world_mut().core.entity_has_casts_shadow(entity);
    let casts_checkbox =
        add_property_checkbox(tree, grid, identity_section, "Casts shadow", casts_shadow);
    bind(
        bindings,
        entity,
        InspectorField::CastsShadow,
        casts_checkbox,
    );

    let tags_snapshot: Vec<String> = tree
        .world_mut()
        .resources
        .entities
        .tags
        .get(&entity)
        .cloned()
        .unwrap_or_default();
    let tags_section = tree.add_property_section(grid, "Tags");
    if tags_snapshot.is_empty() {
        add_property_value(tree, grid, tags_section, "Tags", "(none)");
    } else {
        for tag in &tags_snapshot {
            let row = tree.add_property_row(grid, tags_section, tag);
            let theme = tree
                .world_mut()
                .resources
                .retained_ui
                .theme_state
                .active_theme();
            let font = theme.font_size * 0.85;
            tree.in_parent(row, |tree| {
                let button = tree
                    .add_node()
                    .size((22.0).px(), (22.0).px())
                    .with_text("\u{00d7}", font)
                    .text_center()
                    .color_raw::<UiBase>(vec4(0.7, 0.7, 0.75, 1.0))
                    .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
                    .with_interaction()
                    .with_cursor_icon(winit::window::CursorIcon::Pointer)
                    .entity();
                tag_remove_buttons.insert(button, tag.clone());
            });
        }
    }
    let add_row = tree.add_property_row(grid, tags_section, "Add tag");
    tree.in_parent(add_row, |tree| {
        let input = tree.add_text_input("tag name");
        *tag_input = Some(input);
    });

    if let Some(transform) = tree.world_mut().core.get_local_transform(entity).copied() {
        let section = tree.add_property_section(grid, "Transform");
        let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);

        let tx = add_property_drag_value(
            tree,
            grid,
            section,
            "Translation X",
            transform.translation.x,
            0.05,
            2,
        );
        bind(bindings, entity, InspectorField::TranslationX, tx);
        let ty = add_property_drag_value(
            tree,
            grid,
            section,
            "Translation Y",
            transform.translation.y,
            0.05,
            2,
        );
        bind(bindings, entity, InspectorField::TranslationY, ty);
        let tz = add_property_drag_value(
            tree,
            grid,
            section,
            "Translation Z",
            transform.translation.z,
            0.05,
            2,
        );
        bind(bindings, entity, InspectorField::TranslationZ, tz);

        let rx = add_property_drag_value(
            tree,
            grid,
            section,
            "Rotation X (\u{00b0})",
            roll.to_degrees(),
            0.5,
            1,
        );
        bind(bindings, entity, InspectorField::EulerX, rx);
        let ry = add_property_drag_value(
            tree,
            grid,
            section,
            "Rotation Y (\u{00b0})",
            pitch.to_degrees(),
            0.5,
            1,
        );
        bind(bindings, entity, InspectorField::EulerY, ry);
        let rz = add_property_drag_value(
            tree,
            grid,
            section,
            "Rotation Z (\u{00b0})",
            yaw.to_degrees(),
            0.5,
            1,
        );
        bind(bindings, entity, InspectorField::EulerZ, rz);

        let sx =
            add_property_drag_value(tree, grid, section, "Scale X", transform.scale.x, 0.01, 2);
        bind(bindings, entity, InspectorField::ScaleX, sx);
        let sy =
            add_property_drag_value(tree, grid, section, "Scale Y", transform.scale.y, 0.01, 2);
        bind(bindings, entity, InspectorField::ScaleY, sy);
        let sz =
            add_property_drag_value(tree, grid, section, "Scale Z", transform.scale.z, 0.01, 2);
        bind(bindings, entity, InspectorField::ScaleZ, sz);
    }

    if let Some(light) = tree.world_mut().core.get_light(entity).cloned() {
        use nightshade::ecs::light::components::LightType;
        let section = tree.add_property_section(grid, "Light");
        let type_index = match light.light_type {
            LightType::Directional => 0,
            LightType::Point => 1,
            LightType::Spot => 2,
            LightType::Area => 3,
        };
        let type_dropdown = add_property_dropdown(
            tree,
            grid,
            section,
            "Type",
            &["Directional", "Point", "Spot", "Area"],
            type_index,
        );
        bind(bindings, entity, InspectorField::LightType, type_dropdown);
        let intensity =
            add_property_drag_value(tree, grid, section, "Intensity", light.intensity, 0.05, 2);
        bind(bindings, entity, InspectorField::LightIntensity, intensity);
        let range = add_property_drag_value(tree, grid, section, "Range", light.range, 0.1, 2);
        bind(bindings, entity, InspectorField::LightRange, range);
        let color_picker = add_property_color_picker(
            tree,
            grid,
            section,
            "Color",
            vec4(light.color.x, light.color.y, light.color.z, 1.0),
        );
        bind(bindings, entity, InspectorField::LightColor, color_picker);
        if matches!(light.light_type, LightType::Spot) {
            let inner = add_property_drag_value(
                tree,
                grid,
                section,
                "Inner cone (\u{00b0})",
                light.inner_cone_angle.to_degrees(),
                0.5,
                1,
            );
            bind(bindings, entity, InspectorField::LightInnerCone, inner);
            let outer = add_property_drag_value(
                tree,
                grid,
                section,
                "Outer cone (\u{00b0})",
                light.outer_cone_angle.to_degrees(),
                0.5,
                1,
            );
            bind(bindings, entity, InspectorField::LightOuterCone, outer);
        }
        if matches!(light.light_type, LightType::Area) {
            use nightshade::ecs::light::components::AreaLightShape;
            let shape_index = match light.area_shape {
                AreaLightShape::Rectangle => 0,
                AreaLightShape::Disk => 1,
                AreaLightShape::Sphere => 2,
                AreaLightShape::Tube => 3,
            };
            let shape_dropdown = add_property_dropdown(
                tree,
                grid,
                section,
                "Shape",
                &["Rectangle", "Disk", "Sphere", "Tube"],
                shape_index,
            );
            bind(
                bindings,
                entity,
                InspectorField::LightAreaShape,
                shape_dropdown,
            );
            let width =
                add_property_drag_value(tree, grid, section, "Width", light.area_width, 0.05, 2);
            bind(bindings, entity, InspectorField::LightAreaWidth, width);
            let height =
                add_property_drag_value(tree, grid, section, "Height", light.area_height, 0.05, 2);
            bind(bindings, entity, InspectorField::LightAreaHeight, height);
            let area_radius =
                add_property_drag_value(tree, grid, section, "Radius", light.area_radius, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::LightAreaRadius,
                area_radius,
            );
            let two_sided =
                add_property_checkbox(tree, grid, section, "Two sided", light.area_two_sided);
            bind(
                bindings,
                entity,
                InspectorField::LightAreaTwoSided,
                two_sided,
            );
            let emissive_text = light.area_emissive_texture.clone().unwrap_or_default();
            let emissive = add_property_text_input(
                tree,
                grid,
                section,
                "Emissive texture",
                &emissive_text,
                "path/to/emissive",
            );
            bind(
                bindings,
                entity,
                InspectorField::LightAreaEmissiveTexture,
                emissive,
            );
        }
        let cast = add_property_checkbox(tree, grid, section, "Cast shadows", light.cast_shadows);
        bind(bindings, entity, InspectorField::LightCastsShadow, cast);
        let shadow_bias = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow bias",
            light.shadow_bias,
            0.0001,
            5,
        );
        bind(
            bindings,
            entity,
            InspectorField::LightShadowBias,
            shadow_bias,
        );
        let shadow_resolution = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow resolution",
            light.shadow_resolution as f32,
            32.0,
            0,
        );
        bind(
            bindings,
            entity,
            InspectorField::LightShadowResolution,
            shadow_resolution,
        );
        let shadow_distance = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow distance",
            light.shadow_distance,
            0.5,
            2,
        );
        bind(
            bindings,
            entity,
            InspectorField::LightShadowDistance,
            shadow_distance,
        );
        let cookie_text = light.cookie_texture.clone().unwrap_or_default();
        let cookie = add_property_text_input(
            tree,
            grid,
            section,
            "Cookie texture",
            &cookie_text,
            "path/to/cookie",
        );
        bind(bindings, entity, InspectorField::LightCookieTexture, cookie);
    }

    populate_camera_section(tree, grid, entity, bindings);

    if let Some(mesh) = tree.world_mut().core.get_render_mesh(entity).cloned() {
        let section = tree.add_property_section(grid, "Mesh");
        add_property_value(tree, grid, section, "Name", &mesh.name);
    }

    populate_instanced_mesh_section(tree, grid, entity, bindings);
    populate_material_section(tree, grid, entity, bindings);

    if let Some(volume) = tree.world_mut().core.get_bounding_volume(entity).copied() {
        let half = volume.obb.half_extents;
        let extents = format!("{:.2} x {:.2} x {:.2}", half.x, half.y, half.z);
        let radius = format!("{:.2}", volume.sphere_radius);
        let section = tree.add_property_section(grid, "Bounds");
        add_property_value(tree, grid, section, "Half extents", &extents);
        add_property_value(tree, grid, section, "Sphere radius", &radius);
    }

    populate_animation_section(tree, grid, entity, bindings);
    populate_particle_section(tree, grid, entity, bindings);
    populate_decal_section(tree, grid, entity, bindings);
    populate_audio_section(tree, grid, entity, bindings);
    populate_rigid_body_section(tree, grid, entity, bindings);
    populate_collider_section(tree, grid, entity, bindings);
    populate_character_controller_section(tree, grid, entity, bindings);
    populate_navmesh_section(tree, grid, entity, bindings);
    populate_text_section(tree, grid, entity, bindings);
    populate_misc_section(tree, grid, entity, bindings, scene_snapshot);
    populate_remove_component_section(tree, grid, entity, bindings);
    populate_add_component_section(tree, grid, entity, bindings);
}

fn populate_world_sections(
    tree: &mut UiTreeBuilder,
    new_nodes: &mut Vec<Entity>,
    bindings: &mut HashMap<Entity, InspectorBinding>,
    scene_snapshot: &SceneStructureSnapshot,
) {
    let grid = tree.add_property_grid(120.0);
    new_nodes.push(grid);

    let graphics = tree.world_mut().resources.render_settings.clone();
    let debug = tree.world_mut().resources.debug_draw.clone();

    let mut props = PropertyContext {
        grid,
        section: tree.add_property_section(grid, "Lighting"),
        entity: Entity::default(),
        bindings: &mut *bindings,
    };
    props.color_picker(
        tree,
        InspectorField::AmbientLight,
        "Ambient",
        vec4(
            graphics.ambient_light[0],
            graphics.ambient_light[1],
            graphics.ambient_light[2],
            graphics.ambient_light[3],
        ),
    );

    props.section = tree.add_property_section(grid, "Bloom");
    props.checkbox(
        tree,
        InspectorField::BloomEnabled,
        "Enabled",
        graphics.bloom_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::BloomIntensity,
        "Intensity",
        graphics.bloom_intensity,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::BloomThreshold,
        "Threshold",
        graphics.bloom_threshold,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::BloomKnee,
        "Knee",
        graphics.bloom_knee,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::BloomFilterRadius,
        "Filter Radius",
        graphics.bloom_filter_radius,
        0.001,
        4,
    );

    props.section = tree.add_property_section(grid, "SSAO");
    props.checkbox(
        tree,
        InspectorField::SsaoEnabled,
        "Enabled",
        graphics.ssao_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::SsaoRadius,
        "Radius",
        graphics.ssao_radius,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsaoIntensity,
        "Intensity",
        graphics.ssao_intensity,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsaoBias,
        "Bias",
        graphics.ssao_bias,
        0.001,
        4,
    );
    props.drag_value(
        tree,
        InspectorField::SsaoSampleCount,
        "Samples",
        graphics.ssao_sample_count as f32,
        1.0,
        0,
    );
    props.checkbox(
        tree,
        InspectorField::SsaoVisualization,
        "Visualize",
        debug.ssao_visualization,
    );

    props.section = tree.add_property_section(grid, "SSGI");
    props.checkbox(
        tree,
        InspectorField::SsgiEnabled,
        "Enabled",
        graphics.ssgi_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::SsgiRadius,
        "Radius",
        graphics.ssgi_radius,
        0.1,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsgiIntensity,
        "Intensity",
        graphics.ssgi_intensity,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsgiMaxSteps,
        "Max Steps",
        graphics.ssgi_max_steps as f32,
        1.0,
        0,
    );

    props.section = tree.add_property_section(grid, "SSR");
    props.checkbox(
        tree,
        InspectorField::SsrEnabled,
        "Enabled",
        graphics.ssr_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::SsrMaxSteps,
        "Max Steps",
        graphics.ssr_max_steps as f32,
        1.0,
        0,
    );
    props.drag_value(
        tree,
        InspectorField::SsrThickness,
        "Thickness",
        graphics.ssr_thickness,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::SsrMaxDistance,
        "Max Distance",
        graphics.ssr_max_distance,
        0.5,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsrStride,
        "Stride",
        graphics.ssr_stride,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsrFadeStart,
        "Fade Start",
        graphics.ssr_fade_start,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsrFadeEnd,
        "Fade End",
        graphics.ssr_fade_end,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::SsrIntensity,
        "Intensity",
        graphics.ssr_intensity,
        0.05,
        2,
    );

    props.section = tree.add_property_section(grid, "Fog");
    props.checkbox(
        tree,
        InspectorField::FogEnabled,
        "Enabled",
        graphics.fog.is_some(),
    );
    if let Some(fog) = graphics.fog.as_ref() {
        props.color_picker(
            tree,
            InspectorField::FogColor,
            "Color",
            vec4(fog.color[0], fog.color[1], fog.color[2], 1.0),
        );
        props.drag_value(tree, InspectorField::FogStart, "Start", fog.start, 0.1, 2);
        props.drag_value(tree, InspectorField::FogEnd, "End", fog.end, 0.1, 2);
    }

    props.section = tree.add_property_section(grid, "Depth of Field");
    props.checkbox(
        tree,
        InspectorField::DofEnabled,
        "Enabled",
        graphics.depth_of_field.enabled,
    );
    props.drag_value(
        tree,
        InspectorField::DofFocusDistance,
        "Focus Distance",
        graphics.depth_of_field.focus_distance,
        0.1,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::DofFocusRange,
        "Focus Range",
        graphics.depth_of_field.focus_range,
        0.1,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::DofMaxBlurRadius,
        "Max Blur",
        graphics.depth_of_field.max_blur_radius,
        0.1,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::DofBokehThreshold,
        "Bokeh Threshold",
        graphics.depth_of_field.bokeh_threshold,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::DofBokehIntensity,
        "Bokeh Intensity",
        graphics.depth_of_field.bokeh_intensity,
        0.05,
        2,
    );
    let dof_quality_index = match graphics.depth_of_field.quality {
        nightshade::ecs::graphics::resources::DepthOfFieldQuality::Low => 0,
        nightshade::ecs::graphics::resources::DepthOfFieldQuality::Medium => 1,
        nightshade::ecs::graphics::resources::DepthOfFieldQuality::High => 2,
    };
    props.dropdown(
        tree,
        InspectorField::DofQuality,
        "Quality",
        &["Low", "Medium", "High"],
        dof_quality_index,
    );

    props.section = tree.add_property_section(grid, "Post Processing");
    props.checkbox(
        tree,
        InspectorField::FxaaEnabled,
        "FXAA",
        graphics.fxaa_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::RenderScale,
        "Render Scale",
        graphics.render_scale,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::IblBlendFactor,
        "IBL Blend",
        graphics.ibl_blend_factor,
        0.01,
        2,
    );
    props.checkbox(
        tree,
        InspectorField::UnlitMode,
        "Unlit",
        graphics.unlit_mode,
    );
    let pbr_options: Vec<&str> = nightshade::ecs::graphics::resources::PbrDebugMode::ALL
        .iter()
        .map(|mode| mode.name())
        .collect();
    let pbr_index = nightshade::ecs::graphics::resources::PbrDebugMode::ALL
        .iter()
        .position(|mode| *mode == debug.pbr_debug_mode)
        .unwrap_or(0);
    props.dropdown(
        tree,
        InspectorField::PbrDebug,
        "PBR Debug",
        &pbr_options,
        pbr_index,
    );

    props.section = tree.add_property_section(grid, "Selection Outline");
    props.checkbox(
        tree,
        InspectorField::SelectionOutlineEnabled,
        "Enabled",
        debug.selection_outline_enabled,
    );
    props.color_picker(
        tree,
        InspectorField::SelectionOutlineColor,
        "Color",
        vec4(
            debug.selection_outline_color[0],
            debug.selection_outline_color[1],
            debug.selection_outline_color[2],
            debug.selection_outline_color[3],
        ),
    );

    props.section = tree.add_property_section(grid, "Color Grading");
    props.drag_value(
        tree,
        InspectorField::Exposure,
        "Exposure",
        graphics.color_grading.exposure,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Saturation,
        "Saturation",
        graphics.color_grading.saturation,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Contrast,
        "Contrast",
        graphics.color_grading.contrast,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Brightness,
        "Brightness",
        graphics.color_grading.brightness,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Gamma,
        "Gamma",
        graphics.color_grading.gamma,
        0.01,
        2,
    );

    populate_scene_structure_section(tree, grid, bindings, scene_snapshot);
}

fn populate_scene_structure_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
    snapshot: &SceneStructureSnapshot,
) {
    let mut props = PropertyContext {
        grid,
        section: tree.add_property_section(grid, "Scene Layers"),
        entity: Entity::default(),
        bindings,
    };
    props.button(
        tree,
        InspectorField::Misc(MiscField::SceneAddLayer),
        "Layers",
        "Add layer",
    );
    for layer in &snapshot.layers {
        props.text_input(
            tree,
            InspectorField::Misc(MiscField::SceneLayerName(layer.id.0)),
            &format!("Layer {}", layer.id.0),
            &layer.name,
            "name",
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneLayerOrder(layer.id.0)),
            "Order",
            layer.order as f32,
            1.0,
            0,
        );
        props.checkbox(
            tree,
            InspectorField::Misc(MiscField::SceneLayerEnabled(layer.id.0)),
            "Enabled",
            layer.enabled,
        );
        props.button(
            tree,
            InspectorField::Misc(MiscField::SceneRemoveLayer(layer.id.0)),
            "Remove",
            "Remove",
        );
    }

    props.section = tree.add_property_section(grid, "Scene Chunks");
    props.button(
        tree,
        InspectorField::Misc(MiscField::SceneAddChunk),
        "Chunks",
        "Add chunk",
    );
    for chunk in &snapshot.chunks {
        props.text_input(
            tree,
            InspectorField::Misc(MiscField::SceneChunkName(chunk.id.0)),
            &format!("Chunk {}", chunk.id.0),
            &chunk.name,
            "name",
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkLoadDistance(chunk.id.0)),
            "Load distance",
            chunk.load_distance,
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkUnloadDistance(chunk.id.0)),
            "Unload distance",
            chunk.unload_distance,
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMinX(chunk.id.0)),
            "Bounds Min X",
            chunk.bounds_min[0],
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMinY(chunk.id.0)),
            "Bounds Min Y",
            chunk.bounds_min[1],
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMinZ(chunk.id.0)),
            "Bounds Min Z",
            chunk.bounds_min[2],
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMaxX(chunk.id.0)),
            "Bounds Max X",
            chunk.bounds_max[0],
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMaxY(chunk.id.0)),
            "Bounds Max Y",
            chunk.bounds_max[1],
            0.5,
            2,
        );
        props.drag_value(
            tree,
            InspectorField::Misc(MiscField::SceneChunkBoundsMaxZ(chunk.id.0)),
            "Bounds Max Z",
            chunk.bounds_max[2],
            0.5,
            2,
        );
        props.button(
            tree,
            InspectorField::Misc(MiscField::SceneRemoveChunk(chunk.id.0)),
            "Remove",
            "Remove",
        );
    }

    props.section = tree.add_property_section(grid, "Joints");
    add_property_value(
        tree,
        grid,
        props.section,
        "Hint",
        "Select two entities, then add a joint.",
    );
    let kinds: [(&str, MiscField); 6] = [
        ("Fixed", MiscField::AddFixedJoint),
        ("Revolute", MiscField::AddRevoluteJoint),
        ("Prismatic", MiscField::AddPrismaticJoint),
        ("Spherical", MiscField::AddSphericalJoint),
        ("Rope", MiscField::AddRopeJoint),
        ("Spring", MiscField::AddSpringJoint),
    ];
    for (label, field) in kinds {
        props.button(tree, InspectorField::Misc(field), label, "Add");
    }
    for (index, joint) in snapshot.joints.iter().enumerate() {
        let label_text = format!(
            "{} #{} parent={} child={}",
            scene_joint_kind_name(&joint.joint),
            index,
            joint.parent_entity,
            joint.child_entity
        );
        add_property_value(tree, grid, props.section, "Joint", &label_text);
        props.checkbox(
            tree,
            InspectorField::Misc(MiscField::ToggleJointCollisions(index)),
            "Collisions",
            joint.collisions_enabled,
        );
        props.button(
            tree,
            InspectorField::Misc(MiscField::RemoveJoint(index)),
            "Remove",
            "Remove",
        );
    }
}

fn scene_joint_kind_name(joint: &nightshade::ecs::scene::SceneJoint) -> &'static str {
    use nightshade::ecs::scene::SceneJoint;
    match joint {
        SceneJoint::Fixed { .. } => "Fixed",
        SceneJoint::Revolute { .. } => "Revolute",
        SceneJoint::Prismatic { .. } => "Prismatic",
        SceneJoint::Spherical { .. } => "Spherical",
        SceneJoint::Rope { .. } => "Rope",
        SceneJoint::Spring { .. } => "Spring",
    }
}

fn bind(
    bindings: &mut HashMap<Entity, InspectorBinding>,
    entity: Entity,
    field: InspectorField,
    widget: Entity,
) {
    bindings.insert(widget, InspectorBinding { entity, field });
}

fn add_property_value(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    value: &str,
) {
    let row = tree.add_property_row(grid, section, label);
    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size * 0.9;
    let color = theme.text_color;
    tree.in_parent(row, |tree| {
        tree.add_node()
            .size(100.pct(), (22.0).px())
            .flex_grow(1.0)
            .with_text(value, font)
            .text_left()
            .with_text_overflow(TextOverflow::Ellipsis)
            .color_raw::<UiBase>(color)
            .entity();
    });
}

fn add_property_drag_value(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    initial: f32,
    speed: f32,
    precision: usize,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    tree.in_parent(row, |tree| {
        widget = tree.add_drag_value_configured(
            DragValueConfig::new(f32::MIN, f32::MAX, initial)
                .speed(speed)
                .precision(precision),
        );
    });
    widget
}

fn add_property_checkbox(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    initial: bool,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    tree.in_parent(row, |tree| {
        widget = tree.add_checkbox("", initial);
    });
    widget
}

fn add_property_text_input(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    initial: &str,
    placeholder: &str,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    tree.in_parent(row, |tree| {
        widget = tree.add_text_input_with_value(placeholder, initial);
    });
    widget
}

fn add_property_color_picker(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    initial: Vec4,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    tree.in_parent(row, |tree| {
        widget = tree.add_color_picker(initial);
    });
    widget
}

fn add_property_dropdown(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    options: &[&str],
    initial: usize,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    tree.in_parent(row, |tree| {
        widget = tree.add_dropdown(options, initial);
    });
    widget
}

fn add_property_button(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    section: Entity,
    label: &str,
    button_text: &str,
) -> Entity {
    let row = tree.add_property_row(grid, section, label);
    let mut widget = Entity::default();
    let theme = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme();
    let font = theme.font_size * 0.85;
    tree.in_parent(row, |tree| {
        widget = tree
            .add_node()
            .size(100.pct(), (24.0).px())
            .with_text(button_text, font)
            .text_center()
            .color_raw::<UiBase>(vec4(0.75, 0.75, 0.8, 1.0))
            .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
            .with_interaction()
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .entity();
    });
    widget
}

struct PropertyContext<'a> {
    grid: Entity,
    section: Entity,
    entity: Entity,
    bindings: &'a mut HashMap<Entity, InspectorBinding>,
}

impl PropertyContext<'_> {
    fn drag_value(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        value: f32,
        speed: f32,
        precision: usize,
    ) {
        let widget = add_property_drag_value(
            tree,
            self.grid,
            self.section,
            label,
            value,
            speed,
            precision,
        );
        bind(self.bindings, self.entity, field, widget);
    }

    fn checkbox(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        value: bool,
    ) {
        let widget = add_property_checkbox(tree, self.grid, self.section, label, value);
        bind(self.bindings, self.entity, field, widget);
    }

    fn text_input(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        value: &str,
        placeholder: &str,
    ) {
        let widget =
            add_property_text_input(tree, self.grid, self.section, label, value, placeholder);
        bind(self.bindings, self.entity, field, widget);
    }

    fn color_picker(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        value: Vec4,
    ) {
        let widget = add_property_color_picker(tree, self.grid, self.section, label, value);
        bind(self.bindings, self.entity, field, widget);
    }

    fn dropdown(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        options: &[&str],
        initial: usize,
    ) {
        let widget = add_property_dropdown(tree, self.grid, self.section, label, options, initial);
        bind(self.bindings, self.entity, field, widget);
    }

    fn button(
        &mut self,
        tree: &mut UiTreeBuilder,
        field: InspectorField,
        label: &str,
        button_text: &str,
    ) {
        let widget = add_property_button(tree, self.grid, self.section, label, button_text);
        bind(self.bindings, self.entity, field, widget);
    }
}

fn populate_camera_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(camera) = tree.world_mut().core.get_camera(entity).copied() else {
        return;
    };
    let section = tree.add_property_section(grid, "Camera");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    let active = tree.world_mut().resources.active_camera == Some(entity);
    add_property_value(
        tree,
        grid,
        section,
        "Active",
        if active { "yes" } else { "no" },
    );
    props.button(
        tree,
        InspectorField::Camera(CameraField::SetActive),
        "Activate",
        "Make active camera",
    );

    let kind_index = match camera.projection {
        nightshade::ecs::camera::components::Projection::Perspective(_) => 0,
        nightshade::ecs::camera::components::Projection::Orthographic(_) => 1,
    };
    props.dropdown(
        tree,
        InspectorField::Camera(CameraField::ProjectionKind),
        "Projection",
        &["Perspective", "Orthographic"],
        kind_index,
    );

    match camera.projection {
        nightshade::ecs::camera::components::Projection::Perspective(perspective) => {
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::FovDegrees),
                "FOV (\u{00b0})",
                perspective.y_fov_rad.to_degrees(),
                0.5,
                1,
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::NearPlane),
                "Near",
                perspective.z_near,
                0.001,
                4,
            );
            props.checkbox(
                tree,
                InspectorField::Camera(CameraField::FarOverride),
                "Far override",
                perspective.z_far.is_some(),
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::FarValue),
                "Far",
                perspective.z_far.unwrap_or(1000.0),
                0.5,
                2,
            );
            props.checkbox(
                tree,
                InspectorField::Camera(CameraField::AspectOverride),
                "Aspect override",
                perspective.aspect_ratio.is_some(),
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::AspectValue),
                "Aspect",
                perspective.aspect_ratio.unwrap_or(16.0 / 9.0),
                0.01,
                3,
            );
        }
        nightshade::ecs::camera::components::Projection::Orthographic(ortho) => {
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::OrthoX),
                "X mag",
                ortho.x_mag,
                0.05,
                2,
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::OrthoY),
                "Y mag",
                ortho.y_mag,
                0.05,
                2,
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::OrthoNear),
                "Near",
                ortho.z_near,
                0.001,
                4,
            );
            props.drag_value(
                tree,
                InspectorField::Camera(CameraField::OrthoFar),
                "Far",
                ortho.z_far,
                0.5,
                2,
            );
        }
    }
}

fn populate_material_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(material_ref) = tree.world_mut().core.get_material_ref(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Material");
    add_property_value(tree, grid, section, "Name", &material_ref.name);

    let material = nightshade::ecs::material::resources::material_registry_iter(
        &tree.world_mut().resources.assets.material_registry,
    )
    .find(|(name, _)| name.as_str() == material_ref.name.as_str())
    .map(|(_, material)| material.clone());
    let Some(material) = material else {
        add_property_value(tree, grid, section, "Status", "(not in registry)");
        return;
    };

    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.color_picker(
        tree,
        InspectorField::Material(MaterialField::BaseColor),
        "Base color",
        vec4(
            material.base_color[0],
            material.base_color[1],
            material.base_color[2],
            material.base_color[3],
        ),
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::Metallic),
        "Metallic",
        material.metallic,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::Roughness),
        "Roughness",
        material.roughness,
        0.01,
        3,
    );
    props.color_picker(
        tree,
        InspectorField::Material(MaterialField::EmissiveFactor),
        "Emissive",
        vec4(
            material.emissive_factor[0],
            material.emissive_factor[1],
            material.emissive_factor[2],
            1.0,
        ),
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::EmissiveStrength),
        "Emissive strength",
        material.emissive_strength,
        0.05,
        2,
    );
    let alpha_index = match material.alpha_mode {
        nightshade::ecs::material::components::AlphaMode::Opaque => 0,
        nightshade::ecs::material::components::AlphaMode::Mask => 1,
        nightshade::ecs::material::components::AlphaMode::Blend => 2,
    };
    props.dropdown(
        tree,
        InspectorField::Material(MaterialField::AlphaMode),
        "Alpha mode",
        &["Opaque", "Mask", "Blend"],
        alpha_index,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::AlphaCutoff),
        "Alpha cutoff",
        material.alpha_cutoff,
        0.01,
        3,
    );
    props.checkbox(
        tree,
        InspectorField::Material(MaterialField::Unlit),
        "Unlit",
        material.unlit,
    );
    props.checkbox(
        tree,
        InspectorField::Material(MaterialField::DoubleSided),
        "Double sided",
        material.double_sided,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::NormalScale),
        "Normal scale",
        material.normal_scale,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::OcclusionStrength),
        "Occlusion",
        material.occlusion_strength,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::Ior),
        "IOR",
        material.ior,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::Transmission),
        "Transmission",
        material.transmission_factor,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::Thickness),
        "Thickness",
        material.thickness,
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::ClearcoatFactor),
        "Clearcoat",
        material.clearcoat_factor,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::ClearcoatRoughness),
        "Clearcoat rough",
        material.clearcoat_roughness_factor,
        0.01,
        3,
    );
    props.color_picker(
        tree,
        InspectorField::Material(MaterialField::SheenColor),
        "Sheen color",
        vec4(
            material.sheen_color_factor[0],
            material.sheen_color_factor[1],
            material.sheen_color_factor[2],
            1.0,
        ),
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::SheenRoughness),
        "Sheen rough",
        material.sheen_roughness_factor,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::IridescenceFactor),
        "Iridescence",
        material.iridescence_factor,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Material(MaterialField::AnisotropyStrength),
        "Anisotropy",
        material.anisotropy_strength,
        0.01,
        3,
    );
}

fn populate_animation_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(player) = tree.world_mut().core.get_animation_player(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Animation");
    add_property_value(
        tree,
        grid,
        section,
        "Clips",
        &player.clips.len().to_string(),
    );
    if player.clips.is_empty() {
        return;
    }
    let names: Vec<String> = player
        .clips
        .iter()
        .enumerate()
        .map(|(index, clip)| {
            if clip.name.is_empty() {
                format!("Clip {index}")
            } else {
                clip.name.clone()
            }
        })
        .collect();
    let name_refs: Vec<&str> = names.iter().map(String::as_str).collect();
    let initial = player.current_clip.unwrap_or(0).min(player.clips.len() - 1);
    let dropdown = add_property_dropdown(tree, grid, section, "Clip", &name_refs, initial);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::ClipSelect),
        dropdown,
    );
    let playing = add_property_checkbox(tree, grid, section, "Playing", player.playing);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::Playing),
        playing,
    );
    let looping = add_property_checkbox(tree, grid, section, "Loop", player.looping);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::Looping),
        looping,
    );
    let play_all = add_property_checkbox(tree, grid, section, "Play all", player.play_all);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::PlayAll),
        play_all,
    );
    let speed = add_property_drag_value(tree, grid, section, "Speed", player.speed, 0.05, 2);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::Speed),
        speed,
    );
    let time = add_property_drag_value(tree, grid, section, "Time", player.time, 0.01, 3);
    bind(
        bindings,
        entity,
        InspectorField::Animation(AnimationField::Time),
        time,
    );
}

fn populate_particle_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(emitter) = tree.world_mut().core.get_particle_emitter(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Particles");

    let enabled = add_property_checkbox(tree, grid, section, "Enabled", emitter.enabled);
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::Enabled),
        enabled,
    );
    let type_index = match emitter.emitter_type {
        nightshade::ecs::particles::components::EmitterType::Firework => 0,
        nightshade::ecs::particles::components::EmitterType::Fire => 1,
        nightshade::ecs::particles::components::EmitterType::Smoke => 2,
        nightshade::ecs::particles::components::EmitterType::Sparks => 3,
        nightshade::ecs::particles::components::EmitterType::Trail => 4,
    };
    let type_widget = add_property_dropdown(
        tree,
        grid,
        section,
        "Type",
        &["Firework", "Fire", "Smoke", "Sparks", "Trail"],
        type_index,
    );
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::EmitterType),
        type_widget,
    );
    let shape_index = match emitter.shape {
        nightshade::ecs::particles::components::EmitterShape::Point => 0,
        nightshade::ecs::particles::components::EmitterShape::Sphere { .. } => 1,
        nightshade::ecs::particles::components::EmitterShape::Cone { .. } => 2,
        nightshade::ecs::particles::components::EmitterShape::Box { .. } => 3,
    };
    let shape_widget = add_property_dropdown(
        tree,
        grid,
        section,
        "Shape",
        &["Point", "Sphere", "Cone", "Box"],
        shape_index,
    );
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::Shape),
        shape_widget,
    );
    match emitter.shape {
        nightshade::ecs::particles::components::EmitterShape::Point => {}
        nightshade::ecs::particles::components::EmitterShape::Sphere { radius } => {
            let widget = add_property_drag_value(tree, grid, section, "Radius", radius, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeRadius),
                widget,
            );
        }
        nightshade::ecs::particles::components::EmitterShape::Cone { angle, height } => {
            let angle_widget =
                add_property_drag_value(tree, grid, section, "Cone angle (rad)", angle, 0.01, 3);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeAngle),
                angle_widget,
            );
            let height_widget =
                add_property_drag_value(tree, grid, section, "Cone height", height, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeHeight),
                height_widget,
            );
        }
        nightshade::ecs::particles::components::EmitterShape::Box { half_extents } => {
            let x = add_property_drag_value(tree, grid, section, "Box X", half_extents.x, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeBoxX),
                x,
            );
            let y = add_property_drag_value(tree, grid, section, "Box Y", half_extents.y, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeBoxY),
                y,
            );
            let z = add_property_drag_value(tree, grid, section, "Box Z", half_extents.z, 0.05, 2);
            bind(
                bindings,
                entity,
                InspectorField::Particle(ParticleField::ShapeBoxZ),
                z,
            );
        }
    }
    let scalar_fields: &[(&str, ParticleField, f32, f32, usize)] = &[
        (
            "Pos X",
            ParticleField::PositionX,
            emitter.position.x,
            0.05,
            2,
        ),
        (
            "Pos Y",
            ParticleField::PositionY,
            emitter.position.y,
            0.05,
            2,
        ),
        (
            "Pos Z",
            ParticleField::PositionZ,
            emitter.position.z,
            0.05,
            2,
        ),
        (
            "Dir X",
            ParticleField::DirectionX,
            emitter.direction.x,
            0.05,
            3,
        ),
        (
            "Dir Y",
            ParticleField::DirectionY,
            emitter.direction.y,
            0.05,
            3,
        ),
        (
            "Dir Z",
            ParticleField::DirectionZ,
            emitter.direction.z,
            0.05,
            3,
        ),
        (
            "Spawn rate",
            ParticleField::SpawnRate,
            emitter.spawn_rate,
            1.0,
            1,
        ),
        (
            "Burst count",
            ParticleField::BurstCount,
            emitter.burst_count as f32,
            1.0,
            0,
        ),
        (
            "Lifetime min",
            ParticleField::LifetimeMin,
            emitter.particle_lifetime_min,
            0.05,
            2,
        ),
        (
            "Lifetime max",
            ParticleField::LifetimeMax,
            emitter.particle_lifetime_max,
            0.05,
            2,
        ),
        (
            "Velocity min",
            ParticleField::VelocityMin,
            emitter.initial_velocity_min,
            0.05,
            2,
        ),
        (
            "Velocity max",
            ParticleField::VelocityMax,
            emitter.initial_velocity_max,
            0.05,
            2,
        ),
        (
            "Spread (rad)",
            ParticleField::VelocitySpread,
            emitter.velocity_spread,
            0.01,
            3,
        ),
        (
            "Gravity X",
            ParticleField::GravityX,
            emitter.gravity.x,
            0.05,
            2,
        ),
        (
            "Gravity Y",
            ParticleField::GravityY,
            emitter.gravity.y,
            0.05,
            2,
        ),
        (
            "Gravity Z",
            ParticleField::GravityZ,
            emitter.gravity.z,
            0.05,
            2,
        ),
        ("Drag", ParticleField::Drag, emitter.drag, 0.01, 3),
        (
            "Size start",
            ParticleField::SizeStart,
            emitter.size_start,
            0.01,
            3,
        ),
        (
            "Size end",
            ParticleField::SizeEnd,
            emitter.size_end,
            0.01,
            3,
        ),
        (
            "Emissive",
            ParticleField::EmissiveStrength,
            emitter.emissive_strength,
            0.1,
            2,
        ),
    ];
    for &(label, field, value, step, precision) in scalar_fields {
        let widget = add_property_drag_value(tree, grid, section, label, value, step, precision);
        bind(bindings, entity, InspectorField::Particle(field), widget);
    }
    let one_shot = add_property_checkbox(tree, grid, section, "One shot", emitter.one_shot);
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::OneShot),
        one_shot,
    );
    let post_fields: &[(&str, ParticleField, f32, f32, usize)] = &[
        (
            "Turbulence",
            ParticleField::TurbulenceStrength,
            emitter.turbulence_strength,
            0.01,
            3,
        ),
        (
            "Turbulence freq",
            ParticleField::TurbulenceFrequency,
            emitter.turbulence_frequency,
            0.01,
            3,
        ),
        (
            "Texture index",
            ParticleField::TextureIndex,
            emitter.texture_index as f32,
            1.0,
            0,
        ),
    ];
    for &(label, field, value, step, precision) in post_fields {
        let widget = add_property_drag_value(tree, grid, section, label, value, step, precision);
        bind(bindings, entity, InspectorField::Particle(field), widget);
    }

    let size_curve = emitter.size_curve.clone();
    let opacity_curve = emitter.opacity_curve.clone();
    let death_sub = emitter.death_sub_emitter.clone().unwrap_or_default();
    let trail_sub = emitter.trail_sub_emitter.clone().unwrap_or_default();
    let trail_interval = emitter.trail_spawn_interval;

    let size_section = tree.add_property_section(grid, "Size Curve");
    for (index, key) in size_curve.iter().enumerate() {
        let time_widget = add_property_drag_value(
            tree,
            grid,
            size_section,
            &format!("Time {}", index),
            key.0,
            0.05,
            2,
        );
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::SizeCurveTime(index)),
            time_widget,
        );
        let value_widget = add_property_drag_value(
            tree,
            grid,
            size_section,
            &format!("Size {}", index),
            key.1,
            0.01,
            3,
        );
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::SizeCurveValue(index)),
            value_widget,
        );
        let remove_widget = add_property_button(tree, grid, size_section, "Remove key", "Remove");
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::RemoveSizeCurveKey(index)),
            remove_widget,
        );
    }
    let add_size_widget = add_property_button(tree, grid, size_section, "Size keys", "Add key");
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::AddSizeCurveKey),
        add_size_widget,
    );

    let opacity_section = tree.add_property_section(grid, "Opacity Curve");
    for (index, key) in opacity_curve.iter().enumerate() {
        let time_widget = add_property_drag_value(
            tree,
            grid,
            opacity_section,
            &format!("Time {}", index),
            key.0,
            0.05,
            2,
        );
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::OpacityCurveTime(index)),
            time_widget,
        );
        let value_widget = add_property_drag_value(
            tree,
            grid,
            opacity_section,
            &format!("Opacity {}", index),
            key.1,
            0.05,
            2,
        );
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::OpacityCurveValue(index)),
            value_widget,
        );
        let remove_widget =
            add_property_button(tree, grid, opacity_section, "Remove key", "Remove");
        bind(
            bindings,
            entity,
            InspectorField::Particle(ParticleField::RemoveOpacityCurveKey(index)),
            remove_widget,
        );
    }
    let add_opacity_widget =
        add_property_button(tree, grid, opacity_section, "Opacity keys", "Add key");
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::AddOpacityCurveKey),
        add_opacity_widget,
    );

    let sub_section = tree.add_property_section(grid, "Sub-emitters");
    let death = add_property_text_input(
        tree,
        grid,
        sub_section,
        "On death",
        &death_sub,
        "prefab path",
    );
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::DeathSubEmitter),
        death,
    );
    let trail =
        add_property_text_input(tree, grid, sub_section, "Trail", &trail_sub, "prefab path");
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::TrailSubEmitter),
        trail,
    );
    let trail_interval_widget = add_property_drag_value(
        tree,
        grid,
        sub_section,
        "Trail interval",
        trail_interval,
        0.01,
        3,
    );
    bind(
        bindings,
        entity,
        InspectorField::Particle(ParticleField::TrailSpawnInterval),
        trail_interval_widget,
    );
}

fn populate_decal_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(decal) = tree.world_mut().core.get_decal(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Decal");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.text_input(
        tree,
        InspectorField::Decal(DecalField::Texture),
        "Texture",
        decal.texture.as_deref().unwrap_or(""),
        "path",
    );
    props.text_input(
        tree,
        InspectorField::Decal(DecalField::EmissiveTexture),
        "Emissive tex",
        decal.emissive_texture.as_deref().unwrap_or(""),
        "path",
    );
    props.color_picker(
        tree,
        InspectorField::Decal(DecalField::Color),
        "Color",
        vec4(
            decal.color[0],
            decal.color[1],
            decal.color[2],
            decal.color[3],
        ),
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::EmissiveStrength),
        "Emissive",
        decal.emissive_strength,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::SizeX),
        "Size X",
        decal.size.x,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::SizeY),
        "Size Y",
        decal.size.y,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::Depth),
        "Depth",
        decal.depth,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::NormalThreshold),
        "Normal threshold",
        decal.normal_threshold,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::FadeStart),
        "Fade start",
        decal.fade_start,
        0.5,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Decal(DecalField::FadeEnd),
        "Fade end",
        decal.fade_end,
        0.5,
        2,
    );
}

fn populate_audio_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(source) = tree.world_mut().core.get_audio_source(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Audio");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.text_input(
        tree,
        InspectorField::Audio(AudioField::AudioRef),
        "Reference",
        source.audio_ref.as_deref().unwrap_or(""),
        "audio path",
    );
    props.drag_value(
        tree,
        InspectorField::Audio(AudioField::Volume),
        "Volume",
        source.volume,
        0.01,
        3,
    );
    props.checkbox(
        tree,
        InspectorField::Audio(AudioField::Looping),
        "Loop",
        source.looping,
    );
    props.checkbox(
        tree,
        InspectorField::Audio(AudioField::Playing),
        "Playing",
        source.playing,
    );
    props.checkbox(
        tree,
        InspectorField::Audio(AudioField::Spatial),
        "Spatial",
        source.spatial,
    );
    let bus_index = match source.bus {
        nightshade::ecs::audio::components::AudioBus::Master => 0,
        nightshade::ecs::audio::components::AudioBus::Music => 1,
        nightshade::ecs::audio::components::AudioBus::Sfx => 2,
        nightshade::ecs::audio::components::AudioBus::Ambient => 3,
        nightshade::ecs::audio::components::AudioBus::Voice => 4,
        nightshade::ecs::audio::components::AudioBus::Ui => 5,
    };
    props.dropdown(
        tree,
        InspectorField::Audio(AudioField::Bus),
        "Bus",
        &["Master", "Music", "Sfx", "Ambient", "Voice", "Ui"],
        bus_index,
    );
    props.drag_value(
        tree,
        InspectorField::Audio(AudioField::MinDistance),
        "Min distance",
        source.min_distance,
        0.1,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::Audio(AudioField::MaxDistance),
        "Max distance",
        source.max_distance,
        0.1,
        2,
    );

    let zones = source.reverb_zones.clone();
    for (index, (zone_name, zone_send)) in zones.iter().enumerate() {
        props.text_input(
            tree,
            InspectorField::Audio(AudioField::ReverbZoneName(index)),
            &format!("Reverb zone {}", index),
            zone_name,
            "zone name",
        );
        props.drag_value(
            tree,
            InspectorField::Audio(AudioField::ReverbZoneSend(index)),
            &format!("Reverb send {}", index),
            *zone_send,
            0.1,
            2,
        );
        props.button(
            tree,
            InspectorField::Audio(AudioField::RemoveReverbZone(index)),
            "Remove",
            "Remove",
        );
    }
    props.button(
        tree,
        InspectorField::Audio(AudioField::AddReverbZone),
        "Reverb zones",
        "Add zone",
    );

    let random_pick = source.random_pick;
    let random_clips = source.random_clips.clone();
    props.section = tree.add_property_section(grid, "Random Clips");
    props.checkbox(
        tree,
        InspectorField::Audio(AudioField::RandomPick),
        "Random pick",
        random_pick,
    );
    for (index, clip) in random_clips.iter().enumerate() {
        props.text_input(
            tree,
            InspectorField::Audio(AudioField::RandomClip(index)),
            &format!("Clip {}", index),
            clip,
            "path/to/clip",
        );
        props.button(
            tree,
            InspectorField::Audio(AudioField::RemoveRandomClip(index)),
            "Remove",
            "Remove",
        );
    }
    props.button(
        tree,
        InspectorField::Audio(AudioField::AddRandomClip),
        "Random clips",
        "Add clip",
    );
}

fn populate_rigid_body_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(body) = tree.world_mut().core.get_rigid_body(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Rigid body");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    let type_index = match body.body_type {
        nightshade::ecs::physics::types::RigidBodyType::Dynamic => 0,
        nightshade::ecs::physics::types::RigidBodyType::KinematicPositionBased => 1,
        nightshade::ecs::physics::types::RigidBodyType::KinematicVelocityBased => 2,
        nightshade::ecs::physics::types::RigidBodyType::Fixed => 3,
    };
    props.dropdown(
        tree,
        InspectorField::RigidBody(RigidBodyField::BodyType),
        "Type",
        &["Dynamic", "Kinematic Pos", "Kinematic Vel", "Fixed"],
        type_index,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::Mass),
        "Mass",
        body.mass,
        0.1,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::LinvelX),
        "Linvel X",
        body.linvel[0],
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::LinvelY),
        "Linvel Y",
        body.linvel[1],
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::LinvelZ),
        "Linvel Z",
        body.linvel[2],
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::AngvelX),
        "Angvel X",
        body.angvel[0],
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::AngvelY),
        "Angvel Y",
        body.angvel[1],
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::RigidBody(RigidBodyField::AngvelZ),
        "Angvel Z",
        body.angvel[2],
        0.05,
        3,
    );
    props.checkbox(
        tree,
        InspectorField::RigidBody(RigidBodyField::Ccd),
        "CCD",
        body.ccd_enabled,
    );
}

fn populate_collider_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(collider) = tree.world_mut().core.get_collider(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Collider");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    let shape_index = match &collider.shape {
        ColliderShape::Ball { .. } => 0,
        ColliderShape::Cuboid { .. } => 1,
        ColliderShape::Capsule { .. } => 2,
        ColliderShape::Cylinder { .. } => 3,
        ColliderShape::Cone { .. } => 4,
        ColliderShape::ConvexMesh { .. } => 5,
        ColliderShape::TriMesh { .. } => 6,
        ColliderShape::HeightField { .. } => 7,
    };
    props.dropdown(
        tree,
        InspectorField::Collider(ColliderField::Shape),
        "Shape",
        &[
            "Ball",
            "Cuboid",
            "Capsule",
            "Cylinder",
            "Cone",
            "ConvexMesh",
            "TriMesh",
            "HeightField",
        ],
        shape_index,
    );
    match &collider.shape {
        ColliderShape::Ball { radius } => {
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::BallRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::Cuboid { hx, hy, hz } => {
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CuboidX),
                "Half X",
                *hx,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CuboidY),
                "Half Y",
                *hy,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CuboidZ),
                "Half Z",
                *hz,
                0.05,
                3,
            );
        }
        ColliderShape::Capsule {
            half_height,
            radius,
        } => {
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CapsuleHalfHeight),
                "Half height",
                *half_height,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CapsuleRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::Cylinder {
            half_height,
            radius,
        } => {
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CylinderHalfHeight),
                "Half height",
                *half_height,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::CylinderRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::Cone {
            half_height,
            radius,
        } => {
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::ConeHalfHeight),
                "Half height",
                *half_height,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::Collider(ColliderField::ConeRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::ConvexMesh { vertices } => {
            add_property_value(tree, grid, section, "Vertices", &vertices.len().to_string());
        }
        ColliderShape::TriMesh { vertices, indices } => {
            add_property_value(tree, grid, section, "Vertices", &vertices.len().to_string());
            add_property_value(tree, grid, section, "Triangles", &indices.len().to_string());
        }
        ColliderShape::HeightField {
            nrows,
            ncols,
            heights,
            scale,
        } => {
            let _ = heights;
            let _ = scale;
            add_property_value(
                tree,
                grid,
                section,
                "Resolution",
                &format!("{nrows}x{ncols}"),
            );
        }
    }
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::Friction),
        "Friction",
        collider.friction,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::Restitution),
        "Restitution",
        collider.restitution,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::Density),
        "Density",
        collider.density,
        0.05,
        3,
    );
    props.checkbox(
        tree,
        InspectorField::Collider(ColliderField::IsSensor),
        "Sensor",
        collider.is_sensor,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::CollisionMemberships),
        "Collision Memberships",
        collider.collision_groups.memberships as f32,
        1.0,
        0,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::CollisionFilter),
        "Collision Filter",
        collider.collision_groups.filter as f32,
        1.0,
        0,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::SolverMemberships),
        "Solver Memberships",
        collider.solver_groups.memberships as f32,
        1.0,
        0,
    );
    props.drag_value(
        tree,
        InspectorField::Collider(ColliderField::SolverFilter),
        "Solver Filter",
        collider.solver_groups.filter as f32,
        1.0,
        0,
    );
}

fn populate_character_controller_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(controller) = tree
        .world_mut()
        .core
        .get_character_controller(entity)
        .cloned()
    else {
        return;
    };
    let section = tree.add_property_section(grid, "Character controller");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    let shape_index = match controller.shape {
        ColliderShape::Capsule { .. } => 0,
        ColliderShape::Ball { .. } => 1,
        ColliderShape::Cuboid { .. } => 2,
        _ => 0,
    };
    props.dropdown(
        tree,
        InspectorField::CharacterController(CharacterControllerField::Shape),
        "Shape",
        &["Capsule", "Ball", "Cuboid"],
        shape_index,
    );
    match &controller.shape {
        ColliderShape::Capsule {
            half_height,
            radius,
        } => {
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::CapsuleHalfHeight),
                "Half height",
                *half_height,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::CapsuleRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::Ball { radius } => {
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::BallRadius),
                "Radius",
                *radius,
                0.05,
                3,
            );
        }
        ColliderShape::Cuboid { hx, hy, hz } => {
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::CuboidX),
                "Half X",
                *hx,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::CuboidY),
                "Half Y",
                *hy,
                0.05,
                3,
            );
            props.drag_value(
                tree,
                InspectorField::CharacterController(CharacterControllerField::CuboidZ),
                "Half Z",
                *hz,
                0.05,
                3,
            );
        }
        _ => {}
    }
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::MaxSpeed),
        "Max speed",
        controller.max_speed,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::Acceleration),
        "Acceleration",
        controller.acceleration,
        0.5,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::JumpImpulse),
        "Jump impulse",
        controller.jump_impulse,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::Scale),
        "Scale",
        controller.scale,
        0.05,
        2,
    );
    props.checkbox(
        tree,
        InspectorField::CharacterController(CharacterControllerField::CrouchEnabled),
        "Crouch enabled",
        controller.crouch_enabled,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::CrouchSpeedMultiplier),
        "Crouch speed mult",
        controller.crouch_speed_multiplier,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::SprintSpeedMultiplier),
        "Sprint speed mult",
        controller.sprint_speed_multiplier,
        0.01,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::StandingHalfHeight),
        "Standing half h",
        controller.standing_half_height,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::CrouchingHalfHeight),
        "Crouching half h",
        controller.crouching_half_height,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::ConfigOffset),
        "Config offset",
        controller.config.offset,
        0.001,
        4,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::MaxSlopeClimb),
        "Max slope climb (\u{00b0})",
        controller.config.max_slope_climb_angle.to_degrees(),
        0.5,
        1,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::MinSlopeSlide),
        "Min slope slide (\u{00b0})",
        controller.config.min_slope_slide_angle.to_degrees(),
        0.5,
        1,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::FrictionRate),
        "Friction rate",
        controller.friction_rate,
        0.05,
        2,
    );
    props.drag_value(
        tree,
        InspectorField::CharacterController(CharacterControllerField::AboveMaxFrictionRate),
        "Above-max friction",
        controller.above_max_friction_rate,
        0.05,
        2,
    );
    props.checkbox(
        tree,
        InspectorField::CharacterController(CharacterControllerField::EngineInputEnabled),
        "Engine input",
        controller.engine_input_enabled,
    );
    add_property_value(
        tree,
        grid,
        section,
        "Grounded",
        if controller.grounded { "yes" } else { "no" },
    );
}

fn populate_navmesh_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(agent) = tree.world_mut().core.get_navmesh_agent(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Navmesh agent");
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.drag_value(
        tree,
        InspectorField::Navmesh(NavmeshField::MovementSpeed),
        "Speed",
        agent.movement_speed,
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Navmesh(NavmeshField::ArrivalThreshold),
        "Arrival threshold",
        agent.arrival_threshold,
        0.01,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Navmesh(NavmeshField::PathRecalculationThreshold),
        "Recalc threshold",
        agent.path_recalculation_threshold,
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Navmesh(NavmeshField::AgentRadius),
        "Radius",
        agent.agent_radius,
        0.05,
        3,
    );
    props.drag_value(
        tree,
        InspectorField::Navmesh(NavmeshField::AgentHeight),
        "Height",
        agent.agent_height,
        0.05,
        3,
    );
}

fn populate_text_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(text) = tree.world_mut().core.get_text(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Text");
    let content_string = tree
        .world_mut()
        .resources
        .text
        .cache
        .get_text(text.text_index)
        .unwrap_or("")
        .to_string();
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.text_input(
        tree,
        InspectorField::Text(TextField::Content),
        "Content",
        &content_string,
        "Text",
    );
    props.drag_value(
        tree,
        InspectorField::Text(TextField::FontSize),
        "Font size",
        text.properties.font_size,
        0.5,
        1,
    );
    props.color_picker(
        tree,
        InspectorField::Text(TextField::Color),
        "Color",
        vec4(
            text.properties.color.x,
            text.properties.color.y,
            text.properties.color.z,
            text.properties.color.w,
        ),
    );
}

fn populate_misc_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
    scene_snapshot: &SceneStructureSnapshot,
) {
    let scene_section = tree.add_property_section(grid, "Scene Layer & Chunk");
    let mut layer_options: Vec<String> = vec!["(none)".to_string()];
    let mut layer_index = 0usize;
    for (index, layer) in scene_snapshot.layers.iter().enumerate() {
        layer_options.push(layer.name.clone());
        if scene_snapshot.entity_layer == Some(layer.id) {
            layer_index = index + 1;
        }
    }
    let layer_refs: Vec<&str> = layer_options.iter().map(|s| s.as_str()).collect();
    let mut props = PropertyContext {
        grid,
        section: scene_section,
        entity,
        bindings,
    };
    props.dropdown(
        tree,
        InspectorField::Misc(MiscField::SceneLayerAssignment),
        "Layer",
        &layer_refs,
        layer_index,
    );
    let mut chunk_options: Vec<String> = vec!["(none)".to_string()];
    let mut chunk_index = 0usize;
    for (index, chunk) in scene_snapshot.chunks.iter().enumerate() {
        chunk_options.push(chunk.name.clone());
        if scene_snapshot.entity_chunk == Some(chunk.id) {
            chunk_index = index + 1;
        }
    }
    let chunk_refs: Vec<&str> = chunk_options.iter().map(|s| s.as_str()).collect();
    props.dropdown(
        tree,
        InspectorField::Misc(MiscField::SceneChunkAssignment),
        "Chunk",
        &chunk_refs,
        chunk_index,
    );

    let has_layer = tree.world_mut().core.entity_has_render_layer(entity);
    let has_mask = tree.world_mut().core.entity_has_culling_mask(entity);
    let has_cam_mask = tree.world_mut().core.entity_has_camera_culling_mask(entity);
    let has_ignore = tree.world_mut().core.entity_has_ignore_parent_scale(entity);
    if !has_layer && !has_mask && !has_cam_mask && !has_ignore {
        return;
    }
    props.section = tree.add_property_section(grid, "Layers & masks");
    if let Some(layer) = tree.world_mut().core.get_render_layer(entity).copied() {
        props.dropdown(
            tree,
            InspectorField::Misc(MiscField::RenderLayer),
            "Render layer",
            &["World", "Overlay"],
            if layer.0 == nightshade::ecs::primitives::RenderLayer::OVERLAY {
                1
            } else {
                0
            },
        );
    }
    if let Some(mask) = tree.world_mut().core.get_culling_mask(entity).copied() {
        for bit in 0u8..8 {
            let set = mask.0 & (1u32 << bit) != 0;
            props.checkbox(
                tree,
                InspectorField::Misc(MiscField::CullingMaskBit(bit)),
                &format!("Layer bit {bit}"),
                set,
            );
        }
    }
    if let Some(mask) = tree
        .world_mut()
        .core
        .get_camera_culling_mask(entity)
        .copied()
    {
        for bit in 0u8..8 {
            let set = mask.0 & (1u32 << bit) != 0;
            props.checkbox(
                tree,
                InspectorField::Misc(MiscField::CameraCullingMaskBit(bit)),
                &format!("Camera bit {bit}"),
                set,
            );
        }
    }
    if has_ignore {
        props.checkbox(
            tree,
            InspectorField::Misc(MiscField::IgnoreParentScale),
            "Ignore parent scale",
            true,
        );
    }
    if let Some(lines) = tree.world_mut().core.get_lines(entity) {
        let line_count = lines.lines.len();
        let always_on_top = lines.always_on_top;
        props.section = tree.add_property_section(grid, "Lines");
        add_property_value(
            tree,
            grid,
            props.section,
            "Segments",
            &line_count.to_string(),
        );
        props.checkbox(
            tree,
            InspectorField::Misc(MiscField::LinesAlwaysOnTop),
            "Always on top",
            always_on_top,
        );
        props.button(
            tree,
            InspectorField::Misc(MiscField::LinesClear),
            "Clear lines",
            "Clear",
        );
    }
}

/// A full-width clickable row labelled with `label`, parented under a property
/// section. Used for the add/remove component lists, whose component names are
/// too long for a property row's label column.
fn add_list_button(tree: &mut UiTreeBuilder, section: Entity, label: &str) -> Entity {
    let font = tree
        .world_mut()
        .resources
        .retained_ui
        .theme_state
        .active_theme()
        .font_size
        * 0.85;
    let mut widget = Entity::default();
    tree.in_parent(section, |tree| {
        widget = tree
            .add_node()
            .size(100.pct(), (22.0).px())
            .with_text(label, font)
            .text_left()
            .color_raw::<UiBase>(vec4(0.78, 0.78, 0.82, 1.0))
            .color_raw::<UiHover>(vec4(1.0, 1.0, 1.0, 1.0))
            .with_interaction()
            .with_cursor_icon(winit::window::CursorIcon::Pointer)
            .entity();
    });
    widget
}

fn populate_add_component_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let section = tree.add_property_section(grid, "Add component");
    for (kind, label, exists) in available_components(tree.world_mut(), entity) {
        if exists {
            continue;
        }
        let widget = add_list_button(tree, section, label);
        bind(bindings, entity, InspectorField::AddComponent(kind), widget);
    }
}

fn populate_remove_component_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let present: Vec<(ComponentKind, &'static str)> =
        available_components(tree.world_mut(), entity)
            .into_iter()
            .filter_map(|(kind, label, exists)| if exists { Some((kind, label)) } else { None })
            .collect();
    if present.is_empty() {
        return;
    }
    let section = tree.add_property_section(grid, "Remove component");
    for (kind, label) in present {
        let widget = add_list_button(tree, section, label);
        bind(
            bindings,
            entity,
            InspectorField::RemoveComponent(kind),
            widget,
        );
    }
}

fn available_components(
    world: &mut World,
    entity: Entity,
) -> Vec<(ComponentKind, &'static str, bool)> {
    vec![
        (
            ComponentKind::ParticleEmitter,
            "Particle emitter",
            world.core.entity_has_particle_emitter(entity),
        ),
        (
            ComponentKind::Decal,
            "Decal",
            world.core.entity_has_decal(entity),
        ),
        (
            ComponentKind::AudioSource,
            "Audio source",
            world.core.entity_has_audio_source(entity),
        ),
        (
            ComponentKind::RigidBody,
            "Rigid body",
            world.core.entity_has_rigid_body(entity),
        ),
        (
            ComponentKind::Collider,
            "Collider",
            world.core.entity_has_collider(entity),
        ),
        (
            ComponentKind::CharacterController,
            "Character controller",
            world.core.entity_has_character_controller(entity),
        ),
        (
            ComponentKind::NavmeshAgent,
            "Navmesh agent",
            world.core.entity_has_navmesh_agent(entity),
        ),
        (
            ComponentKind::Camera,
            "Camera",
            world.core.entity_has_camera(entity),
        ),
        (
            ComponentKind::Text,
            "Text",
            world.core.entity_has_text(entity),
        ),
        (
            ComponentKind::AnimationPlayer,
            "Animation player",
            world.core.entity_has_animation_player(entity),
        ),
        (
            ComponentKind::Light,
            "Light",
            world.core.entity_has_light(entity),
        ),
        (
            ComponentKind::Visibility,
            "Visibility",
            world.core.entity_has_visibility(entity),
        ),
        (
            ComponentKind::CastsShadow,
            "Casts shadow",
            world.core.entity_has_casts_shadow(entity),
        ),
        (
            ComponentKind::RenderLayer,
            "Render layer",
            world.core.entity_has_render_layer(entity),
        ),
        (
            ComponentKind::CullingMask,
            "Culling mask",
            world.core.entity_has_culling_mask(entity),
        ),
        (
            ComponentKind::CameraCullingMask,
            "Camera culling mask",
            world.core.entity_has_camera_culling_mask(entity),
        ),
        (
            ComponentKind::IgnoreParentScale,
            "Ignore parent scale",
            world.core.entity_has_ignore_parent_scale(entity),
        ),
        (
            ComponentKind::AudioListener,
            "Audio listener",
            world.core.entity_has_audio_listener(entity),
        ),
        (
            ComponentKind::CollisionListener,
            "Collision listener",
            world.core.entity_has_collision_listener(entity),
        ),
        (
            ComponentKind::PhysicsInterpolation,
            "Physics interpolation",
            world.core.entity_has_physics_interpolation(entity),
        ),
        (
            ComponentKind::MorphWeights,
            "Morph weights",
            world.core.entity_has_morph_weights(entity),
        ),
        (
            ComponentKind::MaterialVariants,
            "Material variants",
            world.core.entity_has_material_variants(entity),
        ),
        (
            ComponentKind::PanOrbitCamera,
            "Pan-orbit camera",
            world.core.entity_has_pan_orbit_camera(entity),
        ),
        (
            ComponentKind::ThirdPersonCamera,
            "Third-person camera",
            world.core.entity_has_third_person_camera(entity),
        ),
        (
            ComponentKind::CameraEnvironment,
            "Camera environment",
            world.core.entity_has_camera_environment(entity),
        ),
        (
            ComponentKind::CameraPostProcess,
            "Camera post-process",
            world.core.entity_has_camera_post_process(entity),
        ),
        (
            ComponentKind::ConstrainedAspect,
            "Constrained aspect",
            world.core.entity_has_constrained_aspect(entity),
        ),
        (
            ComponentKind::ViewportUpdateMode,
            "Viewport update mode",
            world.core.entity_has_viewport_update_mode(entity),
        ),
    ]
}

fn attach_default_component(world: &mut World, entity: Entity, kind: ComponentKind) {
    use nightshade::ecs::world::{
        ANIMATION_PLAYER, AUDIO_SOURCE, CAMERA, CAMERA_CULLING_MASK, CASTS_SHADOW, COLLIDER,
        CULLING_MASK, DECAL, LIGHT, NAVMESH_AGENT, PARTICLE_EMITTER, RENDER_LAYER, RIGID_BODY,
        TEXT, VISIBILITY,
    };
    match kind {
        ComponentKind::ParticleEmitter => {
            world.core.add_components(entity, PARTICLE_EMITTER);
            world.core.set_particle_emitter(
                entity,
                nightshade::ecs::particles::components::ParticleEmitter::default(),
            );
        }
        ComponentKind::Decal => {
            world.core.add_components(entity, DECAL);
            world
                .core
                .set_decal(entity, nightshade::ecs::decal::components::Decal::default());
        }
        ComponentKind::AudioSource => {
            world.core.add_components(entity, AUDIO_SOURCE);
            world.core.set_audio_source(
                entity,
                nightshade::ecs::audio::components::AudioSource::default(),
            );
        }
        ComponentKind::RigidBody => {
            world.core.add_components(entity, RIGID_BODY);
            world
                .core
                .set_rigid_body(entity, RigidBodyComponent::new_dynamic());
        }
        ComponentKind::Collider => {
            world.core.add_components(entity, COLLIDER);
            world
                .core
                .set_collider(entity, ColliderComponent::new_cuboid(0.5, 0.5, 0.5));
        }
        ComponentKind::CharacterController => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::CHARACTER_CONTROLLER);
            world.core.set_character_controller(
                entity,
                CharacterControllerComponent::new_capsule(0.9, 0.3),
            );
        }
        ComponentKind::NavmeshAgent => {
            world.core.add_components(entity, NAVMESH_AGENT);
            world.core.set_navmesh_agent(entity, NavMeshAgent::new());
        }
        ComponentKind::Camera => {
            world.core.add_components(entity, CAMERA);
            world.core.set_camera(
                entity,
                nightshade::ecs::camera::components::Camera::default(),
            );
        }
        ComponentKind::Text => {
            let text_index = world.resources.text.cache.add_text("Text");
            world.core.add_components(entity, TEXT);
            world.core.set_text(
                entity,
                nightshade::ecs::text::components::Text::new(text_index),
            );
        }
        ComponentKind::AnimationPlayer => {
            world.core.add_components(entity, ANIMATION_PLAYER);
            world.core.set_animation_player(
                entity,
                nightshade::ecs::animation::components::AnimationPlayer::default(),
            );
        }
        ComponentKind::Light => {
            world.core.add_components(entity, LIGHT);
            world.core.set_light(entity, Light::default());
        }
        ComponentKind::Visibility => {
            world.core.add_components(entity, VISIBILITY);
            world
                .core
                .set_visibility(entity, Visibility { visible: true });
        }
        ComponentKind::CastsShadow => {
            world.core.add_components(entity, CASTS_SHADOW);
            world.core.set_casts_shadow(entity, CastsShadow);
        }
        ComponentKind::RenderLayer => {
            world.core.add_components(entity, RENDER_LAYER);
            world
                .core
                .set_render_layer(entity, nightshade::ecs::primitives::RenderLayer::default());
        }
        ComponentKind::CullingMask => {
            world.core.add_components(entity, CULLING_MASK);
            world
                .core
                .set_culling_mask(entity, nightshade::ecs::primitives::CullingMask::default());
        }
        ComponentKind::CameraCullingMask => {
            world.core.add_components(entity, CAMERA_CULLING_MASK);
            world.core.set_camera_culling_mask(
                entity,
                nightshade::ecs::primitives::CameraCullingMask::default(),
            );
        }
        ComponentKind::IgnoreParentScale => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::IGNORE_PARENT_SCALE);
            world.core.set_ignore_parent_scale(
                entity,
                nightshade::ecs::transform::components::IgnoreParentScale,
            );
        }
        ComponentKind::AudioListener => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::AUDIO_LISTENER);
            world
                .core
                .set_audio_listener(entity, nightshade::ecs::audio::components::AudioListener);
        }
        ComponentKind::CollisionListener => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::COLLISION_LISTENER);
            world.core.set_collision_listener(
                entity,
                nightshade::ecs::physics::components::CollisionListener,
            );
        }
        ComponentKind::PhysicsInterpolation => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::PHYSICS_INTERPOLATION);
            world.core.set_physics_interpolation(
                entity,
                nightshade::ecs::physics::components::PhysicsInterpolation::default(),
            );
        }
        ComponentKind::MorphWeights => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::MORPH_WEIGHTS);
            world.core.set_morph_weights(
                entity,
                nightshade::ecs::morph::components::MorphWeights::default(),
            );
        }
        ComponentKind::MaterialVariants => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::MATERIAL_VARIANTS);
            world.core.set_material_variants(
                entity,
                nightshade::ecs::material::components::MaterialVariants::default(),
            );
        }
        ComponentKind::PanOrbitCamera => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::PAN_ORBIT_CAMERA);
            world.core.set_pan_orbit_camera(
                entity,
                nightshade::ecs::camera::components::PanOrbitCamera::default(),
            );
        }
        ComponentKind::ThirdPersonCamera => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::THIRD_PERSON_CAMERA);
            world.core.set_third_person_camera(
                entity,
                nightshade::ecs::camera::components::ThirdPersonCamera::default(),
            );
        }
        ComponentKind::CameraEnvironment => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::CAMERA_ENVIRONMENT);
            world.core.set_camera_environment(
                entity,
                nightshade::ecs::camera::components::CameraEnvironment::default(),
            );
        }
        ComponentKind::CameraPostProcess => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::CAMERA_POST_PROCESS);
            world.core.set_camera_post_process(
                entity,
                nightshade::ecs::camera::components::CameraPostProcess::default(),
            );
        }
        ComponentKind::ConstrainedAspect => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::CONSTRAINED_ASPECT);
            world.core.set_constrained_aspect(
                entity,
                nightshade::ecs::camera::components::ConstrainedAspect::default(),
            );
        }
        ComponentKind::ViewportUpdateMode => {
            world
                .core
                .add_components(entity, nightshade::ecs::world::VIEWPORT_UPDATE_MODE);
            world.core.set_viewport_update_mode(
                entity,
                nightshade::ecs::camera::components::ViewportUpdateMode::default(),
            );
        }
    }
}

fn remove_component(world: &mut World, entity: Entity, kind: ComponentKind) {
    use nightshade::ecs::world::{
        ANIMATION_PLAYER, AUDIO_SOURCE, CAMERA, CAMERA_CULLING_MASK, CASTS_SHADOW,
        CHARACTER_CONTROLLER, COLLIDER, CULLING_MASK, DECAL, IGNORE_PARENT_SCALE, LIGHT,
        NAVMESH_AGENT, PARTICLE_EMITTER, RENDER_LAYER, RIGID_BODY, TEXT, VISIBILITY,
    };
    let mask = match kind {
        ComponentKind::ParticleEmitter => PARTICLE_EMITTER,
        ComponentKind::Decal => DECAL,
        ComponentKind::AudioSource => AUDIO_SOURCE,
        ComponentKind::RigidBody => RIGID_BODY,
        ComponentKind::Collider => COLLIDER,
        ComponentKind::CharacterController => CHARACTER_CONTROLLER,
        ComponentKind::NavmeshAgent => NAVMESH_AGENT,
        ComponentKind::Camera => CAMERA,
        ComponentKind::Text => TEXT,
        ComponentKind::AnimationPlayer => ANIMATION_PLAYER,
        ComponentKind::Visibility => VISIBILITY,
        ComponentKind::CastsShadow => CASTS_SHADOW,
        ComponentKind::Light => LIGHT,
        ComponentKind::RenderLayer => RENDER_LAYER,
        ComponentKind::CullingMask => CULLING_MASK,
        ComponentKind::CameraCullingMask => CAMERA_CULLING_MASK,
        ComponentKind::IgnoreParentScale => IGNORE_PARENT_SCALE,
        ComponentKind::AudioListener => nightshade::ecs::world::AUDIO_LISTENER,
        ComponentKind::CollisionListener => nightshade::ecs::world::COLLISION_LISTENER,
        ComponentKind::PhysicsInterpolation => nightshade::ecs::world::PHYSICS_INTERPOLATION,
        ComponentKind::MorphWeights => nightshade::ecs::world::MORPH_WEIGHTS,
        ComponentKind::MaterialVariants => nightshade::ecs::world::MATERIAL_VARIANTS,
        ComponentKind::PanOrbitCamera => nightshade::ecs::world::PAN_ORBIT_CAMERA,
        ComponentKind::ThirdPersonCamera => nightshade::ecs::world::THIRD_PERSON_CAMERA,
        ComponentKind::CameraEnvironment => nightshade::ecs::world::CAMERA_ENVIRONMENT,
        ComponentKind::CameraPostProcess => nightshade::ecs::world::CAMERA_POST_PROCESS,
        ComponentKind::ConstrainedAspect => nightshade::ecs::world::CONSTRAINED_ASPECT,
        ComponentKind::ViewportUpdateMode => nightshade::ecs::world::VIEWPORT_UPDATE_MODE,
    };
    world.core.remove_components(entity, mask);
}

fn apply_character_controller_change(
    world: &mut World,
    entity: Entity,
    field: CharacterControllerField,
    value: &FieldValue,
) {
    let Some(controller) = world.core.get_character_controller_mut(entity) else {
        return;
    };
    match (field, value) {
        (CharacterControllerField::Shape, FieldValue::Index(index)) => {
            controller.shape = match index {
                1 => ColliderShape::Ball { radius: 0.5 },
                2 => ColliderShape::Cuboid {
                    hx: 0.3,
                    hy: 0.9,
                    hz: 0.3,
                },
                _ => ColliderShape::Capsule {
                    half_height: 0.9,
                    radius: 0.3,
                },
            };
        }
        (CharacterControllerField::CapsuleHalfHeight, FieldValue::Float(v)) => {
            if let ColliderShape::Capsule { half_height, .. } = &mut controller.shape {
                *half_height = *v;
            }
        }
        (CharacterControllerField::CapsuleRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Capsule { radius, .. } = &mut controller.shape {
                *radius = *v;
            }
        }
        (CharacterControllerField::BallRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Ball { radius } = &mut controller.shape {
                *radius = *v;
            }
        }
        (CharacterControllerField::CuboidX, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hx, .. } = &mut controller.shape {
                *hx = *v;
            }
        }
        (CharacterControllerField::CuboidY, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hy, .. } = &mut controller.shape {
                *hy = *v;
            }
        }
        (CharacterControllerField::CuboidZ, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hz, .. } = &mut controller.shape {
                *hz = *v;
            }
        }
        (CharacterControllerField::MaxSpeed, FieldValue::Float(v)) => controller.max_speed = *v,
        (CharacterControllerField::Acceleration, FieldValue::Float(v)) => {
            controller.acceleration = *v
        }
        (CharacterControllerField::JumpImpulse, FieldValue::Float(v)) => {
            controller.jump_impulse = *v
        }
        (CharacterControllerField::Scale, FieldValue::Float(v)) => controller.scale = *v,
        (CharacterControllerField::CrouchEnabled, FieldValue::Bool(v)) => {
            controller.crouch_enabled = *v
        }
        (CharacterControllerField::CrouchSpeedMultiplier, FieldValue::Float(v)) => {
            controller.crouch_speed_multiplier = *v
        }
        (CharacterControllerField::SprintSpeedMultiplier, FieldValue::Float(v)) => {
            controller.sprint_speed_multiplier = *v
        }
        (CharacterControllerField::StandingHalfHeight, FieldValue::Float(v)) => {
            controller.standing_half_height = *v
        }
        (CharacterControllerField::CrouchingHalfHeight, FieldValue::Float(v)) => {
            controller.crouching_half_height = *v
        }
        (CharacterControllerField::ConfigOffset, FieldValue::Float(v)) => {
            controller.config.offset = *v
        }
        (CharacterControllerField::MaxSlopeClimb, FieldValue::Float(degrees)) => {
            controller.config.max_slope_climb_angle = degrees.to_radians();
        }
        (CharacterControllerField::MinSlopeSlide, FieldValue::Float(degrees)) => {
            controller.config.min_slope_slide_angle = degrees.to_radians();
        }
        (CharacterControllerField::FrictionRate, FieldValue::Float(v)) => {
            controller.friction_rate = *v
        }
        (CharacterControllerField::AboveMaxFrictionRate, FieldValue::Float(v)) => {
            controller.above_max_friction_rate = *v
        }
        (CharacterControllerField::EngineInputEnabled, FieldValue::Bool(v)) => {
            controller.engine_input_enabled = *v
        }
        _ => {}
    }
}

fn apply_particle_change(
    world: &mut World,
    entity: Entity,
    field: ParticleField,
    value: &FieldValue,
) {
    use nightshade::ecs::particles::components::{EmitterShape, EmitterType};
    let Some(emitter) = world.core.get_particle_emitter_mut(entity) else {
        return;
    };
    match (field, value) {
        (ParticleField::Enabled, FieldValue::Bool(v)) => emitter.enabled = *v,
        (ParticleField::OneShot, FieldValue::Bool(v)) => emitter.one_shot = *v,
        (ParticleField::EmitterType, FieldValue::Index(index)) => {
            emitter.emitter_type = match index {
                0 => EmitterType::Firework,
                1 => EmitterType::Fire,
                2 => EmitterType::Smoke,
                3 => EmitterType::Sparks,
                _ => EmitterType::Trail,
            };
        }
        (ParticleField::Shape, FieldValue::Index(index)) => {
            emitter.shape = match index {
                0 => EmitterShape::Point,
                1 => EmitterShape::Sphere { radius: 1.0 },
                2 => EmitterShape::Cone {
                    angle: 0.3,
                    height: 0.1,
                },
                _ => EmitterShape::Box {
                    half_extents: Vec3::new(0.5, 0.5, 0.5),
                },
            };
        }
        (ParticleField::ShapeRadius, FieldValue::Float(v)) => {
            if let EmitterShape::Sphere { radius } = &mut emitter.shape {
                *radius = *v;
            }
        }
        (ParticleField::ShapeAngle, FieldValue::Float(v)) => {
            if let EmitterShape::Cone { angle, .. } = &mut emitter.shape {
                *angle = *v;
            }
        }
        (ParticleField::ShapeHeight, FieldValue::Float(v)) => {
            if let EmitterShape::Cone { height, .. } = &mut emitter.shape {
                *height = *v;
            }
        }
        (ParticleField::ShapeBoxX, FieldValue::Float(v)) => {
            if let EmitterShape::Box { half_extents } = &mut emitter.shape {
                half_extents.x = *v;
            }
        }
        (ParticleField::ShapeBoxY, FieldValue::Float(v)) => {
            if let EmitterShape::Box { half_extents } = &mut emitter.shape {
                half_extents.y = *v;
            }
        }
        (ParticleField::ShapeBoxZ, FieldValue::Float(v)) => {
            if let EmitterShape::Box { half_extents } = &mut emitter.shape {
                half_extents.z = *v;
            }
        }
        (ParticleField::PositionX, FieldValue::Float(v)) => emitter.position.x = *v,
        (ParticleField::PositionY, FieldValue::Float(v)) => emitter.position.y = *v,
        (ParticleField::PositionZ, FieldValue::Float(v)) => emitter.position.z = *v,
        (ParticleField::DirectionX, FieldValue::Float(v)) => emitter.direction.x = *v,
        (ParticleField::DirectionY, FieldValue::Float(v)) => emitter.direction.y = *v,
        (ParticleField::DirectionZ, FieldValue::Float(v)) => emitter.direction.z = *v,
        (ParticleField::SpawnRate, FieldValue::Float(v)) => emitter.spawn_rate = *v,
        (ParticleField::BurstCount, FieldValue::Float(v)) => {
            emitter.burst_count = v.max(0.0) as u32
        }
        (ParticleField::LifetimeMin, FieldValue::Float(v)) => emitter.particle_lifetime_min = *v,
        (ParticleField::LifetimeMax, FieldValue::Float(v)) => emitter.particle_lifetime_max = *v,
        (ParticleField::VelocityMin, FieldValue::Float(v)) => emitter.initial_velocity_min = *v,
        (ParticleField::VelocityMax, FieldValue::Float(v)) => emitter.initial_velocity_max = *v,
        (ParticleField::VelocitySpread, FieldValue::Float(v)) => emitter.velocity_spread = *v,
        (ParticleField::GravityX, FieldValue::Float(v)) => emitter.gravity.x = *v,
        (ParticleField::GravityY, FieldValue::Float(v)) => emitter.gravity.y = *v,
        (ParticleField::GravityZ, FieldValue::Float(v)) => emitter.gravity.z = *v,
        (ParticleField::Drag, FieldValue::Float(v)) => emitter.drag = *v,
        (ParticleField::SizeStart, FieldValue::Float(v)) => emitter.size_start = *v,
        (ParticleField::SizeEnd, FieldValue::Float(v)) => emitter.size_end = *v,
        (ParticleField::EmissiveStrength, FieldValue::Float(v)) => emitter.emissive_strength = *v,
        (ParticleField::TurbulenceStrength, FieldValue::Float(v)) => {
            emitter.turbulence_strength = *v
        }
        (ParticleField::TurbulenceFrequency, FieldValue::Float(v)) => {
            emitter.turbulence_frequency = *v
        }
        (ParticleField::TextureIndex, FieldValue::Float(v)) => {
            emitter.texture_index = v.max(0.0) as u32
        }
        (ParticleField::SizeCurveTime(index), FieldValue::Float(v)) => {
            if let Some(key) = emitter.size_curve.get_mut(index) {
                key.0 = (*v).clamp(0.0, 1.0);
            }
        }
        (ParticleField::SizeCurveValue(index), FieldValue::Float(v)) => {
            if let Some(key) = emitter.size_curve.get_mut(index) {
                key.1 = *v;
            }
        }
        (ParticleField::AddSizeCurveKey, FieldValue::Bool(true)) => {
            emitter.size_curve.push((0.5, emitter.size_start));
        }
        (ParticleField::RemoveSizeCurveKey(index), FieldValue::Bool(true))
            if index < emitter.size_curve.len() =>
        {
            emitter.size_curve.remove(index);
        }
        (ParticleField::OpacityCurveTime(index), FieldValue::Float(v)) => {
            if let Some(key) = emitter.opacity_curve.get_mut(index) {
                key.0 = (*v).clamp(0.0, 1.0);
            }
        }
        (ParticleField::OpacityCurveValue(index), FieldValue::Float(v)) => {
            if let Some(key) = emitter.opacity_curve.get_mut(index) {
                key.1 = (*v).clamp(0.0, 1.0);
            }
        }
        (ParticleField::AddOpacityCurveKey, FieldValue::Bool(true)) => {
            emitter.opacity_curve.push((0.5, 1.0));
        }
        (ParticleField::RemoveOpacityCurveKey(index), FieldValue::Bool(true))
            if index < emitter.opacity_curve.len() =>
        {
            emitter.opacity_curve.remove(index);
        }
        (ParticleField::DeathSubEmitter, FieldValue::Text(text)) => {
            emitter.death_sub_emitter = if text.is_empty() {
                None
            } else {
                Some(text.clone())
            };
        }
        (ParticleField::TrailSubEmitter, FieldValue::Text(text)) => {
            emitter.trail_sub_emitter = if text.is_empty() {
                None
            } else {
                Some(text.clone())
            };
        }
        (ParticleField::TrailSpawnInterval, FieldValue::Float(v)) => {
            emitter.trail_spawn_interval = (*v).max(0.0);
        }
        _ => {}
    }
}

fn apply_decal_change(world: &mut World, entity: Entity, field: DecalField, value: &FieldValue) {
    let Some(decal) = world.core.get_decal_mut(entity) else {
        return;
    };
    match (field, value) {
        (DecalField::Color, FieldValue::Color(color)) => {
            decal.color = [color.x, color.y, color.z, color.w];
        }
        (DecalField::EmissiveStrength, FieldValue::Float(v)) => decal.emissive_strength = *v,
        (DecalField::SizeX, FieldValue::Float(v)) => decal.size.x = *v,
        (DecalField::SizeY, FieldValue::Float(v)) => decal.size.y = *v,
        (DecalField::Depth, FieldValue::Float(v)) => decal.depth = *v,
        (DecalField::NormalThreshold, FieldValue::Float(v)) => decal.normal_threshold = *v,
        (DecalField::FadeStart, FieldValue::Float(v)) => decal.fade_start = *v,
        (DecalField::FadeEnd, FieldValue::Float(v)) => decal.fade_end = *v,
        (DecalField::Texture, FieldValue::Text(text)) => {
            decal.texture = if text.is_empty() {
                None
            } else {
                Some(text.clone())
            };
        }
        (DecalField::EmissiveTexture, FieldValue::Text(text)) => {
            decal.emissive_texture = if text.is_empty() {
                None
            } else {
                Some(text.clone())
            };
        }
        _ => {}
    }
}

fn apply_audio_change(world: &mut World, entity: Entity, field: AudioField, value: &FieldValue) {
    use nightshade::ecs::audio::components::AudioBus;
    let Some(source) = world.core.get_audio_source_mut(entity) else {
        return;
    };
    match (field, value) {
        (AudioField::AudioRef, FieldValue::Text(text)) => {
            source.audio_ref = if text.is_empty() {
                None
            } else {
                Some(text.clone())
            };
        }
        (AudioField::Volume, FieldValue::Float(v)) => source.volume = *v,
        (AudioField::Looping, FieldValue::Bool(v)) => source.looping = *v,
        (AudioField::Playing, FieldValue::Bool(v)) => source.playing = *v,
        (AudioField::Spatial, FieldValue::Bool(v)) => source.spatial = *v,
        (AudioField::Bus, FieldValue::Index(index)) => {
            source.bus = match index {
                0 => AudioBus::Master,
                1 => AudioBus::Music,
                2 => AudioBus::Sfx,
                3 => AudioBus::Ambient,
                4 => AudioBus::Voice,
                _ => AudioBus::Ui,
            };
        }
        (AudioField::MinDistance, FieldValue::Float(v)) => source.min_distance = *v,
        (AudioField::MaxDistance, FieldValue::Float(v)) => source.max_distance = *v,
        (AudioField::ReverbZoneName(index), FieldValue::Text(text)) => {
            if let Some(zone) = source.reverb_zones.get_mut(index) {
                zone.0 = text.clone();
            }
        }
        (AudioField::ReverbZoneSend(index), FieldValue::Float(value)) => {
            if let Some(zone) = source.reverb_zones.get_mut(index) {
                zone.1 = *value;
            }
        }
        (AudioField::AddReverbZone, FieldValue::Bool(true)) => {
            source
                .reverb_zones
                .push((format!("zone_{}", source.reverb_zones.len()), 0.0));
        }
        (AudioField::RemoveReverbZone(index), FieldValue::Bool(true))
            if index < source.reverb_zones.len() =>
        {
            source.reverb_zones.remove(index);
        }
        (AudioField::RandomPick, FieldValue::Bool(value)) => {
            source.random_pick = *value;
        }
        (AudioField::RandomClip(index), FieldValue::Text(text)) => {
            if let Some(clip) = source.random_clips.get_mut(index) {
                *clip = text.clone();
            }
        }
        (AudioField::AddRandomClip, FieldValue::Bool(true)) => {
            source.random_clips.push(String::new());
        }
        (AudioField::RemoveRandomClip(index), FieldValue::Bool(true))
            if index < source.random_clips.len() =>
        {
            source.random_clips.remove(index);
        }
        _ => {}
    }
}

fn apply_rigid_body_change(
    world: &mut World,
    entity: Entity,
    field: RigidBodyField,
    value: &FieldValue,
) {
    use nightshade::ecs::physics::types::RigidBodyType;
    let Some(body) = world.core.get_rigid_body_mut(entity) else {
        return;
    };
    match (field, value) {
        (RigidBodyField::BodyType, FieldValue::Index(index)) => {
            body.body_type = match index {
                0 => RigidBodyType::Dynamic,
                1 => RigidBodyType::KinematicPositionBased,
                2 => RigidBodyType::KinematicVelocityBased,
                _ => RigidBodyType::Fixed,
            };
        }
        (RigidBodyField::Mass, FieldValue::Float(v)) => body.mass = *v,
        (RigidBodyField::LinvelX, FieldValue::Float(v)) => body.linvel[0] = *v,
        (RigidBodyField::LinvelY, FieldValue::Float(v)) => body.linvel[1] = *v,
        (RigidBodyField::LinvelZ, FieldValue::Float(v)) => body.linvel[2] = *v,
        (RigidBodyField::AngvelX, FieldValue::Float(v)) => body.angvel[0] = *v,
        (RigidBodyField::AngvelY, FieldValue::Float(v)) => body.angvel[1] = *v,
        (RigidBodyField::AngvelZ, FieldValue::Float(v)) => body.angvel[2] = *v,
        (RigidBodyField::Ccd, FieldValue::Bool(v)) => body.ccd_enabled = *v,
        _ => {}
    }
}

fn apply_collider_change(
    world: &mut World,
    entity: Entity,
    field: ColliderField,
    value: &FieldValue,
) {
    if let (ColliderField::Shape, FieldValue::Index(index)) = (field, value) {
        let new_shape = match index {
            0 => ColliderShape::Ball { radius: 0.5 },
            1 => ColliderShape::Cuboid {
                hx: 0.5,
                hy: 0.5,
                hz: 0.5,
            },
            2 => ColliderShape::Capsule {
                half_height: 0.5,
                radius: 0.25,
            },
            3 => ColliderShape::Cylinder {
                half_height: 0.5,
                radius: 0.25,
            },
            4 => ColliderShape::Cone {
                half_height: 0.5,
                radius: 0.25,
            },
            5 => convex_mesh_from_entity(world, entity)
                .unwrap_or(ColliderShape::ConvexMesh { vertices: vec![] }),
            6 => trimesh_from_entity(world, entity).unwrap_or(ColliderShape::TriMesh {
                vertices: vec![],
                indices: vec![],
            }),
            7 => ColliderShape::HeightField {
                nrows: 8,
                ncols: 8,
                heights: vec![0.0; 64],
                scale: [1.0, 1.0, 1.0],
            },
            _ => return,
        };
        if let Some(collider) = world.core.get_collider_mut(entity) {
            collider.shape = new_shape;
        }
        return;
    }
    let Some(collider) = world.core.get_collider_mut(entity) else {
        return;
    };
    match (field, value) {
        (ColliderField::Shape, _) => {}
        (ColliderField::BallRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Ball { radius } = &mut collider.shape {
                *radius = *v;
            }
        }
        (ColliderField::CuboidX, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hx, .. } = &mut collider.shape {
                *hx = *v;
            }
        }
        (ColliderField::CuboidY, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hy, .. } = &mut collider.shape {
                *hy = *v;
            }
        }
        (ColliderField::CuboidZ, FieldValue::Float(v)) => {
            if let ColliderShape::Cuboid { hz, .. } = &mut collider.shape {
                *hz = *v;
            }
        }
        (ColliderField::CapsuleHalfHeight, FieldValue::Float(v)) => {
            if let ColliderShape::Capsule { half_height, .. } = &mut collider.shape {
                *half_height = *v;
            }
        }
        (ColliderField::CapsuleRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Capsule { radius, .. } = &mut collider.shape {
                *radius = *v;
            }
        }
        (ColliderField::CylinderHalfHeight, FieldValue::Float(v)) => {
            if let ColliderShape::Cylinder { half_height, .. } = &mut collider.shape {
                *half_height = *v;
            }
        }
        (ColliderField::CylinderRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Cylinder { radius, .. } = &mut collider.shape {
                *radius = *v;
            }
        }
        (ColliderField::ConeHalfHeight, FieldValue::Float(v)) => {
            if let ColliderShape::Cone { half_height, .. } = &mut collider.shape {
                *half_height = *v;
            }
        }
        (ColliderField::ConeRadius, FieldValue::Float(v)) => {
            if let ColliderShape::Cone { radius, .. } = &mut collider.shape {
                *radius = *v;
            }
        }
        (ColliderField::Friction, FieldValue::Float(v)) => collider.friction = *v,
        (ColliderField::Restitution, FieldValue::Float(v)) => collider.restitution = *v,
        (ColliderField::Density, FieldValue::Float(v)) => collider.density = *v,
        (ColliderField::IsSensor, FieldValue::Bool(v)) => collider.is_sensor = *v,
        (ColliderField::CollisionMemberships, FieldValue::Float(v)) => {
            collider.collision_groups.memberships = (*v).clamp(0.0, u32::MAX as f32) as u32;
        }
        (ColliderField::CollisionFilter, FieldValue::Float(v)) => {
            collider.collision_groups.filter = (*v).clamp(0.0, u32::MAX as f32) as u32;
        }
        (ColliderField::SolverMemberships, FieldValue::Float(v)) => {
            collider.solver_groups.memberships = (*v).clamp(0.0, u32::MAX as f32) as u32;
        }
        (ColliderField::SolverFilter, FieldValue::Float(v)) => {
            collider.solver_groups.filter = (*v).clamp(0.0, u32::MAX as f32) as u32;
        }
        _ => {}
    }
}

fn apply_navmesh_change(
    world: &mut World,
    entity: Entity,
    field: NavmeshField,
    value: &FieldValue,
) {
    let Some(agent) = world.core.get_navmesh_agent_mut(entity) else {
        return;
    };
    match (field, value) {
        (NavmeshField::MovementSpeed, FieldValue::Float(v)) => agent.movement_speed = *v,
        (NavmeshField::ArrivalThreshold, FieldValue::Float(v)) => agent.arrival_threshold = *v,
        (NavmeshField::PathRecalculationThreshold, FieldValue::Float(v)) => {
            agent.path_recalculation_threshold = *v
        }
        (NavmeshField::AgentRadius, FieldValue::Float(v)) => agent.agent_radius = *v,
        (NavmeshField::AgentHeight, FieldValue::Float(v)) => agent.agent_height = *v,
        _ => {}
    }
}

fn apply_camera_change(world: &mut World, entity: Entity, field: CameraField, value: &FieldValue) {
    use nightshade::ecs::camera::components::{OrthographicCamera, PerspectiveCamera, Projection};
    if matches!(field, CameraField::SetActive) {
        if let FieldValue::Bool(true) = value {
            world.resources.active_camera = Some(entity);
        }
        return;
    }
    let Some(camera) = world.core.get_camera_mut(entity) else {
        return;
    };
    match (field, value) {
        (CameraField::ProjectionKind, FieldValue::Index(index)) => {
            camera.projection = match index {
                0 => Projection::Perspective(PerspectiveCamera::default()),
                _ => Projection::Orthographic(OrthographicCamera::default()),
            };
        }
        (CameraField::FovDegrees, FieldValue::Float(degrees)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection {
                perspective.y_fov_rad = degrees.to_radians();
            }
        }
        (CameraField::NearPlane, FieldValue::Float(v)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection {
                perspective.z_near = *v;
            }
        }
        (CameraField::FarOverride, FieldValue::Bool(v)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection {
                perspective.z_far = if *v { Some(1000.0) } else { None };
            }
        }
        (CameraField::FarValue, FieldValue::Float(v)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection
                && perspective.z_far.is_some()
            {
                perspective.z_far = Some(*v);
            }
        }
        (CameraField::AspectOverride, FieldValue::Bool(v)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection {
                perspective.aspect_ratio = if *v { Some(16.0 / 9.0) } else { None };
            }
        }
        (CameraField::AspectValue, FieldValue::Float(v)) => {
            if let Projection::Perspective(perspective) = &mut camera.projection
                && perspective.aspect_ratio.is_some()
            {
                perspective.aspect_ratio = Some(*v);
            }
        }
        (CameraField::OrthoX, FieldValue::Float(v)) => {
            if let Projection::Orthographic(ortho) = &mut camera.projection {
                ortho.x_mag = *v;
            }
        }
        (CameraField::OrthoY, FieldValue::Float(v)) => {
            if let Projection::Orthographic(ortho) = &mut camera.projection {
                ortho.y_mag = *v;
            }
        }
        (CameraField::OrthoNear, FieldValue::Float(v)) => {
            if let Projection::Orthographic(ortho) = &mut camera.projection {
                ortho.z_near = *v;
            }
        }
        (CameraField::OrthoFar, FieldValue::Float(v)) => {
            if let Projection::Orthographic(ortho) = &mut camera.projection {
                ortho.z_far = *v;
            }
        }
        _ => {}
    }
}

fn apply_animation_change(
    world: &mut World,
    entity: Entity,
    field: AnimationField,
    value: &FieldValue,
) {
    let Some(player) = world.core.get_animation_player_mut(entity) else {
        return;
    };
    match (field, value) {
        (AnimationField::Playing, FieldValue::Bool(v)) => player.playing = *v,
        (AnimationField::Looping, FieldValue::Bool(v)) => player.looping = *v,
        (AnimationField::PlayAll, FieldValue::Bool(v)) => player.play_all = *v,
        (AnimationField::Speed, FieldValue::Float(v)) => player.speed = *v,
        (AnimationField::Time, FieldValue::Float(v)) => player.time = *v,
        (AnimationField::ClipSelect, FieldValue::Index(index)) if *index < player.clips.len() => {
            player.current_clip = Some(*index);
            player.time = 0.0;
        }
        _ => {}
    }
}

fn apply_material_change(
    world: &mut World,
    entity: Entity,
    field: MaterialField,
    value: &FieldValue,
) {
    use nightshade::ecs::material::components::AlphaMode;
    let Some(material_ref) = world.core.get_material_ref(entity).cloned() else {
        return;
    };
    let mut material = nightshade::ecs::material::resources::material_registry_iter(
        &world.resources.assets.material_registry,
    )
    .find(|(name, _)| name.as_str() == material_ref.name.as_str())
    .map(|(_, material)| material.clone());
    let Some(material) = material.as_mut() else {
        return;
    };
    match (field, value) {
        (MaterialField::BaseColor, FieldValue::Color(color)) => {
            material.base_color = [color.x, color.y, color.z, color.w];
        }
        (MaterialField::Metallic, FieldValue::Float(v)) => material.metallic = *v,
        (MaterialField::Roughness, FieldValue::Float(v)) => material.roughness = *v,
        (MaterialField::EmissiveFactor, FieldValue::Color(color)) => {
            material.emissive_factor = [color.x, color.y, color.z];
        }
        (MaterialField::EmissiveStrength, FieldValue::Float(v)) => material.emissive_strength = *v,
        (MaterialField::AlphaMode, FieldValue::Index(index)) => {
            material.alpha_mode = match index {
                0 => AlphaMode::Opaque,
                1 => AlphaMode::Mask,
                _ => AlphaMode::Blend,
            };
        }
        (MaterialField::AlphaCutoff, FieldValue::Float(v)) => material.alpha_cutoff = *v,
        (MaterialField::Unlit, FieldValue::Bool(v)) => material.unlit = *v,
        (MaterialField::DoubleSided, FieldValue::Bool(v)) => material.double_sided = *v,
        (MaterialField::NormalScale, FieldValue::Float(v)) => material.normal_scale = *v,
        (MaterialField::OcclusionStrength, FieldValue::Float(v)) => {
            material.occlusion_strength = *v
        }
        (MaterialField::Ior, FieldValue::Float(v)) => material.ior = *v,
        (MaterialField::Transmission, FieldValue::Float(v)) => material.transmission_factor = *v,
        (MaterialField::Thickness, FieldValue::Float(v)) => material.thickness = *v,
        (MaterialField::ClearcoatFactor, FieldValue::Float(v)) => material.clearcoat_factor = *v,
        (MaterialField::ClearcoatRoughness, FieldValue::Float(v)) => {
            material.clearcoat_roughness_factor = *v
        }
        (MaterialField::SheenColor, FieldValue::Color(color)) => {
            material.sheen_color_factor = [color.x, color.y, color.z];
        }
        (MaterialField::SheenRoughness, FieldValue::Float(v)) => {
            material.sheen_roughness_factor = *v
        }
        (MaterialField::IridescenceFactor, FieldValue::Float(v)) => {
            material.iridescence_factor = *v
        }
        (MaterialField::AnisotropyStrength, FieldValue::Float(v)) => {
            material.anisotropy_strength = *v
        }
        _ => return,
    }
    queue_ecs_command(
        world,
        nightshade::ecs::world::commands::EcsCommand::ReloadMaterial {
            name: material_ref.name.clone(),
            material: Box::new(material.clone()),
        },
    );
}

fn apply_text_change(world: &mut World, entity: Entity, field: TextField, value: &FieldValue) {
    match (field, value) {
        (TextField::Content, FieldValue::Text(content)) => {
            let text_index = world.core.get_text(entity).map(|text| text.text_index);
            if let Some(text_index) = text_index {
                world
                    .resources
                    .text
                    .cache
                    .set_text(text_index, content.clone());
            }
            if let Some(text) = world.core.get_text_mut(entity) {
                text.dirty = true;
            }
        }
        (TextField::FontSize, FieldValue::Float(v)) => {
            if let Some(text) = world.core.get_text_mut(entity) {
                text.properties.font_size = *v;
                text.dirty = true;
            }
        }
        (TextField::Color, FieldValue::Color(color)) => {
            if let Some(text) = world.core.get_text_mut(entity) {
                text.properties.color = *color;
                text.dirty = true;
            }
        }
        _ => {}
    }
}

fn apply_misc_scene_change(
    editor_world: &mut EditorWorld,
    entity: Entity,
    field: MiscField,
    value: &FieldValue,
) -> bool {
    use nightshade::ecs::scene::{ChunkId, LayerId, SceneChunkConfig, SceneLayerConfig};
    match (field, value) {
        (MiscField::SceneLayerAssignment, FieldValue::Index(index)) => {
            let layer_id = layer_id_from_index(editor_world, *index);
            editor_world
                .resources
                .editor_scene
                .set_entity_layer(entity, layer_id);
            sync_entity_after_scene_change(editor_world, entity);
            true
        }
        (MiscField::SceneChunkAssignment, FieldValue::Index(index)) => {
            let chunk_id = chunk_id_from_index(editor_world, *index);
            editor_world
                .resources
                .editor_scene
                .set_entity_chunk(entity, chunk_id);
            sync_entity_after_scene_change(editor_world, entity);
            true
        }
        (MiscField::SceneAddLayer, FieldValue::Bool(true)) => {
            let next_id = next_layer_id(editor_world);
            editor_world
                .resources
                .project
                .scene
                .layers
                .push(SceneLayerConfig::new(next_id, format!("Layer {}", next_id)));
            editor_world.resources.project.mark_modified();
            true
        }
        (MiscField::SceneAddChunk, FieldValue::Bool(true)) => {
            let next_id = next_chunk_id(editor_world);
            editor_world
                .resources
                .project
                .scene
                .chunks
                .push(SceneChunkConfig::new(
                    next_id,
                    format!("Chunk {}", next_id),
                    [-50.0, -50.0, -50.0],
                    [50.0, 50.0, 50.0],
                ));
            editor_world.resources.project.mark_modified();
            true
        }
        (MiscField::SceneRemoveLayer(layer_id), FieldValue::Bool(true)) => {
            editor_world
                .resources
                .project
                .scene
                .layers
                .retain(|layer| layer.id != LayerId(layer_id));
            let removals: Vec<nightshade::prelude::Entity> = editor_world
                .resources
                .editor_scene
                .entity_layers
                .iter()
                .filter(|(_, layer)| **layer == LayerId(layer_id))
                .map(|(entity, _)| *entity)
                .collect();
            for entity in removals {
                editor_world
                    .resources
                    .editor_scene
                    .set_entity_layer(entity, None);
            }
            editor_world.resources.project.mark_modified();
            true
        }
        (MiscField::SceneRemoveChunk(chunk_id), FieldValue::Bool(true)) => {
            editor_world
                .resources
                .project
                .scene
                .chunks
                .retain(|chunk| chunk.id != ChunkId(chunk_id));
            let removals: Vec<nightshade::prelude::Entity> = editor_world
                .resources
                .editor_scene
                .entity_chunks
                .iter()
                .filter(|(_, chunk)| **chunk == ChunkId(chunk_id))
                .map(|(entity, _)| *entity)
                .collect();
            for entity in removals {
                editor_world
                    .resources
                    .editor_scene
                    .set_entity_chunk(entity, None);
            }
            editor_world.resources.project.mark_modified();
            true
        }
        (MiscField::SceneLayerName(layer_id), FieldValue::Text(text)) => {
            if let Some(layer) = editor_world
                .resources
                .project
                .scene
                .layers
                .iter_mut()
                .find(|layer| layer.id == LayerId(layer_id))
            {
                layer.name = text.clone();
                editor_world.resources.project.mark_modified();
            }
            true
        }
        (MiscField::SceneLayerOrder(layer_id), FieldValue::Float(value)) => {
            if let Some(layer) = editor_world
                .resources
                .project
                .scene
                .layers
                .iter_mut()
                .find(|layer| layer.id == LayerId(layer_id))
            {
                layer.order = *value as i32;
                editor_world.resources.project.mark_modified();
            }
            true
        }
        (MiscField::SceneLayerEnabled(layer_id), FieldValue::Bool(value)) => {
            if let Some(layer) = editor_world
                .resources
                .project
                .scene
                .layers
                .iter_mut()
                .find(|layer| layer.id == LayerId(layer_id))
            {
                layer.enabled = *value;
                editor_world.resources.project.mark_modified();
            }
            true
        }
        (MiscField::SceneChunkName(chunk_id), FieldValue::Text(text)) => {
            if let Some(chunk) = editor_world
                .resources
                .project
                .scene
                .chunks
                .iter_mut()
                .find(|chunk| chunk.id == ChunkId(chunk_id))
            {
                chunk.name = text.clone();
                editor_world.resources.project.mark_modified();
            }
            true
        }
        (MiscField::SceneChunkLoadDistance(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.load_distance = *value);
            true
        }
        (MiscField::SceneChunkUnloadDistance(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| {
                chunk.unload_distance = *value
            });
            true
        }
        (MiscField::SceneChunkBoundsMinX(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[0] = *value);
            true
        }
        (MiscField::SceneChunkBoundsMinY(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[1] = *value);
            true
        }
        (MiscField::SceneChunkBoundsMinZ(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_min[2] = *value);
            true
        }
        (MiscField::SceneChunkBoundsMaxX(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[0] = *value);
            true
        }
        (MiscField::SceneChunkBoundsMaxY(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[1] = *value);
            true
        }
        (MiscField::SceneChunkBoundsMaxZ(chunk_id), FieldValue::Float(value)) => {
            update_chunk(editor_world, chunk_id, |chunk| chunk.bounds_max[2] = *value);
            true
        }
        (MiscField::AddFixedJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Fixed);
            true
        }
        (MiscField::AddRevoluteJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Revolute);
            true
        }
        (MiscField::AddPrismaticJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Prismatic);
            true
        }
        (MiscField::AddSphericalJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Spherical);
            true
        }
        (MiscField::AddRopeJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Rope);
            true
        }
        (MiscField::AddSpringJoint, FieldValue::Bool(true)) => {
            add_joint_from_selection(editor_world, JointKind::Spring);
            true
        }
        (MiscField::RemoveJoint(index), FieldValue::Bool(true))
            if index < editor_world.resources.project.scene.joints.len() =>
        {
            editor_world.resources.project.scene.joints.remove(index);
            editor_world.resources.project.mark_modified();
            true
        }
        (MiscField::ToggleJointCollisions(index), FieldValue::Bool(value)) => {
            if let Some(joint) = editor_world.resources.project.scene.joints.get_mut(index) {
                joint.collisions_enabled = *value;
                editor_world.resources.project.mark_modified();
            }
            true
        }
        _ => false,
    }
}

#[derive(Clone, Copy)]
enum JointKind {
    Fixed,
    Revolute,
    Prismatic,
    Spherical,
    Rope,
    Spring,
}

fn add_joint_from_selection(editor_world: &mut EditorWorld, kind: JointKind) {
    use nightshade::ecs::scene::{SceneJoint, SceneJointConnection};
    let selected = editor_world.resources.ui.selected_entities.clone();
    if selected.len() < 2 {
        return;
    }
    let parent_entity = selected[selected.len() - 2];
    let child_entity = selected[selected.len() - 1];
    let parent_uuid = editor_world.resources.editor_scene.uuid_for(parent_entity);
    let child_uuid = editor_world.resources.editor_scene.uuid_for(child_entity);
    let (Some(parent_uuid), Some(child_uuid)) = (parent_uuid, child_uuid) else {
        return;
    };
    let joint = match kind {
        JointKind::Fixed => SceneJoint::Fixed {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
        },
        JointKind::Revolute => SceneJoint::Revolute {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
            axis: [0.0, 1.0, 0.0],
            limits: None,
        },
        JointKind::Prismatic => SceneJoint::Prismatic {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
            axis: [1.0, 0.0, 0.0],
            limits: None,
        },
        JointKind::Spherical => SceneJoint::Spherical {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
        },
        JointKind::Rope => SceneJoint::Rope {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
            max_distance: 1.0,
        },
        JointKind::Spring => SceneJoint::Spring {
            parent_anchor: [0.0, 0.0, 0.0],
            child_anchor: [0.0, 0.0, 0.0],
            rest_length: 1.0,
            stiffness: 10.0,
            damping: 1.0,
        },
    };
    editor_world
        .resources
        .project
        .scene
        .joints
        .push(SceneJointConnection {
            parent_entity: parent_uuid,
            child_entity: child_uuid,
            joint,
            collisions_enabled: false,
        });
    editor_world.resources.project.mark_modified();
}

fn next_layer_id(editor_world: &EditorWorld) -> u32 {
    editor_world
        .resources
        .project
        .scene
        .layers
        .iter()
        .map(|layer| layer.id.0)
        .max()
        .map(|value| value + 1)
        .unwrap_or(0)
}

fn next_chunk_id(editor_world: &EditorWorld) -> u32 {
    editor_world
        .resources
        .project
        .scene
        .chunks
        .iter()
        .map(|chunk| chunk.id.0)
        .max()
        .map(|value| value + 1)
        .unwrap_or(0)
}

fn layer_id_from_index(
    editor_world: &EditorWorld,
    index: usize,
) -> Option<nightshade::ecs::scene::LayerId> {
    if index == 0 {
        return None;
    }
    editor_world
        .resources
        .project
        .scene
        .layers
        .get(index - 1)
        .map(|layer| layer.id)
}

fn chunk_id_from_index(
    editor_world: &EditorWorld,
    index: usize,
) -> Option<nightshade::ecs::scene::ChunkId> {
    if index == 0 {
        return None;
    }
    editor_world
        .resources
        .project
        .scene
        .chunks
        .get(index - 1)
        .map(|chunk| chunk.id)
}

fn update_chunk<F: FnOnce(&mut nightshade::ecs::scene::SceneChunkConfig)>(
    editor_world: &mut EditorWorld,
    chunk_id: u32,
    apply: F,
) {
    use nightshade::ecs::scene::ChunkId;
    if let Some(chunk) = editor_world
        .resources
        .project
        .scene
        .chunks
        .iter_mut()
        .find(|chunk| chunk.id == ChunkId(chunk_id))
    {
        apply(chunk);
        editor_world.resources.project.mark_modified();
    }
}

fn sync_entity_after_scene_change(editor_world: &mut EditorWorld, entity: Entity) {
    let project = &mut editor_world.resources.project;
    let editor_scene = &editor_world.resources.editor_scene;
    let Some(uuid) = editor_scene.uuid_for(entity) else {
        return;
    };
    let Some(scene_entity) = project
        .scene
        .entities
        .iter_mut()
        .find(|scene_entity| scene_entity.uuid == uuid)
    else {
        return;
    };
    scene_entity.layer = editor_scene.entity_layer(entity);
    scene_entity.chunk_id = editor_scene.entity_chunk(entity);
    project.mark_modified();
}

fn apply_misc_change(world: &mut World, entity: Entity, field: MiscField, value: &FieldValue) {
    match (field, value) {
        (MiscField::RenderLayer, FieldValue::Index(index)) => {
            let value = if *index == 1 {
                nightshade::ecs::primitives::RenderLayer::OVERLAY
            } else {
                nightshade::ecs::primitives::RenderLayer::WORLD
            };
            world
                .core
                .set_render_layer(entity, nightshade::ecs::primitives::RenderLayer(value));
        }
        (MiscField::CullingMaskBit(bit), FieldValue::Bool(set)) => {
            if let Some(mask) = world.core.get_culling_mask_mut(entity) {
                if *set {
                    mask.0 |= 1u32 << bit;
                } else {
                    mask.0 &= !(1u32 << bit);
                }
            }
        }
        (MiscField::CameraCullingMaskBit(bit), FieldValue::Bool(set)) => {
            if let Some(mask) = world.core.get_camera_culling_mask_mut(entity) {
                if *set {
                    mask.0 |= 1u32 << bit;
                } else {
                    mask.0 &= !(1u32 << bit);
                }
            }
        }
        (MiscField::IgnoreParentScale, FieldValue::Bool(false)) => {
            world
                .core
                .remove_components(entity, nightshade::ecs::world::IGNORE_PARENT_SCALE);
        }
        (MiscField::LinesAlwaysOnTop, FieldValue::Bool(value)) => {
            if let Some(lines) = world.core.get_lines_mut(entity) {
                lines.always_on_top = *value;
                lines.mark_dirty();
            }
        }
        (MiscField::LinesClear, FieldValue::Bool(true)) => {
            if let Some(lines) = world.core.get_lines_mut(entity) {
                lines.clear();
            }
        }
        _ => {}
    }
}

const MAX_VISIBLE_INSTANCE_ROWS: usize = 64;

fn populate_instanced_mesh_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(instanced) = tree.world_mut().core.get_instanced_mesh(entity) else {
        return;
    };
    let mesh_name = instanced.mesh_name.clone();
    let instance_count = instanced.instances.len();
    let visible_count = instance_count.min(MAX_VISIBLE_INSTANCE_ROWS);
    let instances: Vec<nightshade::ecs::mesh::components::InstanceTransform> = instanced
        .instances
        .iter()
        .take(visible_count)
        .copied()
        .collect();
    let custom_data: Vec<[f32; 4]> = (0..visible_count)
        .map(|index| {
            instanced
                .custom_data
                .get(index)
                .map(|data| data.tint)
                .unwrap_or([1.0, 1.0, 1.0, 1.0])
        })
        .collect();

    let section = tree.add_property_section(grid, "Instanced mesh");
    add_property_value(tree, grid, section, "Mesh", &mesh_name);
    add_property_value(
        tree,
        grid,
        section,
        "Instance count",
        &instance_count.to_string(),
    );
    let mut props = PropertyContext {
        grid,
        section,
        entity,
        bindings,
    };
    props.button(
        tree,
        InspectorField::Instance(InstanceField::Add),
        "Add instance",
        "Add",
    );

    for (index, transform) in instances.iter().enumerate() {
        let header = format!("Instance {index}");
        props.section = tree.add_property_section(grid, &header);
        let (roll, pitch, yaw) = quat_to_euler_xyz(&transform.rotation);

        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::TranslationX(index)),
            "Translation X",
            transform.translation.x,
            0.05,
            3,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::TranslationY(index)),
            "Translation Y",
            transform.translation.y,
            0.05,
            3,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::TranslationZ(index)),
            "Translation Z",
            transform.translation.z,
            0.05,
            3,
        );

        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::EulerX(index)),
            "Rotation X (\u{00b0})",
            roll.to_degrees(),
            0.5,
            1,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::EulerY(index)),
            "Rotation Y (\u{00b0})",
            pitch.to_degrees(),
            0.5,
            1,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::EulerZ(index)),
            "Rotation Z (\u{00b0})",
            yaw.to_degrees(),
            0.5,
            1,
        );

        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::ScaleX(index)),
            "Scale X",
            transform.scale.x,
            0.01,
            3,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::ScaleY(index)),
            "Scale Y",
            transform.scale.y,
            0.01,
            3,
        );
        props.drag_value(
            tree,
            InspectorField::Instance(InstanceField::ScaleZ(index)),
            "Scale Z",
            transform.scale.z,
            0.01,
            3,
        );

        let tint = custom_data[index];
        props.color_picker(
            tree,
            InspectorField::Instance(InstanceField::Tint(index)),
            "Tint",
            vec4(tint[0], tint[1], tint[2], tint[3]),
        );

        props.button(
            tree,
            InspectorField::Instance(InstanceField::Remove(index)),
            "Remove instance",
            "Remove",
        );
    }

    if instance_count > visible_count {
        let hidden = instance_count - visible_count;
        let message = format!("+{hidden} more instances (hidden)");
        add_property_value(tree, grid, section, "Hidden", &message);
    }
}

fn apply_instanced_mesh_change(
    world: &mut World,
    entity: Entity,
    field: InstanceField,
    value: &FieldValue,
) -> bool {
    let Some(instanced) = world.core.get_instanced_mesh_mut(entity) else {
        return false;
    };
    match (field, value) {
        (InstanceField::Add, FieldValue::Bool(true)) => {
            instanced.add_instance(nightshade::ecs::mesh::components::InstanceTransform::default());
            true
        }
        (InstanceField::Remove(index), FieldValue::Bool(true)) => {
            instanced.remove_instance(index);
            true
        }
        (
            InstanceField::TranslationX(index)
            | InstanceField::TranslationY(index)
            | InstanceField::TranslationZ(index),
            FieldValue::Float(v),
        ) => {
            if let Some(existing) = instanced.get_instance(index).copied() {
                let mut updated = existing;
                match field {
                    InstanceField::TranslationX(_) => updated.translation.x = *v,
                    InstanceField::TranslationY(_) => updated.translation.y = *v,
                    InstanceField::TranslationZ(_) => updated.translation.z = *v,
                    _ => unreachable!(),
                }
                instanced.set_instance_transform(index, updated);
            }
            false
        }
        (
            InstanceField::EulerX(index)
            | InstanceField::EulerY(index)
            | InstanceField::EulerZ(index),
            FieldValue::Float(degrees),
        ) => {
            if let Some(existing) = instanced.get_instance(index).copied() {
                let mut updated = existing;
                let (mut roll, mut pitch, mut yaw) = quat_to_euler_xyz(&updated.rotation);
                let radians = degrees.to_radians();
                match field {
                    InstanceField::EulerX(_) => roll = radians,
                    InstanceField::EulerY(_) => pitch = radians,
                    InstanceField::EulerZ(_) => yaw = radians,
                    _ => unreachable!(),
                }
                updated.rotation = euler_xyz_to_quat(roll, pitch, yaw);
                instanced.set_instance_transform(index, updated);
            }
            false
        }
        (
            InstanceField::ScaleX(index)
            | InstanceField::ScaleY(index)
            | InstanceField::ScaleZ(index),
            FieldValue::Float(v),
        ) => {
            if let Some(existing) = instanced.get_instance(index).copied() {
                let mut updated = existing;
                let clamped = v.max(0.001);
                match field {
                    InstanceField::ScaleX(_) => updated.scale.x = clamped,
                    InstanceField::ScaleY(_) => updated.scale.y = clamped,
                    InstanceField::ScaleZ(_) => updated.scale.z = clamped,
                    _ => unreachable!(),
                }
                instanced.set_instance_transform(index, updated);
            }
            false
        }
        (InstanceField::Tint(index), FieldValue::Color(color)) => {
            instanced.set_instance_tint(index, [color.x, color.y, color.z, color.w]);
            false
        }
        _ => false,
    }
}

fn collider_mesh_data(world: &World, entity: Entity) -> Option<(Vec<[f32; 3]>, Vec<u32>)> {
    let mesh_name = world.core.get_render_mesh(entity)?.name.clone();
    let registry = &world.resources.assets.mesh_cache.registry;
    let mesh =
        nightshade::ecs::generational_registry::registry_entry_by_name(registry, &mesh_name)?;
    let vertices: Vec<[f32; 3]> = mesh.vertices.iter().map(|vertex| vertex.position).collect();
    let indices = mesh.indices.clone();
    Some((vertices, indices))
}

fn trimesh_from_entity(world: &World, entity: Entity) -> Option<ColliderShape> {
    let (vertices, indices_flat) = collider_mesh_data(world, entity)?;
    if vertices.is_empty() || indices_flat.len() < 3 {
        return None;
    }
    let mut triangles: Vec<[u32; 3]> = Vec::with_capacity(indices_flat.len() / 3);
    for triangle in indices_flat.chunks_exact(3) {
        triangles.push([triangle[0], triangle[1], triangle[2]]);
    }
    Some(ColliderShape::TriMesh {
        vertices,
        indices: triangles,
    })
}

fn convex_mesh_from_entity(world: &World, entity: Entity) -> Option<ColliderShape> {
    let (vertices, _) = collider_mesh_data(world, entity)?;
    if vertices.is_empty() {
        return None;
    }
    Some(ColliderShape::ConvexMesh { vertices })
}
