use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::Sense;
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::StringKey;
use crate::theme::{Border, Step12, StrokeWidth};
use crate::widget_id::{WidgetId, WidgetKey};

use super::keys::{TakeKey, take_activation, take_key};
use super::paint::{GlyphMark, LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ToolbarItem {
    pub id: WidgetId,
    pub label: StringKey,
    pub disabled: bool,
    pub active: bool,
    pub width: Option<LayoutPx>,
    pub tooltip: Option<StringKey>,
}

impl ToolbarItem {
    #[must_use]
    pub const fn new(id: WidgetId, label: StringKey) -> Self {
        Self {
            id,
            label,
            disabled: false,
            active: false,
            width: None,
            tooltip: None,
        }
    }

    #[must_use]
    pub const fn disabled(self, disabled: bool) -> Self {
        Self { disabled, ..self }
    }

    #[must_use]
    pub const fn active(self, active: bool) -> Self {
        Self { active, ..self }
    }

    #[must_use]
    pub const fn with_width(self, width: LayoutPx) -> Self {
        Self {
            width: Some(width),
            ..self
        }
    }

    #[must_use]
    pub const fn with_tooltip(self, tooltip: StringKey) -> Self {
        Self {
            tooltip: Some(tooltip),
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum ToolbarOrientation {
    Horizontal,
    Vertical,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Toolbar<'a> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub items: &'a [ToolbarItem],
    pub item_size: LayoutPx,
    pub item_gap: LayoutPx,
    pub orientation: ToolbarOrientation,
}

impl<'a> Toolbar<'a> {
    #[must_use]
    pub const fn horizontal(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        items: &'a [ToolbarItem],
        item_size: LayoutPx,
        item_gap: LayoutPx,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            items,
            item_size,
            item_gap,
            orientation: ToolbarOrientation::Horizontal,
        }
    }

    #[must_use]
    pub const fn vertical(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        items: &'a [ToolbarItem],
        item_size: LayoutPx,
        item_gap: LayoutPx,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            items,
            item_size,
            item_gap,
            orientation: ToolbarOrientation::Vertical,
        }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct ToolbarResponse {
    pub activated: Option<WidgetId>,
    pub overflow_open: bool,
    pub visible_count: usize,
    pub overflow: Vec<WidgetId>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_toolbar(
    ctx: &mut FrameCtx<'_>,
    toolbar: Toolbar<'_>,
    overflow_open: &mut bool,
) -> ToolbarResponse {
    let Toolbar {
        id,
        rect,
        label,
        items,
        item_size,
        item_gap,
        orientation,
    } = toolbar;
    let layout = layout_items(rect, items, item_size, item_gap, orientation);
    let visible_count = layout.len();
    let needs_overflow = visible_count < items.len();
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::Toolbar).with_label(label));
    let mut paint = Vec::new();
    let mut activated: Option<WidgetId> = None;
    items
        .iter()
        .take(visible_count)
        .zip(layout.iter())
        .for_each(|(item, item_rect)| {
            let result = draw_item(ctx, *item_rect, item);
            paint.extend(result.paint);
            if result.activated && activated.is_none() {
                activated = Some(item.id);
            }
        });
    let overflow_ids: Vec<WidgetId> = items.iter().skip(visible_count).map(|i| i.id).collect();
    if needs_overflow {
        let overflow_rect = overflow_rect(rect, &layout, item_size, item_gap, orientation);
        let overflow_id = id.child(WidgetKey::new("overflow"));
        let interaction = ctx.interact(
            InteractDeclaration::new(overflow_id, overflow_rect, Sense::INTERACTIVE)
                .focusable(true)
                .active(*overflow_open)
                .a11y(
                    AccessNode::new(Role::Button)
                        .with_label(StringKey::new("toolbar.overflow"))
                        .with_expanded(*overflow_open),
                ),
        );
        let live_focused = ctx.is_focused(overflow_id);
        if interaction.click() {
            *overflow_open = !*overflow_open;
        }
        if live_focused && take_activation(ctx.input) {
            *overflow_open = !*overflow_open;
        }
        paint.push(WidgetPaint::Surface {
            rect: overflow_rect,
            fill: if *overflow_open {
                ctx.theme().colors.neutral.step(Step12::SELECTED_BG)
            } else if interaction.hover() {
                ctx.theme().colors.neutral.step(Step12::HOVER_BG)
            } else {
                ctx.theme().colors.neutral.step(Step12::SUBTLE_BG)
            },
            border: Some(Border {
                width: StrokeWidth::HAIRLINE,
                color: ctx.theme().colors.neutral.step(Step12::BORDER),
            }),
            radius: ctx.theme().radius.sm,
            elevation: None,
        });
        paint.push(WidgetPaint::Mark {
            rect: overflow_rect,
            kind: GlyphMark::Ellipsis,
            color: ctx.theme().colors.text_secondary(),
        });
        push_focus_ring(
            ctx,
            &mut paint,
            overflow_rect,
            ctx.theme().radius.sm,
            live_focused,
        );
    } else {
        *overflow_open = false;
    }
    ToolbarResponse {
        activated,
        overflow_open: *overflow_open,
        visible_count,
        overflow: overflow_ids,
        paint,
    }
}

struct ItemDraw {
    activated: bool,
    paint: Vec<WidgetPaint>,
}

fn draw_item(ctx: &mut FrameCtx<'_>, rect: LayoutRect, item: &ToolbarItem) -> ItemDraw {
    let interaction = ctx.interact(
        InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE)
            .focusable(true)
            .disabled(item.disabled)
            .active(item.active)
            .a11y(
                AccessNode::new(Role::Button)
                    .with_label(item.label)
                    .with_disabled(item.disabled)
                    .with_selected(item.active),
            ),
    );
    let live_focused = ctx.is_focused(item.id);
    let activated_via_pointer = !item.disabled && interaction.click();
    let activated_via_key = !item.disabled
        && live_focused
        && take_key(
            ctx.input,
            &[
                TakeKey::named(crate::input::NamedKey::Enter),
                TakeKey::named(crate::input::NamedKey::Space),
            ],
        )
        .is_some();
    let mut paint = vec![WidgetPaint::Surface {
        rect,
        fill: item_fill(ctx, item, &interaction),
        border: None,
        radius: ctx.theme().radius.sm,
        elevation: None,
    }];
    paint.push(WidgetPaint::Label {
        rect,
        text: LabelText::Key(item.label),
        color: if item.disabled {
            ctx.theme().colors.text_disabled()
        } else {
            ctx.theme().colors.text_primary()
        },
        role: ctx.theme().typography.caption,
    });
    push_focus_ring(ctx, &mut paint, rect, ctx.theme().radius.sm, live_focused);
    if let Some(tip_key) = item.tooltip
        && interaction.hover()
    {
        paint.push(WidgetPaint::Tooltip {
            rect: tooltip_rect_below(rect),
            text: LabelText::Key(tip_key),
            anchor: item.id,
            elevation: ctx.theme().elevation.level2,
        });
    }
    ItemDraw {
        activated: activated_via_pointer || activated_via_key,
        paint,
    }
}

fn tooltip_rect_below(anchor: LayoutRect) -> LayoutRect {
    const TIP_WIDTH: f32 = 220.0;
    const TIP_HEIGHT: f32 = 22.0;
    const TIP_GAP: f32 = 4.0;
    let center_x = anchor.origin.x.value() + anchor.size.width.value() * 0.5;
    let origin_x = center_x - TIP_WIDTH * 0.5;
    let origin_y = anchor.origin.y.value() + anchor.size.height.value() + TIP_GAP;
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(origin_x), LayoutPx::new(origin_y)),
        LayoutSize::new(LayoutPx::new(TIP_WIDTH), LayoutPx::new(TIP_HEIGHT)),
    )
}

