//! Editor extensibility: command registry, inspector sections, and extension hooks.

use bevy::prelude::*;
use bevy_egui::{egui, EguiContexts, EguiPrimaryContextPass};

use crate::history::group_selection_with_history;
use crate::operators::{run_immediate_operator, OperatorAvailability};
use crate::state::{EditorMode, PlayPossession};
use crate::ui::helpers::{
    reset_scene_lighting_to_project_defaults, toggle_play_mode, toggle_play_paused,
};
use crate::ui::selection_ops::{focus_editor_camera_on_selection, reset_selected_transforms};
use crate::ui::theme::{apply_editor_theme, PANEL_BG, TEXT, TEXT_DIM};
use crate::ui::UiState;
use crate::viewport::brush_tool::start_draw_brush_tool;
use crate::viewport::{
    intersect_selected_brushes, merge_selected_brushes, selected_brush_count,
    subtract_selected_brushes,
};

/// Named editor command invokable from the palette and BRP.
pub trait EditorCommand: Send + Sync {
    fn name(&self) -> &str;
    fn label(&self) -> &str {
        self.name()
    }
    fn disabled_reason(&self, _world: &World) -> Option<String> {
        None
    }
    fn execute(&self, world: &mut World);
}

#[derive(Resource, Default)]
pub struct EditorCommandRegistry {
    commands: Vec<Box<dyn EditorCommand>>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EditorCommandEntry {
    pub name: String,
    pub label: String,
}

impl EditorCommandRegistry {
    pub fn register(&mut self, command: Box<dyn EditorCommand>) {
        self.commands.push(command);
    }

    pub fn names(&self) -> Vec<String> {
        self.commands
            .iter()
            .map(|cmd| cmd.name().to_string())
            .collect()
    }

    pub fn entries(&self) -> Vec<EditorCommandEntry> {
        self.commands
            .iter()
            .map(|command| EditorCommandEntry {
                name: command.name().to_string(),
                label: command.label().to_string(),
            })
            .collect()
    }

    pub fn command_state(&self, world: &World, name: &str) -> Option<(String, Option<String>)> {
        self.commands
            .iter()
            .find(|cmd| cmd.name() == name)
            .map(|cmd| (cmd.label().to_string(), cmd.disabled_reason(world)))
    }

    pub fn run(&self, world: &mut World, name: &str) -> bool {
        if let Some(command) = self.commands.iter().find(|cmd| cmd.name() == name) {
            command.execute(world);
            true
        } else {
            false
        }
    }
}

/// Custom inspector body for level objects (game crates register via [`register_actor_inspector_section`]).
pub trait ActorInspectorSection: Send + Sync {
    fn id(&self) -> &str;
    fn title(&self) -> &str;
    fn order(&self) -> i32;
    fn applies_to(&self, world: &World, entity: Entity) -> bool;
    fn ui(&self, world: &mut World, ui: &mut egui::Ui, entity: Entity);
}

#[derive(Resource, Default)]
pub struct ActorInspectorSectionRegistry {
    sections: Vec<Box<dyn ActorInspectorSection>>,
}

impl ActorInspectorSectionRegistry {
    pub fn register(&mut self, section: Box<dyn ActorInspectorSection>) {
        self.sections.push(section);
    }

    pub fn sections_for(&self, world: &World, entity: Entity) -> Vec<&dyn ActorInspectorSection> {
        let mut matched: Vec<_> = self
            .sections
            .iter()
            .filter(|section| section.applies_to(world, entity))
            .map(|section| section.as_ref())
            .collect();
        matched.sort_by_key(|section| section.order());
        matched
    }

