use avian3d::prelude::ColliderConstructor;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};

/// Stable persisted identity for an actor. Bevy [`Entity`] IDs are runtime-only.
#[derive(
    Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActorId(pub String);

impl ActorId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }
}

/// Stable scene-document identity and its authored subscene references.
///
/// This is persisted as a reflected scene resource. Cross-scene ownership never
/// depends on runtime [`Entity`] identifiers.
#[derive(Resource, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Resource, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct SceneComposition {
    pub scene_id: String,
    #[serde(default)]
    pub subscenes: Vec<SubsceneReference>,
}

/// One stable, project-relative reference in a [`SceneComposition`].
#[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct SubsceneReference {
    pub id: String,
    pub path: String,
    #[serde(default = "default_true")]
    pub visible: bool,
    #[serde(default = "default_true")]
    pub locked: bool,
}

impl Default for SubsceneReference {
    fn default() -> Self {
        Self {
            id: String::new(),
            path: String::new(),
            visible: true,
            locked: true,
        }
    }
}

/// User-facing actor display name, separate from Bevy's runtime [`Name`].
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct ActorName(pub String);

impl ActorName {
    pub fn new(name: impl Into<String>) -> Self {
        Self(name.into())
    }
}

/// Stable identity for repeatable authoring items such as renderer slots.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct ComponentInstanceId(pub String);

impl ComponentInstanceId {
    pub fn new(id: impl Into<String>) -> Self {
        Self(id.into())
    }

    pub fn is_empty(&self) -> bool {
        self.0.trim().is_empty()
    }
}

/// Editor-only component ordering metadata. Runtime systems must not depend on it.
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct InspectorOrder {
    /// Stable component IDs used by current scenes.
    #[serde(default)]
    pub component_ids: Vec<String>,
    /// Legacy reflected type paths used by schema-v3 scenes.
    #[serde(default)]
    pub component_type_names: Vec<String>,
    /// Legacy schema-v3 storage. New authoring writes active state to
    /// [`AuthoringComponentStates`]; this field remains readable so old scenes can
    /// be upgraded without losing disabled components.
    #[serde(default)]
    pub component_states: Vec<InspectorComponentState>,
}

/// Persisted active state for authoring components.
///
/// This is intentionally separate from [`InspectorOrder`]: hydration and runtime
/// behavior must not depend on how the editor happens to arrange component cards.
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AuthoringComponentStates {
    #[serde(default)]
    pub states: Vec<InspectorComponentState>,
}

/// Per-component inspector metadata that must survive scene save/load.
#[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct InspectorComponentState {
    /// Stable authoring component identity used by current scenes.
    #[serde(default)]
    pub component_id: String,
    /// Legacy schema-v3 reflected type path.
    #[serde(default)]
    pub type_name: String,
    #[serde(default = "default_true")]
    pub active: bool,
}

impl Default for InspectorComponentState {
    fn default() -> Self {
        Self {
            component_id: String::new(),
            type_name: String::new(),
            active: true,
        }
    }
}

impl InspectorOrder {
    pub fn is_component_active(&self, type_name: &str) -> bool {
        let component_id = authoring_component_key(type_name);
        self.component_states
            .iter()
            .find(|state| {
                state.component_id == component_id
                    || state.type_name == type_name
                    || state.type_name == component_id
            })
            .map(|state| state.active)
            .unwrap_or(true)
    }

    pub fn set_component_active(&mut self, type_name: impl Into<String>, active: bool) {
        let type_name = type_name.into();
        let component_id = authoring_component_key(&type_name).to_string();
        if let Some(state) = self.component_states.iter_mut().find(|state| {
            state.component_id == component_id
                || state.type_name == type_name
                || state.type_name == component_id
        }) {
            state.component_id = component_id;
            state.type_name.clear();
            state.active = active;
        } else {
            self.component_states.push(InspectorComponentState {
                component_id,
                type_name: String::new(),
                active,
            });
        }
    }

    pub fn ensure_component_order(&mut self, present: &[&str]) {
        for type_name in present {
            let component_id = authoring_component_key(type_name);
            if !self
                .component_ids
                .iter()
                .any(|existing| existing == component_id)
            {
                self.component_ids.push(component_id.to_string());
            }
        }
        // Once the editor touches order, the stable-ID representation is
        // authoritative and legacy paths no longer need to be re-saved.
        self.component_type_names.clear();
    }

    pub fn ordered_components<'a>(&self, present: &[&'a str]) -> Vec<&'a str> {
        let mut ordered = Vec::new();
        for component_id in &self.component_ids {
            if let Some(present_type) = present
                .iter()
                .find(|present| authoring_component_key(present) == component_id)
            {
                ordered.push(*present_type);
            }
        }
        for type_name in &self.component_type_names {
            if let Some(present_type) = present.iter().find(|present| **present == type_name) {
                ordered.push(*present_type);
            }
        }
        for type_name in present {
            if !ordered.iter().any(|ordered_type| ordered_type == type_name) {
                ordered.push(*type_name);
            }
        }
        ordered
    }

    pub fn move_component(&mut self, type_name: &str, offset: isize, present: &[&str]) -> bool {
        self.ensure_component_order(present);
        let Some(index) = self
            .component_ids
            .iter()
            .position(|existing| existing == authoring_component_key(type_name))
        else {
            return false;
        };
        let target =
            (index as isize + offset).clamp(0, self.component_ids.len() as isize - 1) as usize;
        if target == index {
            return false;
        }
        let item = self.component_ids.remove(index);
        self.component_ids.insert(target, item);
        true
    }
}

impl AuthoringComponentStates {
    pub fn is_component_active(&self, type_name: &str) -> bool {
        let component_id = authoring_component_key(type_name);
        self.states
            .iter()
            .find(|state| {
                state.component_id == component_id
                    || state.type_name == type_name
                    || state.type_name == component_id
            })
            .map(|state| state.active)
            .unwrap_or(true)
    }

    pub fn set_component_active(&mut self, type_name: impl Into<String>, active: bool) {
        let type_name = type_name.into();
        let component_id = authoring_component_key(&type_name).to_string();
        if let Some(state) = self.states.iter_mut().find(|state| {
            state.component_id == component_id
                || state.type_name == type_name
                || state.type_name == component_id
        }) {
            state.component_id = component_id;
            state.type_name.clear();
            state.active = active;
        } else {
            self.states.push(InspectorComponentState {
                component_id,
                type_name: String::new(),
                active,
            });
        }
    }

    pub fn remove_component(&mut self, type_name: &str) {
        let component_id = authoring_component_key(type_name);
        self.states.retain(|state| {
            state.component_id != component_id
                && state.type_name != type_name
                && state.type_name != component_id
        });
    }
}

