mod actor_inspector;
mod animation_inspector;
mod asset_browser;
mod audio_inspector;
mod build;
pub mod component_registry;
mod diagnostics;
mod dock_tabs;
mod fonts;
pub(crate) mod helpers;
mod hierarchy;
pub mod hierarchy_ops;
pub mod hierarchy_state;
pub(crate) mod inspector;
mod layout;
mod material_library;
mod menu;
pub(crate) mod navigation_inspector;
mod play_controls;
mod post_process_volume_ui;
mod scene_tabs;
pub mod selection_ops;
mod status_bar;
pub(crate) mod theme;
mod toolbar;
mod viewport_chrome;
mod widgets;

use std::collections::HashSet;

use bevy::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow};
use bevy_egui::{egui, EguiContext, EguiPrimaryContextPass, PrimaryEguiContext};
use bevy_inspector_egui::bevy_inspector::hierarchy::SelectedEntities;
use egui_dock::{DockArea, DockState};
use egui_phosphor_icons::icons;
use serde::{Deserialize, Serialize};

use crate::project_io::UserPreferences;
use crate::selection::SelectedEntity;
use crate::state::EditorMode;

pub(crate) use asset_browser::{
    adopt_material_conflict_save_as, reload_material_after_file_conflict,
    validate_material_conflict_destination,
};
pub use build::BuildPanel;
pub use diagnostics::{BrushDiagnosticsPanel, DiagnosticsPanel};
pub use layout::LayoutSaveTimer;
pub use viewport_chrome::ViewportUiState;

use diagnostics::diagnostics_window;
use dock_tabs::PanelNodes;
use hierarchy_state::HierarchyPanelState;
use layout::{load_dock_layout, mark_layout_dirty_if_changed, tick_layout_save};
use menu::top_menu_bar;
use status_bar::status_bar_ui;
use theme::{apply_editor_theme, editor_dock_style};
use toolbar::editor_toolbar_panel;
use viewport_chrome::viewport_tab_ui;

use crate::settings_ui::{project_settings_window, ProjectSettingsPanel};

#[derive(Resource)]
pub struct UiState {
    pub dock_state: DockState<EditorTab>,
    pub viewport_rect: egui::Rect,
    pub pointer_in_viewport: bool,
    /// Pointer position inside the viewport image (logical screen coords), for picking.
    pub viewport_pointer_pos: Option<egui::Pos2>,
    pub selected_entities: SelectedEntities,
    pub renaming_entity: Option<Entity>,
    pub rename_buffer: String,
    pub(crate) inspector_collapsed_components: HashSet<String>,
    pub(crate) panel_nodes: PanelNodes,
    last_mode_tab: Option<EditorMode>,
}

#[derive(Resource, Default)]
struct DockTabRequest(Option<EditorTab>);

pub fn egui_captures_keyboard(ctx: &egui::Context) -> bool {
    ctx.egui_wants_keyboard_input()
}

pub fn egui_captures_keyboard_from_world(world: &mut World) -> bool {
    let Ok(egui_context) = world
        .query_filtered::<&mut EguiContext, With<PrimaryEguiContext>>()
        .single(world)
    else {
        return false;
    };
    let mut egui_context = egui_context.clone();
    egui_captures_keyboard(egui_context.get_mut())
}

pub fn viewport_keyboard_shortcuts_active(
    ui_state: &UiState,
    ctx: &egui::Context,
    buttons: &ButtonInput<MouseButton>,
) -> bool {
    ui_state.pointer_in_viewport
        && !egui_captures_keyboard(ctx)
        && !buttons.any_pressed([MouseButton::Right, MouseButton::Middle])
}

impl UiState {
    pub fn default_layout() -> Self {
        Self::from_prefs(&UserPreferences::default())
    }

    pub fn from_prefs(prefs: &UserPreferences) -> Self {
        let layout = load_dock_layout(prefs);
        let fallback_nodes = layout.panel_nodes.unwrap_or(PanelNodes {
            main_view: layout.main_view_node,
            hierarchy: layout.main_view_node,
            inspector: layout.main_view_node,
            bottom: layout.main_view_node,
        });
        let panel_nodes = PanelNodes::discover(&layout.dock_state, fallback_nodes);
        Self {
            dock_state: layout.dock_state,
            viewport_rect: egui::Rect::NOTHING,
            pointer_in_viewport: false,
            viewport_pointer_pos: None,
            selected_entities: SelectedEntities::default(),
            renaming_entity: None,
            rename_buffer: String::new(),
            inspector_collapsed_components: HashSet::new(),
            panel_nodes,
            last_mode_tab: None,
        }
    }

