use std::collections::HashMap;

use nalgebra_glm::{Vec2, Vec4};

use crate::ecs::ui::components::{DragPayload, TileId};

use crate::ecs::text::components::{TextMesh, TextProperties};

use crate::prelude::*;
#[derive(Clone, Debug)]
pub struct ActiveDrag {
    pub source: freecs::Entity,
    pub payload: DragPayload,
    pub start_pos: Vec2,
    pub current_pos: Vec2,
}
use crate::ecs::ui::theme::ThemeState;
use crate::ecs::ui::types::{Rect, UiTextInstance};
use crate::render::wgpu::passes::geometry::{UiImage, UiLayer, UiRect};

#[derive(Clone)]
pub struct CachedTextMesh {
    pub mesh: TextMesh,
    pub slot_generation: u64,
    pub font_size: f32,
    pub wrap_width: Option<f32>,
    pub monospace_width: Option<f32>,
}

#[derive(Clone, Debug)]
pub enum UiEvent {
    ButtonClicked(freecs::Entity),
    SliderChanged {
        entity: freecs::Entity,
        value: f32,
    },
    ToggleChanged {
        entity: freecs::Entity,
        value: bool,
    },
    CheckboxChanged {
        entity: freecs::Entity,
        value: bool,
    },
    RadioChanged {
        entity: freecs::Entity,
        group_id: u32,
        option_index: usize,
    },
    TabChanged {
        entity: freecs::Entity,
        tab_index: usize,
    },
    TextInputChanged {
        entity: freecs::Entity,
        text: String,
    },
    TextInputSubmitted {
        entity: freecs::Entity,
        text: String,
    },
    DropdownChanged {
        entity: freecs::Entity,
        selected_index: usize,
    },
    MenuItemClicked {
        entity: freecs::Entity,
        item_index: usize,
    },
    ColorPickerChanged {
        entity: freecs::Entity,
        color: Vec4,
    },
    SelectableLabelClicked {
        entity: freecs::Entity,
        selected: bool,
    },
    DragValueChanged {
        entity: freecs::Entity,
        value: f32,
    },
    ContextMenuItemClicked {
        entity: freecs::Entity,
        item_index: usize,
        /// Caller-supplied tag from `ContextMenuBuilder::item_tagged`
        /// (or `checkable_tagged`), echoed back so apps can dispatch
        /// on a stable id instead of the item's positional index.
        /// Defaults to `0` for items added via the untagged
        /// `item`/`checkable` methods.
        tag: u32,
    },
    TreeNodeSelected {
        tree: freecs::Entity,
        node: freecs::Entity,
        selected: bool,
    },
    TreeNodeToggled {
        tree: freecs::Entity,
        node: freecs::Entity,
        expanded: bool,
    },
    TreeNodeContextMenu {
        tree: freecs::Entity,
        node: freecs::Entity,
        position: Vec2,
    },
    TreeNodeExpandRequested {
        tree: freecs::Entity,
        node: freecs::Entity,
        user_data: u64,
    },
    ModalClosed {
        entity: freecs::Entity,
        confirmed: bool,
    },
    CommandPaletteExecuted {
        entity: freecs::Entity,
        command_index: usize,
    },
    TileTabActivated {
        container: freecs::Entity,
        pane_id: TileId,
    },
    TileTabClosed {
        container: freecs::Entity,
        pane_id: TileId,
        title: String,
    },
    TileSplitterMoved {
        container: freecs::Entity,
        split_id: TileId,
        ratio: f32,
    },
    DragStarted {
        source: freecs::Entity,
        payload: DragPayload,
    },
    DragDropped {
        source: freecs::Entity,
        target: freecs::Entity,
        payload: DragPayload,
    },
    DragCancelled {
        source: freecs::Entity,
    },
    DragEnter {
        target: freecs::Entity,
        source: freecs::Entity,
        payload: DragPayload,
    },
    DragOver {
        target: freecs::Entity,
        source: freecs::Entity,
        position: Vec2,
    },
    DragLeave {
        target: freecs::Entity,
        source: freecs::Entity,
    },
    ShortcutTriggered {
        command_index: usize,
    },
    CanvasClicked {
        entity: freecs::Entity,
        command_id: u32,
        position: Vec2,
    },
    VirtualListItemClicked {
        entity: freecs::Entity,
        item_index: usize,
    },
    VirtualListItemRightClicked {
        entity: freecs::Entity,
        item_index: usize,
        screen_position: Vec2,
    },
    TextAreaChanged {
        entity: freecs::Entity,
        text: String,
    },
    RichTextEditorChanged {
        entity: freecs::Entity,
        text: String,
    },
    DataGridFilterChanged {
        entity: freecs::Entity,
        filters: Vec<String>,
    },
    RangeSliderChanged {
        entity: freecs::Entity,
        low: f32,
        high: f32,
    },
    DataGridCellEdited {
        entity: freecs::Entity,
        row: usize,
        column: usize,
        text: String,
    },
    BreadcrumbClicked {
        entity: freecs::Entity,
        segment_index: usize,
    },
    SplitterChanged {
        entity: freecs::Entity,
        ratio: f32,
    },
    MultiSelectChanged {
        entity: freecs::Entity,
        selected_indices: Vec<usize>,
    },
    DatePickerChanged {
        entity: freecs::Entity,
        year: i32,
        month: u32,
        day: u32,
    },
}