/// Returns whether a persisted authoring component is enabled.
///
/// `legacy_order` is consulted only when an older scene has not yet been migrated
/// to [`AuthoringComponentStates`].
pub fn authoring_component_active(
    states: Option<&AuthoringComponentStates>,
    legacy_order: Option<&InspectorOrder>,
    type_name: &str,
) -> bool {
    states
        .map(|states| states.is_component_active(type_name))
        .or_else(|| legacy_order.map(|order| order.is_component_active(type_name)))
        .unwrap_or(true)
}

pub fn inspector_component_active(order: Option<&InspectorOrder>, type_name: &str) -> bool {
    order
        .map(|order| order.is_component_active(type_name))
        .unwrap_or(true)
}

// Stable authoring component identities. These are editor/protocol keys; reflected
// Rust type paths remain serialization details and may change during refactors.
pub const AUTHORING_COMPONENT_PRIMITIVE: &str = "render.primitive";
pub const AUTHORING_COMPONENT_BRUSH: &str = "render.brush";
pub const AUTHORING_COMPONENT_STATIC_MESH_RENDERER: &str = "render.static_mesh_renderer";
pub const AUTHORING_COMPONENT_SKINNED_MESH_RENDERER: &str = "render.skinned_mesh_renderer";
pub const AUTHORING_COMPONENT_MATERIAL: &str = "render.material";
pub const AUTHORING_COMPONENT_LIGHT: &str = "render.light";
pub const AUTHORING_COMPONENT_ANIMATION_CONTROLLER: &str = "animation.controller";
pub const AUTHORING_COMPONENT_AUDIO_SOURCE: &str = "audio.source";
pub const AUTHORING_COMPONENT_AUDIO_LISTENER: &str = "audio.listener";
pub const AUTHORING_COMPONENT_RIGID_BODY: &str = "physics.rigid_body";
pub const AUTHORING_COMPONENT_COLLIDER: &str = "physics.collider";
pub const AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY: &str = "physics.legacy_body";
pub const AUTHORING_COMPONENT_PLAYER_SPAWN: &str = "gameplay.player_spawn";
pub const AUTHORING_COMPONENT_WEAPON_SPAWN: &str = "gameplay.weapon_spawn";
pub const AUTHORING_COMPONENT_TRIGGER_VOLUME: &str = "gameplay.trigger_volume";
pub const AUTHORING_COMPONENT_TEAM_SPAWN: &str = "gameplay.team_spawn";
pub const AUTHORING_COMPONENT_OBJECTIVE: &str = "gameplay.objective";
pub const AUTHORING_COMPONENT_POST_PROCESS_VOLUME: &str = "render.post_process_volume";
pub const AUTHORING_COMPONENT_NAVIGATION_BOUNDS: &str = "navigation.bounds";
pub const AUTHORING_COMPONENT_NAVIGATION_OBSTACLE: &str = "navigation.obstacle";
pub const AUTHORING_COMPONENT_NAVIGATION_AREA: &str = "navigation.area";
pub const AUTHORING_COMPONENT_NAVIGATION_LINK: &str = "navigation.link";

/// Resolves either a stable authoring ID or a reflected Rust type path to its
/// stable editor/protocol identity. Unknown extension keys are returned as
/// `None` and remain usable as-is by generic state/order storage.
pub fn authoring_component_id(key: &str) -> Option<&'static str> {
    match key {
        AUTHORING_COMPONENT_PRIMITIVE | COMPONENT_PRIMITIVE => Some(AUTHORING_COMPONENT_PRIMITIVE),
        AUTHORING_COMPONENT_BRUSH | COMPONENT_BRUSH_DESC => Some(AUTHORING_COMPONENT_BRUSH),
        AUTHORING_COMPONENT_STATIC_MESH_RENDERER | COMPONENT_STATIC_MESH_RENDERER => {
            Some(AUTHORING_COMPONENT_STATIC_MESH_RENDERER)
        }
        AUTHORING_COMPONENT_SKINNED_MESH_RENDERER | "shared::animation::SkinnedMeshRenderer" => {
            Some(AUTHORING_COMPONENT_SKINNED_MESH_RENDERER)
        }
        AUTHORING_COMPONENT_MATERIAL | COMPONENT_MATERIAL_DESC => {
            Some(AUTHORING_COMPONENT_MATERIAL)
        }
        AUTHORING_COMPONENT_LIGHT | COMPONENT_LIGHT_DESC => Some(AUTHORING_COMPONENT_LIGHT),
        AUTHORING_COMPONENT_ANIMATION_CONTROLLER | "shared::animation::AnimationControllerDesc" => {
            Some(AUTHORING_COMPONENT_ANIMATION_CONTROLLER)
        }
        AUTHORING_COMPONENT_AUDIO_SOURCE | COMPONENT_AUDIO_SOURCE_DESC => {
            Some(AUTHORING_COMPONENT_AUDIO_SOURCE)
        }
        AUTHORING_COMPONENT_AUDIO_LISTENER | COMPONENT_AUDIO_LISTENER_DESC => {
            Some(AUTHORING_COMPONENT_AUDIO_LISTENER)
        }
        AUTHORING_COMPONENT_RIGID_BODY | COMPONENT_RIGID_BODY_DESC => {
            Some(AUTHORING_COMPONENT_RIGID_BODY)
        }
        AUTHORING_COMPONENT_COLLIDER | COMPONENT_COLLIDER_DESC => {
            Some(AUTHORING_COMPONENT_COLLIDER)
        }
        AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY | COMPONENT_PHYSICS_BODY => {
            Some(AUTHORING_COMPONENT_LEGACY_PHYSICS_BODY)
        }
        AUTHORING_COMPONENT_PLAYER_SPAWN | COMPONENT_PLAYER_SPAWN => {
            Some(AUTHORING_COMPONENT_PLAYER_SPAWN)
        }
        AUTHORING_COMPONENT_WEAPON_SPAWN | COMPONENT_WEAPON_SPAWN => {
            Some(AUTHORING_COMPONENT_WEAPON_SPAWN)
        }
        AUTHORING_COMPONENT_TRIGGER_VOLUME | COMPONENT_TRIGGER_VOLUME => {
            Some(AUTHORING_COMPONENT_TRIGGER_VOLUME)
        }
        AUTHORING_COMPONENT_TEAM_SPAWN | COMPONENT_TEAM_SPAWN => {
            Some(AUTHORING_COMPONENT_TEAM_SPAWN)
        }
        AUTHORING_COMPONENT_OBJECTIVE | COMPONENT_OBJECTIVE_MARKER => {
            Some(AUTHORING_COMPONENT_OBJECTIVE)
        }
        AUTHORING_COMPONENT_POST_PROCESS_VOLUME | COMPONENT_POST_PROCESS_VOLUME => {
            Some(AUTHORING_COMPONENT_POST_PROCESS_VOLUME)
        }
        AUTHORING_COMPONENT_NAVIGATION_BOUNDS | "shared::navigation::NavigationBounds" => {
            Some(AUTHORING_COMPONENT_NAVIGATION_BOUNDS)
        }
        AUTHORING_COMPONENT_NAVIGATION_OBSTACLE | "shared::navigation::NavigationObstacle" => {
            Some(AUTHORING_COMPONENT_NAVIGATION_OBSTACLE)
        }
        AUTHORING_COMPONENT_NAVIGATION_AREA | "shared::navigation::NavigationArea" => {
            Some(AUTHORING_COMPONENT_NAVIGATION_AREA)
        }
        AUTHORING_COMPONENT_NAVIGATION_LINK | "shared::navigation::NavigationLink" => {
            Some(AUTHORING_COMPONENT_NAVIGATION_LINK)
        }
        _ => None,
    }
}