    #[expect(
        deprecated,
        reason = "bevy_egui exposes a Context during EguiPrimaryContextPass, so one top-level panel must bridge to the new Ui-based panel API"
    )]
    fn ui(&mut self, world: &mut World, ctx: &mut egui::Context, dt: f32) {
        apply_editor_theme(ctx);
        let selected_before_ui = world.resource::<SelectedEntity>().0;

        let mode = *world.resource::<State<EditorMode>>().get();
        if self.last_mode_tab != Some(mode) {
            self.last_mode_tab = Some(mode);
            if mode == EditorMode::Playing {
                dock_tabs::ensure_viewport_for_play(&mut self.dock_state, &self.panel_nodes);
            }
        }

        self.pointer_in_viewport = false;
        self.viewport_pointer_pos = None;

        let playing = mode == EditorMode::Playing;
        let cursor_grabbed = world
            .query_filtered::<&CursorOptions, With<PrimaryWindow>>()
            .single(world)
            .is_ok_and(|cursor| cursor.grab_mode != CursorGrabMode::None);
        if playing && cursor_grabbed {
            ctx.input_mut(|input| {
                input.focused = false;
            });
        }

        egui::CentralPanel::no_frame().show(ctx, |root_ui| {
            top_menu_bar(
                world,
                root_ui,
                &self.selected_entities,
                &mut self.dock_state,
                &mut self.panel_nodes,
            );

            editor_toolbar_panel(world, root_ui, &mut self.selected_entities);
            status_bar_ui(world, root_ui, &self.selected_entities, mode);

            let mut viewer = TabViewer {
                world,
                viewport_rect: &mut self.viewport_rect,
                pointer_in_viewport: &mut self.pointer_in_viewport,
                viewport_pointer_pos: &mut self.viewport_pointer_pos,
                playing,
                cursor_grabbed,
                selected_entities: &mut self.selected_entities,
                renaming_entity: &mut self.renaming_entity,
                rename_buffer: &mut self.rename_buffer,
            };

            DockArea::new(&mut self.dock_state)
                .style(editor_dock_style(ctx))
                .show_inside(root_ui, &mut viewer);
        });

        if let Some(tab) = world.resource_mut::<DockTabRequest>().0.take() {
            dock_tabs::open_and_focus_tab(&mut self.dock_state, tab, &self.panel_nodes);
        }

        self.panel_nodes = PanelNodes::discover(&self.dock_state, self.panel_nodes);

        mark_layout_dirty_if_changed(
            world,
            &self.dock_state,
            self.panel_nodes.main_view(),
            self.panel_nodes,
        );
        tick_layout_save(world, dt);

        let mut diagnostics_open = world.resource::<DiagnosticsPanel>().open;
        diagnostics_window(world, ctx, &mut diagnostics_open);
        world.resource_mut::<DiagnosticsPanel>().open = diagnostics_open;

        let mut brush_diagnostics_open = world.resource::<BrushDiagnosticsPanel>().open;
        diagnostics::brush_diagnostics_window(world, ctx, &mut brush_diagnostics_open);
        world.resource_mut::<BrushDiagnosticsPanel>().open = brush_diagnostics_open;

        let mut rendering_open = world
            .resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
            .open;
        crate::rendering_diagnostics::rendering_diagnostics_window(world, ctx, &mut rendering_open);
        world
            .resource_mut::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
            .open = rendering_open;

        crate::session::session_resume_window(world, ctx);

        world.resource_scope(|world, mut panel: Mut<ProjectSettingsPanel>| {
            project_settings_window(world, ctx, &mut panel);
        });

        crate::collaboration::file_conflict_modal(world, ctx);

        if ctx.input(|input| input.key_pressed(egui::Key::F1)) {
            world.resource_mut::<ViewportUiState>().shortcuts_open = true;
        }

        let requested_selection = world.resource::<SelectedEntity>().0;
        reconcile_operator_selection(
            &mut self.selected_entities,
            selected_before_ui,
            requested_selection,
        );
        let selected = self.selected_entities.as_slice().first().copied();
        world.resource_mut::<SelectedEntity>().0 = selected;
    }
}

