//! Module → editor extension API.
//!
//! The engine's [`Module`](oxide_engine::app::Module) trait registers systems,
//! component types, asset loaders, and resources on an
//! [`App`](oxide_engine::app::App). This module is its **editor-side companion**:
//! one trait — [`EditorModule`] — through which a module contributes the UI it
//! needs the editor to host on its behalf.
//!
//! Specifically, a module can add:
//!
//! - **Menu items** in the top menu bar (e.g. `"File/Open Recent"`),
//! - **Dockable panels** in the docking shell (e.g. an "Audio Mixer"),
//! - **Viewport tools** that take over input on the 3D viewport (gizmos,
//!   measurement, paint),
//! - **Component inspectors** that render rich editors for the module's
//!   component types (keyed by their
//!   [`TypeRegistry`](oxide_engine::reflect::TypeRegistry) name), and
//! - **Settings pages** that drive the module's
//!   [`Settings`](oxide_engine::settings::Settings) section in the Preferences
//!   window.
//!
//! All five plug into the editor through one registry — [`EditorExtensions`] —
//! consumed by the docking shell. The shell never edits its own source to host
//! a new module's UI; this is *the* mechanism by which "anyone can write a
//! module" that extends both engine logic and the editor.
//!
//! ## Why a separate trait
//!
//! The engine has no egui dependency, so the editor hook can't live on the
//! engine's `Module` trait without dragging UI types into the engine. Two
//! traits implemented on the same struct keeps the engine GUI-free and lets the
//! editor binary register the same module on both sides:
//!
//! ```ignore
//! struct MyModule;
//! impl oxide_engine::app::Module for MyModule { /* … systems, types */ }
//! impl oxide_editor::extension::EditorModule for MyModule { /* … panels */ }
//! ```
//!
//! ## Attribution
//!
//! Every contribution remembers which module added it. Removing a module
//! ([`EditorExtensions::remove_module`]) removes all of its contributions in
//! one shot — the same lifecycle the engine's
//! [`App::remove_module`](oxide_engine::app::App::remove_module) gives systems,
//! types, and loaders. Disabling a module
//! ([`set_module_enabled`](EditorExtensions::set_module_enabled)) keeps the
//! contributions registered but hides them from the shell, so toggling a
//! module in Preferences is reversible without rebuilding the registry.
//!
//! ## Render closures
//!
//! Panel / inspector / settings-page closures take only `&mut egui::Ui` in
//! Stage 6 piece 5 (registration). Piece 6 — the docking shell — refines the
//! signatures to pass through the editor's runtime context. Modules that need
//! shared state today should capture it through interior mutability
//! (`Rc<RefCell<...>>`).

use std::collections::BTreeMap;

/// Where a panel prefers to be docked the first time the user opens it.
///
/// The shell may override this when restoring a saved layout; it is only a
/// hint, not a guarantee.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DockLocation {
    /// Pinned to the left side of the main area (hierarchies, project browser).
    Left,
    /// Pinned to the right side (properties / inspector).
    Right,
    /// Pinned to the bottom (console, logs, timeline).
    Bottom,
    /// The main central tab area (viewport, code, asset preview).
    Center,
    /// A floating window outside the dock layout.
    Floating,
}

/// One top-menu-bar item contributed by a module.
///
/// `path` uses `/` as a separator and identifies the menu tree, e.g.
/// `"File/New Project"` or `"View/Layout/Default"`. The shell groups items by
/// their leading segments.
pub struct MenuItem {
    /// Slash-separated path through the menu tree.
    pub path: String,
    /// Optional human-readable shortcut hint (e.g. `"Ctrl+N"`). Not bound by
    /// this API — the actual key binding lives in the Stage-7 input map.
    pub shortcut: Option<String>,
    /// Invoked when the item is clicked. The shell decides when to call it.
    pub action: Box<dyn FnMut()>,
}

/// A dockable panel contributed by a module.
pub struct Panel {
    /// Stable name; doubles as the tab title and the lookup key.
    pub name: String,
    /// Where the panel prefers to dock initially.
    pub default_dock: DockLocation,
    /// Renders the panel's contents into `ui` each frame the panel is visible.
    pub render: Box<dyn FnMut(&mut egui::Ui)>,
}

/// A viewport tool — usually a gizmo or a brush — that takes over the 3D
/// viewport's input while active.
pub struct ViewportTool {
    /// Stable name (e.g. `"Translate"`, `"Sculpt"`); identifies the tool in
    /// menus, toolbars, and shortcut tables.
    pub name: String,
    /// Called once when the tool becomes the active viewport tool. Use it to
    /// reset transient state or hook into the editor's command stack.
    pub on_activate: Box<dyn FnMut()>,
}

