use std::collections::HashMap;

use nalgebra_glm::{Vec2, Vec4};

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

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

#[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};

pub struct SecondaryWindowUiBuffers {
    pub z_sorted_nodes: Vec<(freecs::Entity, f32)>,
    pub frame_rects: Vec<UiRect>,
    pub frame_ui_images: Vec<UiImage>,
    pub frame_text_meshes: Vec<UiTextInstance>,
}

#[derive(Clone)]
pub struct CachedTextMesh {
    pub mesh: TextMesh,
    pub slot_generation: u64,
    pub font_size: f32,
    pub wrap_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,
    },
    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,
    },
    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::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::DataGridFilterChanged { .. }
            | UiEvent::DataGridCellEdited { .. } => false,
            UiEvent::TextAreaChanged { .. }
            | UiEvent::RichTextEditorChanged { .. }
            | UiEvent::RangeSliderChanged { .. }
            | UiEvent::BreadcrumbClicked { .. }
            | UiEvent::SplitterChanged { .. }
            | UiEvent::MultiSelectChanged { .. }
            | UiEvent::DatePickerChanged { .. } => true,
        }
    }
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Default)]
pub struct PropertyId(pub u32);

#[derive(Clone)]
pub enum PropertyValue {
    F32(f32),
    Bool(bool),
    Usize(usize),
    String(String),
    Vec4(Vec4),
}

pub trait FromPropertyValue: Sized {
    fn from_property_value(value: &PropertyValue) -> Option<Self>;
    fn default_value() -> Self;
}

pub trait IntoPropertyValue {
    fn into_property_value(self) -> PropertyValue;
}

macro_rules! impl_property_value {
    ($variant:ident, $ty:ty, $default:expr, copy) => {
        impl FromPropertyValue for $ty {
            fn from_property_value(value: &PropertyValue) -> Option<Self> {
                match value {
                    PropertyValue::$variant(v) => Some(*v),
                    _ => None,
                }
            }
            fn default_value() -> Self {
                $default
            }
        }
        impl IntoPropertyValue for $ty {
            fn into_property_value(self) -> PropertyValue {
                PropertyValue::$variant(self)
            }
        }
    };
    ($variant:ident, $ty:ty, $default:expr, clone) => {
        impl FromPropertyValue for $ty {
            fn from_property_value(value: &PropertyValue) -> Option<Self> {
                match value {
                    PropertyValue::$variant(v) => Some(v.clone()),
                    _ => None,
                }
            }
            fn default_value() -> Self {
                $default
            }
        }
        impl IntoPropertyValue for $ty {
            fn into_property_value(self) -> PropertyValue {
                PropertyValue::$variant(self)
            }
        }
    };
}

impl_property_value!(F32, f32, 0.0, copy);
impl_property_value!(Bool, bool, false, copy);
impl_property_value!(Usize, usize, 0, copy);
impl_property_value!(String, String, String::new(), clone);
impl_property_value!(Vec4, Vec4, Vec4::new(0.0, 0.0, 0.0, 0.0), copy);

pub(crate) struct BoundProperty {
    pub value: PropertyValue,
    pub entity: freecs::Entity,
    pub dirty_from_widget: bool,
    pub dirty_from_code: bool,
}

pub type UiEventHandler = Box<dyn FnMut(&mut crate::ecs::world::World, &UiEvent)>;
pub type PropertyReaction = Box<dyn FnMut(&PropertyValue, &mut crate::ecs::world::World)>;
pub type ClickReaction = Box<dyn FnMut(&mut crate::ecs::world::World)>;
pub type SubmitReaction = Box<dyn FnMut(String, &mut crate::ecs::world::World)>;
pub type ConfirmReaction = Box<dyn FnMut(bool, &mut crate::ecs::world::World)>;
pub type MenuSelectReaction = Box<dyn FnMut(usize, &mut crate::ecs::world::World)>;
pub type CommandReaction = Box<dyn FnMut(usize, &mut crate::ecs::world::World)>;
pub type TreeSelectReaction = Box<dyn FnMut(freecs::Entity, &mut crate::ecs::world::World)>;
pub type TreeContextMenuReaction =
    Box<dyn FnMut(freecs::Entity, Vec2, &mut crate::ecs::world::World)>;
pub type MultiSelectReaction = Box<dyn FnMut(Vec<usize>, &mut crate::ecs::world::World)>;
pub type DateChangedReaction = Box<dyn FnMut(i32, u32, u32, &mut crate::ecs::world::World)>;
pub type ChangedReaction = Box<dyn FnMut(&mut crate::ecs::world::World)>;