fn reconcile_operator_selection(
    selected_entities: &mut SelectedEntities,
    selected_before_ui: Option<Entity>,
    requested_selection: Option<Entity>,
) {
    let selected_after_ui = selected_entities.as_slice().first().copied();
    if requested_selection == selected_before_ui || selected_after_ui != selected_before_ui {
        return;
    }
    selected_entities.clear();
    if let Some(entity) = requested_selection {
        selected_entities.select_replace(entity);
    }
}

fn tab_title(icon: egui_phosphor_icons::Icon, label: &str) -> egui::WidgetText {
    let mut job = egui::text::LayoutJob::default();
    job.append(
        icon.as_str(),
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(13.0, egui::FontFamily::Name("phosphor-regular".into())),
            color: theme::TEXT,
            ..Default::default()
        },
    );
    job.append(
        &format!(" {label}"),
        0.0,
        egui::TextFormat {
            font_id: egui::FontId::new(13.0, egui::FontFamily::Proportional),
            color: theme::TEXT,
            ..Default::default()
        },
    );
    job.into()
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EditorTab {
    Viewport,
    /// Legacy saved-layout tab. Loaded layouts are migrated to [`EditorTab::Viewport`].
    GameView,
    Hierarchy,
    Inspector,
    Toolbar,
    AssetBrowser,
    MaterialLibrary,
    Build,
}

pub struct EditorUiPlugin;

impl Plugin for EditorUiPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(UiState::default_layout())
            .add_plugins(fonts::EditorFontsPlugin)
            .init_resource::<DiagnosticsPanel>()
            .init_resource::<BuildPanel>()
            .init_resource::<BrushDiagnosticsPanel>()
            .init_resource::<component_registry::EditorComponentRegistry>()
            .init_resource::<crate::rendering_diagnostics::RenderingDiagnosticsPanel>()
            .init_resource::<asset_browser::AssetBrowserUiState>()
            .init_resource::<material_library::MaterialLibraryState>()
            .init_resource::<material_library::MaterialLibraryCatalog>()
            .init_resource::<DockTabRequest>()
            .init_resource::<inspector::InspectorClipboard>()
            .init_resource::<inspector::InspectorPanelState>()
            .init_resource::<animation_inspector::AnimationInspectorState>()
            .init_resource::<navigation_inspector::NavigationEditorState>()
            .init_resource::<ViewportUiState>()
            .init_resource::<LayoutSaveTimer>()
            .add_systems(
                Startup,
                init_hierarchy_panel_from_prefs
                    .after(crate::project_io::load_user_preferences_on_startup),
            )
            .add_systems(Startup, validate_editor_component_registry)
            .add_systems(
                Startup,
                apply_ui_state_from_prefs
                    .after(crate::project_io::load_user_preferences_on_startup),
            )
            .add_systems(
                EguiPrimaryContextPass,
                material_library::refresh_material_library_catalog.before(show_ui_system),
            )
            .add_systems(EguiPrimaryContextPass, show_ui_system)
            .add_systems(
                Update,
                (
                    hierarchy_state::tick_hierarchy_prefs_save,
                    build::tick_build_job,
                    navigation_inspector::sync_navigation_state,
                ),
            );
    }
}

fn validate_editor_component_registry(world: &mut World) {
    let errors = world
        .resource::<component_registry::EditorComponentRegistry>()
        .validate(world)
        .err();
    if let Some(errors) = errors {
        panic!(
            "invalid authoring component registry:\n{}",
            errors.join("\n")
        );
    }
}

fn apply_ui_state_from_prefs(mut ui_state: ResMut<UiState>, prefs: Res<UserPreferences>) {
    *ui_state = UiState::from_prefs(&prefs);
}

fn init_hierarchy_panel_from_prefs(mut commands: Commands, prefs: Res<UserPreferences>) {
    commands.insert_resource(HierarchyPanelState::from_prefs(&prefs));
}

