//! The editor's mutable runtime state.
//!
//! Split out from the shell so [commands](crate::commands) can mutate exactly
//! the data that participates in undo/redo without taking a borrow of the
//! whole shell (which also owns dock layout, dialog flags, and UI buffers).
//!
//! `EditorState` is the `C` parameter every editor `Command<C>` uses.

use std::collections::HashMap;

use oxide_engine::asset::{AssetDatabase, AssetServer};
use oxide_engine::input::{ActionMap, ActionOverrides};
use oxide_engine::layer::{GroupRegistry, LayerRegistry};
use oxide_engine::prelude::*;
use oxide_engine::project::{Project, RecentProjects};
use oxide_engine::reflect::TypeRegistry;
use oxide_engine::settings::Settings;

use crate::bindings;
use crate::gizmo::{GizmoDrag, GizmoMode, SnapSettings};

/// The data the editor mutates over a session: the scene the user is editing,
/// the current selection, the asset server, the open project (if any), the
/// typed settings store, and the editor's input action bindings.
///
/// Held by the shell; commands operate on `&mut EditorState` so the change is
/// guaranteed to flow through the same pipeline whether the user clicks a
/// menu, drags a gizmo, or runs a script (Stage 10).
pub struct EditorState {
    /// The scene currently open in the viewport / hierarchy.
    pub scene: Scene,
    /// The entity the inspector is bound to, if any.
    pub selected: Option<Entity>,
    /// The asset server shared by every loader (gltf, future texture/audio).
    /// Cloneable [`Arc`-backed handle](oxide_engine::asset::AssetServer) — cheap
    /// to hand to the file watcher.
    pub assets: AssetServer,
    /// The typed settings store. The shell registers core sections at startup
    /// (including the [`SETTINGS_SECTION`](crate::bindings::SETTINGS_SECTION)
    /// for [`actions`](Self::actions)) and modules add their own through the
    /// [extension API](crate::extension).
    pub settings: Settings,
    /// The editor's input action bindings (camera, future gizmo hotkeys, …).
    /// Default bindings are registered by
    /// [`bindings::register_defaults`](crate::bindings::register_defaults);
    /// the preferences UI reads / mutates this map directly, and the
    /// [`ActionOverrides`](oxide_engine::input::ActionOverrides) settings
    /// section stays in sync so a write-back through
    /// [`Settings::export`](oxide_engine::settings::Settings::export)
    /// captures the user's remap.
    pub actions: ActionMap,
    /// The open project, if any. `None` means the user is working in an
    /// unsaved scratch scene (handy for quick tinkering before saving).
    pub project: Option<Project>,
    /// The open project's asset database — the bridge between stable asset
    /// references (`AssetUid`/[`AssetRef<T>`](oxide_engine::asset::AssetRef)) and
    /// files under `assets/`. `Some` exactly when a [`project`](Self::project)
    /// is open; the shell scans it on open and rescans when the file watcher
    /// reports asset changes. The asset browser lists from it and the inspector
    /// asset-picker resolves through it.
    pub asset_db: Option<AssetDatabase>,
    /// The cross-session most-recently-used project list shown in the
    /// `File / Open Recent` submenu.
    pub recent: RecentProjects,
    /// Transform-gizmo UI state: active tool (translate / rotate / scale),
    /// snap settings, and the in-progress drag if any. The viewport reads
    /// this each frame to paint handles and dispatch drags; the inspector
    /// reads it to highlight the active axis. Default is
    /// [`GizmoMode::Translate`] with the default [`SnapSettings`].
    pub gizmo: GizmoState,
    /// The reflection registry that lets the inspector edit any registered
    /// component generically — list an entity's components, enumerate each
    /// one's fields, and get/set a single field by name. Seeded with the
    /// built-in reflected types (`Transform`, `Node`); modules add their own
    /// through the extension API. This is what makes the inspector
    /// reflection-driven instead of hand-coded per type.
    pub registry: TypeRegistry,
    /// Per-entity inspector order for **modular** components (the ones the
    /// user adds and reorders). Entries persist across re-selection. Anything
    /// currently on the entity that isn't in the map is appended in whatever
    /// order the registry reports it, so components inserted outside the
    /// inspector (e.g. by a script or `set_ron`) still show up.
    ///
    /// *Node-baked* components — `Node`, `Transform`, `Layer` — render in a
    /// fixed canonical order above this list and are not tracked here.
    pub component_order: HashMap<Entity, Vec<&'static str>>,
    /// Project-wide layer names (which single layer each entity's [`Layer`]
    /// index means). Seeded with a small common set (`Default`, `UI`, `Player`,
    /// `World`); later work persists this to the open project's settings so a
    /// team can name layers like Unity's Layer Inspector. Layers are the
    /// *single-valued* membership concept — one per entity.
    pub layer_registry: LayerRegistry,
    /// Project-wide gameplay group names — the *multi-valued* counterpart to
    /// [`layer_registry`](Self::layer_registry). An entity is on one layer but
    /// in any number of groups (stored in its
    /// [`Tags`](oxide_engine::layer::Tags) component). The registry is the
    /// project's fixed vocabulary, so the inspector offers groups to pick from
    /// rather than free-typed strings. Empty until the user defines groups in
    /// the Groups editor.
    pub group_registry: GroupRegistry,
    /// The UI document currently open in the **UI Canvas** panel, if any. The
    /// canvas edits this `UiPanel`'s widget tree (via the
    /// [`WidgetPath`](oxide_engine::ui::WidgetPath) authoring primitives) and
    /// saves it as a `ui/` asset. `None` means the canvas shows its empty state.
    pub ui_doc: Option<UiDoc>,
    /// Named spawn templates backing the hierarchy's add-menu. Seeded with the
    /// built-in prefabs (`Empty`, `Cube`, `Sphere`, `Plane`, `Camera`,
    /// `Directional Light`); each spawns an entity already carrying the
    /// matching components via the reflection [`registry`](Self::registry).
    pub prefab_registry: PrefabRegistry,
    /// Whether the editor is editing, playing, or paused (Stage 8.7). Drives
    /// whether the host runner ticks the engine [`Schedule`] and gates the
    /// play toolbar. Always [`PlayState::Editing`] at startup.
    pub play: PlayState,
    /// The scene as it was the instant **Play** was pressed, used to restore it
    /// bit-for-bit on **Stop** so play-mode mutations never corrupt the authored
    /// scene. `Some` exactly while [`play`](Self::play) is not
    /// [`Editing`](PlayState::Editing). See [`enter_play`](Self::enter_play) /
    /// [`stop`](Self::stop).
    pub play_snapshot: Option<SceneSnapshot>,
}