pub fn authoring_component_key(key: &str) -> &str {
    authoring_component_id(key).unwrap_or(key)
}

pub const COMPONENT_PRIMITIVE: &str = "shared::components::Primitive";
pub const COMPONENT_BRUSH_DESC: &str = "shared::components::BrushDesc";
pub const COMPONENT_STATIC_MESH_RENDERER: &str = "shared::components::StaticMeshRenderer";
pub const COMPONENT_MATERIAL_DESC: &str = "shared::components::MaterialDesc";
pub const COMPONENT_MATERIAL_OVERRIDE: &str = "shared::components::MaterialOverride";
pub const COMPONENT_LIGHT_DESC: &str = "shared::components::LightDesc";
pub const COMPONENT_RIGID_BODY_DESC: &str = "shared::components::RigidBodyDesc";
pub const COMPONENT_COLLIDER_DESC: &str = "shared::components::ColliderDesc";
pub const COMPONENT_PHYSICS_BODY: &str = "shared::components::PhysicsBody";
pub const COMPONENT_PLAYER_SPAWN: &str = "shared::components::PlayerSpawn";
pub const COMPONENT_WEAPON_SPAWN: &str = "shared::components::WeaponSpawn";
pub const COMPONENT_TRIGGER_VOLUME: &str = "shared::components::TriggerVolume";
pub const COMPONENT_TEAM_SPAWN: &str = "shared::components::TeamSpawn";
pub const COMPONENT_OBJECTIVE_MARKER: &str = "shared::components::ObjectiveMarker";
pub const COMPONENT_PREFAB_INSTANCE: &str = "shared::components::PrefabInstance";
pub const COMPONENT_POST_PROCESS_VOLUME: &str = "shared::components::PostProcessVolumeDesc";
pub const COMPONENT_PROJECT_SUN: &str = "shared::components::ProjectSun";
pub const COMPONENT_AUDIO_SOURCE_DESC: &str = "shared::components::AudioSourceDesc";
pub const COMPONENT_AUDIO_LISTENER_DESC: &str = "shared::components::AudioListenerDesc";

/// Stable imported sub-asset identifier used by authored audio clip references.
pub const AUDIO_CLIP_SUB_ASSET_ID: &str = "audio:source";

/// Reference to an imported content-browser asset or sub-asset.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct EditorAssetRef {
    /// Stable project asset registry UUID string.
    pub asset_id: String,
    /// Stable imported sub-asset ID inside the generated artifact.
    #[serde(default, alias = "mesh_label")]
    pub sub_asset_id: String,
    /// UI label cached for inspectors. The asset registry/artifact remains authoritative.
    #[serde(default)]
    pub label: String,
    /// Optional loadable project path cached for runtime hydration when the
    /// referenced asset is not available through an imported artifact lookup.
    #[serde(default)]
    pub source_path: Option<String>,
}

impl EditorAssetRef {
    pub fn new(
        asset_id: impl Into<String>,
        sub_asset_id: impl Into<String>,
        label: impl Into<String>,
    ) -> Self {
        Self {
            asset_id: asset_id.into(),
            sub_asset_id: sub_asset_id.into(),
            label: label.into(),
            source_path: None,
        }
    }

    pub fn with_source_path(mut self, source_path: impl Into<String>) -> Self {
        self.source_path = Some(source_path.into());
        self
    }

    pub fn is_resolved(&self) -> bool {
        !self.asset_id.trim().is_empty() && !self.sub_asset_id.trim().is_empty()
    }
}

/// Distance curve used by an authored spatial audio source.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum AudioRolloff {
    Linear,
    #[default]
    Inverse,
    Exponential,
}

/// Serializable distance attenuation parameters for spatial audio playback.
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AudioAttenuationDesc {
    /// Radius in meters inside which the source plays at full gain.
    #[serde(default = "default_audio_min_distance")]
    pub min_distance: f32,
    /// Radius in meters at which distance attenuation reaches silence.
    #[serde(default = "default_audio_max_distance")]
    pub max_distance: f32,
    #[serde(default)]
    pub rolloff: AudioRolloff,
    /// Curve strength. `1.0` is the neutral authored value.
    #[serde(default = "default_audio_rolloff_factor")]
    pub rolloff_factor: f32,
}

impl Default for AudioAttenuationDesc {
    fn default() -> Self {
        Self {
            min_distance: default_audio_min_distance(),
            max_distance: default_audio_max_distance(),
            rolloff: AudioRolloff::default(),
            rolloff_factor: default_audio_rolloff_factor(),
        }
    }
}

fn default_audio_min_distance() -> f32 {
    1.0
}

fn default_audio_max_distance() -> f32 {
    25.0
}

fn default_audio_rolloff_factor() -> f32 {
    1.0
}

fn default_audio_pitch() -> f32 {
    1.0
}

fn default_audio_spatial_blend() -> f32 {
    1.0
}

fn default_audio_bus() -> String {
    settings::AUDIO_BUS_SFX_ID.to_string()
}

fn default_audio_ear_gap() -> f32 {
    0.2
}

/// Scene-authored audio source. Runtime playback components are derived from this descriptor.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AudioSourceDesc {
    #[serde(default)]
    pub clip: Option<EditorAssetRef>,
    /// Source gain in decibels before bus gain is applied.
    #[serde(default)]
    pub gain_db: f32,
    #[serde(default = "default_audio_pitch")]
    pub pitch: f32,
    #[serde(default)]
    pub looping: bool,
    #[serde(default = "default_true")]
    pub autoplay: bool,
    /// Continuous authored blend where `0.0` is 2D and `1.0` is fully spatial.
    #[serde(default = "default_audio_spatial_blend")]
    pub spatial_blend: f32,
    #[serde(default)]
    pub attenuation: AudioAttenuationDesc,
    /// Stable project audio bus ID.
    #[serde(default = "default_audio_bus")]
    pub bus: String,
}