    pub fn draw_sections(&self, world: &mut World, ui: &mut egui::Ui, entity: Entity) {
        let mut matched: Vec<_> = self
            .sections
            .iter()
            .filter(|section| section.applies_to(world, entity))
            .collect();
        matched.sort_by_key(|section| section.order());
        for section in matched {
            section.ui(world, ui, entity);
        }
    }
}

pub fn register_actor_inspector_section(app: &mut App, section: Box<dyn ActorInspectorSection>) {
    if !app
        .world()
        .contains_resource::<ActorInspectorSectionRegistry>()
    {
        app.init_resource::<ActorInspectorSectionRegistry>();
    }
    app.world_mut()
        .resource_mut::<ActorInspectorSectionRegistry>()
        .register(section);
}

/// Optional extension point for registering editor panels and menu hooks.
pub trait EditorExtension: Send + Sync {
    fn name(&self) -> &str;
    fn register(&self, _app: &mut App) {}
}

/// Formal editor plugin surface for game-specific panels (H5).
pub trait EditorPlugin: Send + Sync {
    fn name(&self) -> &str;
    fn register(&self, app: &mut App);
}

#[derive(Resource, Default)]
pub struct EditorExtensions {
    pub extensions: Vec<Box<dyn EditorExtension>>,
}

#[derive(Resource, Default)]
pub struct RegisteredEditorPlugins {
    pub plugins: Vec<Box<dyn EditorPlugin>>,
}

#[derive(Resource, Default)]
pub struct CommandPalette {
    pub open: bool,
    pub filter: String,
    pub pending_run: Option<String>,
    pub focus_filter: bool,
    pub selected_index: usize,
}

pub struct ExtensibilityPlugin;

impl Plugin for ExtensibilityPlugin {
    fn build(&self, app: &mut App) {
        app.init_resource::<EditorCommandRegistry>()
            .init_resource::<ActorInspectorSectionRegistry>()
            .init_resource::<EditorExtensions>()
            .init_resource::<RegisteredEditorPlugins>()
            .init_resource::<CommandPalette>()
            .init_resource::<crate::command_queue::PendingEditorCommands>()
            .add_systems(Startup, register_builtin_commands)
            .add_systems(Update, run_palette_commands)
            .add_systems(EguiPrimaryContextPass, command_palette_ui);
    }
}

pub fn register_editor_plugin(app: &mut App, plugin: Box<dyn EditorPlugin>) {
    plugin.register(app);
    app.world_mut()
        .resource_mut::<RegisteredEditorPlugins>()
        .plugins
        .push(plugin);
}

fn register_builtin_commands(mut registry: ResMut<EditorCommandRegistry>) {
    registry.register(Box::new(TogglePlayCommand));
    registry.register(Box::new(TogglePlayPausedCommand));
    registry.register(Box::new(TogglePossessionCommand));
    registry.register(Box::new(ResetLightingCommand));
    registry.register(Box::new(GroupSelectionCommand));
    registry.register(Box::new(FocusSelectionCommand));
    registry.register(Box::new(ResetSelectionTransformCommand));
    registry.register(Box::new(CreatePostProcessVolumeCommand));
    registry.register(Box::new(FocusActiveVolumesCommand));
    registry.register(Box::new(SelectVolumesAtCameraCommand));
    registry.register(Box::new(DrawBrushCommand));
    registry.register(Box::new(IntersectBrushesCommand));
    registry.register(Box::new(MergeBrushesCommand));
    registry.register(Box::new(SubtractBrushesCommand));
}

fn run_palette_commands(world: &mut World) {
    let pending = world.resource_mut::<CommandPalette>().pending_run.take();
    if let Some(name) = pending {
        dispatch_editor_command(world, &name);
    }
    let names = std::mem::take(
        &mut world
            .resource_mut::<crate::command_queue::PendingEditorCommands>()
            .queue,
    );
    for name in names {
        dispatch_editor_command(world, &name);
    }
}

fn dispatch_editor_command(world: &mut World, name: &str) {
    let Some((label, disabled_reason)) =
        world.resource_scope(|world, registry: Mut<EditorCommandRegistry>| {
            registry.command_state(world, name)
        })
    else {
        warn!("unknown editor command: {name}");
        return;
    };

    let name_for_commit = name.to_string();
    let name_for_status = name.to_string();
    let label_for_commit = label.clone();
    run_immediate_operator(
        world,
        &name_for_status,
        &label,
        match disabled_reason {
            Some(reason) => OperatorAvailability::Disabled(reason),
            None => OperatorAvailability::Ready,
        },
        move |world| {
            let ran = world.resource_scope(|world, registry: Mut<EditorCommandRegistry>| {
                registry.run(world, &name_for_commit)
            });
            if ran {
                Ok(())
            } else {
                Err(format!("Unknown editor command: {name_for_commit}"))
            }
        },
    );

    trace!("dispatched editor operator command {label_for_commit}");
}

struct TogglePlayCommand;

impl EditorCommand for TogglePlayCommand {
    fn name(&self) -> &str {
        "play.toggle"
    }