#[derive(Default)]
pub struct RetainedUiState {
    pub enabled: bool,
    pub theme_state: ThemeState,
    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 z_sorted_nodes: Vec<(freecs::Entity, f32)>,
    pub frame_rects: Vec<UiRect>,
    pub frame_ui_images: Vec<UiImage>,
    pub frame_text_meshes: Vec<UiTextInstance>,
    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,
    pub current_time: f64,
    pub delta_time: f32,
    pub last_click: Option<(freecs::Entity, f64, Vec2)>,
    pub background_color: Option<Vec4>,
    pub tooltip_state: Option<TooltipActiveState>,
    pub popup_entities: Vec<freecs::Entity>,
    pub reserved_areas: ReservedAreas,
    pub next_focus_order: i32,
    pub frame_events: Vec<UiEvent>,
    pub focus_ring_visible: bool,
    pub toast_container: Option<freecs::Entity>,
    pub toast_entries: Vec<ToastEntry>,
    pub text_mesh_cache: Vec<Option<CachedTextMesh>>,
    pub text_mesh_cache_dpi: f32,
    pub animation_states: HashMap<(freecs::Entity, u32), (f32, f64)>,
    pub active_drag: Option<ActiveDrag>,
    pub drag_over_entity: Option<freecs::Entity>,
    pub shortcut_bindings: Vec<(crate::ecs::ui::components::ShortcutBinding, usize)>,
    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 bubbled_events: Vec<BubbledUiEvent>,
    pub(crate) composite_widgets: HashMap<freecs::Entity, Box<dyn AnyCompositeWidget>>,
    pub overlay_rects: Vec<UiRect>,
    pub overlay_text: Vec<OverlayText>,
    pub last_compute_viewport: Option<(u32, u32)>,
    pub last_compute_dpi: f32,
    pub last_compute_text_generation: u64,
    pub layout_dirty: bool,
    pub render_dirty: bool,
    pub cached_node_rect_count: usize,
    pub cached_node_image_count: usize,
    pub cached_node_text_count: usize,
    pub secondary_buffers: HashMap<usize, SecondaryWindowUiBuffers>,
    pub text_slot_character_colors: HashMap<usize, Vec<Option<Vec4>>>,
    pub event_handlers: HashMap<freecs::Entity, Vec<UiEventHandler>>,
    pub picking_grid: Option<crate::ecs::ui::picking::PickingGrid>,
    pub custom_state_count: usize,
    pub(crate) bound_properties: HashMap<PropertyId, BoundProperty>,
    pub next_property_id: u32,
    pub(crate) named_properties: HashMap<String, PropertyId>,
    pub(crate) property_reactions: HashMap<String, Vec<PropertyReaction>>,
    pub(crate) click_reactions: HashMap<freecs::Entity, Vec<ClickReaction>>,
    pub(crate) submit_reactions: HashMap<freecs::Entity, Vec<SubmitReaction>>,
    pub(crate) confirm_reactions: HashMap<freecs::Entity, Vec<ConfirmReaction>>,
    pub(crate) menu_select_reactions: HashMap<freecs::Entity, Vec<MenuSelectReaction>>,
    pub(crate) command_reactions: HashMap<freecs::Entity, Vec<CommandReaction>>,
    pub(crate) tree_select_reactions: HashMap<freecs::Entity, Vec<TreeSelectReaction>>,
    pub(crate) tree_context_menu_reactions: HashMap<freecs::Entity, Vec<TreeContextMenuReaction>>,
    pub(crate) multi_select_reactions: HashMap<freecs::Entity, Vec<MultiSelectReaction>>,
    pub(crate) date_changed_reactions: HashMap<freecs::Entity, Vec<DateChangedReaction>>,
    pub(crate) changed_reactions: HashMap<freecs::Entity, Vec<ChangedReaction>>,
    pub subpixel_text_rendering: bool,
    pub reduced_motion: bool,
    pub announce_queue: Vec<String>,
    pub clipboard_text: String,
    pub radio_groups: HashMap<u32, Vec<freecs::Entity>>,
    pub selectable_label_groups: HashMap<u32, Vec<freecs::Entity>>,
    #[cfg(feature = "syntax_highlighting")]
    pub syntax_highlighter: Option<crate::ecs::ui::syntax::SyntaxHighlighter>,
}