impl Default for AudioSourceDesc {
    fn default() -> Self {
        Self {
            clip: None,
            gain_db: 0.0,
            pitch: default_audio_pitch(),
            looping: false,
            autoplay: true,
            spatial_blend: default_audio_spatial_blend(),
            attenuation: AudioAttenuationDesc::default(),
            bus: default_audio_bus(),
        }
    }
}

/// Scene-authored spatial listener. Runtime selection resolves enabled listeners by priority.
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct AudioListenerDesc {
    #[serde(default = "default_true")]
    pub enabled: bool,
    #[serde(default)]
    pub priority: i32,
    /// Distance in meters between the listener's virtual ears.
    #[serde(default = "default_audio_ear_gap")]
    pub ear_gap: f32,
}

impl Default for AudioListenerDesc {
    fn default() -> Self {
        Self {
            enabled: true,
            priority: 0,
            ear_gap: default_audio_ear_gap(),
        }
    }
}

/// Marker for entities that belong to editable/savable level data.
#[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct LevelObject;

/// Runtime marker for helper meshes that must not enter Solari raytracing.
#[derive(Component, Reflect, Default, Debug, Clone, Copy)]
#[reflect(Component, Default, Debug)]
pub struct RaytracingExcluded;

/// Marks the player's spawn location for editor Play mode.
///
/// Place on a level object and set its `Transform` to choose where Play starts.
/// If none exists, the game default spawn is used.
#[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct PlayerSpawn;

/// Marks the runtime/project default directional sun.
///
/// Scene-authored directional [`LightDesc`] entities can override this default
/// so levels can own their lighting without duplicating sun contribution.
#[derive(Component, Reflect, Default, Debug, Clone, Copy)]
#[reflect(Component, Default, Debug)]
pub struct ProjectSun;

/// Simple primitive meshes that can be authored without external assets.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum PrimitiveShape {
    #[default]
    Box,
    Sphere,
    Ramp,
}

/// Reflectable authoring primitive. Hydration turns this into `Mesh3d`.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Primitive {
    pub shape: PrimitiveShape,
    pub size: Vec3,
}

impl Primitive {
    pub fn cuboid(size: Vec3) -> Self {
        Self {
            shape: PrimitiveShape::Box,
            size,
        }
    }

    pub fn sphere(radius: f32) -> Self {
        Self {
            shape: PrimitiveShape::Sphere,
            size: Vec3::splat(radius * 2.0),
        }
    }

    pub fn ramp(size: Vec3) -> Self {
        Self {
            shape: PrimitiveShape::Ramp,
            size,
        }
    }
}

impl Default for Primitive {
    fn default() -> Self {
        Self::cuboid(Vec3::ONE)
    }
}

/// Brush boolean role. Subtractive brushes are authored now but CSG is applied
/// by later tooling.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum BrushKind {
    #[default]
    Additive,
    SubtractiveMarker,
}

/// Serializable convex brush plane. Plane equation is `normal.dot(point) = distance`.
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct BrushPlaneDesc {
    pub normal: Vec3,
    pub distance: f32,
}

impl Default for BrushPlaneDesc {
    fn default() -> Self {
        Self {
            normal: Vec3::Y,
            distance: 0.0,
        }
    }
}

/// One polygon face on a convex authoring brush.
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct BrushFaceDesc {
    #[serde(default)]
    pub id: ComponentInstanceId,
    pub plane: BrushPlaneDesc,
    #[serde(default)]
    pub vertices: Vec<Vec3>,
    #[serde(default)]
    pub material: Option<EditorAssetRef>,
    #[serde(default)]
    pub texture: Option<EditorAssetRef>,
    #[serde(default)]
    pub uv_offset: Vec2,
    #[serde(default = "default_uv_scale")]
    pub uv_scale: Vec2,
    #[serde(default)]
    pub uv_rotation: f32,
    #[serde(default)]
    pub smoothing_group: u32,
}

impl Default for BrushFaceDesc {
    fn default() -> Self {
        Self {
            id: ComponentInstanceId::default(),
            plane: BrushPlaneDesc::default(),
            vertices: Vec::new(),
            material: None,
            texture: None,
            uv_offset: Vec2::ZERO,
            uv_scale: default_uv_scale(),
            uv_rotation: 0.0,
            smoothing_group: 0,
        }
    }
}

fn default_uv_scale() -> Vec2 {
    Vec2::ONE
}

/// Convex brush authoring data. Hydration turns this into generated mesh
/// children while the scene stores only this component.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct BrushDesc {
    pub kind: BrushKind,
    #[serde(default)]
    pub faces: Vec<BrushFaceDesc>,
    #[serde(default = "default_true")]
    pub cast_shadows: bool,
    #[serde(default = "default_true")]
    pub receive_shadows: bool,
}

impl BrushDesc {
    pub fn cuboid(size: Vec3) -> Self {
        let hx = size.x.max(0.001) * 0.5;
        let hy = size.y.max(0.001) * 0.5;
        let hz = size.z.max(0.001) * 0.5;
        let faces = vec![
            brush_face(
                "face:+x",
                Vec3::X,
                vec![
                    Vec3::new(hx, -hy, -hz),
                    Vec3::new(hx, hy, -hz),
                    Vec3::new(hx, hy, hz),
                    Vec3::new(hx, -hy, hz),
                ],
            ),
            brush_face(
                "face:-x",
                Vec3::NEG_X,
                vec![
                    Vec3::new(-hx, -hy, hz),
                    Vec3::new(-hx, hy, hz),
                    Vec3::new(-hx, hy, -hz),
                    Vec3::new(-hx, -hy, -hz),
                ],
            ),
            brush_face(
                "face:+y",
                Vec3::Y,
                vec![
                    Vec3::new(-hx, hy, -hz),
                    Vec3::new(-hx, hy, hz),
                    Vec3::new(hx, hy, hz),
                    Vec3::new(hx, hy, -hz),
                ],
            ),
            brush_face(
                "face:-y",
                Vec3::NEG_Y,
                vec![
                    Vec3::new(-hx, -hy, hz),
                    Vec3::new(-hx, -hy, -hz),
                    Vec3::new(hx, -hy, -hz),
                    Vec3::new(hx, -hy, hz),
                ],
            ),
            brush_face(
                "face:+z",
                Vec3::Z,
                vec![
                    Vec3::new(hx, -hy, hz),
                    Vec3::new(hx, hy, hz),
                    Vec3::new(-hx, hy, hz),
                    Vec3::new(-hx, -hy, hz),
                ],
            ),
            brush_face(
                "face:-z",
                Vec3::NEG_Z,
                vec![
                    Vec3::new(-hx, -hy, -hz),
                    Vec3::new(-hx, hy, -hz),
                    Vec3::new(hx, hy, -hz),
                    Vec3::new(hx, -hy, -hz),
                ],
            ),
        ];
        Self {
            kind: BrushKind::Additive,
            faces,
            cast_shadows: true,
            receive_shadows: true,
        }
    }