/// Whether the editor is authoring the scene or running it (Stage 8.7).
///
/// In [`Playing`](Self::Playing) the host runner ticks the engine
/// [`Schedule`](oxide_engine::app::Schedule) each frame; [`Paused`](Self::Paused)
/// freezes ticking but keeps the scene live so a single **Step** can advance one
/// fixed tick and the inspector can still edit fields; [`Editing`](Self::Editing)
/// is the normal authoring state where no systems run. Pressing **Play**
/// snapshots the scene and pressing **Stop** restores it (see
/// [`EditorState::enter_play`] / [`EditorState::stop`]).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PlayState {
    /// Authoring; the engine schedule is not ticked.
    #[default]
    Editing,
    /// Running; the schedule is ticked every frame.
    Playing,
    /// Running but frozen; the schedule is ticked only one fixed step per Step.
    Paused,
}

/// Editor-only transform-gizmo state held on [`EditorState`].
///
/// Decoupled from the gizmo *math* in [`crate::gizmo`] so the math stays
/// pure-logic (rays in, transforms out) and this struct carries only the
/// per-session UI choices.
pub struct GizmoState {
    /// Which tool is active (toggle with W / E / R while the cursor is
    /// over the Viewport tab and the camera is in orbit mode).
    pub mode: GizmoMode,
    /// The snap step sizes applied during a drag while the snap modifier
    /// (Ctrl by default) is held.
    pub snap: SnapSettings,
    /// `Some` while the user is mid-drag on a handle; the runner
    /// recomputes the target's transform each frame via
    /// [`crate::gizmo::apply_drag`].
    pub drag: Option<GizmoDrag>,
}