/// An editor for one reflected component type, keyed by the same name the
/// component is registered under in the
/// [`TypeRegistry`](oxide_engine::reflect::TypeRegistry). The shell calls
/// `render` from the Inspector panel when a selected entity has the component.
pub struct ComponentInspector {
    /// Matches the `name` passed to
    /// [`App::register_type`](oxide_engine::app::App::register_type).
    pub type_name: String,
    /// Renders an editor for the component into `ui`.
    pub render: Box<dyn FnMut(&mut egui::Ui)>,
}

/// A page in the Preferences window driving one
/// [`Settings`](oxide_engine::settings::Settings) section.
pub struct SettingsPage {
    /// Matches the `name` passed to
    /// [`Settings::register`](oxide_engine::settings::Settings::register).
    pub section_name: String,
    /// Title shown in the Preferences sidebar (defaults to `section_name` when
    /// the contributor leaves it empty).
    pub title: String,
    /// Renders the page's controls into `ui`.
    pub render: Box<dyn FnMut(&mut egui::Ui)>,
}

/// Editor-side companion to the engine's
/// [`Module`](oxide_engine::app::Module) trait.
///
/// Implement on the same type that implements `Module` (or on a separate
/// editor-only struct) and pass it to
/// [`EditorExtensions::add_module`]. Everything `build_editor` registers is
/// attributed to this module and can be removed atomically with
/// [`EditorExtensions::remove_module`].
pub trait EditorModule: 'static {
    /// A stable, unique name — should match the paired engine `Module::name`
    /// when both halves describe the same module, so the editor and engine
    /// agree on enable/disable.
    fn name(&self) -> &'static str;

    /// Registers UI contributions on `ext`.
    fn build_editor(&self, ext: &mut EditorExtensions);
}

/// Internal record tying any contribution to its source module and an
/// enabled/disabled flag inherited from the module.
struct Entry<T> {
    module: &'static str,
    value: T,
}

/// Registry of every UI contribution made by every editor module. The docking
/// shell reads this in Piece 6 to assemble the menu bar, dock layout, viewport
/// toolbox, inspector, and Preferences window.
#[derive(Default)]
pub struct EditorExtensions {
    menu_items: Vec<Entry<MenuItem>>,
    panels: Vec<Entry<Panel>>,
    viewport_tools: Vec<Entry<ViewportTool>>,
    inspectors: BTreeMap<String, Entry<ComponentInspector>>,
    settings_pages: BTreeMap<String, Entry<SettingsPage>>,
    modules: Vec<&'static str>,
    enabled: BTreeMap<&'static str, bool>,
    /// Set only while a module's `build_editor` is running, so individual
    /// `add_*` helpers can attribute the contribution without taking the
    /// module name as an argument.
    current_module: Option<&'static str>,
}

impl EditorExtensions {
    /// A fresh, empty registry.
    pub fn new() -> Self {
        Self::default()
    }

    /// Registers `module` and runs its
    /// [`build_editor`](EditorModule::build_editor). Re-adding a module with
    /// the same name first removes the old one, so callers don't have to dance
    /// around stale contributions when reloading.
    pub fn add_module<M: EditorModule>(&mut self, module: M) {
        let name = module.name();
        if self.modules.contains(&name) {
            self.remove_module(name);
        }
        self.modules.push(name);
        self.enabled.insert(name, true);
        self.current_module = Some(name);
        module.build_editor(self);
        self.current_module = None;
    }

    /// Removes every contribution registered by the named module. Returns
    /// whether the module was present.
    pub fn remove_module(&mut self, name: &str) -> bool {
        if !self.modules.contains(&name) {
            return false;
        }
        self.menu_items.retain(|e| e.module != name);
        self.panels.retain(|e| e.module != name);
        self.viewport_tools.retain(|e| e.module != name);
        self.inspectors.retain(|_, e| e.module != name);
        self.settings_pages.retain(|_, e| e.module != name);
        self.modules.retain(|m| *m != name);
        self.enabled.remove(name);
        true
    }

    /// Whether the named module is currently registered (independent of
    /// enabled-state).
    pub fn has_module(&self, name: &str) -> bool {
        self.modules.contains(&name)
    }

    /// Toggles whether contributions from the named module are visible to the
    /// shell. The contributions stay registered so re-enabling is instant.
    /// Returns whether the module was present.
    pub fn set_module_enabled(&mut self, name: &str, enabled: bool) -> bool {
        if let Some(slot) = self.enabled.get_mut(name) {
            *slot = enabled;
            true
        } else {
            false
        }
    }