impl UiEvent {
    /// Returns the primary entity that this event targets.
    pub fn target_entity(&self) -> freecs::Entity {
        match self {
            UiEvent::ButtonClicked(entity) => *entity,
            UiEvent::SliderChanged { entity, .. } => *entity,
            UiEvent::ToggleChanged { entity, .. } => *entity,
            UiEvent::CheckboxChanged { entity, .. } => *entity,
            UiEvent::RadioChanged { entity, .. } => *entity,
            UiEvent::TabChanged { entity, .. } => *entity,
            UiEvent::TextInputChanged { entity, .. } => *entity,
            UiEvent::TextInputSubmitted { entity, .. } => *entity,
            UiEvent::DropdownChanged { entity, .. } => *entity,
            UiEvent::MenuItemClicked { entity, .. } => *entity,
            UiEvent::ColorPickerChanged { entity, .. } => *entity,
            UiEvent::SelectableLabelClicked { entity, .. } => *entity,
            UiEvent::DragValueChanged { entity, .. } => *entity,
            UiEvent::ContextMenuItemClicked { entity, .. } => *entity,
            UiEvent::TreeNodeSelected { node, .. } => *node,
            UiEvent::TreeNodeToggled { node, .. } => *node,
            UiEvent::TreeNodeContextMenu { node, .. } => *node,
            UiEvent::TreeNodeExpandRequested { node, .. } => *node,
            UiEvent::ModalClosed { entity, .. } => *entity,
            UiEvent::CommandPaletteExecuted { entity, .. } => *entity,
            UiEvent::TileTabActivated { container, .. } => *container,
            UiEvent::TileTabClosed { container, .. } => *container,
            UiEvent::TileSplitterMoved { container, .. } => *container,
            UiEvent::DragStarted { source, .. } => *source,
            UiEvent::DragDropped { target, .. } => *target,
            UiEvent::DragCancelled { source, .. } => *source,
            UiEvent::DragEnter { target, .. } => *target,
            UiEvent::DragOver { target, .. } => *target,
            UiEvent::DragLeave { target, .. } => *target,
            UiEvent::ShortcutTriggered { .. } => freecs::Entity::default(),
            UiEvent::CanvasClicked { entity, .. } => *entity,
            UiEvent::VirtualListItemClicked { entity, .. } => *entity,
            UiEvent::VirtualListItemRightClicked { entity, .. } => *entity,
            UiEvent::TextAreaChanged { entity, .. } => *entity,
            UiEvent::RichTextEditorChanged { entity, .. } => *entity,
            UiEvent::DataGridFilterChanged { entity, .. } => *entity,
            UiEvent::RangeSliderChanged { entity, .. } => *entity,
            UiEvent::DataGridCellEdited { entity, .. } => *entity,
            UiEvent::BreadcrumbClicked { entity, .. } => *entity,
            UiEvent::SplitterChanged { entity, .. } => *entity,
            UiEvent::MultiSelectChanged { entity, .. } => *entity,
            UiEvent::DatePickerChanged { entity, .. } => *entity,
        }
    }

