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,
    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),
    Grass(GrassField),
    GrassInteractor(GrassInteractorField),
    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,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GrassField {
    Enabled,
    WindStrength,
    WindFrequency,
    WindDirX,
    WindDirZ,
    InteractionRadius,
    InteractionStrength,
    CastShadows,
    ReceiveShadows,
    BladesPerPatch,
    PatchSize,
    StreamRadius,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum GrassInteractorField {
    Radius,
    Strength,
}

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,
    GrassRegion,
    GrassInteractor,
    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 => 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::Grass(_) => Some(SnapshotKind::GrassRegion),
            Self::GrassInteractor(_) => Some(SnapshotKind::GrassInteractor),
            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::GrassRegion => SnapshotKind::GrassRegion,
        ComponentKind::GrassInteractor => SnapshotKind::GrassInteractor,
        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(Rl(vec2(70.0, 0.0)) + Ab(vec2(0.0, 22.0)))
                .with_text("World", font)
                .text_left()
                .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,
                    _ => 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::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::Grass(field), value) => {
            apply_grass_change(world, entity, field, value);
        }
        (InspectorField::GrassInteractor(field), value) => {
            apply_grass_interactor_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(96.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",
    );
    bindings.insert(
        name_input,
        InspectorBinding {
            entity,
            field: InspectorField::Name,
        },
    );
    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);
    bindings.insert(
        visibility_checkbox,
        InspectorBinding {
            entity,
            field: InspectorField::Visibility,
        },
    );
    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);
    bindings.insert(
        casts_checkbox,
        InspectorBinding {
            entity,
            field: InspectorField::CastsShadow,
        },
    );

    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,
        );
        bindings.insert(
            tx,
            InspectorBinding {
                entity,
                field: InspectorField::TranslationX,
            },
        );
        let ty = add_property_drag_value(
            tree,
            grid,
            section,
            "Translation Y",
            transform.translation.y,
            0.05,
            2,
        );
        bindings.insert(
            ty,
            InspectorBinding {
                entity,
                field: InspectorField::TranslationY,
            },
        );
        let tz = add_property_drag_value(
            tree,
            grid,
            section,
            "Translation Z",
            transform.translation.z,
            0.05,
            2,
        );
        bindings.insert(
            tz,
            InspectorBinding {
                entity,
                field: InspectorField::TranslationZ,
            },
        );

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

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

    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,
        };
        let type_dropdown = add_property_dropdown(
            tree,
            grid,
            section,
            "Type",
            &["Directional", "Point", "Spot"],
            type_index,
        );
        bindings.insert(
            type_dropdown,
            InspectorBinding {
                entity,
                field: InspectorField::LightType,
            },
        );
        let intensity =
            add_property_drag_value(tree, grid, section, "Intensity", light.intensity, 0.05, 2);
        bindings.insert(
            intensity,
            InspectorBinding {
                entity,
                field: InspectorField::LightIntensity,
            },
        );
        let range = add_property_drag_value(tree, grid, section, "Range", light.range, 0.1, 2);
        bindings.insert(
            range,
            InspectorBinding {
                entity,
                field: InspectorField::LightRange,
            },
        );
        let color_picker = add_property_color_picker(
            tree,
            grid,
            section,
            "Color",
            vec4(light.color.x, light.color.y, light.color.z, 1.0),
        );
        bindings.insert(
            color_picker,
            InspectorBinding {
                entity,
                field: InspectorField::LightColor,
            },
        );
        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,
            );
            bindings.insert(
                inner,
                InspectorBinding {
                    entity,
                    field: InspectorField::LightInnerCone,
                },
            );
            let outer = add_property_drag_value(
                tree,
                grid,
                section,
                "Outer cone (\u{00b0})",
                light.outer_cone_angle.to_degrees(),
                0.5,
                1,
            );
            bindings.insert(
                outer,
                InspectorBinding {
                    entity,
                    field: InspectorField::LightOuterCone,
                },
            );
        }
        let cast = add_property_checkbox(tree, grid, section, "Cast shadows", light.cast_shadows);
        bindings.insert(
            cast,
            InspectorBinding {
                entity,
                field: InspectorField::LightCastsShadow,
            },
        );
        let shadow_bias = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow bias",
            light.shadow_bias,
            0.0001,
            5,
        );
        bindings.insert(
            shadow_bias,
            InspectorBinding {
                entity,
                field: InspectorField::LightShadowBias,
            },
        );
        let shadow_resolution = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow resolution",
            light.shadow_resolution as f32,
            32.0,
            0,
        );
        bindings.insert(
            shadow_resolution,
            InspectorBinding {
                entity,
                field: InspectorField::LightShadowResolution,
            },
        );
        let shadow_distance = add_property_drag_value(
            tree,
            grid,
            section,
            "Shadow distance",
            light.shadow_distance,
            0.5,
            2,
        );
        bindings.insert(
            shadow_distance,
            InspectorBinding {
                entity,
                field: InspectorField::LightShadowDistance,
            },
        );
        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",
        );
        bindings.insert(
            cookie,
            InspectorBinding {
                entity,
                field: InspectorField::LightCookieTexture,
            },
        );
    }

    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_grass_section(tree, grid, entity, bindings);
    populate_grass_interactor_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 lighting_section = tree.add_property_section(grid, "Lighting");
    let ambient_widget = add_property_color_picker(
        tree,
        grid,
        lighting_section,
        "Ambient",
        vec4(
            graphics.ambient_light[0],
            graphics.ambient_light[1],
            graphics.ambient_light[2],
            graphics.ambient_light[3],
        ),
    );
    bindings.insert(
        ambient_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::AmbientLight,
        },
    );

    let bloom_section = tree.add_property_section(grid, "Bloom");
    let bloom_enabled =
        add_property_checkbox(tree, grid, bloom_section, "Enabled", graphics.bloom_enabled);
    bindings.insert(
        bloom_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::BloomEnabled,
        },
    );
    let bloom_intensity = add_property_drag_value(
        tree,
        grid,
        bloom_section,
        "Intensity",
        graphics.bloom_intensity,
        0.01,
        3,
    );
    bindings.insert(
        bloom_intensity,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::BloomIntensity,
        },
    );
    let bloom_threshold = add_property_drag_value(
        tree,
        grid,
        bloom_section,
        "Threshold",
        graphics.bloom_threshold,
        0.05,
        2,
    );
    bindings.insert(
        bloom_threshold,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::BloomThreshold,
        },
    );
    let bloom_knee = add_property_drag_value(
        tree,
        grid,
        bloom_section,
        "Knee",
        graphics.bloom_knee,
        0.01,
        3,
    );
    bindings.insert(
        bloom_knee,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::BloomKnee,
        },
    );
    let bloom_filter_radius = add_property_drag_value(
        tree,
        grid,
        bloom_section,
        "Filter Radius",
        graphics.bloom_filter_radius,
        0.001,
        4,
    );
    bindings.insert(
        bloom_filter_radius,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::BloomFilterRadius,
        },
    );

    let ssao_section = tree.add_property_section(grid, "SSAO");
    let ssao_enabled =
        add_property_checkbox(tree, grid, ssao_section, "Enabled", graphics.ssao_enabled);
    bindings.insert(
        ssao_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoEnabled,
        },
    );
    let ssao_radius = add_property_drag_value(
        tree,
        grid,
        ssao_section,
        "Radius",
        graphics.ssao_radius,
        0.05,
        2,
    );
    bindings.insert(
        ssao_radius,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoRadius,
        },
    );
    let ssao_intensity = add_property_drag_value(
        tree,
        grid,
        ssao_section,
        "Intensity",
        graphics.ssao_intensity,
        0.05,
        2,
    );
    bindings.insert(
        ssao_intensity,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoIntensity,
        },
    );
    let ssao_bias = add_property_drag_value(
        tree,
        grid,
        ssao_section,
        "Bias",
        graphics.ssao_bias,
        0.001,
        4,
    );
    bindings.insert(
        ssao_bias,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoBias,
        },
    );
    let ssao_samples = add_property_drag_value(
        tree,
        grid,
        ssao_section,
        "Samples",
        graphics.ssao_sample_count as f32,
        1.0,
        0,
    );
    bindings.insert(
        ssao_samples,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoSampleCount,
        },
    );
    let ssao_visualization = add_property_checkbox(
        tree,
        grid,
        ssao_section,
        "Visualize",
        debug.ssao_visualization,
    );
    bindings.insert(
        ssao_visualization,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsaoVisualization,
        },
    );

    let ssgi_section = tree.add_property_section(grid, "SSGI");
    let ssgi_enabled =
        add_property_checkbox(tree, grid, ssgi_section, "Enabled", graphics.ssgi_enabled);
    bindings.insert(
        ssgi_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsgiEnabled,
        },
    );
    let ssgi_radius = add_property_drag_value(
        tree,
        grid,
        ssgi_section,
        "Radius",
        graphics.ssgi_radius,
        0.1,
        2,
    );
    bindings.insert(
        ssgi_radius,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsgiRadius,
        },
    );
    let ssgi_intensity = add_property_drag_value(
        tree,
        grid,
        ssgi_section,
        "Intensity",
        graphics.ssgi_intensity,
        0.05,
        2,
    );
    bindings.insert(
        ssgi_intensity,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsgiIntensity,
        },
    );
    let ssgi_max_steps = add_property_drag_value(
        tree,
        grid,
        ssgi_section,
        "Max Steps",
        graphics.ssgi_max_steps as f32,
        1.0,
        0,
    );
    bindings.insert(
        ssgi_max_steps,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsgiMaxSteps,
        },
    );

    let ssr_section = tree.add_property_section(grid, "SSR");
    let ssr_enabled =
        add_property_checkbox(tree, grid, ssr_section, "Enabled", graphics.ssr_enabled);
    bindings.insert(
        ssr_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrEnabled,
        },
    );
    let ssr_max_steps = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Max Steps",
        graphics.ssr_max_steps as f32,
        1.0,
        0,
    );
    bindings.insert(
        ssr_max_steps,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrMaxSteps,
        },
    );
    let ssr_thickness = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Thickness",
        graphics.ssr_thickness,
        0.01,
        3,
    );
    bindings.insert(
        ssr_thickness,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrThickness,
        },
    );
    let ssr_max_distance = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Max Distance",
        graphics.ssr_max_distance,
        0.5,
        2,
    );
    bindings.insert(
        ssr_max_distance,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrMaxDistance,
        },
    );
    let ssr_stride = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Stride",
        graphics.ssr_stride,
        0.05,
        2,
    );
    bindings.insert(
        ssr_stride,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrStride,
        },
    );
    let ssr_fade_start = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Fade Start",
        graphics.ssr_fade_start,
        0.01,
        2,
    );
    bindings.insert(
        ssr_fade_start,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrFadeStart,
        },
    );
    let ssr_fade_end = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Fade End",
        graphics.ssr_fade_end,
        0.01,
        2,
    );
    bindings.insert(
        ssr_fade_end,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrFadeEnd,
        },
    );
    let ssr_intensity = add_property_drag_value(
        tree,
        grid,
        ssr_section,
        "Intensity",
        graphics.ssr_intensity,
        0.05,
        2,
    );
    bindings.insert(
        ssr_intensity,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SsrIntensity,
        },
    );

    let fog_section = tree.add_property_section(grid, "Fog");
    let fog_enabled =
        add_property_checkbox(tree, grid, fog_section, "Enabled", graphics.fog.is_some());
    bindings.insert(
        fog_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::FogEnabled,
        },
    );
    if let Some(fog) = graphics.fog.as_ref() {
        let fog_color = add_property_color_picker(
            tree,
            grid,
            fog_section,
            "Color",
            vec4(fog.color[0], fog.color[1], fog.color[2], 1.0),
        );
        bindings.insert(
            fog_color,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::FogColor,
            },
        );
        let fog_start =
            add_property_drag_value(tree, grid, fog_section, "Start", fog.start, 0.1, 2);
        bindings.insert(
            fog_start,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::FogStart,
            },
        );
        let fog_end = add_property_drag_value(tree, grid, fog_section, "End", fog.end, 0.1, 2);
        bindings.insert(
            fog_end,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::FogEnd,
            },
        );
    }

    let dof_section = tree.add_property_section(grid, "Depth of Field");
    let dof_enabled = add_property_checkbox(
        tree,
        grid,
        dof_section,
        "Enabled",
        graphics.depth_of_field.enabled,
    );
    bindings.insert(
        dof_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofEnabled,
        },
    );
    let dof_focus_distance = add_property_drag_value(
        tree,
        grid,
        dof_section,
        "Focus Distance",
        graphics.depth_of_field.focus_distance,
        0.1,
        2,
    );
    bindings.insert(
        dof_focus_distance,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofFocusDistance,
        },
    );
    let dof_focus_range = add_property_drag_value(
        tree,
        grid,
        dof_section,
        "Focus Range",
        graphics.depth_of_field.focus_range,
        0.1,
        2,
    );
    bindings.insert(
        dof_focus_range,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofFocusRange,
        },
    );
    let dof_blur_radius = add_property_drag_value(
        tree,
        grid,
        dof_section,
        "Max Blur",
        graphics.depth_of_field.max_blur_radius,
        0.1,
        2,
    );
    bindings.insert(
        dof_blur_radius,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofMaxBlurRadius,
        },
    );
    let dof_bokeh_threshold = add_property_drag_value(
        tree,
        grid,
        dof_section,
        "Bokeh Threshold",
        graphics.depth_of_field.bokeh_threshold,
        0.05,
        2,
    );
    bindings.insert(
        dof_bokeh_threshold,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofBokehThreshold,
        },
    );
    let dof_bokeh_intensity = add_property_drag_value(
        tree,
        grid,
        dof_section,
        "Bokeh Intensity",
        graphics.depth_of_field.bokeh_intensity,
        0.05,
        2,
    );
    bindings.insert(
        dof_bokeh_intensity,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofBokehIntensity,
        },
    );
    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,
    };
    let dof_quality = add_property_dropdown(
        tree,
        grid,
        dof_section,
        "Quality",
        &["Low", "Medium", "High"],
        dof_quality_index,
    );
    bindings.insert(
        dof_quality,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::DofQuality,
        },
    );

    let post_section = tree.add_property_section(grid, "Post Processing");
    let fxaa = add_property_checkbox(tree, grid, post_section, "FXAA", graphics.fxaa_enabled);
    bindings.insert(
        fxaa,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::FxaaEnabled,
        },
    );
    let render_scale = add_property_drag_value(
        tree,
        grid,
        post_section,
        "Render Scale",
        graphics.render_scale,
        0.05,
        2,
    );
    bindings.insert(
        render_scale,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::RenderScale,
        },
    );
    let ibl_blend = add_property_drag_value(
        tree,
        grid,
        post_section,
        "IBL Blend",
        graphics.ibl_blend_factor,
        0.01,
        2,
    );
    bindings.insert(
        ibl_blend,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::IblBlendFactor,
        },
    );
    let unlit = add_property_checkbox(tree, grid, post_section, "Unlit", graphics.unlit_mode);
    bindings.insert(
        unlit,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::UnlitMode,
        },
    );
    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);
    let pbr_dropdown = add_property_dropdown(
        tree,
        grid,
        post_section,
        "PBR Debug",
        &pbr_options,
        pbr_index,
    );
    bindings.insert(
        pbr_dropdown,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::PbrDebug,
        },
    );

    let outline_section = tree.add_property_section(grid, "Selection Outline");
    let outline_enabled = add_property_checkbox(
        tree,
        grid,
        outline_section,
        "Enabled",
        debug.selection_outline_enabled,
    );
    bindings.insert(
        outline_enabled,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SelectionOutlineEnabled,
        },
    );
    let outline_color = add_property_color_picker(
        tree,
        grid,
        outline_section,
        "Color",
        vec4(
            debug.selection_outline_color[0],
            debug.selection_outline_color[1],
            debug.selection_outline_color[2],
            debug.selection_outline_color[3],
        ),
    );
    bindings.insert(
        outline_color,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::SelectionOutlineColor,
        },
    );

    let grading_section = tree.add_property_section(grid, "Color Grading");
    let exposure_widget = add_property_drag_value(
        tree,
        grid,
        grading_section,
        "Exposure",
        graphics.color_grading.exposure,
        0.01,
        3,
    );
    bindings.insert(
        exposure_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Exposure,
        },
    );
    let saturation_widget = add_property_drag_value(
        tree,
        grid,
        grading_section,
        "Saturation",
        graphics.color_grading.saturation,
        0.01,
        2,
    );
    bindings.insert(
        saturation_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Saturation,
        },
    );
    let contrast_widget = add_property_drag_value(
        tree,
        grid,
        grading_section,
        "Contrast",
        graphics.color_grading.contrast,
        0.01,
        2,
    );
    bindings.insert(
        contrast_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Contrast,
        },
    );
    let brightness_widget = add_property_drag_value(
        tree,
        grid,
        grading_section,
        "Brightness",
        graphics.color_grading.brightness,
        0.01,
        2,
    );
    bindings.insert(
        brightness_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Brightness,
        },
    );
    let gamma_widget = add_property_drag_value(
        tree,
        grid,
        grading_section,
        "Gamma",
        graphics.color_grading.gamma,
        0.01,
        2,
    );
    bindings.insert(
        gamma_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Gamma,
        },
    );

    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 layers_section = tree.add_property_section(grid, "Scene Layers");
    let add_layer_widget = add_property_button(tree, grid, layers_section, "Layers", "Add layer");
    bindings.insert(
        add_layer_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Misc(MiscField::SceneAddLayer),
        },
    );
    for layer in &snapshot.layers {
        let name_widget = add_property_text_input(
            tree,
            grid,
            layers_section,
            &format!("Layer {}", layer.id.0),
            &layer.name,
            "name",
        );
        bindings.insert(
            name_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneLayerName(layer.id.0)),
            },
        );
        let order_widget = add_property_drag_value(
            tree,
            grid,
            layers_section,
            "Order",
            layer.order as f32,
            1.0,
            0,
        );
        bindings.insert(
            order_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneLayerOrder(layer.id.0)),
            },
        );
        let enabled_widget =
            add_property_checkbox(tree, grid, layers_section, "Enabled", layer.enabled);
        bindings.insert(
            enabled_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneLayerEnabled(layer.id.0)),
            },
        );
        let remove_widget = add_property_button(tree, grid, layers_section, "Remove", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneRemoveLayer(layer.id.0)),
            },
        );
    }

    let chunks_section = tree.add_property_section(grid, "Scene Chunks");
    let add_chunk_widget = add_property_button(tree, grid, chunks_section, "Chunks", "Add chunk");
    bindings.insert(
        add_chunk_widget,
        InspectorBinding {
            entity: Entity::default(),
            field: InspectorField::Misc(MiscField::SceneAddChunk),
        },
    );
    for chunk in &snapshot.chunks {
        let name_widget = add_property_text_input(
            tree,
            grid,
            chunks_section,
            &format!("Chunk {}", chunk.id.0),
            &chunk.name,
            "name",
        );
        bindings.insert(
            name_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkName(chunk.id.0)),
            },
        );
        let load = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Load distance",
            chunk.load_distance,
            0.5,
            2,
        );
        bindings.insert(
            load,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkLoadDistance(chunk.id.0)),
            },
        );
        let unload = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Unload distance",
            chunk.unload_distance,
            0.5,
            2,
        );
        bindings.insert(
            unload,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkUnloadDistance(chunk.id.0)),
            },
        );
        let min_x = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Min X",
            chunk.bounds_min[0],
            0.5,
            2,
        );
        bindings.insert(
            min_x,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMinX(chunk.id.0)),
            },
        );
        let min_y = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Min Y",
            chunk.bounds_min[1],
            0.5,
            2,
        );
        bindings.insert(
            min_y,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMinY(chunk.id.0)),
            },
        );
        let min_z = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Min Z",
            chunk.bounds_min[2],
            0.5,
            2,
        );
        bindings.insert(
            min_z,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMinZ(chunk.id.0)),
            },
        );
        let max_x = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Max X",
            chunk.bounds_max[0],
            0.5,
            2,
        );
        bindings.insert(
            max_x,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxX(chunk.id.0)),
            },
        );
        let max_y = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Max Y",
            chunk.bounds_max[1],
            0.5,
            2,
        );
        bindings.insert(
            max_y,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxY(chunk.id.0)),
            },
        );
        let max_z = add_property_drag_value(
            tree,
            grid,
            chunks_section,
            "Bounds Max Z",
            chunk.bounds_max[2],
            0.5,
            2,
        );
        bindings.insert(
            max_z,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneChunkBoundsMaxZ(chunk.id.0)),
            },
        );
        let remove_widget = add_property_button(tree, grid, chunks_section, "Remove", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::SceneRemoveChunk(chunk.id.0)),
            },
        );
    }

    let joints_section = tree.add_property_section(grid, "Joints");
    add_property_value(
        tree,
        grid,
        joints_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 {
        let widget = add_property_button(tree, grid, joints_section, label, "Add");
        bindings.insert(
            widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(field),
            },
        );
    }
    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, joints_section, "Joint", &label_text);
        let collisions = add_property_checkbox(
            tree,
            grid,
            joints_section,
            "Collisions",
            joint.collisions_enabled,
        );
        bindings.insert(
            collisions,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::ToggleJointCollisions(index)),
            },
        );
        let remove_widget = add_property_button(tree, grid, joints_section, "Remove", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity: Entity::default(),
                field: InspectorField::Misc(MiscField::RemoveJoint(index)),
            },
        );
    }
}

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 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()
            .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
}

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 active = tree.world_mut().resources.active_camera == Some(entity);
    add_property_value(
        tree,
        grid,
        section,
        "Active",
        if active { "yes" } else { "no" },
    );
    let activate_button =
        add_property_button(tree, grid, section, "Activate", "Make active camera");
    bindings.insert(
        activate_button,
        InspectorBinding {
            entity,
            field: InspectorField::Camera(CameraField::SetActive),
        },
    );

    let kind_index = match camera.projection {
        nightshade::ecs::camera::components::Projection::Perspective(_) => 0,
        nightshade::ecs::camera::components::Projection::Orthographic(_) => 1,
    };
    let kind_widget = add_property_dropdown(
        tree,
        grid,
        section,
        "Projection",
        &["Perspective", "Orthographic"],
        kind_index,
    );
    bindings.insert(
        kind_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Camera(CameraField::ProjectionKind),
        },
    );

    match camera.projection {
        nightshade::ecs::camera::components::Projection::Perspective(perspective) => {
            let fov = add_property_drag_value(
                tree,
                grid,
                section,
                "FOV (\u{00b0})",
                perspective.y_fov_rad.to_degrees(),
                0.5,
                1,
            );
            bindings.insert(
                fov,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::FovDegrees),
                },
            );
            let near =
                add_property_drag_value(tree, grid, section, "Near", perspective.z_near, 0.001, 4);
            bindings.insert(
                near,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::NearPlane),
                },
            );
            let far_override = add_property_checkbox(
                tree,
                grid,
                section,
                "Far override",
                perspective.z_far.is_some(),
            );
            bindings.insert(
                far_override,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::FarOverride),
                },
            );
            let far_value = add_property_drag_value(
                tree,
                grid,
                section,
                "Far",
                perspective.z_far.unwrap_or(1000.0),
                0.5,
                2,
            );
            bindings.insert(
                far_value,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::FarValue),
                },
            );
            let aspect_override = add_property_checkbox(
                tree,
                grid,
                section,
                "Aspect override",
                perspective.aspect_ratio.is_some(),
            );
            bindings.insert(
                aspect_override,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::AspectOverride),
                },
            );
            let aspect_value = add_property_drag_value(
                tree,
                grid,
                section,
                "Aspect",
                perspective.aspect_ratio.unwrap_or(16.0 / 9.0),
                0.01,
                3,
            );
            bindings.insert(
                aspect_value,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::AspectValue),
                },
            );
        }
        nightshade::ecs::camera::components::Projection::Orthographic(ortho) => {
            let x_mag = add_property_drag_value(tree, grid, section, "X mag", ortho.x_mag, 0.05, 2);
            bindings.insert(
                x_mag,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::OrthoX),
                },
            );
            let y_mag = add_property_drag_value(tree, grid, section, "Y mag", ortho.y_mag, 0.05, 2);
            bindings.insert(
                y_mag,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::OrthoY),
                },
            );
            let near = add_property_drag_value(tree, grid, section, "Near", ortho.z_near, 0.001, 4);
            bindings.insert(
                near,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::OrthoNear),
                },
            );
            let far = add_property_drag_value(tree, grid, section, "Far", ortho.z_far, 0.5, 2);
            bindings.insert(
                far,
                InspectorBinding {
                    entity,
                    field: InspectorField::Camera(CameraField::OrthoFar),
                },
            );
        }
    }
}

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 base_color = add_property_color_picker(
        tree,
        grid,
        section,
        "Base color",
        vec4(
            material.base_color[0],
            material.base_color[1],
            material.base_color[2],
            material.base_color[3],
        ),
    );
    bindings.insert(
        base_color,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::BaseColor),
        },
    );
    let metallic =
        add_property_drag_value(tree, grid, section, "Metallic", material.metallic, 0.01, 3);
    bindings.insert(
        metallic,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Metallic),
        },
    );
    let roughness = add_property_drag_value(
        tree,
        grid,
        section,
        "Roughness",
        material.roughness,
        0.01,
        3,
    );
    bindings.insert(
        roughness,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Roughness),
        },
    );
    let emissive = add_property_color_picker(
        tree,
        grid,
        section,
        "Emissive",
        vec4(
            material.emissive_factor[0],
            material.emissive_factor[1],
            material.emissive_factor[2],
            1.0,
        ),
    );
    bindings.insert(
        emissive,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::EmissiveFactor),
        },
    );
    let emissive_strength = add_property_drag_value(
        tree,
        grid,
        section,
        "Emissive strength",
        material.emissive_strength,
        0.05,
        2,
    );
    bindings.insert(
        emissive_strength,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::EmissiveStrength),
        },
    );
    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,
    };
    let alpha_mode = add_property_dropdown(
        tree,
        grid,
        section,
        "Alpha mode",
        &["Opaque", "Mask", "Blend"],
        alpha_index,
    );
    bindings.insert(
        alpha_mode,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::AlphaMode),
        },
    );
    let alpha_cutoff = add_property_drag_value(
        tree,
        grid,
        section,
        "Alpha cutoff",
        material.alpha_cutoff,
        0.01,
        3,
    );
    bindings.insert(
        alpha_cutoff,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::AlphaCutoff),
        },
    );
    let unlit = add_property_checkbox(tree, grid, section, "Unlit", material.unlit);
    bindings.insert(
        unlit,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Unlit),
        },
    );
    let double_sided =
        add_property_checkbox(tree, grid, section, "Double sided", material.double_sided);
    bindings.insert(
        double_sided,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::DoubleSided),
        },
    );
    let normal_scale = add_property_drag_value(
        tree,
        grid,
        section,
        "Normal scale",
        material.normal_scale,
        0.01,
        3,
    );
    bindings.insert(
        normal_scale,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::NormalScale),
        },
    );
    let occlusion = add_property_drag_value(
        tree,
        grid,
        section,
        "Occlusion",
        material.occlusion_strength,
        0.01,
        3,
    );
    bindings.insert(
        occlusion,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::OcclusionStrength),
        },
    );
    let ior = add_property_drag_value(tree, grid, section, "IOR", material.ior, 0.01, 3);
    bindings.insert(
        ior,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Ior),
        },
    );
    let transmission = add_property_drag_value(
        tree,
        grid,
        section,
        "Transmission",
        material.transmission_factor,
        0.01,
        3,
    );
    bindings.insert(
        transmission,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Transmission),
        },
    );
    let thickness = add_property_drag_value(
        tree,
        grid,
        section,
        "Thickness",
        material.thickness,
        0.05,
        3,
    );
    bindings.insert(
        thickness,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::Thickness),
        },
    );
    let clearcoat = add_property_drag_value(
        tree,
        grid,
        section,
        "Clearcoat",
        material.clearcoat_factor,
        0.01,
        3,
    );
    bindings.insert(
        clearcoat,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::ClearcoatFactor),
        },
    );
    let clearcoat_roughness = add_property_drag_value(
        tree,
        grid,
        section,
        "Clearcoat rough",
        material.clearcoat_roughness_factor,
        0.01,
        3,
    );
    bindings.insert(
        clearcoat_roughness,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::ClearcoatRoughness),
        },
    );
    let sheen_color = add_property_color_picker(
        tree,
        grid,
        section,
        "Sheen color",
        vec4(
            material.sheen_color_factor[0],
            material.sheen_color_factor[1],
            material.sheen_color_factor[2],
            1.0,
        ),
    );
    bindings.insert(
        sheen_color,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::SheenColor),
        },
    );
    let sheen_roughness = add_property_drag_value(
        tree,
        grid,
        section,
        "Sheen rough",
        material.sheen_roughness_factor,
        0.01,
        3,
    );
    bindings.insert(
        sheen_roughness,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::SheenRoughness),
        },
    );
    let iridescence = add_property_drag_value(
        tree,
        grid,
        section,
        "Iridescence",
        material.iridescence_factor,
        0.01,
        3,
    );
    bindings.insert(
        iridescence,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::IridescenceFactor),
        },
    );
    let anisotropy = add_property_drag_value(
        tree,
        grid,
        section,
        "Anisotropy",
        material.anisotropy_strength,
        0.01,
        3,
    );
    bindings.insert(
        anisotropy,
        InspectorBinding {
            entity,
            field: InspectorField::Material(MaterialField::AnisotropyStrength),
        },
    );
}

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);
    bindings.insert(
        dropdown,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::ClipSelect),
        },
    );
    let playing = add_property_checkbox(tree, grid, section, "Playing", player.playing);
    bindings.insert(
        playing,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::Playing),
        },
    );
    let looping = add_property_checkbox(tree, grid, section, "Loop", player.looping);
    bindings.insert(
        looping,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::Looping),
        },
    );
    let play_all = add_property_checkbox(tree, grid, section, "Play all", player.play_all);
    bindings.insert(
        play_all,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::PlayAll),
        },
    );
    let speed = add_property_drag_value(tree, grid, section, "Speed", player.speed, 0.05, 2);
    bindings.insert(
        speed,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::Speed),
        },
    );
    let time = add_property_drag_value(tree, grid, section, "Time", player.time, 0.01, 3);
    bindings.insert(
        time,
        InspectorBinding {
            entity,
            field: InspectorField::Animation(AnimationField::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);
    bindings.insert(
        enabled,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::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,
    );
    bindings.insert(
        type_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::EmitterType),
        },
    );
    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,
    );
    bindings.insert(
        shape_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::Shape),
        },
    );
    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);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeRadius),
                },
            );
        }
        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);
            bindings.insert(
                angle_widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeAngle),
                },
            );
            let height_widget =
                add_property_drag_value(tree, grid, section, "Cone height", height, 0.05, 2);
            bindings.insert(
                height_widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeHeight),
                },
            );
        }
        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);
            bindings.insert(
                x,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeBoxX),
                },
            );
            let y = add_property_drag_value(tree, grid, section, "Box Y", half_extents.y, 0.05, 2);
            bindings.insert(
                y,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeBoxY),
                },
            );
            let z = add_property_drag_value(tree, grid, section, "Box Z", half_extents.z, 0.05, 2);
            bindings.insert(
                z,
                InspectorBinding {
                    entity,
                    field: InspectorField::Particle(ParticleField::ShapeBoxZ),
                },
            );
        }
    }
    let position_x =
        add_property_drag_value(tree, grid, section, "Pos X", emitter.position.x, 0.05, 2);
    bindings.insert(
        position_x,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::PositionX),
        },
    );
    let position_y =
        add_property_drag_value(tree, grid, section, "Pos Y", emitter.position.y, 0.05, 2);
    bindings.insert(
        position_y,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::PositionY),
        },
    );
    let position_z =
        add_property_drag_value(tree, grid, section, "Pos Z", emitter.position.z, 0.05, 2);
    bindings.insert(
        position_z,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::PositionZ),
        },
    );
    let direction_x =
        add_property_drag_value(tree, grid, section, "Dir X", emitter.direction.x, 0.05, 3);
    bindings.insert(
        direction_x,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::DirectionX),
        },
    );
    let direction_y =
        add_property_drag_value(tree, grid, section, "Dir Y", emitter.direction.y, 0.05, 3);
    bindings.insert(
        direction_y,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::DirectionY),
        },
    );
    let direction_z =
        add_property_drag_value(tree, grid, section, "Dir Z", emitter.direction.z, 0.05, 3);
    bindings.insert(
        direction_z,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::DirectionZ),
        },
    );
    let spawn_rate = add_property_drag_value(
        tree,
        grid,
        section,
        "Spawn rate",
        emitter.spawn_rate,
        1.0,
        1,
    );
    bindings.insert(
        spawn_rate,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::SpawnRate),
        },
    );
    let burst_count = add_property_drag_value(
        tree,
        grid,
        section,
        "Burst count",
        emitter.burst_count as f32,
        1.0,
        0,
    );
    bindings.insert(
        burst_count,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::BurstCount),
        },
    );
    let lifetime_min = add_property_drag_value(
        tree,
        grid,
        section,
        "Lifetime min",
        emitter.particle_lifetime_min,
        0.05,
        2,
    );
    bindings.insert(
        lifetime_min,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::LifetimeMin),
        },
    );
    let lifetime_max = add_property_drag_value(
        tree,
        grid,
        section,
        "Lifetime max",
        emitter.particle_lifetime_max,
        0.05,
        2,
    );
    bindings.insert(
        lifetime_max,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::LifetimeMax),
        },
    );
    let velocity_min = add_property_drag_value(
        tree,
        grid,
        section,
        "Velocity min",
        emitter.initial_velocity_min,
        0.05,
        2,
    );
    bindings.insert(
        velocity_min,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::VelocityMin),
        },
    );
    let velocity_max = add_property_drag_value(
        tree,
        grid,
        section,
        "Velocity max",
        emitter.initial_velocity_max,
        0.05,
        2,
    );
    bindings.insert(
        velocity_max,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::VelocityMax),
        },
    );
    let spread = add_property_drag_value(
        tree,
        grid,
        section,
        "Spread (rad)",
        emitter.velocity_spread,
        0.01,
        3,
    );
    bindings.insert(
        spread,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::VelocitySpread),
        },
    );
    let gravity_x =
        add_property_drag_value(tree, grid, section, "Gravity X", emitter.gravity.x, 0.05, 2);
    bindings.insert(
        gravity_x,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::GravityX),
        },
    );
    let gravity_y =
        add_property_drag_value(tree, grid, section, "Gravity Y", emitter.gravity.y, 0.05, 2);
    bindings.insert(
        gravity_y,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::GravityY),
        },
    );
    let gravity_z =
        add_property_drag_value(tree, grid, section, "Gravity Z", emitter.gravity.z, 0.05, 2);
    bindings.insert(
        gravity_z,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::GravityZ),
        },
    );
    let drag_widget = add_property_drag_value(tree, grid, section, "Drag", emitter.drag, 0.01, 3);
    bindings.insert(
        drag_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::Drag),
        },
    );
    let size_start = add_property_drag_value(
        tree,
        grid,
        section,
        "Size start",
        emitter.size_start,
        0.01,
        3,
    );
    bindings.insert(
        size_start,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::SizeStart),
        },
    );
    let size_end =
        add_property_drag_value(tree, grid, section, "Size end", emitter.size_end, 0.01, 3);
    bindings.insert(
        size_end,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::SizeEnd),
        },
    );
    let emissive = add_property_drag_value(
        tree,
        grid,
        section,
        "Emissive",
        emitter.emissive_strength,
        0.1,
        2,
    );
    bindings.insert(
        emissive,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::EmissiveStrength),
        },
    );
    let one_shot = add_property_checkbox(tree, grid, section, "One shot", emitter.one_shot);
    bindings.insert(
        one_shot,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::OneShot),
        },
    );
    let turbulence = add_property_drag_value(
        tree,
        grid,
        section,
        "Turbulence",
        emitter.turbulence_strength,
        0.01,
        3,
    );
    bindings.insert(
        turbulence,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::TurbulenceStrength),
        },
    );
    let turbulence_frequency = add_property_drag_value(
        tree,
        grid,
        section,
        "Turbulence freq",
        emitter.turbulence_frequency,
        0.01,
        3,
    );
    bindings.insert(
        turbulence_frequency,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::TurbulenceFrequency),
        },
    );
    let texture_index = add_property_drag_value(
        tree,
        grid,
        section,
        "Texture index",
        emitter.texture_index as f32,
        1.0,
        0,
    );
    bindings.insert(
        texture_index,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::TextureIndex),
        },
    );

    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,
        );
        bindings.insert(
            time_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::SizeCurveTime(index)),
            },
        );
        let value_widget = add_property_drag_value(
            tree,
            grid,
            size_section,
            &format!("Size {}", index),
            key.1,
            0.01,
            3,
        );
        bindings.insert(
            value_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::SizeCurveValue(index)),
            },
        );
        let remove_widget = add_property_button(tree, grid, size_section, "Remove key", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::RemoveSizeCurveKey(index)),
            },
        );
    }
    let add_size_widget = add_property_button(tree, grid, size_section, "Size keys", "Add key");
    bindings.insert(
        add_size_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::AddSizeCurveKey),
        },
    );

    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,
        );
        bindings.insert(
            time_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::OpacityCurveTime(index)),
            },
        );
        let value_widget = add_property_drag_value(
            tree,
            grid,
            opacity_section,
            &format!("Opacity {}", index),
            key.1,
            0.05,
            2,
        );
        bindings.insert(
            value_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::OpacityCurveValue(index)),
            },
        );
        let remove_widget =
            add_property_button(tree, grid, opacity_section, "Remove key", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Particle(ParticleField::RemoveOpacityCurveKey(index)),
            },
        );
    }
    let add_opacity_widget =
        add_property_button(tree, grid, opacity_section, "Opacity keys", "Add key");
    bindings.insert(
        add_opacity_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::AddOpacityCurveKey),
        },
    );

    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",
    );
    bindings.insert(
        death,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::DeathSubEmitter),
        },
    );
    let trail =
        add_property_text_input(tree, grid, sub_section, "Trail", &trail_sub, "prefab path");
    bindings.insert(
        trail,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::TrailSubEmitter),
        },
    );
    let trail_interval_widget = add_property_drag_value(
        tree,
        grid,
        sub_section,
        "Trail interval",
        trail_interval,
        0.01,
        3,
    );
    bindings.insert(
        trail_interval_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Particle(ParticleField::TrailSpawnInterval),
        },
    );
}

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 texture_input = add_property_text_input(
        tree,
        grid,
        section,
        "Texture",
        decal.texture.as_deref().unwrap_or(""),
        "path",
    );
    bindings.insert(
        texture_input,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::Texture),
        },
    );
    let emissive_input = add_property_text_input(
        tree,
        grid,
        section,
        "Emissive tex",
        decal.emissive_texture.as_deref().unwrap_or(""),
        "path",
    );
    bindings.insert(
        emissive_input,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::EmissiveTexture),
        },
    );
    let color = add_property_color_picker(
        tree,
        grid,
        section,
        "Color",
        vec4(
            decal.color[0],
            decal.color[1],
            decal.color[2],
            decal.color[3],
        ),
    );
    bindings.insert(
        color,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::Color),
        },
    );
    let emissive = add_property_drag_value(
        tree,
        grid,
        section,
        "Emissive",
        decal.emissive_strength,
        0.05,
        2,
    );
    bindings.insert(
        emissive,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::EmissiveStrength),
        },
    );
    let size_x = add_property_drag_value(tree, grid, section, "Size X", decal.size.x, 0.05, 2);
    bindings.insert(
        size_x,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::SizeX),
        },
    );
    let size_y = add_property_drag_value(tree, grid, section, "Size Y", decal.size.y, 0.05, 2);
    bindings.insert(
        size_y,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::SizeY),
        },
    );
    let depth = add_property_drag_value(tree, grid, section, "Depth", decal.depth, 0.05, 2);
    bindings.insert(
        depth,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::Depth),
        },
    );
    let normal_threshold = add_property_drag_value(
        tree,
        grid,
        section,
        "Normal threshold",
        decal.normal_threshold,
        0.01,
        3,
    );
    bindings.insert(
        normal_threshold,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::NormalThreshold),
        },
    );
    let fade_start =
        add_property_drag_value(tree, grid, section, "Fade start", decal.fade_start, 0.5, 2);
    bindings.insert(
        fade_start,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::FadeStart),
        },
    );
    let fade_end = add_property_drag_value(tree, grid, section, "Fade end", decal.fade_end, 0.5, 2);
    bindings.insert(
        fade_end,
        InspectorBinding {
            entity,
            field: InspectorField::Decal(DecalField::FadeEnd),
        },
    );
}

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 audio_ref = add_property_text_input(
        tree,
        grid,
        section,
        "Reference",
        source.audio_ref.as_deref().unwrap_or(""),
        "audio path",
    );
    bindings.insert(
        audio_ref,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::AudioRef),
        },
    );
    let volume = add_property_drag_value(tree, grid, section, "Volume", source.volume, 0.01, 3);
    bindings.insert(
        volume,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::Volume),
        },
    );
    let looping = add_property_checkbox(tree, grid, section, "Loop", source.looping);
    bindings.insert(
        looping,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::Looping),
        },
    );
    let playing = add_property_checkbox(tree, grid, section, "Playing", source.playing);
    bindings.insert(
        playing,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::Playing),
        },
    );
    let spatial = add_property_checkbox(tree, grid, section, "Spatial", source.spatial);
    bindings.insert(
        spatial,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::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,
    };
    let bus = add_property_dropdown(
        tree,
        grid,
        section,
        "Bus",
        &["Master", "Music", "Sfx", "Ambient", "Voice", "Ui"],
        bus_index,
    );
    bindings.insert(
        bus,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::Bus),
        },
    );
    let min_distance = add_property_drag_value(
        tree,
        grid,
        section,
        "Min distance",
        source.min_distance,
        0.1,
        2,
    );
    bindings.insert(
        min_distance,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::MinDistance),
        },
    );
    let max_distance = add_property_drag_value(
        tree,
        grid,
        section,
        "Max distance",
        source.max_distance,
        0.1,
        2,
    );
    bindings.insert(
        max_distance,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::MaxDistance),
        },
    );

    let zones = source.reverb_zones.clone();
    for (index, (zone_name, zone_send)) in zones.iter().enumerate() {
        let name_widget = add_property_text_input(
            tree,
            grid,
            section,
            &format!("Reverb zone {}", index),
            zone_name,
            "zone name",
        );
        bindings.insert(
            name_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Audio(AudioField::ReverbZoneName(index)),
            },
        );
        let send_widget = add_property_drag_value(
            tree,
            grid,
            section,
            &format!("Reverb send {}", index),
            *zone_send,
            0.1,
            2,
        );
        bindings.insert(
            send_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Audio(AudioField::ReverbZoneSend(index)),
            },
        );
        let remove_widget = add_property_button(tree, grid, section, "Remove", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Audio(AudioField::RemoveReverbZone(index)),
            },
        );
    }
    let add_widget = add_property_button(tree, grid, section, "Reverb zones", "Add zone");
    bindings.insert(
        add_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::AddReverbZone),
        },
    );

    let random_pick = source.random_pick;
    let random_clips = source.random_clips.clone();
    let random_section = tree.add_property_section(grid, "Random Clips");
    let random_pick_widget =
        add_property_checkbox(tree, grid, random_section, "Random pick", random_pick);
    bindings.insert(
        random_pick_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::RandomPick),
        },
    );
    for (index, clip) in random_clips.iter().enumerate() {
        let clip_widget = add_property_text_input(
            tree,
            grid,
            random_section,
            &format!("Clip {}", index),
            clip,
            "path/to/clip",
        );
        bindings.insert(
            clip_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Audio(AudioField::RandomClip(index)),
            },
        );
        let remove_widget = add_property_button(tree, grid, random_section, "Remove", "Remove");
        bindings.insert(
            remove_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Audio(AudioField::RemoveRandomClip(index)),
            },
        );
    }
    let add_clip_widget =
        add_property_button(tree, grid, random_section, "Random clips", "Add clip");
    bindings.insert(
        add_clip_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Audio(AudioField::AddRandomClip),
        },
    );
}

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 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,
    };
    let body_type = add_property_dropdown(
        tree,
        grid,
        section,
        "Type",
        &["Dynamic", "Kinematic Pos", "Kinematic Vel", "Fixed"],
        type_index,
    );
    bindings.insert(
        body_type,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::BodyType),
        },
    );
    let mass = add_property_drag_value(tree, grid, section, "Mass", body.mass, 0.1, 3);
    bindings.insert(
        mass,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::Mass),
        },
    );
    let linvel_x =
        add_property_drag_value(tree, grid, section, "Linvel X", body.linvel[0], 0.05, 3);
    bindings.insert(
        linvel_x,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::LinvelX),
        },
    );
    let linvel_y =
        add_property_drag_value(tree, grid, section, "Linvel Y", body.linvel[1], 0.05, 3);
    bindings.insert(
        linvel_y,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::LinvelY),
        },
    );
    let linvel_z =
        add_property_drag_value(tree, grid, section, "Linvel Z", body.linvel[2], 0.05, 3);
    bindings.insert(
        linvel_z,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::LinvelZ),
        },
    );
    let angvel_x =
        add_property_drag_value(tree, grid, section, "Angvel X", body.angvel[0], 0.05, 3);
    bindings.insert(
        angvel_x,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::AngvelX),
        },
    );
    let angvel_y =
        add_property_drag_value(tree, grid, section, "Angvel Y", body.angvel[1], 0.05, 3);
    bindings.insert(
        angvel_y,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::AngvelY),
        },
    );
    let angvel_z =
        add_property_drag_value(tree, grid, section, "Angvel Z", body.angvel[2], 0.05, 3);
    bindings.insert(
        angvel_z,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::AngvelZ),
        },
    );
    let ccd = add_property_checkbox(tree, grid, section, "CCD", body.ccd_enabled);
    bindings.insert(
        ccd,
        InspectorBinding {
            entity,
            field: InspectorField::RigidBody(RigidBodyField::Ccd),
        },
    );
}

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 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,
    };
    let shape = add_property_dropdown(
        tree,
        grid,
        section,
        "Shape",
        &[
            "Ball",
            "Cuboid",
            "Capsule",
            "Cylinder",
            "Cone",
            "ConvexMesh",
            "TriMesh",
            "HeightField",
        ],
        shape_index,
    );
    bindings.insert(
        shape,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::Shape),
        },
    );
    match &collider.shape {
        ColliderShape::Ball { radius } => {
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::BallRadius),
                },
            );
        }
        ColliderShape::Cuboid { hx, hy, hz } => {
            let x = add_property_drag_value(tree, grid, section, "Half X", *hx, 0.05, 3);
            bindings.insert(
                x,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CuboidX),
                },
            );
            let y = add_property_drag_value(tree, grid, section, "Half Y", *hy, 0.05, 3);
            bindings.insert(
                y,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CuboidY),
                },
            );
            let z = add_property_drag_value(tree, grid, section, "Half Z", *hz, 0.05, 3);
            bindings.insert(
                z,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CuboidZ),
                },
            );
        }
        ColliderShape::Capsule {
            half_height,
            radius,
        } => {
            let hh =
                add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
            bindings.insert(
                hh,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CapsuleHalfHeight),
                },
            );
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CapsuleRadius),
                },
            );
        }
        ColliderShape::Cylinder {
            half_height,
            radius,
        } => {
            let hh =
                add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
            bindings.insert(
                hh,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CylinderHalfHeight),
                },
            );
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::CylinderRadius),
                },
            );
        }
        ColliderShape::Cone {
            half_height,
            radius,
        } => {
            let hh =
                add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
            bindings.insert(
                hh,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::ConeHalfHeight),
                },
            );
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Collider(ColliderField::ConeRadius),
                },
            );
        }
        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}"),
            );
        }
    }
    let friction =
        add_property_drag_value(tree, grid, section, "Friction", collider.friction, 0.01, 3);
    bindings.insert(
        friction,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::Friction),
        },
    );
    let restitution = add_property_drag_value(
        tree,
        grid,
        section,
        "Restitution",
        collider.restitution,
        0.01,
        3,
    );
    bindings.insert(
        restitution,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::Restitution),
        },
    );
    let density =
        add_property_drag_value(tree, grid, section, "Density", collider.density, 0.05, 3);
    bindings.insert(
        density,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::Density),
        },
    );
    let is_sensor = add_property_checkbox(tree, grid, section, "Sensor", collider.is_sensor);
    bindings.insert(
        is_sensor,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::IsSensor),
        },
    );
    let collision_membership_widget = add_property_drag_value(
        tree,
        grid,
        section,
        "Collision Memberships",
        collider.collision_groups.memberships as f32,
        1.0,
        0,
    );
    bindings.insert(
        collision_membership_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::CollisionMemberships),
        },
    );
    let collision_filter_widget = add_property_drag_value(
        tree,
        grid,
        section,
        "Collision Filter",
        collider.collision_groups.filter as f32,
        1.0,
        0,
    );
    bindings.insert(
        collision_filter_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::CollisionFilter),
        },
    );
    let solver_membership_widget = add_property_drag_value(
        tree,
        grid,
        section,
        "Solver Memberships",
        collider.solver_groups.memberships as f32,
        1.0,
        0,
    );
    bindings.insert(
        solver_membership_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::SolverMemberships),
        },
    );
    let solver_filter_widget = add_property_drag_value(
        tree,
        grid,
        section,
        "Solver Filter",
        collider.solver_groups.filter as f32,
        1.0,
        0,
    );
    bindings.insert(
        solver_filter_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Collider(ColliderField::SolverFilter),
        },
    );
}

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 shape_index = match controller.shape {
        ColliderShape::Capsule { .. } => 0,
        ColliderShape::Ball { .. } => 1,
        ColliderShape::Cuboid { .. } => 2,
        _ => 0,
    };
    let shape = add_property_dropdown(
        tree,
        grid,
        section,
        "Shape",
        &["Capsule", "Ball", "Cuboid"],
        shape_index,
    );
    bindings.insert(
        shape,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::Shape),
        },
    );
    match &controller.shape {
        ColliderShape::Capsule {
            half_height,
            radius,
        } => {
            let hh =
                add_property_drag_value(tree, grid, section, "Half height", *half_height, 0.05, 3);
            bindings.insert(
                hh,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(
                        CharacterControllerField::CapsuleHalfHeight,
                    ),
                },
            );
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(
                        CharacterControllerField::CapsuleRadius,
                    ),
                },
            );
        }
        ColliderShape::Ball { radius } => {
            let widget = add_property_drag_value(tree, grid, section, "Radius", *radius, 0.05, 3);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(
                        CharacterControllerField::BallRadius,
                    ),
                },
            );
        }
        ColliderShape::Cuboid { hx, hy, hz } => {
            let x = add_property_drag_value(tree, grid, section, "Half X", *hx, 0.05, 3);
            bindings.insert(
                x,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(CharacterControllerField::CuboidX),
                },
            );
            let y = add_property_drag_value(tree, grid, section, "Half Y", *hy, 0.05, 3);
            bindings.insert(
                y,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(CharacterControllerField::CuboidY),
                },
            );
            let z = add_property_drag_value(tree, grid, section, "Half Z", *hz, 0.05, 3);
            bindings.insert(
                z,
                InspectorBinding {
                    entity,
                    field: InspectorField::CharacterController(CharacterControllerField::CuboidZ),
                },
            );
        }
        _ => {}
    }
    let max_speed = add_property_drag_value(
        tree,
        grid,
        section,
        "Max speed",
        controller.max_speed,
        0.05,
        2,
    );
    bindings.insert(
        max_speed,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::MaxSpeed),
        },
    );
    let acceleration = add_property_drag_value(
        tree,
        grid,
        section,
        "Acceleration",
        controller.acceleration,
        0.5,
        2,
    );
    bindings.insert(
        acceleration,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::Acceleration),
        },
    );
    let jump = add_property_drag_value(
        tree,
        grid,
        section,
        "Jump impulse",
        controller.jump_impulse,
        0.05,
        2,
    );
    bindings.insert(
        jump,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::JumpImpulse),
        },
    );
    let scale = add_property_drag_value(tree, grid, section, "Scale", controller.scale, 0.05, 2);
    bindings.insert(
        scale,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::Scale),
        },
    );
    let crouch_enabled = add_property_checkbox(
        tree,
        grid,
        section,
        "Crouch enabled",
        controller.crouch_enabled,
    );
    bindings.insert(
        crouch_enabled,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::CrouchEnabled),
        },
    );
    let crouch_mult = add_property_drag_value(
        tree,
        grid,
        section,
        "Crouch speed mult",
        controller.crouch_speed_multiplier,
        0.01,
        2,
    );
    bindings.insert(
        crouch_mult,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::CrouchSpeedMultiplier,
            ),
        },
    );
    let sprint_mult = add_property_drag_value(
        tree,
        grid,
        section,
        "Sprint speed mult",
        controller.sprint_speed_multiplier,
        0.01,
        2,
    );
    bindings.insert(
        sprint_mult,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::SprintSpeedMultiplier,
            ),
        },
    );
    let standing = add_property_drag_value(
        tree,
        grid,
        section,
        "Standing half h",
        controller.standing_half_height,
        0.01,
        3,
    );
    bindings.insert(
        standing,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::StandingHalfHeight,
            ),
        },
    );
    let crouching = add_property_drag_value(
        tree,
        grid,
        section,
        "Crouching half h",
        controller.crouching_half_height,
        0.01,
        3,
    );
    bindings.insert(
        crouching,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::CrouchingHalfHeight,
            ),
        },
    );
    let offset = add_property_drag_value(
        tree,
        grid,
        section,
        "Config offset",
        controller.config.offset,
        0.001,
        4,
    );
    bindings.insert(
        offset,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::ConfigOffset),
        },
    );
    let max_slope = add_property_drag_value(
        tree,
        grid,
        section,
        "Max slope climb (\u{00b0})",
        controller.config.max_slope_climb_angle.to_degrees(),
        0.5,
        1,
    );
    bindings.insert(
        max_slope,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::MaxSlopeClimb),
        },
    );
    let min_slope = add_property_drag_value(
        tree,
        grid,
        section,
        "Min slope slide (\u{00b0})",
        controller.config.min_slope_slide_angle.to_degrees(),
        0.5,
        1,
    );
    bindings.insert(
        min_slope,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::MinSlopeSlide),
        },
    );
    let friction = add_property_drag_value(
        tree,
        grid,
        section,
        "Friction rate",
        controller.friction_rate,
        0.05,
        2,
    );
    bindings.insert(
        friction,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(CharacterControllerField::FrictionRate),
        },
    );
    let above_max = add_property_drag_value(
        tree,
        grid,
        section,
        "Above-max friction",
        controller.above_max_friction_rate,
        0.05,
        2,
    );
    bindings.insert(
        above_max,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::AboveMaxFrictionRate,
            ),
        },
    );
    let engine_input = add_property_checkbox(
        tree,
        grid,
        section,
        "Engine input",
        controller.engine_input_enabled,
    );
    bindings.insert(
        engine_input,
        InspectorBinding {
            entity,
            field: InspectorField::CharacterController(
                CharacterControllerField::EngineInputEnabled,
            ),
        },
    );
    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 speed =
        add_property_drag_value(tree, grid, section, "Speed", agent.movement_speed, 0.05, 3);
    bindings.insert(
        speed,
        InspectorBinding {
            entity,
            field: InspectorField::Navmesh(NavmeshField::MovementSpeed),
        },
    );
    let arrival = add_property_drag_value(
        tree,
        grid,
        section,
        "Arrival threshold",
        agent.arrival_threshold,
        0.01,
        3,
    );
    bindings.insert(
        arrival,
        InspectorBinding {
            entity,
            field: InspectorField::Navmesh(NavmeshField::ArrivalThreshold),
        },
    );
    let recalc = add_property_drag_value(
        tree,
        grid,
        section,
        "Recalc threshold",
        agent.path_recalculation_threshold,
        0.05,
        3,
    );
    bindings.insert(
        recalc,
        InspectorBinding {
            entity,
            field: InspectorField::Navmesh(NavmeshField::PathRecalculationThreshold),
        },
    );
    let radius =
        add_property_drag_value(tree, grid, section, "Radius", agent.agent_radius, 0.05, 3);
    bindings.insert(
        radius,
        InspectorBinding {
            entity,
            field: InspectorField::Navmesh(NavmeshField::AgentRadius),
        },
    );
    let height =
        add_property_drag_value(tree, grid, section, "Height", agent.agent_height, 0.05, 3);
    bindings.insert(
        height,
        InspectorBinding {
            entity,
            field: InspectorField::Navmesh(NavmeshField::AgentHeight),
        },
    );
}

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 content = add_property_text_input(tree, grid, section, "Content", &content_string, "Text");
    bindings.insert(
        content,
        InspectorBinding {
            entity,
            field: InspectorField::Text(TextField::Content),
        },
    );
    let font_size = add_property_drag_value(
        tree,
        grid,
        section,
        "Font size",
        text.properties.font_size,
        0.5,
        1,
    );
    bindings.insert(
        font_size,
        InspectorBinding {
            entity,
            field: InspectorField::Text(TextField::FontSize),
        },
    );
    let color = add_property_color_picker(
        tree,
        grid,
        section,
        "Color",
        vec4(
            text.properties.color.x,
            text.properties.color.y,
            text.properties.color.z,
            text.properties.color.w,
        ),
    );
    bindings.insert(
        color,
        InspectorBinding {
            entity,
            field: InspectorField::Text(TextField::Color),
        },
    );
}