/// 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 world.ui_bubbled_events_for(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)]
pub struct ReservedAreas {
    pub left: f32,
    pub right: f32,
    pub top: f32,
    pub bottom: f32,
}

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

#[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.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.overlay_text.push(OverlayText {
            text: text.to_string(),
            position,
            properties,
            clip_rect,
            layer,
            z_index,
        });
    }

    pub fn animate(
        &mut self,
        entity: freecs::Entity,
        property: u32,
        target: f32,
        speed: f32,
    ) -> f32 {
        let key = (entity, property);
        let current_time = self.current_time;
        let delta_time = self.delta_time;

        let entry = self
            .animation_states
            .entry(key)
            .or_insert((target, current_time));
        entry.1 = current_time;

        let current = entry.0;
        let diff = target - current;
        if diff.abs() < 0.001 {
            entry.0 = target;
            return target;
        }

        let new_value = current + diff * (speed * delta_time).min(1.0);
        entry.0 = new_value;
        new_value
    }

    pub(crate) fn register_named_property(&mut self, name: &str, property_id: PropertyId) {
        debug_assert!(
            !self.named_properties.contains_key(name),
            "Duplicate named property: {name}"
        );
        self.named_properties.insert(name.to_string(), property_id);
    }

    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 remove_secondary_window(&mut self, window_index: usize) {
        self.secondary_buffers.remove(&window_index);
    }

    pub fn cleanup_stale_animations(&mut self) {
        let current_time = self.current_time;
        self.animation_states
            .retain(|_, (_, last_accessed)| current_time - *last_accessed < 2.0);
    }

    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,
        });
    }

    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,
        });
    }
}

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 = rgba_to_hsla(from);
    let to_hsla = rgba_to_hsla(to);

    let mut h_diff = to_hsla.x - from_hsla.x;
    if h_diff > 0.5 {
        h_diff -= 1.0;
    } else if h_diff < -0.5 {
        h_diff += 1.0;
    }

    let h = (from_hsla.x + h_diff * t).rem_euclid(1.0);
    let s = from_hsla.y + (to_hsla.y - from_hsla.y) * t;
    let l = from_hsla.z + (to_hsla.z - from_hsla.z) * t;
    let a = from_hsla.w + (to_hsla.w - from_hsla.w) * t;

    hsla_to_rgba(Vec4::new(h, s, l, a))
}

fn rgba_to_hsla(color: Vec4) -> Vec4 {
    let r = color.x;
    let g = color.y;
    let b = color.z;
    let a = color.w;

    let max = r.max(g).max(b);
    let min = r.min(g).min(b);
    let l = (max + min) * 0.5;

    if (max - min).abs() < f32::EPSILON {
        return Vec4::new(0.0, 0.0, l, a);
    }

    let d = max - min;
    let s = if l > 0.5 {
        d / (2.0 - max - min)
    } else {
        d / (max + min)
    };

    let h = if (max - r).abs() < f32::EPSILON {
        let mut h = (g - b) / d;
        if g < b {
            h += 6.0;
        }
        h / 6.0
    } else if (max - g).abs() < f32::EPSILON {
        ((b - r) / d + 2.0) / 6.0
    } else {
        ((r - g) / d + 4.0) / 6.0
    };

    Vec4::new(h, s, l, a)
}

fn hsla_to_rgba(hsla: Vec4) -> Vec4 {
    let h = hsla.x;
    let s = hsla.y;
    let l = hsla.z;
    let a = hsla.w;

    if s.abs() < f32::EPSILON {
        return Vec4::new(l, l, l, a);
    }

    let q = if l < 0.5 {
        l * (1.0 + s)
    } else {
        l + s - l * s
    };
    let p = 2.0 * l - q;

    let r = hue_to_rgb(p, q, h + 1.0 / 3.0);
    let g = hue_to_rgb(p, q, h);
    let b = hue_to_rgb(p, q, h - 1.0 / 3.0);

    Vec4::new(r, g, b, a)
}

fn hue_to_rgb(p: f32, q: f32, mut t: f32) -> f32 {
    if t < 0.0 {
        t += 1.0;
    }
    if t > 1.0 {
        t -= 1.0;
    }
    if t < 1.0 / 6.0 {
        return p + (q - p) * 6.0 * t;
    }
    if t < 0.5 {
        return q;
    }
    if t < 2.0 / 3.0 {
        return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
    }
    p
}

#[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,
            },
            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);
        }
    }
}