    /// Returns `true` if this event type should bubble up the parent chain.
    /// Most interaction events bubble. Layout and animation lifecycle events
    /// do not.
    pub fn bubbles(&self) -> bool {
        match self {
            UiEvent::ButtonClicked(_)
            | UiEvent::SliderChanged { .. }
            | UiEvent::ToggleChanged { .. }
            | UiEvent::CheckboxChanged { .. }
            | UiEvent::RadioChanged { .. }
            | UiEvent::TabChanged { .. }
            | UiEvent::TextInputChanged { .. }
            | UiEvent::TextInputSubmitted { .. }
            | UiEvent::DropdownChanged { .. }
            | UiEvent::MenuItemClicked { .. }
            | UiEvent::ColorPickerChanged { .. }
            | UiEvent::SelectableLabelClicked { .. }
            | UiEvent::DragValueChanged { .. }
            | UiEvent::ContextMenuItemClicked { .. }
            | UiEvent::TreeNodeSelected { .. }
            | UiEvent::TreeNodeToggled { .. } => true,
            UiEvent::TreeNodeContextMenu { .. }
            | UiEvent::TreeNodeExpandRequested { .. }
            | UiEvent::ModalClosed { .. }
            | UiEvent::CommandPaletteExecuted { .. }
            | UiEvent::TileTabActivated { .. }
            | UiEvent::TileTabClosed { .. }
            | UiEvent::TileSplitterMoved { .. }
            | UiEvent::DragStarted { .. }
            | UiEvent::DragDropped { .. }
            | UiEvent::DragCancelled { .. }
            | UiEvent::DragEnter { .. }
            | UiEvent::DragOver { .. }
            | UiEvent::DragLeave { .. }
            | UiEvent::ShortcutTriggered { .. }
            | UiEvent::CanvasClicked { .. }
            | UiEvent::VirtualListItemClicked { .. }
            | UiEvent::VirtualListItemRightClicked { .. }
            | UiEvent::DataGridFilterChanged { .. }
            | UiEvent::DataGridCellEdited { .. } => false,
            UiEvent::TextAreaChanged { .. }
            | UiEvent::RichTextEditorChanged { .. }
            | UiEvent::RangeSliderChanged { .. }
            | UiEvent::BreadcrumbClicked { .. }
            | UiEvent::SplitterChanged { .. }
            | UiEvent::MultiSelectChanged { .. }
            | UiEvent::DatePickerChanged { .. } => true,
        }
    }
}

#[derive(Default, Debug)]
pub struct UiImageLayerAllocator {
    pub next_layer: u32,
    pub free_layers: Vec<u32>,
}

impl UiImageLayerAllocator {
    pub const MAX_LAYERS: u32 = crate::render::wgpu::ui_texture_array::UI_TEXTURE_MAX_LAYERS;

    pub fn allocate(&mut self) -> Option<u32> {
        if let Some(layer) = self.free_layers.pop() {
            return Some(layer);
        }
        if self.next_layer >= Self::MAX_LAYERS {
            return None;
        }
        let layer = self.next_layer;
        self.next_layer += 1;
        Some(layer)
    }

    pub fn release(&mut self, layer: u32) {
        if layer >= Self::MAX_LAYERS {
            return;
        }
        if self.free_layers.contains(&layer) {
            return;
        }
        self.free_layers.push(layer);
    }
}

#[derive(Default, Debug)]
pub struct RenderSlotAllocator {
    pub rect_slots: std::collections::HashMap<freecs::Entity, u32>,
    pub image_slots: std::collections::HashMap<freecs::Entity, u32>,
    pub text_slots: std::collections::HashMap<freecs::Entity, u32>,
    pub free_rect_slots: Vec<u32>,
    pub free_image_slots: Vec<u32>,
    pub free_text_slots: Vec<u32>,
    pub next_rect_slot: u32,
    pub next_image_slot: u32,
    pub next_text_slot: u32,
    pub dirty_rect_slots: std::collections::HashSet<u32>,
    pub dirty_image_slots: std::collections::HashSet<u32>,
    pub dirty_text_slots: std::collections::HashSet<u32>,
}

pub fn allocate_rect_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
    if let Some(&slot) = allocator.rect_slots.get(&entity) {
        return slot;
    }
    let slot = allocator.free_rect_slots.pop().unwrap_or_else(|| {
        let s = allocator.next_rect_slot;
        allocator.next_rect_slot += 1;
        s
    });
    allocator.rect_slots.insert(entity, slot);
    allocator.dirty_rect_slots.insert(slot);
    slot
}

pub fn release_rect_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
    if let Some(slot) = allocator.rect_slots.remove(&entity) {
        allocator.free_rect_slots.push(slot);
        allocator.dirty_rect_slots.remove(&slot);
    }
}

pub fn mark_rect_dirty(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
    if let Some(&slot) = allocator.rect_slots.get(&entity) {
        allocator.dirty_rect_slots.insert(slot);
    }
}