fn item_fill(
    ctx: &FrameCtx<'_>,
    item: &ToolbarItem,
    interaction: &crate::hit_test::Interaction,
) -> crate::theme::Color {
    let neutral = ctx.theme().colors.neutral;
    if item.disabled {
        crate::theme::Color::TRANSPARENT
    } else if item.active || interaction.pressed() {
        neutral.step(Step12::SELECTED_BG)
    } else if interaction.hover() {
        neutral.step(Step12::HOVER_BG)
    } else {
        crate::theme::Color::TRANSPARENT
    }
}

fn item_extent(item: &ToolbarItem, fallback: LayoutPx) -> f32 {
    item.width.unwrap_or(fallback).value()
}

fn layout_items(
    rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    gap: LayoutPx,
    orientation: ToolbarOrientation,
) -> Vec<LayoutRect> {
    let available = match orientation {
        ToolbarOrientation::Horizontal => rect.size.width.value(),
        ToolbarOrientation::Vertical => rect.size.height.value(),
    };
    let total_extent: f32 = items
        .iter()
        .enumerate()
        .map(|(i, it)| item_extent(it, item_size) + if i == 0 { 0.0 } else { gap.value() })
        .sum();
    let overflows = total_extent > available + FIT_EPSILON_PX;
    let cap = if overflows {
        (available - item_size.value() - gap.value()).max(0.0)
    } else {
        available
    };
    let mut offset = 0.0_f32;
    items
        .iter()
        .enumerate()
        .scan((), |(), (i, item)| {
            let extent = item_extent(item, item_size);
            let lead = if i == 0 { 0.0 } else { gap.value() };
            let next = offset + lead + extent;
            if overflows && next > cap + FIT_EPSILON_PX {
                return None;
            }
            let single = single_item_rect(rect, offset + lead, extent, orientation);
            offset = next;
            Some(single)
        })
        .collect()
}