impl Default for GizmoState {
    fn default() -> Self {
        Self {
            mode: GizmoMode::Translate,
            snap: SnapSettings::default(),
            drag: None,
        }
    }
}

/// An open UI document in the editor's **UI Canvas**.
///
/// Holds the [`UiPanel`] being authored, the asset it loads from / saves to (if
/// it has been saved), the currently selected widget (by
/// [`WidgetPath`](oxide_engine::ui::WidgetPath)), and whether there are unsaved
/// edits. The same `UiPanel` RON the canvas writes is what the runtime loads.
pub struct UiDoc {
    /// The panel (widget tree + pixel/world size) being edited.
    pub panel: UiPanel,
    /// The `ui/` asset this document is saved as, once saved.
    pub asset: Option<AssetUid>,
    /// The widget the property panel is bound to (root by default).
    pub selected: WidgetPath,
    /// Whether the document has edits not yet written to disk.
    pub dirty: bool,
}

impl UiDoc {
    /// A new, empty document: a single full-bleed column root at a 1280×720
    /// authoring resolution. Not yet associated with an asset.
    pub fn new() -> Self {
        let root = Widget::column().with_id("root").with_style(UiLayoutStyle {
            width: UiSizing::Grow(1.0),
            height: UiSizing::Grow(1.0),
            ..Default::default()
        });
        Self {
            panel: UiPanel::new(root, Vec2::new(1280.0, 720.0), Vec2::new(2.0, 1.125)),
            asset: None,
            selected: WidgetPath::root(),
            dirty: false,
        }
    }
}

impl Default for UiDoc {
    fn default() -> Self {
        Self::new()
    }
}

impl EditorState {
    /// A blank state with an empty scene, no open project, and the editor's
    /// default action bindings registered (`F` toggle, WASD/QE move, Shift
    /// sprint — see [`bindings`](crate::bindings)).
    pub fn new() -> Self {
        Self::with_scene(Scene::new())
    }

    /// Like [`new`](Self::new) but starting from a populated scene — used by
    /// the shell so the editor has something visible on launch.
    pub fn with_scene(scene: Scene) -> Self {
        let mut actions = ActionMap::new();
        bindings::register_defaults(&mut actions);
        let mut settings = Settings::new();
        settings.register::<ActionOverrides>(bindings::SETTINGS_SECTION);
        let mut registry = TypeRegistry::new();
        register_builtin_types(&mut registry);
        // Seed a small, generally-useful set of named layers (besides the
        // built-in "Default" at index 0). These are common filter slots, not
        // generic "Layer 1 / Layer 2" filler; the user renames or extends them
        // in the Layer Names editor.
        let mut layer_registry = LayerRegistry::new();
        layer_registry.set(1, "UI");
        layer_registry.set(2, "Player");
        layer_registry.set(3, "World");
        Self {
            scene,
            selected: None,
            assets: AssetServer::new(),
            settings,
            actions,
            project: None,
            asset_db: None,
            recent: RecentProjects::new(8),
            gizmo: GizmoState::default(),
            registry,
            component_order: HashMap::new(),
            layer_registry,
            group_registry: GroupRegistry::new(),
            ui_doc: None,
            prefab_registry: builtin_prefabs(),
            play: PlayState::Editing,
            play_snapshot: None,
        }
    }

    /// Whether the editor is currently running the scene
    /// ([`Playing`](PlayState::Playing) or [`Paused`](PlayState::Paused)) — the
    /// states in which the authored scene is "live" and will be restored on Stop.
    pub fn is_in_play(&self) -> bool {
        self.play != PlayState::Editing
    }

    /// Enters **Play**: snapshots the current scene (so Stop can restore it) and
    /// transitions to [`Playing`](PlayState::Playing). No-op if already playing
    /// or paused — re-entering must not overwrite the original snapshot.
    pub fn enter_play(&mut self) {
        if self.is_in_play() {
            return;
        }
        self.play_snapshot = Some(self.scene.snapshot(&self.registry));
        self.play = PlayState::Playing;
    }