pub fn allocate_image_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
    if let Some(&slot) = allocator.image_slots.get(&entity) {
        return slot;
    }
    let slot = allocator.free_image_slots.pop().unwrap_or_else(|| {
        let s = allocator.next_image_slot;
        allocator.next_image_slot += 1;
        s
    });
    allocator.image_slots.insert(entity, slot);
    allocator.dirty_image_slots.insert(slot);
    slot
}

pub fn release_image_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
    if let Some(slot) = allocator.image_slots.remove(&entity) {
        allocator.free_image_slots.push(slot);
        allocator.dirty_image_slots.remove(&slot);
    }
}

pub fn allocate_text_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) -> u32 {
    if let Some(&slot) = allocator.text_slots.get(&entity) {
        return slot;
    }
    let slot = allocator.free_text_slots.pop().unwrap_or_else(|| {
        let s = allocator.next_text_slot;
        allocator.next_text_slot += 1;
        s
    });
    allocator.text_slots.insert(entity, slot);
    allocator.dirty_text_slots.insert(slot);
    slot
}

pub fn release_text_slot(allocator: &mut RenderSlotAllocator, entity: freecs::Entity) {
    if let Some(slot) = allocator.text_slots.remove(&entity) {
        allocator.free_text_slots.push(slot);
        allocator.dirty_text_slots.remove(&slot);
    }
}

pub fn clear_render_slot_dirty(allocator: &mut RenderSlotAllocator) {
    allocator.dirty_rect_slots.clear();
    allocator.dirty_image_slots.clear();
    allocator.dirty_text_slots.clear();
}

/// Per-frame mirror of input state that the retained UI cares about. Populated
/// each frame by `ui_retained_input_sync_system` from the global input resource
/// (or from the focused secondary window if applicable). Read by widget
/// handlers and the layout system.
#[derive(Default)]
pub struct RetainedUiInputMirror {
    pub frame_chars: Vec<char>,
    pub frame_keys: Vec<(winit::keyboard::KeyCode, bool)>,
    pub ctrl_held: bool,
    pub shift_held: bool,
    pub scroll_delta: Vec2,
}

/// Wall-clock and per-frame delta in seconds. Driven by the timing system
/// each frame and read by transition, animation, and drag tick handlers.
#[derive(Default, Debug, Clone, Copy)]
pub struct RetainedUiTiming {
    pub current_time: f64,
    pub delta_time: f32,
}

/// Frame-level dirty flags and the cached tail-counts used to truncate
/// [`RetainedUiFrame`] buffers on a non-dirty frame. Also holds per-entity
/// `node_versions` and the global frame counter.
#[derive(Default)]
pub struct RetainedUiDirty {
    pub layout_dirty: bool,
    pub render_dirty: bool,
    pub global_version: u64,
    pub last_compute_viewport: Option<(u32, u32)>,
    pub last_compute_dpi: f32,
    pub last_compute_text_generation: u64,
    pub cached_node_rect_count: usize,
    pub cached_node_image_count: usize,
    pub cached_node_text_count: usize,
    pub node_versions: std::collections::HashMap<freecs::Entity, u64>,
}

/// Cached text-mesh data and per-character color overrides, keyed by text
/// slot. Reset when DPI changes; otherwise stable across frames.
#[derive(Default)]
pub struct RetainedUiTextCache {
    pub mesh: Vec<Option<CachedTextMesh>>,
    pub mesh_dpi: f32,
    pub character_colors: HashMap<usize, Vec<Option<Vec4>>>,
    pub character_background_colors: HashMap<usize, Vec<Option<Vec4>>>,
}

/// Active drag-and-drop session and the entity currently under the
/// pointer if drop tracking is on.
#[derive(Default, Debug)]
pub struct RetainedUiDragDrop {
    pub active: Option<ActiveDrag>,
    pub over_entity: Option<freecs::Entity>,
}

/// Reduced-motion preference, screen-reader announce queue, and the test-id
/// lookup driven by `with_test_id`.
#[derive(Default)]
pub struct RetainedUiAccessibility {
    pub reduced_motion: bool,
    pub announce_queue: Vec<String>,
    pub test_id_map: HashMap<String, freecs::Entity>,
    pub named_entities: HashMap<String, freecs::Entity>,
}

/// Cross-entity groupings: radio mutually-exclusive groups, selectable-label
/// groups, and registered keyboard shortcuts.
#[derive(Default)]
pub struct RetainedUiGroups {
    pub radio: HashMap<u32, Vec<freecs::Entity>>,
    pub selectable_labels: HashMap<u32, Vec<freecs::Entity>>,
    pub shortcuts: Vec<(crate::ecs::ui::components::ShortcutBinding, usize)>,
}