    fn label(&self) -> &str {
        "Toggle Play / Edit"
    }

    fn execute(&self, world: &mut World) {
        toggle_play_mode(world);
    }
}

struct TogglePlayPausedCommand;

impl EditorCommand for TogglePlayPausedCommand {
    fn name(&self) -> &str {
        "play.toggle_pause"
    }

    fn label(&self) -> &str {
        "Pause / Resume Simulation"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (*world.resource::<State<EditorMode>>().get() != EditorMode::Playing)
            .then(|| "Enter Play mode before pausing the simulation".to_string())
    }

    fn execute(&self, world: &mut World) {
        toggle_play_paused(world);
    }
}

struct TogglePossessionCommand;

impl EditorCommand for TogglePossessionCommand {
    fn name(&self) -> &str {
        "play.toggle_possession"
    }

    fn label(&self) -> &str {
        "Possess / Eject Player"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (*world.resource::<State<EditorMode>>().get() != EditorMode::Playing)
            .then(|| "Enter Play mode before toggling possession".to_string())
    }

    fn execute(&self, world: &mut World) {
        let next = match *world.resource::<PlayPossession>() {
            PlayPossession::Possessed => PlayPossession::Ejected,
            PlayPossession::Ejected => PlayPossession::Possessed,
        };
        *world.resource_mut::<PlayPossession>() = next;
    }
}

struct ResetLightingCommand;

impl EditorCommand for ResetLightingCommand {
    fn name(&self) -> &str {
        "scene.reset_lighting"
    }

    fn label(&self) -> &str {
        "Reset Scene Lighting"
    }

    fn execute(&self, world: &mut World) {
        reset_scene_lighting_to_project_defaults(world);
    }
}

struct GroupSelectionCommand;

impl EditorCommand for GroupSelectionCommand {
    fn name(&self) -> &str {
        "selection.group"
    }

    fn label(&self) -> &str {
        "Group Selection"
    }

    fn execute(&self, world: &mut World) {
        let selected: Vec<Entity> = world
            .resource::<UiState>()
            .selected_entities
            .iter()
            .collect();
        group_selection_with_history(world, &selected);
    }
}

struct FocusSelectionCommand;

impl EditorCommand for FocusSelectionCommand {
    fn name(&self) -> &str {
        "selection.focus"
    }

    fn label(&self) -> &str {
        "Focus Selection"
    }

    fn execute(&self, world: &mut World) {
        focus_editor_camera_on_selection(world);
    }
}

struct ResetSelectionTransformCommand;

impl EditorCommand for ResetSelectionTransformCommand {
    fn name(&self) -> &str {
        "selection.reset_transform"
    }

    fn label(&self) -> &str {
        "Reset Selection Transform"
    }

    fn execute(&self, world: &mut World) {
        reset_selected_transforms(world);
    }
}

struct DrawBrushCommand;

impl EditorCommand for DrawBrushCommand {
    fn name(&self) -> &str {
        "brush.draw"
    }

    fn label(&self) -> &str {
        "Draw Brush"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (*world.resource::<State<EditorMode>>().get() != EditorMode::Editing)
            .then(|| "Enter Edit mode before drawing brushes".to_string())
    }