    pub fn extruded_prism(base_vertices: &[Vec3], height: f32) -> Option<Self> {
        let mut base = crate::brush_math::normalize_floor_polygon(base_vertices).ok()?;
        crate::brush_math::validate_floor_polygon(&base).ok()?;
        let height = height.max(0.001);
        if crate::brush_math::signed_area_xz(&base) < 0.0 {
            base.reverse();
        }
        let top: Vec<Vec3> = base
            .iter()
            .map(|vertex| *vertex + Vec3::Y * height)
            .collect();
        let mut top_face = top;
        top_face.reverse();

        let mut faces = Vec::with_capacity(base.len() + 2);
        faces.push(brush_face("face:+y", Vec3::Y, top_face));
        faces.push(brush_face("face:-y", Vec3::NEG_Y, base.clone()));
        for index in 0..base.len() {
            let next = (index + 1) % base.len();
            let a = base[index];
            let b = base[next];
            let edge = b - a;
            let normal = Vec3::new(edge.z, 0.0, -edge.x).normalize_or_zero();
            if normal.length_squared() <= f32::EPSILON {
                return None;
            }
            faces.push(brush_face(
                &format!("face:side:{index}"),
                normal,
                vec![a, a + Vec3::Y * height, b + Vec3::Y * height, b],
            ));
        }

        Some(Self {
            kind: BrushKind::Additive,
            faces,
            cast_shadows: true,
            receive_shadows: true,
        })
    }
}

impl Default for BrushDesc {
    fn default() -> Self {
        Self::cuboid(Vec3::ONE)
    }
}

fn brush_face(id: &str, normal: Vec3, vertices: Vec<Vec3>) -> BrushFaceDesc {
    let distance = vertices
        .first()
        .map(|vertex| normal.dot(*vertex))
        .unwrap_or(0.0);
    BrushFaceDesc {
        id: ComponentInstanceId::new(id),
        plane: BrushPlaneDesc { normal, distance },
        vertices,
        ..Default::default()
    }
}

/// Legacy static mesh collision setting kept only for loading older scenes.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum StaticMeshColliderMode {
    #[default]
    None,
    /// Generate an Avian triangle mesh collider from the loaded render mesh.
    TrimeshFromMesh,
}

/// Backward-compatible alias for the previous static mesh reference name.
pub type StaticMeshAssetRef = EditorAssetRef;

/// One material slot on a static mesh renderer entry.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticMeshMaterialSlot {
    pub slot_index: usize,
    pub name: String,
    /// Bevy sub-asset label for the source material, if the importer provided one.
    pub source_material_label: Option<String>,
    /// Optional per-instance material override.
    pub override_material: Option<MaterialDesc>,
}

/// One mesh draw slot owned by a [`StaticMeshRenderer`].
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MeshRenderSlot {
    #[serde(default)]
    pub id: ComponentInstanceId,
    pub name: String,
    pub mesh: EditorAssetRef,
    /// Stable renderer material slot used by this draw part.
    #[serde(default)]
    pub material_slot_id: ComponentInstanceId,
    /// Legacy inline imported-material reference. Schema-v4 migration moves this into the
    /// renderer's shared `RendererMaterialSet`.
    #[serde(default)]
    pub material: Option<EditorAssetRef>,
    #[serde(default)]
    pub local_transform: Transform,
    #[serde(default = "default_true")]
    pub visible: bool,
    #[serde(default = "default_true")]
    pub cast_shadows: bool,
    #[serde(default = "default_true")]
    pub receive_shadows: bool,
}

impl Default for MeshRenderSlot {
    fn default() -> Self {
        Self {
            id: ComponentInstanceId::default(),
            name: "Mesh".into(),
            mesh: EditorAssetRef::default(),
            material_slot_id: ComponentInstanceId::default(),
            material: None,
            local_transform: Transform::default(),
            visible: true,
            cast_shadows: true,
            receive_shadows: true,
        }
    }
}

/// Backward-compatible alias for old code paths while scenes migrate to slots.
pub type StaticMeshRendererEntry = MeshRenderSlot;

/// Public renderer-foundation name for one static draw part.
pub type StaticMeshPart = MeshRenderSlot;

/// Reflectable static mesh renderer. Hydration turns entries into child `Mesh3d`
/// entities and source/default materials.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct StaticMeshRenderer {
    #[serde(default, alias = "entries")]
    pub slots: Vec<MeshRenderSlot>,
    #[serde(default)]
    pub materials: crate::RendererMaterialSet,
}

impl StaticMeshRenderer {
    pub fn empty() -> Self {
        Self {
            slots: Vec::new(),
            materials: crate::RendererMaterialSet::default(),
        }
    }

    pub fn single(mut slot: MeshRenderSlot) -> Self {
        if slot.material_slot_id.0.trim().is_empty() {
            slot.material_slot_id = ComponentInstanceId::new(format!("slot:{}", slot.id.0));
        }
        let source_material = slot.material.clone().map(crate::MaterialRef::new);
        let materials = crate::RendererMaterialSet {
            slots: vec![crate::RendererMaterialSlot {
                id: slot.material_slot_id.clone(),
                name: slot.name.clone(),
                source_material,
                material: None,
            }],
            orphaned_assignments: Vec::new(),
        };
        Self {
            slots: vec![slot],
            materials,
        }
    }

    pub fn material_slot_for_part(
        &self,
        part: &MeshRenderSlot,
    ) -> Option<&crate::RendererMaterialSlot> {
        let slot_id = if part.material_slot_id.0.trim().is_empty() {
            &part.id
        } else {
            &part.material_slot_id
        };
        self.materials.slot(slot_id)
    }
}

impl Default for StaticMeshRenderer {
    fn default() -> Self {
        Self::empty()
    }
}

fn default_true() -> bool {
    true
}

/// How a material should be resolved at runtime.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum MaterialShaderKind {
    #[default]
    StandardLit,
    Unlit,
    Custom,
}

/// Reference to a built-in or custom shader schema.
#[derive(Reflect, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct ShaderRefDesc {
    pub kind: MaterialShaderKind,
    #[serde(default)]
    pub schema_path: Option<String>,
    #[serde(default)]
    pub shader_path: Option<String>,
}

impl Default for ShaderRefDesc {
    fn default() -> Self {
        Self {
            kind: MaterialShaderKind::StandardLit,
            schema_path: None,
            shader_path: None,
        }
    }
}