/// Pointer interaction state. Tracks which entity is hovered, focused,
/// active, the last click for double-click detection, and the cursor icon
/// the UI wants the OS to display.
#[derive(Default, Debug)]
pub struct RetainedUiInteraction {
    pub focused_entity: Option<freecs::Entity>,
    pub hovered_entity: Option<freecs::Entity>,
    pub active_entity: Option<freecs::Entity>,
    pub requested_cursor: Option<winit::window::CursorIcon>,
    pub applied_cursor: Option<winit::window::CursorIcon>,
    pub last_click: Option<(freecs::Entity, f64, Vec2)>,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NavDirection {
    Up,
    Down,
    Left,
    Right,
}

#[derive(Debug)]
pub struct RetainedUiGamepadNav {
    pub held_direction: Option<NavDirection>,
    pub next_repeat_at: f64,
    pub deadzone: f32,
    pub initial_repeat_delay: f64,
    pub repeat_interval: f64,
    pub enabled: bool,
}

impl Default for RetainedUiGamepadNav {
    fn default() -> Self {
        Self {
            held_direction: None,
            next_repeat_at: 0.0,
            deadzone: 0.5,
            initial_repeat_delay: 0.4,
            repeat_interval: 0.12,
            enabled: true,
        }
    }
}

/// Floating overlay entities and their visibility flags. Tooltips,
/// popups, toasts, modals, and the dock-target indicator live here.
#[derive(Default, Debug)]
pub struct RetainedUiOverlays {
    pub tooltip_state: Option<TooltipActiveState>,
    pub popup_entities: Vec<freecs::Entity>,
    pub toast_container: Option<freecs::Entity>,
    pub toast_entries: Vec<ToastEntry>,
    pub toast_corner: ToastCorner,
    pub dock_indicator_active: bool,
    pub dock_indicator_panel: Option<freecs::Entity>,
    pub active_context_menu: Option<freecs::Entity>,
    pub active_modal: Option<freecs::Entity>,
    pub focus_ring_visible: bool,
}

/// Per-frame draw record buffers built by `ui_layout_render_sync_system` and
/// consumed by the GPU prepare passes. These reset each frame the layout is
/// dirty, otherwise they're truncated back to the last cached counts.
#[derive(Default)]
pub struct RetainedUiFrame {
    pub rects: Vec<UiRect>,
    pub rect_entities: Vec<Option<freecs::Entity>>,
    pub ui_images: Vec<UiImage>,
    pub text_meshes: Vec<UiTextInstance>,
    pub overlay_rects: Vec<UiRect>,
    pub overlay_text: Vec<OverlayText>,
    pub events: Vec<UiEvent>,
    pub bubbled_events: Vec<BubbledUiEvent>,
}

#[derive(Clone, Debug)]
pub struct TopProgressBar {
    pub value: Option<f32>,
    pub indeterminate: bool,
    pub phase: f32,
    pub height: f32,
    pub color: Option<Vec4>,
}

impl Default for TopProgressBar {
    fn default() -> Self {
        Self {
            value: None,
            indeterminate: false,
            phase: 0.0,
            height: 2.0,
            color: None,
        }
    }
}

impl TopProgressBar {
    pub fn set(&mut self, value: f32) {
        self.value = Some(value.clamp(0.0, 1.0));
        self.indeterminate = false;
    }

    pub fn set_indeterminate(&mut self) {
        self.value = None;
        self.indeterminate = true;
    }

    pub fn clear(&mut self) {
        self.value = None;
        self.indeterminate = false;
    }

    pub fn is_active(&self) -> bool {
        self.value.is_some() || self.indeterminate
    }
}

#[derive(Default, PartialEq, Eq, Clone, Copy, Debug)]
pub enum UiBreakpoint {
    Compact,
    Medium,
    #[default]
    Wide,
}

impl UiBreakpoint {
    pub fn from_width(width: f32) -> Self {
        if width < 720.0 {
            UiBreakpoint::Compact
        } else if width < 1024.0 {
            UiBreakpoint::Medium
        } else {
            UiBreakpoint::Wide
        }
    }

    pub fn is_compact(self) -> bool {
        matches!(self, UiBreakpoint::Compact)
    }