const FIT_EPSILON_PX: f32 = 0.5;

fn single_item_rect(
    rect: LayoutRect,
    lead_offset: f32,
    extent: f32,
    orientation: ToolbarOrientation,
) -> LayoutRect {
    match orientation {
        ToolbarOrientation::Horizontal => LayoutRect::new(
            LayoutPos::new(
                LayoutPx::new(rect.origin.x.value() + lead_offset),
                rect.origin.y,
            ),
            LayoutSize::new(LayoutPx::new(extent), rect.size.height),
        ),
        ToolbarOrientation::Vertical => LayoutRect::new(
            LayoutPos::new(
                rect.origin.x,
                LayoutPx::new(rect.origin.y.value() + lead_offset),
            ),
            LayoutSize::new(rect.size.width, LayoutPx::new(extent)),
        ),
    }
}

fn overflow_rect(
    rect: LayoutRect,
    laid_out: &[LayoutRect],
    item_size: LayoutPx,
    gap: LayoutPx,
    orientation: ToolbarOrientation,
) -> LayoutRect {
    let lead_offset = laid_out.last().map_or(0.0, |last| match orientation {
        ToolbarOrientation::Horizontal => {
            (last.origin.x.value() - rect.origin.x.value()) + last.size.width.value() + gap.value()
        }
        ToolbarOrientation::Vertical => {
            (last.origin.y.value() - rect.origin.y.value()) + last.size.height.value() + gap.value()
        }
    });
    single_item_rect(rect, lead_offset, item_size.value(), orientation)
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use super::{Toolbar, ToolbarItem, show_toolbar};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        FrameInstant, InputSnapshot, PointerButton, PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

    fn toolbar_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("toolbar"))
    }

    fn items(count: usize) -> Vec<ToolbarItem> {
        (0..count as u64)
            .map(|i| {
                ToolbarItem::new(
                    toolbar_id().child_indexed(WidgetKey::new("item"), i),
                    StringKey::new("toolbar.icon"),
                )
            })
            .collect()
    }

    fn render(
        items: &[ToolbarItem],
        rect: LayoutRect,
        overflow_open: &mut bool,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::ToolbarResponse, HitState) {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                snap,
                focus,
                &table,
                StringTable::empty(),
                &mut hits,
                prev,
                &mut a11y,
                &mut shaper,
            );
            show_toolbar(
                &mut ctx,
                Toolbar::horizontal(
                    toolbar_id(),
                    rect,
                    StringKey::new("test.toolbar"),
                    items,
                    LayoutPx::new(28.0),
                    LayoutPx::new(4.0),
                ),
                overflow_open,
            )
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    #[test]
    fn fits_all_items_no_overflow() {
        let items = items(3);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut snap,
            &prev,
        );
        assert_eq!(response.visible_count, 3);
        assert!(response.overflow.is_empty());
    }

    #[test]
    fn truncates_to_make_room_for_overflow_button() {
        let items = items(5);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut snap,
            &prev,
        );
        assert!(response.visible_count < 5);
        assert!(!response.overflow.is_empty());
    }

    #[test]
    fn click_visible_item_activates() {
        let items = items(3);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(14.0));
        let mut last: Option<super::ToolbarResponse> = None;
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(
                    &items,
                    rect,
                    &mut overflow_open,
                    &mut focus,
                    &mut snap,
                    &prev,
                );
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert_eq!(response.activated, Some(items[1].id));
    }

    #[test]
    fn click_overflow_button_toggles_state() {
        let items = items(8);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let initial_visible = {
            let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
            let (response, _) = render(
                &items,
                rect,
                &mut overflow_open,
                &mut focus,
                &mut snap,
                &prev,
            );
            response.visible_count
        };
        #[allow(clippy::cast_precision_loss, reason = "small index in test")]
        let initial_visible_f32 = initial_visible as f32;
        let overflow_x = initial_visible_f32 * (28.0 + 4.0) + 14.0;
        let overflow_pos = LayoutPos::new(LayoutPx::new(overflow_x), LayoutPx::new(14.0));
        [
            press(overflow_pos),
            release(overflow_pos),
            idle(overflow_pos),
        ]
        .into_iter()
        .for_each(|mut snap| {
            let (_, next) = render(
                &items,
                rect,
                &mut overflow_open,
                &mut focus,
                &mut snap,
                &prev,
            );
            prev = next;
        });
        assert!(overflow_open);
    }

    fn press(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn release(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn idle(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s
    }

    #[test]
    fn keyboard_space_on_focused_item_activates_it() {
        use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey};

        let items = items(3);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap_focus = InputSnapshot::idle(FrameInstant::ZERO);
        focus.request_focus(items[1].id);
        let _ = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut snap_focus,
            &prev,
        );
        assert_eq!(focus.focused(), Some(items[1].id));

        let mut snap_space = InputSnapshot::idle(FrameInstant::ZERO);
        snap_space.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Space),
            ModifierMask::NONE,
        ));
        let (response, _) = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut snap_space,
            &prev,
        );
        assert_eq!(response.activated, Some(items[1].id));
    }

    #[test]
    fn keyboard_enter_on_focused_item_activates_it() {
        use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey};

        let items = items(2);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(160.0), LayoutPx::new(28.0)),
        );
        let mut overflow_open = false;
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut warm = InputSnapshot::idle(FrameInstant::ZERO);
        focus.request_focus(items[0].id);
        let _ = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut warm,
            &prev,
        );

        let mut enter = InputSnapshot::idle(FrameInstant::ZERO);
        enter.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let (response, _) = render(
            &items,
            rect,
            &mut overflow_open,
            &mut focus,
            &mut enter,
            &prev,
        );
        assert_eq!(response.activated, Some(items[0].id));
    }
}