fn populate_grass_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(region) = tree.world_mut().core.get_grass_region(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Grass region");
    let enabled = add_property_checkbox(tree, grid, section, "Enabled", region.enabled);
    bindings.insert(
        enabled,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::Enabled),
        },
    );
    let blades = add_property_drag_value(
        tree,
        grid,
        section,
        "Blades / patch",
        region.config.blades_per_patch as f32,
        1.0,
        0,
    );
    bindings.insert(
        blades,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::BladesPerPatch),
        },
    );
    let patch_size = add_property_drag_value(
        tree,
        grid,
        section,
        "Patch size",
        region.config.patch_size,
        0.1,
        2,
    );
    bindings.insert(
        patch_size,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::PatchSize),
        },
    );
    let stream_radius = add_property_drag_value(
        tree,
        grid,
        section,
        "Stream radius",
        region.config.stream_radius,
        1.0,
        1,
    );
    bindings.insert(
        stream_radius,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::StreamRadius),
        },
    );
    let wind_strength = add_property_drag_value(
        tree,
        grid,
        section,
        "Wind strength",
        region.config.wind_strength,
        0.05,
        2,
    );
    bindings.insert(
        wind_strength,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::WindStrength),
        },
    );
    let wind_freq = add_property_drag_value(
        tree,
        grid,
        section,
        "Wind freq",
        region.config.wind_frequency,
        0.05,
        2,
    );
    bindings.insert(
        wind_freq,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::WindFrequency),
        },
    );
    let wind_x = add_property_drag_value(
        tree,
        grid,
        section,
        "Wind dir X",
        region.config.wind_direction[0],
        0.05,
        3,
    );
    bindings.insert(
        wind_x,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::WindDirX),
        },
    );
    let wind_z = add_property_drag_value(
        tree,
        grid,
        section,
        "Wind dir Z",
        region.config.wind_direction[1],
        0.05,
        3,
    );
    bindings.insert(
        wind_z,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::WindDirZ),
        },
    );
    let interaction_radius = add_property_drag_value(
        tree,
        grid,
        section,
        "Interaction r",
        region.config.interaction_radius,
        0.05,
        2,
    );
    bindings.insert(
        interaction_radius,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::InteractionRadius),
        },
    );
    let interaction_strength = add_property_drag_value(
        tree,
        grid,
        section,
        "Interaction s",
        region.config.interaction_strength,
        0.05,
        2,
    );
    bindings.insert(
        interaction_strength,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::InteractionStrength),
        },
    );
    let cast_shadows = add_property_checkbox(
        tree,
        grid,
        section,
        "Cast shadows",
        region.config.cast_shadows,
    );
    bindings.insert(
        cast_shadows,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::CastShadows),
        },
    );
    let receive_shadows = add_property_checkbox(
        tree,
        grid,
        section,
        "Receive shadows",
        region.config.receive_shadows,
    );
    bindings.insert(
        receive_shadows,
        InspectorBinding {
            entity,
            field: InspectorField::Grass(GrassField::ReceiveShadows),
        },
    );
}