    fn execute(&self, world: &mut World) {
        start_draw_brush_tool(world);
    }
}

struct IntersectBrushesCommand;

impl EditorCommand for IntersectBrushesCommand {
    fn name(&self) -> &str {
        "brush.intersect"
    }

    fn label(&self) -> &str {
        "Brush Intersect"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (selected_brush_count(world) < 2).then(|| "Select at least two brushes".to_string())
    }

    fn execute(&self, world: &mut World) {
        intersect_selected_brushes(world);
    }
}

struct MergeBrushesCommand;

impl EditorCommand for MergeBrushesCommand {
    fn name(&self) -> &str {
        "brush.merge_convex"
    }

    fn label(&self) -> &str {
        "Brush Convex Merge"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (selected_brush_count(world) < 2).then(|| "Select at least two brushes".to_string())
    }

    fn execute(&self, world: &mut World) {
        merge_selected_brushes(world);
    }
}

struct SubtractBrushesCommand;

impl EditorCommand for SubtractBrushesCommand {
    fn name(&self) -> &str {
        "brush.subtract"
    }

    fn label(&self) -> &str {
        "Brush Subtract"
    }

    fn disabled_reason(&self, world: &World) -> Option<String> {
        (selected_brush_count(world) < 2).then(|| "Select at least two brushes".to_string())
    }

    fn execute(&self, world: &mut World) {
        subtract_selected_brushes(world);
    }
}

struct CreatePostProcessVolumeCommand;

impl EditorCommand for CreatePostProcessVolumeCommand {
    fn name(&self) -> &str {
        "rendering.create_volume"
    }

    fn label(&self) -> &str {
        "Create Post-process Volume"
    }

    fn execute(&self, world: &mut World) {
        crate::rendering_diagnostics::spawn_post_process_volume_at_camera(world);
    }
}

struct FocusActiveVolumesCommand;

impl EditorCommand for FocusActiveVolumesCommand {
    fn name(&self) -> &str {
        "rendering.focus_active_volumes"
    }

    fn label(&self) -> &str {
        "Focus Active Post-process Volumes"
    }

    fn execute(&self, world: &mut World) {
        crate::rendering_diagnostics::select_volumes_at_camera(world);
        focus_editor_camera_on_selection(world);
    }
}

struct SelectVolumesAtCameraCommand;

impl EditorCommand for SelectVolumesAtCameraCommand {
    fn name(&self) -> &str {
        "rendering.select_volumes_at_camera"
    }

    fn label(&self) -> &str {
        "Select Volumes at Camera"
    }