    /// Toggles between [`Playing`](PlayState::Playing) and
    /// [`Paused`](PlayState::Paused). No-op while [`Editing`](PlayState::Editing)
    /// (there is nothing to pause).
    pub fn toggle_pause(&mut self) {
        self.play = match self.play {
            PlayState::Playing => PlayState::Paused,
            PlayState::Paused => PlayState::Playing,
            PlayState::Editing => return,
        };
    }

    /// Stops play and restores the scene to its pre-play snapshot bit-for-bit,
    /// then returns to [`Editing`](PlayState::Editing). The restored scene has
    /// fresh entity handles, so the selection and any in-flight gizmo drag are
    /// cleared (the old [`Entity`] no longer exists). No-op while already
    /// editing.
    ///
    /// A failed restore (corrupt component RON) leaves the live scene in place
    /// but still returns to editing; the caller may log the returned error.
    pub fn stop(&mut self) -> Result<(), SceneError> {
        if !self.is_in_play() {
            return Ok(());
        }
        let result = match self.play_snapshot.take() {
            Some(snapshot) => snapshot.restore(&self.registry).map(|scene| {
                self.scene = scene;
            }),
            None => Ok(()),
        };
        self.selected = None;
        self.gizmo.drag = None;
        self.play = PlayState::Editing;
        result
    }

    /// Mirrors the current [`actions`](Self::actions) overrides into the
    /// `input.bindings` settings section so the next
    /// [`Settings::export`](oxide_engine::settings::Settings::export) round-
    /// trips them. Called by the shell after every binding edit.
    pub fn sync_action_overrides_to_settings(&mut self) {
        let overrides = self.actions.overrides();
        self.settings
            .set::<ActionOverrides>(bindings::SETTINGS_SECTION, overrides);
    }

    /// Applies any [`ActionOverrides`] previously
    /// [`Settings::import`](oxide_engine::settings::Settings::import)'d into
    /// the `input.bindings` section on top of the registered defaults.
    /// Called by the host runner at startup, after loading the on-disk
    /// preferences file. No-op if the section is empty or unregistered.
    pub fn apply_action_overrides_from_settings(&mut self) {
        if let Some(o) = self
            .settings
            .get::<ActionOverrides>(bindings::SETTINGS_SECTION)
        {
            // Clone to release the immutable borrow before mutating actions.
            let o = o.clone();
            self.actions.apply_overrides(&o);
        }
    }
}

impl Default for EditorState {
    fn default() -> Self {
        Self::new()
    }
}

/// Registers the engine's built-in reflected component types under stable
/// names. Kept separate so the shell (and tests) seed a registry identically,
/// and so modules layer their own `register_reflected` calls on top.
///
/// Transform and Node are reflected but **not** addable (every scene entity
/// already carries them). `MeshRenderer` is addable, so it shows up in the
/// inspector's "Add Component" menu and is copied by Duplicate. `PrimitiveShape`
/// registers as an enum so its inspector widget is a dropdown.
fn register_builtin_types(registry: &mut TypeRegistry) {
    // Node-baked components: reflected so the inspector can read/write them,
    // but **not** addable — every entity carries them inherently
    // (auto-attached on `Scene::spawn`), so the Add Component menu must not
    // offer to attach a duplicate.
    registry.register_reflected::<Transform>("Transform");
    registry.register_reflected::<Node>("Node");
    registry.register_reflected::<oxide_engine::layer::Layer>("Layer");
    // Modular components: addable from the inspector. Having several distinct
    // addable types is what lets the user attach more than one component to a
    // node and drag-reorder them (an archetypal ECS allows only one component
    // of a given type per entity, so a *second* mesh lives on a child — see the
    // Add Component menu's "as child" path).
    registry.register_addable::<oxide_engine::render::MeshRenderer>("MeshRenderer");
    registry.register_enum::<oxide_engine::render::PrimitiveShape>("PrimitiveShape");
    registry.register_addable::<oxide_engine::render::Camera>("Camera");
    registry.register_addable::<oxide_engine::render::DirectionalLight>("DirectionalLight");

    // Stage-9 physics components: addable from the inspector and captured by the
    // play-mode snapshot (so Stop reverts a simulated body). No per-type editor
    // code — the reflection-driven inspector renders them from their fields, with
    // the two shape/kind enums shown as dropdowns.
    registry.register_addable::<oxide_physics::RigidBody>("RigidBody");
    registry.register_enum::<oxide_physics::RigidBodyKind>("RigidBodyKind");
    registry.register_addable::<oxide_physics::Collider>("Collider");
    registry.register_enum::<oxide_physics::ColliderShape>("ColliderShape");
    registry.register_addable::<oxide_physics::CharacterController>("CharacterController");

    // Stage-10 scripting: the Script component is addable from the inspector and
    // captured by the play-mode snapshot (so Stop reverts a script attach/detach).
    // Its `source` field is an `AssetRef<ScriptAsset>`, which the inspector shows
    // as a picker filtered to the `scripts/` folder.
    registry.register_addable::<oxide_script::Script>("Script");
}