/// Typed material parameter value exposed by a shader schema.
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum MaterialParameterValue {
    Bool(bool),
    Float(f32),
    Vec2(Vec2),
    Vec3(Vec3),
    Color(ColorDesc),
    Enum(String),
}

impl Default for MaterialParameterValue {
    fn default() -> Self {
        Self::Float(0.0)
    }
}

#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialParameter {
    pub name: String,
    pub value: MaterialParameterValue,
}

#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialTextureBinding {
    pub name: String,
    pub texture: Option<EditorAssetRef>,
}

/// Per-render-slot material override stored on scene actors.
#[derive(Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialSlotOverride {
    pub slot_id: ComponentInstanceId,
    #[serde(default)]
    pub base_material: Option<EditorAssetRef>,
    pub material: MaterialDesc,
}

/// Actor-level material overrides. Shared material assets are edited by asset
/// inspectors, while actor inspectors write only to this component.
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct MaterialOverride {
    #[serde(default)]
    pub slots: Vec<MaterialSlotOverride>,
}

/// Serializable color description. This avoids coupling saved scenes to any
/// internal color representation details.
#[derive(Reflect, Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, Serialize, Deserialize)]
pub struct ColorDesc {
    pub r: f32,
    pub g: f32,
    pub b: f32,
    pub a: f32,
}

impl ColorDesc {
    pub const fn srgb(r: f32, g: f32, b: f32) -> Self {
        Self { r, g, b, a: 1.0 }
    }

    pub fn to_color(self) -> Color {
        Color::srgba(self.r, self.g, self.b, self.a)
    }
}

impl Default for ColorDesc {
    fn default() -> Self {
        Self::srgb(0.8, 0.8, 0.8)
    }
}

/// Reflectable authoring material. Hydration turns this into `StandardMaterial`.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct MaterialDesc {
    #[serde(default)]
    pub shader: ShaderRefDesc,
    pub base_color: ColorDesc,
    pub metallic: f32,
    pub roughness: f32,
    /// Emissive radiance tint. Values are multiplied by `emissive_intensity`.
    #[serde(default = "default_emissive_color")]
    pub emissive_color: ColorDesc,
    /// Emissive luminance multiplier in nits. `0.0` means non-emissive.
    #[serde(default)]
    pub emissive_intensity: f32,
    /// Optional base color (albedo) texture, path relative to `assets/`.
    pub base_color_texture: Option<String>,
    /// Optional emissive texture, path relative to `assets/`.
    #[serde(default)]
    pub emissive_texture: Option<String>,
    /// Optional normal map texture, path relative to `assets/`.
    pub normal_map_texture: Option<String>,
    /// Optional metallic/roughness texture, path relative to `assets/`.
    pub metallic_roughness_texture: Option<String>,
    /// When set, references a [`crate::MaterialAsset`] RON under `assets/materials/`.
    #[serde(default)]
    pub material_asset_path: Option<String>,
    /// Shader-schema-driven scalar/vector/color values.
    #[serde(default)]
    pub parameters: Vec<MaterialParameter>,
    /// Shader-schema-driven texture references.
    #[serde(default)]
    pub textures: Vec<MaterialTextureBinding>,
}

impl MaterialDesc {
    pub fn new(base_color: ColorDesc, metallic: f32, roughness: f32) -> Self {
        Self {
            shader: ShaderRefDesc::default(),
            base_color,
            metallic,
            roughness,
            emissive_color: default_emissive_color(),
            emissive_intensity: 0.0,
            base_color_texture: None,
            emissive_texture: None,
            normal_map_texture: None,
            metallic_roughness_texture: None,
            material_asset_path: None,
            parameters: Vec::new(),
            textures: Vec::new(),
        }
    }
}

fn default_emissive_color() -> ColorDesc {
    ColorDesc::srgb(1.0, 1.0, 1.0)
}

impl Default for MaterialDesc {
    fn default() -> Self {
        Self::new(ColorDesc::default(), 0.0, 0.65)
    }
}

/// Reflectable reference to an imported 3D model scene (glTF/GLB or FBX).
/// Hydration turns this into a `WorldAssetRoot` on the entity.
#[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct ModelRef {
    /// Stable asset-registry UUID. Empty only for legacy or unresolved references.
    #[serde(default)]
    pub asset_id: String,
    /// Asset path relative to `assets/`, e.g. `models/foo.glb` or `models/foo.fbx`.
    pub path: String,
    /// Scene index within the model file (`#SceneN` for FBX; glTF scene index otherwise).
    pub scene_index: usize,
}

impl ModelRef {
    pub fn new(path: impl Into<String>) -> Self {
        Self {
            asset_id: String::new(),
            path: path.into(),
            scene_index: 0,
        }
    }

    pub fn with_asset_id(mut self, asset_id: impl Into<String>) -> Self {
        self.asset_id = asset_id.into();
        self
    }

    /// Asset-server path for FBX `#SceneN` labels (see `bevy_ufbx`).
    pub fn fbx_scene_asset_path(path: &str, scene_index: usize) -> String {
        format!("{path}#Scene{scene_index}")
    }
}

/// Reflectable reference to a saved `.scn.ron` dynamic scene under `assets/`.
/// Hydration turns this into a `DynamicWorldRoot`.
#[derive(Component, Reflect, Default, Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct PrefabRef {
    /// Asset path relative to `assets/`, e.g. "prefabs/foo.scn.ron".
    pub path: String,
}

impl PrefabRef {
    pub fn new(path: impl Into<String>) -> Self {
        Self { path: path.into() }
    }
}

/// Marks a placed prefab instance with stable registry identity.
#[derive(Component, Reflect, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct PrefabInstance {
    /// Stable asset registry UUID string.
    pub asset_id: String,
    /// Source prefab path relative to `assets/`.
    pub source_path: String,
    /// Optional per-instance transform override serialized as RON (v1: unused).
    #[serde(default)]
    pub overrides_ron: Option<String>,
}

impl PrefabInstance {
    pub fn new(asset_id: impl Into<String>, source_path: impl Into<String>) -> Self {
        Self {
            asset_id: asset_id.into(),
            source_path: source_path.into(),
            overrides_ron: None,
        }
    }
}

/// Weapon pickup / spawn pad for FPS gameplay authoring.
#[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct WeaponSpawn {
    pub weapon_id: String,
}

/// Per-field post-process overrides (`None` = inherit project settings).
#[derive(Reflect, Debug, Clone, Default, Serialize, Deserialize)]
#[reflect(Default, Debug, Serialize, Deserialize)]
pub struct PostProcessVolumeOverrides {
    pub exposure_ev100: Option<f32>,
    pub fog_color: Option<ColorDesc>,
    pub fog_density: Option<f32>,
    pub bloom: Option<bool>,
    pub taa: Option<bool>,
    pub ssao: Option<bool>,
    pub tonemapping_aces: Option<bool>,
    pub atmosphere: Option<bool>,
    pub hdr: Option<bool>,
}