    fn execute(&self, world: &mut World) {
        crate::rendering_diagnostics::select_volumes_at_camera(world);
    }
}

fn command_palette_ui(
    mut contexts: EguiContexts,
    mut palette: ResMut<CommandPalette>,
    registry: Res<EditorCommandRegistry>,
) -> Result {
    let Ok(ctx) = contexts.ctx_mut() else {
        return Ok(());
    };

    apply_editor_theme(ctx);

    if ctx.input(|input| input.modifiers.command && input.key_pressed(egui::Key::P)) {
        palette.open = true;
        palette.filter.clear();
        palette.focus_filter = true;
        palette.selected_index = 0;
    }

    if !palette.open {
        return Ok(());
    }

    let mut open = palette.open;
    let palette_width = (ctx.content_rect().width() - 32.0).clamp(320.0, 520.0);
    egui::Window::new("Command Palette")
        .open(&mut open)
        .anchor(egui::Align2::CENTER_TOP, egui::vec2(0.0, 64.0))
        .collapsible(false)
        .resizable(false)
        .default_width(palette_width)
        .min_width(palette_width)
        .max_width(palette_width)
        .frame(egui::Frame::window(&ctx.global_style()).fill(PANEL_BG))
        .show(ctx, |ui| {
            let filter_response = ui.add(
                egui::TextEdit::singleline(&mut palette.filter)
                    .hint_text("Search commands...")
                    .desired_width(f32::INFINITY),
            );
            if palette.focus_filter {
                filter_response.request_focus();
                palette.focus_filter = false;
            }
            if filter_response.changed() {
                palette.selected_index = 0;
            }

            let filtered_entries = filtered_command_entries(&registry, &palette.filter);
            if !filtered_entries.is_empty() {
                palette.selected_index = palette.selected_index.min(filtered_entries.len() - 1);
            } else {
                palette.selected_index = 0;
            }

            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown))
                && !filtered_entries.is_empty()
            {
                palette.selected_index = (palette.selected_index + 1) % filtered_entries.len();
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp))
                && !filtered_entries.is_empty()
            {
                palette.selected_index = if palette.selected_index == 0 {
                    filtered_entries.len() - 1
                } else {
                    palette.selected_index - 1
                };
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Enter)) {
                if let Some(entry) = filtered_entries.get(palette.selected_index) {
                    palette.pending_run = Some(entry.name.clone());
                    palette.open = false;
                    ui.close();
                }
            }
            if ui.input_mut(|input| input.consume_key(egui::Modifiers::NONE, egui::Key::Escape)) {
                palette.open = false;
                ui.close();
            }

            ui.separator();
            egui::ScrollArea::vertical()
                .max_height(320.0)
                .show(ui, |ui| {
                    if filtered_entries.is_empty() {
                        ui.weak("No matching commands");
                    }
                    for (index, entry) in filtered_entries.iter().enumerate() {
                        let selected = index == palette.selected_index;
                        let response = ui.add_sized(
                            [ui.available_width(), 40.0],
                            egui::Button::selectable(selected, command_palette_row(entry)),
                        );
                        if selected {
                            response.scroll_to_me(Some(egui::Align::Center));
                        }
                        if response.clicked() {
                            palette.selected_index = index;
                            palette.pending_run = Some(entry.name.clone());
                            palette.open = false;
                            ui.close();
                        }
                    }
                });
        });
    palette.open = open;

    Ok(())
}

fn filtered_command_entries(
    registry: &EditorCommandRegistry,
    filter: &str,
) -> Vec<EditorCommandEntry> {
    let filter = filter.trim().to_lowercase();
    registry
        .entries()
        .into_iter()
        .filter(|entry| {
            filter.is_empty()
                || entry.name.to_lowercase().contains(&filter)
                || entry.label.to_lowercase().contains(&filter)
        })
        .collect()
}

fn command_palette_row(entry: &EditorCommandEntry) -> egui::WidgetText {
    let mut job = egui::text::LayoutJob::default();
    job.append(
        &entry.label,
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(13.0, egui::FontFamily::Proportional),
            color: TEXT,
            ..Default::default()
        },
    );
    job.append(
        &format!("\n{}", entry.name),
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(11.0, egui::FontFamily::Monospace),
            color: TEXT_DIM,
            ..Default::default()
        },
    );
    job.into()
}

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

    #[test]
    fn command_filter_matches_human_label_and_stable_id() {
        let mut registry = EditorCommandRegistry::default();
        registry.register(Box::new(TogglePlayCommand));
        registry.register(Box::new(ResetLightingCommand));

        let by_label = filtered_command_entries(&registry, "scene lighting");
        assert_eq!(by_label.len(), 1);
        assert_eq!(by_label[0].name, "scene.reset_lighting");

        let by_id = filtered_command_entries(&registry, "play.toggle");
        assert_eq!(by_id.len(), 1);
        assert_eq!(by_id[0].label, "Toggle Play / Edit");
    }

    #[test]
    fn empty_command_filter_preserves_registration_order() {
        let mut registry = EditorCommandRegistry::default();
        registry.register(Box::new(TogglePlayCommand));
        registry.register(Box::new(ResetLightingCommand));

        let entries = filtered_command_entries(&registry, "  ");
        assert_eq!(entries[0].name, "play.toggle");
        assert_eq!(entries[1].name, "scene.reset_lighting");
    }
}