    /// Whether the named module's contributions are currently enabled. Returns
    /// `false` for unknown modules.
    pub fn is_module_enabled(&self, name: &str) -> bool {
        self.enabled.get(name).copied().unwrap_or(false)
    }

    /// Registered module names, in insertion order.
    pub fn modules(&self) -> impl Iterator<Item = &'static str> + '_ {
        self.modules.iter().copied()
    }

    // --- contribution helpers (called from `build_editor`) -----------------

    /// Adds a menu item. Panics if called outside a module's `build_editor` —
    /// every contribution must be attributable to some module.
    pub fn add_menu_item(
        &mut self,
        path: impl Into<String>,
        action: impl FnMut() + 'static,
    ) -> &mut Self {
        self.add_menu_item_full(MenuItem {
            path: path.into(),
            shortcut: None,
            action: Box::new(action),
        })
    }

    /// Adds a menu item with a fully-specified [`MenuItem`] (lets the caller
    /// set a shortcut hint).
    pub fn add_menu_item_full(&mut self, item: MenuItem) -> &mut Self {
        let module = self.expect_module("add_menu_item");
        self.menu_items.push(Entry {
            module,
            value: item,
        });
        self
    }

    /// Adds a dockable panel. `default_dock` is a placement hint; the shell
    /// may override when restoring a saved layout.
    pub fn add_panel(
        &mut self,
        name: impl Into<String>,
        default_dock: DockLocation,
        render: impl FnMut(&mut egui::Ui) + 'static,
    ) -> &mut Self {
        let module = self.expect_module("add_panel");
        self.panels.push(Entry {
            module,
            value: Panel {
                name: name.into(),
                default_dock,
                render: Box::new(render),
            },
        });
        self
    }

    /// Adds a viewport tool (gizmo, brush, …).
    pub fn add_viewport_tool(
        &mut self,
        name: impl Into<String>,
        on_activate: impl FnMut() + 'static,
    ) -> &mut Self {
        let module = self.expect_module("add_viewport_tool");
        self.viewport_tools.push(Entry {
            module,
            value: ViewportTool {
                name: name.into(),
                on_activate: Box::new(on_activate),
            },
        });
        self
    }

    /// Adds a component inspector keyed by the type's reflection name.
    /// Re-registering a name overwrites the previous inspector (most-recently-
    /// added module wins; this lets a project override a base module's
    /// inspector if it has reason to).
    pub fn add_inspector(
        &mut self,
        type_name: impl Into<String>,
        render: impl FnMut(&mut egui::Ui) + 'static,
    ) -> &mut Self {
        let module = self.expect_module("add_inspector");
        let type_name = type_name.into();
        self.inspectors.insert(
            type_name.clone(),
            Entry {
                module,
                value: ComponentInspector {
                    type_name,
                    render: Box::new(render),
                },
            },
        );
        self
    }

    /// Adds a Preferences page driving the named settings section.
    pub fn add_settings_page(
        &mut self,
        section_name: impl Into<String>,
        title: impl Into<String>,
        render: impl FnMut(&mut egui::Ui) + 'static,
    ) -> &mut Self {
        let module = self.expect_module("add_settings_page");
        let section_name = section_name.into();
        let title = title.into();
        let title = if title.is_empty() {
            section_name.clone()
        } else {
            title
        };
        self.settings_pages.insert(
            section_name.clone(),
            Entry {
                module,
                value: SettingsPage {
                    section_name,
                    title,
                    render: Box::new(render),
                },
            },
        );
        self
    }

    // --- shell-facing lookups ---------------------------------------------

    /// Slash-separated paths of every currently-enabled menu item, in the
    /// order they were contributed.
    pub fn menu_item_paths(&self) -> impl Iterator<Item = &str> {
        self.iter_menu_items().map(|i| i.path.as_str())
    }

    /// Names of every currently-enabled panel.
    pub fn panel_names(&self) -> impl Iterator<Item = &str> {
        self.iter_panels().map(|p| p.name.as_str())
    }

    /// Names of every currently-enabled viewport tool.
    pub fn viewport_tool_names(&self) -> impl Iterator<Item = &str> {
        self.iter_viewport_tools().map(|t| t.name.as_str())
    }

    /// Reflection-keyed type names that currently have an inspector
    /// registered.
    pub fn inspector_type_names(&self) -> impl Iterator<Item = &str> {
        self.iter_inspectors().map(|i| i.type_name.as_str())
    }

    /// Settings-section names that currently have a Preferences page
    /// registered.
    pub fn settings_page_names(&self) -> impl Iterator<Item = &str> {
        self.iter_settings_pages().map(|p| p.section_name.as_str())
    }