    pub fn is_medium_or_narrower(self) -> bool {
        matches!(self, UiBreakpoint::Compact | UiBreakpoint::Medium)
    }
}

#[derive(Default, Clone, Copy, Debug)]
pub struct UiViewport {
    pub size: Vec2,
    pub breakpoint: UiBreakpoint,
}

#[derive(Default)]
pub struct RetainedUiState {
    pub enabled: bool,
    pub theme_state: ThemeState,
    pub interaction: RetainedUiInteraction,
    pub overlays: RetainedUiOverlays,
    pub timing: RetainedUiTiming,
    pub dirty: RetainedUiDirty,
    pub text_cache: RetainedUiTextCache,
    pub drag: RetainedUiDragDrop,
    pub accessibility: RetainedUiAccessibility,
    pub groups: RetainedUiGroups,
    pub z_sorted_nodes: Vec<(freecs::Entity, f32)>,
    /// Reused entity buffer for the per-frame color and transform blend passes,
    /// so those systems gather their working set without allocating each frame.
    pub blend_scratch: Vec<freecs::Entity>,
    pub frame: RetainedUiFrame,
    pub taffy: crate::ecs::ui::taffy_layout::UiTaffy,
    /// Absolute rects for every flow/grid descendant resolved by a single Taffy
    /// region compute, keyed by entity. The layout traversal fills this once per
    /// region root and reads it back when descending, so a nested flow container
    /// does not rebuild and recompute the Taffy subtree its ancestor already laid
    /// out. Reused across frames to avoid per-compute allocation.
    pub taffy_region_rects: std::collections::HashMap<freecs::Entity, crate::ecs::ui::types::Rect>,
    pub render_slots: RenderSlotAllocator,
    pub input: RetainedUiInputMirror,
    pub background_color: Option<Vec4>,
    pub top_progress_bar: TopProgressBar,
    pub icon_set: crate::ecs::ui::icons::IconSet,
    pub dock_state: DockState,
    pub gamepad_nav: RetainedUiGamepadNav,
    pub next_focus_order: i32,
    /// Monotonic source of widget tab indices, shared by every `UiTreeBuilder`
    /// (including `from_parent` subtrees) so tab order never collides across
    /// independently constructed trees.
    pub next_tab_index: i32,
    pub viewport: UiViewport,
    pub picking_grid: Option<crate::ecs::ui::picking::PickingGrid>,
    pub custom_state_count: usize,
    pub clipboard_text: String,
    /// Registry of chrome widgets (menu bar, toolbar, status bar, shortcuts)
    /// installed on the window.
    pub window_chrome: WindowChrome,
}

/// First-class registry of the chrome widgets that belong to a window.
/// Each field stores the entity of the corresponding widget when one is
/// installed, or `None` when that window has no such chrome.
#[derive(Default, Clone, Debug)]
pub struct WindowChrome {
    pub menu_bar: Option<freecs::Entity>,
    pub toolbar: Option<freecs::Entity>,
    pub status_bar: Option<freecs::Entity>,
    /// Keyboard shortcuts that are scoped to this window. Activations
    /// outside the owning window are ignored.
    pub shortcuts: Vec<WindowShortcut>,
}

#[derive(Clone, Debug)]
pub struct WindowShortcut {
    pub binding: crate::ecs::ui::components::ShortcutBinding,
    pub command_index: usize,
}

/// An event that has been propagated up the parent chain from its original
/// target entity. Call [`stop_propagation`](BubbledUiEvent::stop_propagation)
/// to prevent ancestor entities from seeing this event.
///
/// ```ignore
/// for event in ui_bubbled_events_for(world, container) {
///     if let UiEvent::ButtonClicked(_) = &event.event {
///         // A button somewhere inside `container` was clicked
///     }
/// }
/// ```
#[derive(Clone, Debug)]
pub struct BubbledUiEvent {
    pub event: UiEvent,
    pub target: freecs::Entity,
    pub ancestor: freecs::Entity,
    pub stopped: bool,
}

impl BubbledUiEvent {
    /// Marks this event as stopped so that further ancestor entities will
    /// not receive it from [`World::ui_bubbled_events_for`].
    pub fn stop_propagation(&mut self) {
        self.stopped = true;
    }