impl PostProcessVolumeOverrides {
    pub fn override_count(&self) -> u32 {
        let mut count = 0u32;
        if self.exposure_ev100.is_some() {
            count += 1;
        }
        if self.fog_color.is_some() {
            count += 1;
        }
        if self.fog_density.is_some() {
            count += 1;
        }
        if self.bloom.is_some() {
            count += 1;
        }
        if self.taa.is_some() {
            count += 1;
        }
        if self.ssao.is_some() {
            count += 1;
        }
        if self.tonemapping_aces.is_some() {
            count += 1;
        }
        if self.atmosphere.is_some() {
            count += 1;
        }
        if self.hdr.is_some() {
            count += 1;
        }
        count
    }
}

/// Scene-authored post-process volume (axis-aligned box in local space).
#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct PostProcessVolumeDesc {
    pub half_extents: Vec3,
    pub priority: i32,
    pub blend_distance: f32,
    pub overrides: PostProcessVolumeOverrides,
    /// Path to `assets/post_fx/*.ron`, if any.
    pub fullscreen_effect: Option<String>,
    /// Optional `assets/rendering_profiles/*.ron` preset seed.
    pub profile: Option<String>,
    /// Editor-friendly label for HUD / Rendering panel.
    pub label: Option<String>,
}

impl Default for PostProcessVolumeDesc {
    fn default() -> Self {
        Self {
            half_extents: Vec3::new(4.0, 2.0, 4.0),
            priority: 0,
            blend_distance: 2.0,
            overrides: PostProcessVolumeOverrides::default(),
            fullscreen_effect: None,
            profile: None,
            label: None,
        }
    }
}

/// Axis-aligned trigger volume for gameplay events.
#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct TriggerVolume {
    pub half_extents: Vec3,
    pub event_name: String,
}

impl Default for TriggerVolume {
    fn default() -> Self {
        Self {
            half_extents: Vec3::new(2.0, 2.0, 2.0),
            event_name: "trigger".into(),
        }
    }
}

/// Team spawn point marker.
#[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct TeamSpawn {
    pub team_id: u8,
}

/// Objective / game mode marker.
#[derive(Component, Reflect, Default, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct ObjectiveMarker {
    pub objective_id: String,
}

/// Stable sibling order among entities sharing the same parent in the hierarchy outliner.
#[derive(Component, Reflect, Default, Debug, Clone, Copy, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct HierarchySiblingIndex(pub i32);

/// Authoring visibility saved with the scene; hydration maps this to ECS [`Visibility`].
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct EditorVisibility {
    pub visible: bool,
}

impl Default for EditorVisibility {
    fn default() -> Self {
        Self { visible: true }
    }
}

/// Declared role of a level object for inspector routing and save validation.
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Debug, PartialEq, Serialize, Deserialize)]
pub enum ActorKind {
    Empty,
    Brush,
    StaticMesh,
    SkinnedMesh,
    ImportedModel,
    Light,
    PrefabAnchor,
    PlayerSpawn,
    WeaponSpawn,
    TriggerVolume,
    PostProcessVolume,
    TeamSpawn,
    Objective,
    AudioSource,
    AudioListener,
    Navigation,
}

/// Inspector upper bound for point/spot [`LightDesc::intensity`] (lumens).
///
/// Bevy's `PointLight` / `SpotLight` default is `light_consts::lumens::VERY_LARGE_CINEMA_LIGHT`
/// (1M lumens) for outdoor exposure (~12–15 EV100). This cap is 2× that reference so authored
/// lights can exceed the old 200k ceiling without unbounded sliders.
pub const AUTHORING_POINT_SPOT_LUMENS_MAX: f32 = 2_000_000.0;

/// Inspector upper bound for directional [`LightDesc::intensity`] (lux).
pub const AUTHORING_DIRECTIONAL_LUX_MAX: f32 = 150_000.0;

/// Serializable light kinds for editor-authored lighting.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum AuthoringLightKind {
    #[default]
    Point,
    Spot,
    Directional,
}

/// Reflectable authoring light. Hydration turns this into Bevy light components.
#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct LightDesc {
    pub kind: AuthoringLightKind,
    pub color: ColorDesc,
    /// Lumens (point/spot) or lux (directional).
    pub intensity: f32,
    /// Attenuation range in meters (point/spot).
    pub range: f32,
    pub shadows: bool,
    /// Spot inner cone half-angle in degrees.
    #[serde(default = "default_spot_inner_angle_deg")]
    pub inner_angle_deg: f32,
    /// Spot outer cone half-angle in degrees.
    #[serde(default = "default_spot_outer_angle_deg")]
    pub outer_angle_deg: f32,
}

fn default_spot_inner_angle_deg() -> f32 {
    25.0
}

fn default_spot_outer_angle_deg() -> f32 {
    35.0
}

impl Default for LightDesc {
    fn default() -> Self {
        Self::for_kind(AuthoringLightKind::Point)
    }
}

impl LightDesc {
    /// Sensible defaults when spawning a builtin light from the asset browser.
    pub fn for_kind(kind: AuthoringLightKind) -> Self {
        match kind {
            AuthoringLightKind::Point => Self {
                kind,
                color: ColorDesc::srgb(1.0, 0.95, 0.85),
                intensity: 25_000.0,
                range: 25.0,
                shadows: true,
                inner_angle_deg: default_spot_inner_angle_deg(),
                outer_angle_deg: default_spot_outer_angle_deg(),
            },
            AuthoringLightKind::Spot => Self {
                kind,
                color: ColorDesc::srgb(1.0, 0.95, 0.85),
                intensity: 30_000.0,
                range: 35.0,
                shadows: true,
                inner_angle_deg: default_spot_inner_angle_deg(),
                outer_angle_deg: default_spot_outer_angle_deg(),
            },
            AuthoringLightKind::Directional => Self {
                kind,
                color: ColorDesc::srgb(1.0, 0.95, 0.85),
                // Matches `ProjectSettings::rendering.sun_illuminance` default; spawn code
                // refreshes this from live project settings when placing in the scene.
                intensity: 100_000.0,
                range: 0.0,
                shadows: true,
                inner_angle_deg: default_spot_inner_angle_deg(),
                outer_angle_deg: default_spot_outer_angle_deg(),
            },
        }
    }
}

/// Runtime body type in a serialized-friendly form.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum AuthoringRigidBody {
    #[default]
    Static,
    Kinematic,
    Dynamic,
}