/// The built-in prefabs the hierarchy add-menu offers. Data-driven via
/// [`ComponentSpec`]: each prefab is a node name plus the components to attach,
/// applied on spawn through the reflection registry. The type names here must
/// match those registered in [`register_builtin_types`].
fn builtin_prefabs() -> PrefabRegistry {
    use oxide_engine::render::{Camera, DirectionalLight, MeshRenderer, PrimitiveShape};

    let mut reg = PrefabRegistry::new();
    // A bare node — just the node-baked Node/Transform/Layer.
    reg.register(Prefab::new("Empty"));
    // Primitive meshes (each a MeshRenderer with the matching shape).
    for (name, shape) in [
        ("Cube", PrimitiveShape::Cube),
        ("Sphere", PrimitiveShape::Sphere),
        ("Plane", PrimitiveShape::Plane),
    ] {
        let mesh = MeshRenderer {
            shape,
            ..MeshRenderer::default()
        };
        if let Some(spec) = ComponentSpec::of("MeshRenderer", &mesh) {
            reg.register(Prefab::new(name).with(spec));
        }
    }
    // Viewpoint + light entities.
    if let Some(spec) = ComponentSpec::of("Camera", &Camera::default()) {
        reg.register(Prefab::new("Camera").with(spec));
    }
    if let Some(spec) = ComponentSpec::of("DirectionalLight", &DirectionalLight::default()) {
        reg.register(Prefab::new("Directional Light").with(spec));
    }
    reg
}

#[cfg(test)]
mod tests {
    use super::*;
    use oxide_engine::math::{Transform, Vec3};

    /// An editor state with one entity, ready to play.
    fn state_with_entity() -> (EditorState, Entity) {
        let mut state = EditorState::new();
        let e = state.scene.spawn("thing", Transform::IDENTITY);
        (state, e)
    }

    #[test]
    fn enter_play_snapshots_and_sets_playing() {
        let (mut state, _) = state_with_entity();
        assert_eq!(state.play, PlayState::Editing);
        assert!(state.play_snapshot.is_none());
        state.enter_play();
        assert_eq!(state.play, PlayState::Playing);
        assert!(state.play_snapshot.is_some());
    }

    #[test]
    fn re_entering_play_does_not_overwrite_the_snapshot() {
        let (mut state, e) = state_with_entity();
        state.enter_play();
        let original = state.play_snapshot.clone();
        // Mutate, then (defensively) call enter_play again — the snapshot must
        // remain the *pre-play* one so Stop still reverts correctly.
        state
            .scene
            .set_local_transform(e, Transform::from_translation(Vec3::X));
        state.enter_play();
        assert_eq!(state.play_snapshot, original);
    }

    #[test]
    fn toggle_pause_flips_only_while_in_play() {
        let (mut state, _) = state_with_entity();
        // No-op while editing.
        state.toggle_pause();
        assert_eq!(state.play, PlayState::Editing);
        state.enter_play();
        state.toggle_pause();
        assert_eq!(state.play, PlayState::Paused);
        state.toggle_pause();
        assert_eq!(state.play, PlayState::Playing);
    }