fn populate_grass_interactor_section(
    tree: &mut UiTreeBuilder,
    grid: Entity,
    entity: Entity,
    bindings: &mut HashMap<Entity, InspectorBinding>,
) {
    let Some(interactor) = tree.world_mut().core.get_grass_interactor(entity).cloned() else {
        return;
    };
    let section = tree.add_property_section(grid, "Grass interactor");
    let radius = add_property_drag_value(tree, grid, section, "Radius", interactor.radius, 0.05, 3);
    bindings.insert(
        radius,
        InspectorBinding {
            entity,
            field: InspectorField::GrassInteractor(GrassInteractorField::Radius),
        },
    );
    let strength = add_property_drag_value(
        tree,
        grid,
        section,
        "Strength",
        interactor.strength,
        0.05,
        3,
    );
    bindings.insert(
        strength,
        InspectorBinding {
            entity,
            field: InspectorField::GrassInteractor(GrassInteractorField::Strength),
        },
    );
}

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 layer_widget =
        add_property_dropdown(tree, grid, scene_section, "Layer", &layer_refs, layer_index);
    bindings.insert(
        layer_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Misc(MiscField::SceneLayerAssignment),
        },
    );
    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();
    let chunk_widget =
        add_property_dropdown(tree, grid, scene_section, "Chunk", &chunk_refs, chunk_index);
    bindings.insert(
        chunk_widget,
        InspectorBinding {
            entity,
            field: InspectorField::Misc(MiscField::SceneChunkAssignment),
        },
    );

    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;
    }
    let section = tree.add_property_section(grid, "Layers & masks");
    if let Some(layer) = tree.world_mut().core.get_render_layer(entity).copied() {
        let widget = add_property_dropdown(
            tree,
            grid,
            section,
            "Render layer",
            &["World", "Overlay"],
            if layer.0 == nightshade::ecs::primitives::RenderLayer::OVERLAY {
                1
            } else {
                0
            },
        );
        bindings.insert(
            widget,
            InspectorBinding {
                entity,
                field: InspectorField::Misc(MiscField::RenderLayer),
            },
        );
    }
    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;
            let widget =
                add_property_checkbox(tree, grid, section, &format!("Layer bit {bit}"), set);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Misc(MiscField::CullingMaskBit(bit)),
                },
            );
        }
    }
    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;
            let widget =
                add_property_checkbox(tree, grid, section, &format!("Camera bit {bit}"), set);
            bindings.insert(
                widget,
                InspectorBinding {
                    entity,
                    field: InspectorField::Misc(MiscField::CameraCullingMaskBit(bit)),
                },
            );
        }
    }
    if has_ignore {
        let widget = add_property_checkbox(tree, grid, section, "Ignore parent scale", true);
        bindings.insert(
            widget,
            InspectorBinding {
                entity,
                field: InspectorField::Misc(MiscField::IgnoreParentScale),
            },
        );
    }
    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;
        let lines_section = tree.add_property_section(grid, "Lines");
        add_property_value(
            tree,
            grid,
            lines_section,
            "Segments",
            &line_count.to_string(),
        );
        let always_on_top_widget =
            add_property_checkbox(tree, grid, lines_section, "Always on top", always_on_top);
        bindings.insert(
            always_on_top_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Misc(MiscField::LinesAlwaysOnTop),
            },
        );
        let clear_widget = add_property_button(tree, grid, lines_section, "Clear lines", "Clear");
        bindings.insert(
            clear_widget,
            InspectorBinding {
                entity,
                field: InspectorField::Misc(MiscField::LinesClear),
            },
        );
    }
}

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_property_button(tree, grid, section, label, "Add");
        bindings.insert(
            widget,
            InspectorBinding {
                entity,
                field: InspectorField::AddComponent(kind),
            },
        );
    }
}

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_property_button(tree, grid, section, label, "Remove");
        bindings.insert(
            widget,
            InspectorBinding {
                entity,
                field: InspectorField::RemoveComponent(kind),
            },
        );
    }
}

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::GrassRegion,
            "Grass region",
            world.core.entity_has_grass_region(entity),
        ),
        (
            ComponentKind::GrassInteractor,
            "Grass interactor",
            world.core.entity_has_grass_interactor(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, GRASS_INTERACTOR, GRASS_REGION, 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::GrassRegion => {
            world.core.add_components(entity, GRASS_REGION);
            world.core.set_grass_region(
                entity,
                nightshade::ecs::grass::components::GrassRegion::default(),
            );
        }
        ComponentKind::GrassInteractor => {
            world.core.add_components(entity, GRASS_INTERACTOR);
            world.core.set_grass_interactor(
                entity,
                nightshade::ecs::grass::components::GrassInteractor::new(1.0, 1.0),
            );
        }
        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, GRASS_INTERACTOR, GRASS_REGION,
        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::GrassRegion => GRASS_REGION,
        ComponentKind::GrassInteractor => GRASS_INTERACTOR,
        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_grass_change(world: &mut World, entity: Entity, field: GrassField, value: &FieldValue) {
    let Some(region) = world.core.get_grass_region_mut(entity) else {
        return;
    };
    match (field, value) {
        (GrassField::Enabled, FieldValue::Bool(v)) => region.enabled = *v,
        (GrassField::WindStrength, FieldValue::Float(v)) => region.config.wind_strength = *v,
        (GrassField::WindFrequency, FieldValue::Float(v)) => region.config.wind_frequency = *v,
        (GrassField::WindDirX, FieldValue::Float(v)) => region.config.wind_direction[0] = *v,
        (GrassField::WindDirZ, FieldValue::Float(v)) => region.config.wind_direction[1] = *v,
        (GrassField::InteractionRadius, FieldValue::Float(v)) => {
            region.config.interaction_radius = *v
        }
        (GrassField::InteractionStrength, FieldValue::Float(v)) => {
            region.config.interaction_strength = *v
        }
        (GrassField::CastShadows, FieldValue::Bool(v)) => region.config.cast_shadows = *v,
        (GrassField::ReceiveShadows, FieldValue::Bool(v)) => region.config.receive_shadows = *v,
        (GrassField::BladesPerPatch, FieldValue::Float(v)) => {
            region.config.blades_per_patch = v.max(1.0) as u32;
        }
        (GrassField::PatchSize, FieldValue::Float(v)) => region.config.patch_size = *v,
        (GrassField::StreamRadius, FieldValue::Float(v)) => {
            region.config.stream_radius = *v;
            region.config.unload_radius = *v + 20.0;
        }
        _ => {}
    }
}

fn apply_grass_interactor_change(
    world: &mut World,
    entity: Entity,
    field: GrassInteractorField,
    value: &FieldValue,
) {
    let Some(interactor) = world.core.get_grass_interactor_mut(entity) else {
        return;
    };
    match (field, value) {
        (GrassInteractorField::Radius, FieldValue::Float(v)) => interactor.radius = *v,
        (GrassInteractorField::Strength, FieldValue::Float(v)) => interactor.strength = *v,
        _ => {}
    }
}

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 add_button = add_property_button(tree, grid, section, "Add instance", "Add");
    bindings.insert(
        add_button,
        InspectorBinding {
            entity,
            field: InspectorField::Instance(InstanceField::Add),
        },
    );

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

        let tx = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Translation X",
            transform.translation.x,
            0.05,
            3,
        );
        bindings.insert(
            tx,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::TranslationX(index)),
            },
        );
        let ty = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Translation Y",
            transform.translation.y,
            0.05,
            3,
        );
        bindings.insert(
            ty,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::TranslationY(index)),
            },
        );
        let tz = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Translation Z",
            transform.translation.z,
            0.05,
            3,
        );
        bindings.insert(
            tz,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::TranslationZ(index)),
            },
        );

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

        let sx = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Scale X",
            transform.scale.x,
            0.01,
            3,
        );
        bindings.insert(
            sx,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::ScaleX(index)),
            },
        );
        let sy = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Scale Y",
            transform.scale.y,
            0.01,
            3,
        );
        bindings.insert(
            sy,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::ScaleY(index)),
            },
        );
        let sz = add_property_drag_value(
            tree,
            grid,
            instance_section,
            "Scale Z",
            transform.scale.z,
            0.01,
            3,
        );
        bindings.insert(
            sz,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::ScaleZ(index)),
            },
        );

        let tint = custom_data[index];
        let tint_picker = add_property_color_picker(
            tree,
            grid,
            instance_section,
            "Tint",
            vec4(tint[0], tint[1], tint[2], tint[3]),
        );
        bindings.insert(
            tint_picker,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::Tint(index)),
            },
        );

        let remove_button =
            add_property_button(tree, grid, instance_section, "Remove instance", "Remove");
        bindings.insert(
            remove_button,
            InspectorBinding {
                entity,
                field: InspectorField::Instance(InstanceField::Remove(index)),
            },
        );
    }

    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 })
}