/// Reflectable authoring rigid body separated from collider geometry.
#[derive(Component, Reflect, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct RigidBodyDesc {
    pub body: AuthoringRigidBody,
}

impl Default for RigidBodyDesc {
    fn default() -> Self {
        Self {
            body: AuthoringRigidBody::Static,
        }
    }
}

/// Mesh collider cooking options for imported static mesh assets.
#[derive(Reflect, Default, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum MeshColliderCooking {
    #[default]
    Default,
}

/// Serializable collider shape independent of rigid body settings.
#[derive(Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Default, Debug, PartialEq, Serialize, Deserialize)]
pub enum ColliderShapeDesc {
    Cuboid {
        x_length: f32,
        y_length: f32,
        z_length: f32,
    },
    Sphere {
        radius: f32,
    },
    Capsule {
        radius: f32,
        height: f32,
    },
    StaticMesh {
        meshes: Vec<EditorAssetRef>,
        cooking: MeshColliderCooking,
    },
}

impl Default for ColliderShapeDesc {
    fn default() -> Self {
        Self::Cuboid {
            x_length: 1.0,
            y_length: 1.0,
            z_length: 1.0,
        }
    }
}

impl ColliderShapeDesc {
    pub fn static_mesh(meshes: Vec<EditorAssetRef>) -> Self {
        Self::StaticMesh {
            meshes,
            cooking: MeshColliderCooking::Default,
        }
    }

    pub fn to_constructor(&self) -> Option<ColliderConstructor> {
        match self {
            ColliderShapeDesc::Cuboid {
                x_length,
                y_length,
                z_length,
            } => Some(ColliderConstructor::Cuboid {
                x_length: *x_length,
                y_length: *y_length,
                z_length: *z_length,
            }),
            ColliderShapeDesc::Sphere { radius } => {
                Some(ColliderConstructor::Sphere { radius: *radius })
            }
            ColliderShapeDesc::Capsule { radius, height } => Some(ColliderConstructor::Capsule {
                radius: *radius,
                height: *height,
            }),
            ColliderShapeDesc::StaticMesh { .. } => None,
        }
    }
}

/// Reflectable authoring collider. Static mesh colliders are hydrated on
/// generated mesh children so they can reuse Bevy's mesh-to-collider path.
#[derive(Component, Reflect, Debug, Clone, PartialEq, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct ColliderDesc {
    #[serde(default = "default_true")]
    pub enabled: bool,
    #[serde(default)]
    pub is_trigger: bool,
    pub shape: ColliderShapeDesc,
}

impl ColliderDesc {
    pub fn static_cuboid(size: Vec3) -> Self {
        Self {
            enabled: true,
            is_trigger: false,
            shape: ColliderShapeDesc::Cuboid {
                x_length: size.x,
                y_length: size.y,
                z_length: size.z,
            },
        }
    }

    pub fn static_sphere(radius: f32) -> Self {
        Self {
            enabled: true,
            is_trigger: false,
            shape: ColliderShapeDesc::Sphere { radius },
        }
    }

    pub fn static_mesh(meshes: Vec<EditorAssetRef>) -> Self {
        Self {
            enabled: true,
            is_trigger: false,
            shape: ColliderShapeDesc::static_mesh(meshes),
        }
    }
}

impl Default for ColliderDesc {
    fn default() -> Self {
        Self::static_cuboid(Vec3::ONE)
    }
}

/// Reflectable authoring physics. Hydration turns this into `RigidBody` and
/// Avian's reflectable `ColliderConstructor` component.
#[derive(Component, Reflect, Debug, Clone, Serialize, Deserialize)]
#[reflect(Component, Default, Debug, Serialize, Deserialize)]
pub struct PhysicsBody {
    pub body: AuthoringRigidBody,
    pub collider: ColliderConstructor,
}

impl PhysicsBody {
    pub fn static_cuboid(size: Vec3) -> Self {
        Self {
            body: AuthoringRigidBody::Static,
            collider: ColliderConstructor::Cuboid {
                x_length: size.x,
                y_length: size.y,
                z_length: size.z,
            },
        }
    }

    pub fn static_sphere(radius: f32) -> Self {
        Self {
            body: AuthoringRigidBody::Static,
            collider: ColliderConstructor::Sphere { radius },
        }
    }
}

impl Default for PhysicsBody {
    fn default() -> Self {
        Self::static_cuboid(Vec3::ONE)
    }
}

#[cfg(test)]
mod tests {
    use super::{
        AudioListenerDesc, AudioRolloff, AudioSourceDesc, EditorAssetRef, ModelRef,
        AUDIO_CLIP_SUB_ASSET_ID,
    };
    use bevy::light::light_consts;

    #[test]
    fn point_spot_lumen_max_covers_bevy_cinema_reference() {
        const {
            assert!(
                super::AUTHORING_POINT_SPOT_LUMENS_MAX
                    >= light_consts::lumens::VERY_LARGE_CINEMA_LIGHT
            );
        }
    }

    #[test]
    fn fbx_scene_asset_path_format() {
        assert_eq!(
            ModelRef::fbx_scene_asset_path("models/foo.fbx", 0),
            "models/foo.fbx#Scene0"
        );
        assert_eq!(
            ModelRef::fbx_scene_asset_path("models/foo.fbx", 2),
            "models/foo.fbx#Scene2"
        );
    }

    #[test]
    fn legacy_model_ref_without_asset_id_remains_readable() {
        let model: ModelRef = ron::from_str(
            r#"(
                path: "assets/models/legacy.glb",
                scene_index: 2,
            )"#,
        )
        .expect("legacy ModelRef should deserialize");

        assert!(model.asset_id.is_empty());
        assert_eq!(model.path, "assets/models/legacy.glb");
        assert_eq!(model.scene_index, 2);
    }

    #[test]
    fn audio_authoring_descriptors_round_trip() {
        let source = AudioSourceDesc {
            clip: Some(
                EditorAssetRef::new("clip-id", AUDIO_CLIP_SUB_ASSET_ID, "Alarm")
                    .with_source_path("assets/audio/alarm.ogg"),
            ),
            gain_db: -3.0,
            pitch: 0.9,
            looping: true,
            ..Default::default()
        };
        let serialized = ron::to_string(&(source.clone(), AudioListenerDesc::default())).unwrap();
        let (parsed_source, parsed_listener): (AudioSourceDesc, AudioListenerDesc) =
            ron::from_str(&serialized).unwrap();

        assert_eq!(parsed_source, source);
        assert_eq!(parsed_listener, AudioListenerDesc::default());
        assert_eq!(parsed_source.attenuation.rolloff, AudioRolloff::Inverse);
        assert_eq!(parsed_source.bus, settings::AUDIO_BUS_SFX_ID);
    }
}