pub(crate) fn show_ui_system(world: &mut World) {
    let Ok(egui_context) = world
        .query_filtered::<&mut EguiContext, With<PrimaryEguiContext>>()
        .single(world)
    else {
        return;
    };
    let mut egui_context = egui_context.clone();
    let dt = world
        .get_resource::<Time>()
        .map(|time| time.delta_secs())
        .unwrap_or(0.0);

    world.resource_scope::<UiState, _>(|world, mut ui_state| {
        ui_state.ui(world, egui_context.get_mut(), dt);
    });
}

struct TabViewer<'a> {
    world: &'a mut World,
    viewport_rect: &'a mut egui::Rect,
    pointer_in_viewport: &'a mut bool,
    viewport_pointer_pos: &'a mut Option<egui::Pos2>,
    playing: bool,
    cursor_grabbed: bool,
    selected_entities: &'a mut SelectedEntities,
    renaming_entity: &'a mut Option<Entity>,
    rename_buffer: &'a mut String,
}

impl egui_dock::TabViewer for TabViewer<'_> {
    type Tab = EditorTab;

    fn ui(&mut self, ui: &mut egui::Ui, tab: &mut Self::Tab) {
        match tab {
            EditorTab::Viewport | EditorTab::GameView => {
                viewport_tab_ui(
                    self.world,
                    ui,
                    self.viewport_rect,
                    self.pointer_in_viewport,
                    self.viewport_pointer_pos,
                    self.selected_entities,
                    self.playing,
                    self.cursor_grabbed,
                );
            }
            EditorTab::Hierarchy => {
                hierarchy::hierarchy_panel_ui(
                    self.world,
                    ui,
                    self.selected_entities,
                    self.renaming_entity,
                    self.rename_buffer,
                );
            }
            EditorTab::Inspector => match self.selected_entities.as_slice() {
                &[entity] if self.world.get_entity(entity).is_ok() => {
                    actor_inspector::draw_actor_inspector(self.world, ui, entity);
                }
                &[entity] => {
                    ui.label(format!("Entity {entity:?} is no longer in the world."));
                }
                entities => {
                    actor_inspector::draw_multi_actor_inspector(self.world, ui, entities);
                }
            },
            EditorTab::Toolbar => {
                ui.label("Toolbar moved below the menu bar (Play, spawn, save).");
            }
            EditorTab::AssetBrowser => {
                asset_browser::asset_browser_ui(self.world, ui, self.selected_entities);
            }
            EditorTab::MaterialLibrary => {
                material_library::material_library_ui(self.world, ui, self.selected_entities);
            }
            EditorTab::Build => build::build_ui(self.world, ui),
        }
    }

    fn title(&mut self, tab: &mut Self::Tab) -> egui::WidgetText {
        let (icon, label) = match tab {
            EditorTab::Viewport | EditorTab::GameView => (icons::FILM_SLATE, "Viewport"),
            EditorTab::Hierarchy => (icons::LIST_BULLETS, "Hierarchy"),
            EditorTab::Inspector => (icons::SLIDERS, "Inspector"),
            EditorTab::Toolbar => (icons::WRENCH, "Toolbar"),
            EditorTab::AssetBrowser => (icons::FOLDER, "Asset Browser"),
            EditorTab::MaterialLibrary => (icons::PALETTE, "Material Library"),
            EditorTab::Build => (icons::HAMMER, "Build"),
        };
        tab_title(icon, label)
    }

    fn clear_background(&self, tab: &Self::Tab) -> bool {
        match tab {
            EditorTab::Viewport | EditorTab::GameView => true,
            _ => true,
        }
    }
}

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

    #[test]
    fn operator_selection_wins_when_ui_selection_did_not_change() {
        let before = Entity::from_bits(1);
        let spawned = Entity::from_bits(2);
        let mut selected = SelectedEntities::default();
        selected.select_replace(before);

        reconcile_operator_selection(&mut selected, Some(before), Some(spawned));

        assert_eq!(selected.as_slice(), &[spawned]);
    }

    #[test]
    fn direct_ui_selection_wins_over_stale_operator_resource() {
        let before = Entity::from_bits(1);
        let clicked = Entity::from_bits(2);
        let requested = Entity::from_bits(3);
        let mut selected = SelectedEntities::default();
        selected.select_replace(clicked);

        reconcile_operator_selection(&mut selected, Some(before), Some(requested));

        assert_eq!(selected.as_slice(), &[clicked]);
    }
}