    #[test]
    fn stop_restores_the_scene_and_clears_play_state() {
        let (mut state, e) = state_with_entity();
        let before = state.scene.to_ron().unwrap();
        state.selected = Some(e);
        state.enter_play();
        // Simulate a play-mode mutation (as a tick would).
        state
            .scene
            .set_local_transform(e, Transform::from_translation(Vec3::new(5.0, 0.0, 0.0)));
        assert_ne!(state.scene.to_ron().unwrap(), before);

        state.stop().unwrap();
        assert_eq!(state.play, PlayState::Editing);
        assert!(state.play_snapshot.is_none());
        // Scene reverted bit-for-bit; selection dropped (handles changed).
        assert_eq!(state.scene.to_ron().unwrap(), before);
        assert!(state.selected.is_none());
    }

    #[test]
    fn stop_while_editing_is_a_noop() {
        let (mut state, _) = state_with_entity();
        let before = state.scene.to_ron().unwrap();
        state.stop().unwrap();
        assert_eq!(state.play, PlayState::Editing);
        assert_eq!(state.scene.to_ron().unwrap(), before);
    }

    #[test]
    fn physics_components_are_addable_and_reflected() {
        let state = EditorState::new();
        // Editable via the reflection-driven inspector and offered in the Add
        // Component menu (addable), with no per-type editor code.
        for name in ["RigidBody", "Collider", "CharacterController"] {
            assert!(state.registry.is_registered(name), "{name} not registered");
        }
    }

    #[test]
    fn the_script_component_is_addable_and_reflected() {
        // Stage-10 dual-editability: Script is registered like any other
        // component, so the inspector offers it in Add Component and renders its
        // fields generically.
        let state = EditorState::new();
        assert!(state.registry.is_registered("Script"));
    }

    #[test]
    fn stop_reverts_a_script_attach() {
        // Attaching a Script during play must be undone on Stop — the snapshot
        // captures the reflected Script component like any other.
        let mut state = EditorState::new();
        let e = state.scene.spawn("scripted", Transform::IDENTITY);
        state.enter_play();
        // The "running game" attaches a script at play time.
        state
            .scene
            .world_mut()
            .insert_one(e, oxide_script::Script::default())
            .unwrap();
        state.stop().unwrap();

        let restored = state
            .scene
            .entities()
            .find(|&e| state.scene.name(e).as_deref() == Some("scripted"))
            .expect("the entity should be restored");
        assert!(
            state.scene.get::<oxide_script::Script>(restored).is_none(),
            "the play-time script attach should be reverted on Stop"
        );
    }

    #[test]
    fn stop_reverts_a_simulated_physics_body() {
        // A body that "fell" during play must be restored on Stop — the snapshot
        // captures reflected physics components like any other.
        let mut state = EditorState::new();
        let e = state.scene.spawn(
            "ball",
            Transform::from_translation(Vec3::new(0.0, 5.0, 0.0)),
        );
        state
            .scene
            .world_mut()
            .insert_one(e, oxide_physics::RigidBody::default())
            .unwrap();
        state
            .scene
            .world_mut()
            .insert_one(e, oxide_physics::Collider::ball(0.5))
            .unwrap();

        state.enter_play();
        // Simulate physics moving the body down (as the play tick would).
        state
            .scene
            .set_local_transform(e, Transform::from_translation(Vec3::new(0.0, 1.0, 0.0)));
        state.stop().unwrap();

        // Snapshot restore respawns entities (handles change), so find by name
        // and confirm both the Transform and the physics components came back.
        let restored = state
            .scene
            .entities()
            .find(|&e| state.scene.name(e).as_deref() == Some("ball"))
            .expect("the ball entity should be restored");
        assert_eq!(
            state.scene.world_transform(restored).unwrap().translation,
            Vec3::new(0.0, 5.0, 0.0),
            "transform should revert to the pre-play pose"
        );
        let collider = state
            .scene
            .get::<oxide_physics::Collider>(restored)
            .expect("the Collider component should be restored");
        assert_eq!(collider.radius, 0.5);
        assert!(state
            .scene
            .get::<oxide_physics::RigidBody>(restored)
            .is_some());
    }
}