    /// Whether an inspector is registered for the given reflection name and
    /// the contributing module is enabled.
    pub fn has_inspector_for(&self, type_name: &str) -> bool {
        self.inspectors
            .get(type_name)
            .map(|e| self.is_enabled(e.module))
            .unwrap_or(false)
    }

    /// Whether a Preferences page is registered for the given section name
    /// and the contributing module is enabled.
    pub fn has_settings_page_for(&self, section_name: &str) -> bool {
        self.settings_pages
            .get(section_name)
            .map(|e| self.is_enabled(e.module))
            .unwrap_or(false)
    }

    /// Iterates the enabled menu items themselves (gives the shell direct
    /// access to actions/shortcuts when rendering).
    pub fn iter_menu_items(&self) -> impl Iterator<Item = &MenuItem> {
        self.menu_items
            .iter()
            .filter(|e| self.is_enabled(e.module))
            .map(|e| &e.value)
    }

    /// Mutably iterates the enabled menu items so the shell can invoke each
    /// item's `FnMut` action when the user clicks it.
    pub fn iter_menu_items_mut(&mut self) -> impl Iterator<Item = &mut MenuItem> {
        let enabled = &self.enabled;
        self.menu_items
            .iter_mut()
            .filter(move |e| enabled.get(e.module).copied().unwrap_or(false))
            .map(|e| &mut e.value)
    }

    /// Iterates the enabled panels.
    pub fn iter_panels(&self) -> impl Iterator<Item = &Panel> {
        self.panels
            .iter()
            .filter(|e| self.is_enabled(e.module))
            .map(|e| &e.value)
    }

    /// Mutably iterates the enabled panels so the shell can call each panel's
    /// `FnMut` render closure each frame.
    pub fn iter_panels_mut(&mut self) -> impl Iterator<Item = &mut Panel> {
        let enabled = &self.enabled;
        self.panels
            .iter_mut()
            .filter(move |e| enabled.get(e.module).copied().unwrap_or(false))
            .map(|e| &mut e.value)
    }

    /// Iterates the enabled viewport tools.
    pub fn iter_viewport_tools(&self) -> impl Iterator<Item = &ViewportTool> {
        self.viewport_tools
            .iter()
            .filter(|e| self.is_enabled(e.module))
            .map(|e| &e.value)
    }

    /// Iterates the enabled component inspectors (in stable name order).
    pub fn iter_inspectors(&self) -> impl Iterator<Item = &ComponentInspector> {
        self.inspectors
            .values()
            .filter(|e| self.is_enabled(e.module))
            .map(|e| &e.value)
    }

    /// Iterates the enabled settings pages (in stable section-name order).
    pub fn iter_settings_pages(&self) -> impl Iterator<Item = &SettingsPage> {
        self.settings_pages
            .values()
            .filter(|e| self.is_enabled(e.module))
            .map(|e| &e.value)
    }

    /// The total number of contributions of every kind, across every
    /// registered module. Mostly for tests and diagnostics.
    pub fn contribution_count(&self) -> usize {
        self.menu_items.len()
            + self.panels.len()
            + self.viewport_tools.len()
            + self.inspectors.len()
            + self.settings_pages.len()
    }

    fn is_enabled(&self, module: &str) -> bool {
        self.enabled.get(module).copied().unwrap_or(false)
    }