    /// Returns `true` if [`stop_propagation`](Self::stop_propagation) has
    /// been called on this event.
    pub fn stopped(&self) -> bool {
        self.stopped
    }
}

#[derive(Clone, Debug)]
pub struct TooltipActiveState {
    pub entity: freecs::Entity,
    pub hover_start: f64,
    pub text: String,
    pub tooltip_entity: Option<freecs::Entity>,
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct ReservedAreas {
    pub left: f32,
    pub right: f32,
    pub top: f32,
    pub bottom: f32,
}

#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
pub struct PanelDockConfig {
    pub id: String,
    pub title: String,
    pub kind: crate::ecs::ui::components::UiPanelKind,
    pub focus_order: i32,
    pub size: f32,
    pub visible: bool,
    pub collapsed: bool,
    pub floating_position: Option<[f32; 2]>,
    pub floating_size: Option<[f32; 2]>,
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct WindowDockLayout {
    pub reserved_areas: ReservedAreas,
    pub panels: Vec<PanelDockConfig>,
}

#[derive(Clone, Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct DockState {
    pub primary: WindowDockLayout,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ToastSeverity {
    Info,
    Success,
    Warning,
    Error,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum ToastCorner {
    TopLeft,
    TopRight,
    BottomLeft,
    #[default]
    BottomRight,
}

#[derive(Clone, Debug)]
pub struct ToastEntry {
    pub entity: freecs::Entity,
    pub spawn_time: f64,
    pub duration: f32,
    pub dismissing: bool,
}

pub struct DrawRectParams {
    pub position: Vec2,
    pub size: Vec2,
    pub color: Vec4,
    pub corner_radius: f32,
    pub border_width: f32,
    pub border_color: Vec4,
    pub layer: UiLayer,
    pub z_index: i32,
}

pub struct OverlayText {
    pub text: String,
    pub position: Vec2,
    pub properties: TextProperties,
    pub clip_rect: Option<Rect>,
    pub layer: UiLayer,
    pub z_index: i32,
}

impl RetainedUiState {
    pub fn draw_overlay_rect(&mut self, rect: UiRect) {
        self.frame.overlay_rects.push(rect);
    }

    pub fn draw_overlay_text(
        &mut self,
        text: &str,
        position: Vec2,
        properties: TextProperties,
        clip_rect: Option<Rect>,
        layer: UiLayer,
        z_index: i32,
    ) {
        self.frame.overlay_text.push(OverlayText {
            text: text.to_string(),
            position,
            properties,
            clip_rect,
            layer,
            z_index,
        });
    }

    pub fn register_custom_state(&mut self) -> usize {
        let index = crate::ecs::ui::state::STATE_COUNT + self.custom_state_count;
        self.custom_state_count += 1;
        index
    }

    pub fn total_state_count(&self) -> usize {
        crate::ecs::ui::state::STATE_COUNT + self.custom_state_count
    }

    pub fn interaction_for_active(&self) -> &RetainedUiInteraction {
        &self.interaction
    }

    pub fn interaction_for_active_mut(&mut self) -> &mut RetainedUiInteraction {
        &mut self.interaction
    }

    pub fn events_for_active(&self) -> &[UiEvent] {
        &self.frame.events
    }

    pub fn events_for_active_mut(&mut self) -> &mut Vec<UiEvent> {
        &mut self.frame.events
    }

    pub fn draw_rect(&mut self, position: Vec2, size: Vec2, color: Vec4) {
        self.frame.rects.push(UiRect {
            position,
            size,
            color,
            corner_radius: 0.0,
            border_width: 0.0,
            border_color: Vec4::new(0.0, 0.0, 0.0, 0.0),
            rotation: 0.0,
            clip_rect: None,
            layer: UiLayer::FloatingPanels,
            z_index: 0,
            shadow: None,
            effect_kind: 0,
            effect_params: [0.0; 4],
            quad_corners: None,
        });
    }

    pub fn draw_rect_styled(&mut self, params: DrawRectParams) {
        self.frame.rects.push(UiRect {
            position: params.position,
            size: params.size,
            color: params.color,
            corner_radius: params.corner_radius,
            border_width: params.border_width,
            border_color: params.border_color,
            rotation: 0.0,
            clip_rect: None,
            layer: params.layer,
            z_index: params.z_index,
            shadow: None,
            effect_kind: 0,
            effect_params: [0.0; 4],
            quad_corners: None,
        });
    }
}

pub fn blend_color(from: Vec4, to: Vec4, t: f32) -> Vec4 {
    if t <= 0.0 {
        return from;
    }
    if t >= 1.0 {
        return to;
    }

    let from_hsla = crate::ecs::ui::color::Hsla::from_rgba(from);
    let to_hsla = crate::ecs::ui::color::Hsla::from_rgba(to);

    crate::ecs::ui::color::Hsla {
        hue: crate::ecs::ui::color::lerp_hue(from_hsla.hue, to_hsla.hue, t),
        saturation: from_hsla.saturation + (to_hsla.saturation - from_hsla.saturation) * t,
        lightness: from_hsla.lightness + (to_hsla.lightness - from_hsla.lightness) * t,
        alpha: from_hsla.alpha + (to_hsla.alpha - from_hsla.alpha) * t,
    }
    .to_rgba()
}

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

    fn dummy_entity(id: u32) -> freecs::Entity {
        freecs::Entity { id, generation: 0 }
    }

    #[test]
    fn button_clicked_bubbles() {
        assert!(UiEvent::ButtonClicked(dummy_entity(0)).bubbles());
    }

    #[test]
    fn slider_changed_bubbles() {
        assert!(
            UiEvent::SliderChanged {
                entity: dummy_entity(0),
                value: 0.0
            }
            .bubbles()
        );
    }

    #[test]
    fn tree_node_context_menu_does_not_bubble() {
        assert!(
            !UiEvent::TreeNodeContextMenu {
                tree: dummy_entity(0),
                node: dummy_entity(1),
                position: Vec2::zeros(),
            }
            .bubbles()
        );
    }

    #[test]
    fn modal_closed_does_not_bubble() {
        assert!(
            !UiEvent::ModalClosed {
                entity: dummy_entity(0),
                confirmed: true,
            }
            .bubbles()
        );
    }

    #[test]
    fn target_entity_button() {
        let entity = dummy_entity(42);
        assert_eq!(UiEvent::ButtonClicked(entity).target_entity(), entity);
    }

    #[test]
    fn target_entity_tree_node_selected() {
        let tree = dummy_entity(1);
        let node = dummy_entity(2);
        assert_eq!(
            UiEvent::TreeNodeSelected {
                tree,
                node,
                selected: true
            }
            .target_entity(),
            node
        );
    }

    #[test]
    fn target_entity_modal_closed() {
        let entity = dummy_entity(5);
        assert_eq!(
            UiEvent::ModalClosed {
                entity,
                confirmed: false
            }
            .target_entity(),
            entity
        );
    }

    #[test]
    fn all_bubbling_events_bubble() {
        let entity = dummy_entity(0);
        let bubbling_events = [
            UiEvent::ButtonClicked(entity),
            UiEvent::SliderChanged { entity, value: 0.0 },
            UiEvent::ToggleChanged {
                entity,
                value: false,
            },
            UiEvent::CheckboxChanged {
                entity,
                value: false,
            },
            UiEvent::RadioChanged {
                entity,
                group_id: 0,
                option_index: 0,
            },
            UiEvent::TabChanged {
                entity,
                tab_index: 0,
            },
            UiEvent::TextInputChanged {
                entity,
                text: String::new(),
            },
            UiEvent::TextInputSubmitted {
                entity,
                text: String::new(),
            },
            UiEvent::DropdownChanged {
                entity,
                selected_index: 0,
            },
            UiEvent::MenuItemClicked {
                entity,
                item_index: 0,
            },
            UiEvent::ColorPickerChanged {
                entity,
                color: Vec4::zeros(),
            },
            UiEvent::SelectableLabelClicked {
                entity,
                selected: false,
            },
            UiEvent::DragValueChanged { entity, value: 0.0 },
            UiEvent::ContextMenuItemClicked {
                entity,
                item_index: 0,
                tag: 0,
            },
            UiEvent::TreeNodeSelected {
                tree: entity,
                node: entity,
                selected: false,
            },
            UiEvent::TreeNodeToggled {
                tree: entity,
                node: entity,
                expanded: false,
            },
        ];
        for event in &bubbling_events {
            assert!(event.bubbles(), "Expected {:?} to bubble", event);
        }
    }

    #[test]
    fn all_non_bubbling_events_do_not_bubble() {
        let entity = dummy_entity(0);
        let non_bubbling_events = [
            UiEvent::TreeNodeContextMenu {
                tree: entity,
                node: entity,
                position: Vec2::zeros(),
            },
            UiEvent::ModalClosed {
                entity,
                confirmed: false,
            },
            UiEvent::CommandPaletteExecuted {
                entity,
                command_index: 0,
            },
            UiEvent::TileTabActivated {
                container: entity,
                pane_id: TileId(0),
            },
            UiEvent::TileTabClosed {
                container: entity,
                pane_id: TileId(0),
                title: String::new(),
            },
            UiEvent::TileSplitterMoved {
                container: entity,
                split_id: TileId(0),
                ratio: 0.5,
            },
        ];
        for event in &non_bubbling_events {
            assert!(!event.bubbles(), "Expected {:?} not to bubble", event);
        }
    }
}