    fn expect_module(&self, helper: &str) -> &'static str {
        self.current_module.unwrap_or_else(|| {
            panic!("EditorExtensions::{helper} called outside a module's build_editor")
        })
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A minimal module that exercises every contribution kind. Used both by
    /// the unit tests here and by the integration test in `tests/src/lib.rs`
    /// (where it proves the Stage-6 criterion: a module adds a menu item, a
    /// panel, and a settings page through the public API with no editor-core
    /// edits).
    struct DemoModule;
    impl EditorModule for DemoModule {
        fn name(&self) -> &'static str {
            "demo"
        }
        fn build_editor(&self, ext: &mut EditorExtensions) {
            ext.add_menu_item("Demo/Hello", || {});
            ext.add_panel("Demo Panel", DockLocation::Right, |_ui| {});
            ext.add_viewport_tool("Demo Tool", || {});
            ext.add_inspector("DemoComponent", |_ui| {});
            ext.add_settings_page("demo", "Demo", |_ui| {});
        }
    }

    struct OverlapModule;
    impl EditorModule for OverlapModule {
        fn name(&self) -> &'static str {
            "overlap"
        }
        fn build_editor(&self, ext: &mut EditorExtensions) {
            ext.add_menu_item("File/Quit", || {});
            ext.add_inspector("DemoComponent", |_ui| {});
        }
    }

    #[test]
    fn add_module_registers_each_contribution_kind() {
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);

        assert!(ext.has_module("demo"));
        assert!(ext.is_module_enabled("demo"));
        assert_eq!(ext.modules().collect::<Vec<_>>(), vec!["demo"]);

        assert_eq!(
            ext.menu_item_paths().collect::<Vec<_>>(),
            vec!["Demo/Hello"]
        );
        assert_eq!(ext.panel_names().collect::<Vec<_>>(), vec!["Demo Panel"]);
        assert_eq!(
            ext.viewport_tool_names().collect::<Vec<_>>(),
            vec!["Demo Tool"]
        );
        assert!(ext.has_inspector_for("DemoComponent"));
        assert!(ext.has_settings_page_for("demo"));
        assert_eq!(ext.contribution_count(), 5);
    }

    #[test]
    fn remove_module_drops_every_contribution() {
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);
        assert_eq!(ext.contribution_count(), 5);

        assert!(ext.remove_module("demo"));
        assert!(!ext.has_module("demo"));
        assert_eq!(ext.contribution_count(), 0);
        assert!(!ext.has_inspector_for("DemoComponent"));
        assert!(!ext.has_settings_page_for("demo"));

        // Removing twice is a no-op.
        assert!(!ext.remove_module("demo"));
    }

    #[test]
    fn disabling_a_module_hides_its_contributions_without_removing() {
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);
        assert!(ext.set_module_enabled("demo", false));
        assert!(!ext.is_module_enabled("demo"));

        // Hidden from every shell-facing lookup…
        assert_eq!(ext.menu_item_paths().count(), 0);
        assert_eq!(ext.panel_names().count(), 0);
        assert!(!ext.has_inspector_for("DemoComponent"));
        assert!(!ext.has_settings_page_for("demo"));
        // …but still registered, so re-enabling is instant.
        assert_eq!(ext.contribution_count(), 5);

        assert!(ext.set_module_enabled("demo", true));
        assert_eq!(ext.panel_names().count(), 1);
    }

    #[test]
    fn re_adding_a_module_replaces_its_contributions() {
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);
        ext.add_module(DemoModule);
        // Still one module, contributions are not duplicated.
        assert_eq!(ext.modules().collect::<Vec<_>>(), vec!["demo"]);
        assert_eq!(ext.contribution_count(), 5);
    }

    #[test]
    fn later_module_overrides_inspector_for_same_type() {
        // Both modules register an inspector for "DemoComponent". The
        // last-registered wins, but attribution remains correct: removing the
        // override exposes nothing (the original was overwritten, not
        // stacked), which is the simple-and-predictable behavior to ship for
        // piece 5. Stacking would let a project layer multiple inspectors on
        // one type — possible future refinement, not needed now.
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);
        ext.add_module(OverlapModule);

        assert!(ext.has_inspector_for("DemoComponent"));
        let owners: Vec<&'static str> = ext.inspectors.values().map(|e| e.module).collect();
        assert_eq!(owners, vec!["overlap"]);
    }

    #[test]
    fn modules_dont_see_each_others_contributions_when_disabled() {
        let mut ext = EditorExtensions::new();
        ext.add_module(DemoModule);
        ext.add_module(OverlapModule);

        // Two menu items total; disabling overlap hides only its item.
        assert_eq!(ext.menu_item_paths().count(), 2);
        ext.set_module_enabled("overlap", false);
        let visible: Vec<&str> = ext.menu_item_paths().collect();
        assert_eq!(visible, vec!["Demo/Hello"]);
    }

    #[test]
    #[should_panic(expected = "outside a module's build_editor")]
    fn contributing_outside_build_editor_panics() {
        // Catches the easy mistake of calling add_panel on a bare
        // EditorExtensions — every contribution must be attributable to a
        // module, otherwise remove_module would leave orphans behind.
        let mut ext = EditorExtensions::new();
        ext.add_panel("Orphan", DockLocation::Center, |_ui| {});
    }

    #[test]
    fn settings_page_defaults_title_to_section_name() {
        struct M;
        impl EditorModule for M {
            fn name(&self) -> &'static str {
                "m"
            }
            fn build_editor(&self, ext: &mut EditorExtensions) {
                ext.add_settings_page("audio", "", |_ui| {});
            }
        }
        let mut ext = EditorExtensions::new();
        ext.add_module(M);
        let page = ext.iter_settings_pages().next().unwrap();
        assert_eq!(page.title, "audio");
    }
}
