use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::{Sense, ZLayer};
use crate::layout::{LayoutDirection, 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_key};
use super::paint::{GlyphMark, HorizontalAlign, IconSlot, IconTint, LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ToolbarItem {
    pub id: WidgetId,
    pub label: StringKey,
    pub icon: Option<IconSlot>,
    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,
            icon: None,
            disabled: false,
            active: false,
            width: None,
            tooltip: None,
        }
    }

    #[must_use]
    pub const fn with_icon(self, icon: IconSlot) -> Self {
        Self {
            icon: Some(icon),
            ..self
        }
    }

    #[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 ToolbarOverflowConfig {
    pub open: bool,
    pub label: StringKey,
}

impl ToolbarOverflowConfig {
    #[must_use]
    pub const fn new(label: StringKey) -> Self {
        Self { open: false, label }
    }

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

#[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,
    pub overflow: Option<ToolbarOverflowConfig>,
    pub rows: usize,
}

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,
            overflow: None,
            rows: 1,
        }
    }

    #[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,
            overflow: None,
            rows: 1,
        }
    }

    #[must_use]
    pub const fn with_overflow(mut self, overflow: ToolbarOverflowConfig) -> Self {
        self.overflow = Some(overflow);
        self
    }

    #[must_use]
    pub const fn with_rows(mut self, rows: usize) -> Self {
        self.rows = rows;
        self
    }
}

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

#[must_use]
pub fn show_toolbar(ctx: &mut FrameCtx<'_>, toolbar: Toolbar<'_>) -> ToolbarResponse {
    let Toolbar {
        id,
        rect,
        label,
        items,
        item_size,
        item_gap,
        orientation,
        overflow,
        rows,
    } = toolbar;
    let direction = ctx.direction();
    let plan = mirror_plan(
        layout_with_overflow(
            rect,
            items,
            item_size,
            item_gap,
            orientation,
            overflow,
            rows,
        ),
        rect,
        orientation,
        direction,
    );
    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(plan.visible.len())
        .zip(plan.visible.iter())
        .for_each(|(item, item_rect)| {
            let result = draw_item(ctx, *item_rect, item, ItemStyle::Toolbar);
            paint.extend(result.paint);
            if result.activated && activated.is_none() {
                activated = Some(item.id);
            }
        });
    let mut overflow_toggled = false;
    let mut popover_paint = Vec::new();
    let mut popup_consumed_click = false;
    if let (Some(cfg), Some(chevron_rect)) = (overflow, plan.chevron) {
        let chevron_id = overflow_chevron_id(id);
        let result = draw_chevron(ctx, chevron_rect, chevron_id, cfg);
        paint.extend(result.paint);
        overflow_toggled = result.activated;
        if cfg.open && plan.hidden_count > 0 {
            let hidden_items = &items[items.len() - plan.hidden_count..];
            let popup =
                render_overflow_popup(ctx, chevron_rect, hidden_items, item_size, orientation);
            popover_paint.extend(popup.paint);
            popup_consumed_click = popup.consumed_click;
            if let Some(act) = popup.activated
                && activated.is_none()
            {
                activated = Some(act);
            }
        }
    }
    ToolbarResponse {
        activated,
        visible_count: plan.visible.len(),
        overflow_count: plan.hidden_count,
        overflow_toggled,
        popup_consumed_click,
        paint,
        popover_paint,
    }
}

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum ItemStyle {
    Toolbar,
    PopupRow,
}

fn draw_item(
    ctx: &mut FrameCtx<'_>,
    rect: LayoutRect,
    item: &ToolbarItem,
    style: ItemStyle,
) -> ItemDraw {
    let decl = InteractDeclaration::new(item.id, rect, Sense::INTERACTIVE)
        .focusable(true)
        .disabled(item.disabled && style == ItemStyle::Toolbar)
        .active(item.active)
        .a11y(
            AccessNode::new(Role::Button)
                .with_label(item.label)
                .with_disabled(item.disabled)
                .with_selected(item.active),
        );
    let decl = match style {
        ItemStyle::Toolbar => decl,
        ItemStyle::PopupRow => decl.at_z(ZLayer::POPUP),
    };
    let interaction = ctx.interact(decl);
    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, style),
        border: item_border(ctx, item, &interaction, style),
        radius: ctx.theme().radius.sm,
        elevation: None,
    }];
    let label_color = if item.disabled {
        ctx.theme().colors.text_disabled()
    } else {
        ctx.theme().colors.text_primary()
    };
    match (style, item.icon) {
        (ItemStyle::Toolbar, Some(slot)) => {
            let (icon_rect, label_rect, align) = icon_item_rects(rect, slot.footprint);
            paint.push(slot.paint_in(icon_rect, IconTint::from_disabled(item.disabled)));
            paint.push(WidgetPaint::AlignedLabel {
                rect: label_rect,
                text: LabelText::Key(item.label),
                color: label_color,
                role: ctx.theme().typography.label,
                align,
            });
        }
        (ItemStyle::Toolbar, None) => {
            paint.push(WidgetPaint::Label {
                rect,
                text: LabelText::Key(item.label),
                color: label_color,
                role: ctx.theme().typography.label,
            });
        }
        (ItemStyle::PopupRow, _) => {
            paint.push(WidgetPaint::AlignedLabel {
                rect: popup_label_rect(rect),
                text: LabelText::Key(item.label),
                color: label_color,
                role: ctx.theme().typography.label,
                align: HorizontalAlign::Start,
            });
        }
    }
    push_focus_ring(ctx, &mut paint, rect, ctx.theme().radius.sm, live_focused);
    if style == ItemStyle::Toolbar
        && 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,
        consumed_click: interaction.click() || interaction.pressed(),
        paint,
    }
}

const ICON_STACK_MIN_LABEL_PX: f32 = 12.0;
const ICON_STACK_MAX_LABEL_PX: f32 = 16.0;
const ICON_INLINE_PAD_PX: f32 = 4.0;

fn icon_item_rects(
    rect: LayoutRect,
    footprint: LayoutPx,
) -> (LayoutRect, LayoutRect, HorizontalAlign) {
    let spare = rect.size.height.value() - footprint.value();
    if spare >= ICON_STACK_MIN_LABEL_PX {
        let label_height = spare.min(ICON_STACK_MAX_LABEL_PX);
        let icon_height = LayoutPx::saturating_nonneg(rect.size.height.value() - label_height);
        let icon_rect = LayoutRect::new(rect.origin, LayoutSize::new(rect.size.width, icon_height));
        let label_rect = LayoutRect::new(
            LayoutPos::new(
                rect.origin.x,
                LayoutPx::new(rect.origin.y.value() + icon_height.value()),
            ),
            LayoutSize::new(rect.size.width, LayoutPx::new(label_height)),
        );
        (icon_rect, label_rect, HorizontalAlign::Center)
    } else {
        let column = footprint.value() + 2.0 * ICON_INLINE_PAD_PX;
        let icon_rect = LayoutRect::new(
            rect.origin,
            LayoutSize::new(LayoutPx::new(column), rect.size.height),
        );
        let label_rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::new(rect.origin.x.value() + column), rect.origin.y),
            LayoutSize::new(
                LayoutPx::saturating_nonneg(rect.size.width.value() - column),
                rect.size.height,
            ),
        );
        (icon_rect, label_rect, HorizontalAlign::Start)
    }
}

fn popup_label_rect(row: LayoutRect) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(row.origin.x.value() + POPUP_LABEL_INSET_PX),
            row.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(row.size.width.value() - 2.0 * POPUP_LABEL_INSET_PX),
            row.size.height,
        ),
    )
}

fn draw_chevron(
    ctx: &mut FrameCtx<'_>,
    rect: LayoutRect,
    chevron_id: WidgetId,
    cfg: ToolbarOverflowConfig,
) -> ItemDraw {
    let interaction = ctx.interact(
        InteractDeclaration::new(chevron_id, rect, Sense::INTERACTIVE)
            .focusable(true)
            .active(cfg.open)
            .a11y(
                AccessNode::new(Role::Button)
                    .with_label(cfg.label)
                    .with_expanded(cfg.open),
            ),
    );
    let live_focused = ctx.is_focused(chevron_id);
    let activated_via_pointer = interaction.click();
    let activated_via_key = live_focused
        && take_key(
            ctx.input,
            &[
                TakeKey::named(crate::input::NamedKey::Enter),
                TakeKey::named(crate::input::NamedKey::Space),
            ],
        )
        .is_some();
    let fill = if cfg.open || interaction.pressed() {
        ctx.theme().colors.neutral.step(Step12::SELECTED_BG)
    } else if interaction.hover() {
        ctx.theme().colors.neutral.step(Step12::HOVER_BG)
    } else {
        crate::theme::Color::TRANSPARENT
    };
    let mut paint = vec![
        WidgetPaint::Surface {
            rect,
            fill,
            border: None,
            radius: ctx.theme().radius.sm,
            elevation: None,
        },
        WidgetPaint::Mark {
            rect,
            kind: GlyphMark::Ellipsis,
            color: ctx.theme().colors.text_primary(),
        },
    ];
    push_focus_ring(ctx, &mut paint, rect, ctx.theme().radius.sm, live_focused);
    ItemDraw {
        activated: activated_via_pointer || activated_via_key,
        consumed_click: interaction.click() || interaction.pressed(),
        paint,
    }
}

struct PopupRender {
    paint: Vec<WidgetPaint>,
    activated: Option<WidgetId>,
    consumed_click: bool,
}

fn render_overflow_popup(
    ctx: &mut FrameCtx<'_>,
    chevron_rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    orientation: ToolbarOrientation,
) -> PopupRender {
    let primary_pressed_now = !ctx.input.buttons_pressed.is_empty();
    let pointer_now = ctx.input.pointer.map(|p| p.position);
    let label_font_px = ctx.theme().typography.label.size.as_px_f32();
    let max_label_w = items
        .iter()
        .map(|it| {
            super::paint::estimate_label_width_px(
                ctx.strings.resolve(it.label),
                label_font_px,
                POPUP_LABEL_PADDING_PX,
            )
        })
        .fold(POPUP_MIN_WIDTH_PX, f32::max);
    let popup_width = max_label_w.min(POPUP_MAX_WIDTH_PX);
    let row_height = item_size.value().max(POPUP_MIN_ROW_HEIGHT_PX);
    #[allow(
        clippy::cast_precision_loss,
        reason = "overflow item count fits in f32 mantissa"
    )]
    let popup_height = row_height * items.len() as f32 + 2.0 * POPUP_PADDING_PX;
    let (origin_x, origin_y) = popup_origin(chevron_rect, popup_width, popup_height, orientation);
    let popup_rect = LayoutRect::new(
        LayoutPos::new(LayoutPx::new(origin_x), LayoutPx::new(origin_y)),
        LayoutSize::new(
            LayoutPx::new(popup_width),
            LayoutPx::saturating_nonneg(popup_height),
        ),
    );
    let elevation = ctx.theme().elevation.level2;
    let mut paint = vec![WidgetPaint::Surface {
        rect: popup_rect,
        fill: ctx.theme().colors.surface(elevation.surface),
        border: elevation.border,
        radius: ctx.theme().radius.sm,
        elevation: Some(elevation),
    }];
    let mut activated: Option<WidgetId> = None;
    let mut consumed_click = false;
    items.iter().enumerate().for_each(|(i, item)| {
        #[allow(
            clippy::cast_precision_loss,
            reason = "popup row index fits in f32 mantissa"
        )]
        let y = popup_rect.origin.y.value() + POPUP_PADDING_PX + i as f32 * row_height;
        let row_rect = LayoutRect::new(
            LayoutPos::new(
                LayoutPx::new(popup_rect.origin.x.value() + POPUP_PADDING_PX),
                LayoutPx::new(y),
            ),
            LayoutSize::new(
                LayoutPx::saturating_nonneg(popup_width - 2.0 * POPUP_PADDING_PX),
                LayoutPx::new(row_height),
            ),
        );
        let result = draw_item(ctx, row_rect, item, ItemStyle::PopupRow);
        paint.extend(result.paint);
        if result.activated && activated.is_none() {
            activated = Some(item.id);
        }
        consumed_click |= result.consumed_click;
    });
    let press_inside_popup =
        primary_pressed_now && pointer_now.is_some_and(|p| popup_rect.contains(p));
    consumed_click |= press_inside_popup;
    PopupRender {
        paint,
        activated,
        consumed_click,
    }
}

fn popup_origin(
    chevron: LayoutRect,
    popup_w: f32,
    popup_h: f32,
    orientation: ToolbarOrientation,
) -> (f32, f32) {
    match orientation {
        ToolbarOrientation::Horizontal => {
            let right = chevron.origin.x.value() + chevron.size.width.value();
            let origin_y = chevron.origin.y.value() + chevron.size.height.value() + POPUP_GAP_PX;
            ((right - popup_w).max(0.0), origin_y)
        }
        ToolbarOrientation::Vertical => {
            let bottom = chevron.origin.y.value() + chevron.size.height.value();
            let origin_x = chevron.origin.x.value() + chevron.size.width.value() + POPUP_GAP_PX;
            (origin_x, (bottom - popup_h).max(0.0))
        }
    }
}

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,
    style: ItemStyle,
) -> crate::theme::Color {
    let accent = ctx.theme().colors.accent;
    let show_hover_when_disabled = style == ItemStyle::PopupRow;
    if item.disabled && !show_hover_when_disabled {
        crate::theme::Color::TRANSPARENT
    } else if item.active || interaction.pressed() {
        accent.step(Step12::SELECTED_BG)
    } else if interaction.hover() {
        accent.step(Step12::HOVER_BG)
    } else {
        crate::theme::Color::TRANSPARENT
    }
}

fn item_border(
    ctx: &FrameCtx<'_>,
    item: &ToolbarItem,
    interaction: &crate::hit_test::Interaction,
    style: ItemStyle,
) -> Option<Border> {
    if style != ItemStyle::Toolbar || item.disabled {
        return None;
    }
    let highlighted = item.active || interaction.pressed() || interaction.hover();
    highlighted.then(|| Border {
        width: StrokeWidth::HAIRLINE,
        color: ctx.theme().colors.accent.step(Step12::BORDER),
    })
}

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

#[must_use]
pub fn overflow_chevron_id(toolbar_id: WidgetId) -> WidgetId {
    toolbar_id.child(WidgetKey::new("overflow.chevron"))
}

struct LayoutPlan {
    visible: Vec<LayoutRect>,
    chevron: Option<LayoutRect>,
    hidden_count: usize,
}

fn mirror_plan(
    plan: LayoutPlan,
    bar: LayoutRect,
    orientation: ToolbarOrientation,
    direction: LayoutDirection,
) -> LayoutPlan {
    match (orientation, direction) {
        (ToolbarOrientation::Horizontal, LayoutDirection::Rtl) => LayoutPlan {
            visible: plan
                .visible
                .into_iter()
                .map(|r| r.mirror_horizontally_within(bar, direction))
                .collect(),
            chevron: plan
                .chevron
                .map(|r| r.mirror_horizontally_within(bar, direction)),
            hidden_count: plan.hidden_count,
        },
        _ => plan,
    }
}

fn layout_with_overflow(
    rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    gap: LayoutPx,
    orientation: ToolbarOrientation,
    overflow: Option<ToolbarOverflowConfig>,
    rows: usize,
) -> LayoutPlan {
    if rows > 1 && orientation == ToolbarOrientation::Horizontal {
        return LayoutPlan {
            visible: lay_out_grid(rect, items, item_size, gap, rows),
            chevron: None,
            hidden_count: 0,
        };
    }
    let chevron_extent = item_size.value();
    let available = match orientation {
        ToolbarOrientation::Horizontal => rect.size.width.value(),
        ToolbarOrientation::Vertical => rect.size.height.value(),
    };
    let total_needed = total_extent(items, item_size, gap);
    let all_fit = total_needed <= available + FIT_EPSILON_PX;
    if all_fit {
        return LayoutPlan {
            visible: lay_out_items(rect, items, item_size, gap, orientation, available),
            chevron: None,
            hidden_count: 0,
        };
    }
    if overflow.is_none() {
        let visible = lay_out_items(rect, items, item_size, gap, orientation, available);
        let hidden_count = items.len().saturating_sub(visible.len());
        return LayoutPlan {
            visible,
            chevron: None,
            hidden_count,
        };
    }
    let budget_for_items = (available - chevron_extent - gap.value()).max(0.0);
    let visible = lay_out_items(rect, items, item_size, gap, orientation, budget_for_items);
    let hidden_count = items.len().saturating_sub(visible.len());
    if hidden_count == 0 {
        return LayoutPlan {
            visible,
            chevron: None,
            hidden_count: 0,
        };
    }
    let chevron_offset = visible.last().map_or(0.0, |r| match orientation {
        ToolbarOrientation::Horizontal => {
            r.origin.x.value() + r.size.width.value() - rect.origin.x.value() + gap.value()
        }
        ToolbarOrientation::Vertical => {
            r.origin.y.value() + r.size.height.value() - rect.origin.y.value() + gap.value()
        }
    });
    let chevron = single_item_rect(rect, chevron_offset, chevron_extent, orientation);
    LayoutPlan {
        visible,
        chevron: Some(chevron),
        hidden_count,
    }
}

fn total_extent(items: &[ToolbarItem], item_size: LayoutPx, gap: LayoutPx) -> f32 {
    items
        .iter()
        .enumerate()
        .map(|(i, it)| item_extent(it, item_size) + if i == 0 { 0.0 } else { gap.value() })
        .sum()
}

fn lay_out_items(
    rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    gap: LayoutPx,
    orientation: ToolbarOrientation,
    available: f32,
) -> Vec<LayoutRect> {
    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 next > available + FIT_EPSILON_PX {
                return None;
            }
            let single = single_item_rect(rect, offset + lead, extent, orientation);
            offset = next;
            Some(single)
        })
        .collect()
}

fn lay_out_grid(
    rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    gap: LayoutPx,
    rows: usize,
) -> Vec<LayoutRect> {
    let rows = rows.max(1);
    #[allow(
        clippy::cast_precision_loss,
        reason = "toolbar row counts fit the f32 mantissa"
    )]
    let rows_f = rows as f32;
    let row_height = ((rect.size.height.value() - (rows_f - 1.0) * gap.value()) / rows_f).max(0.0);
    let col_width = |col: usize| -> f32 {
        (0..rows)
            .filter_map(|r| items.get(col * rows + r))
            .map(|it| item_extent(it, item_size))
            .fold(0.0_f32, f32::max)
    };
    let col_x: Vec<f32> = (0..items.len().div_ceil(rows))
        .scan(rect.origin.x.value(), |x, col| {
            let cur = *x;
            *x += col_width(col) + gap.value();
            Some(cur)
        })
        .collect();
    items
        .iter()
        .enumerate()
        .map(|(i, _)| {
            let col = i / rows;
            #[allow(
                clippy::cast_precision_loss,
                reason = "toolbar row index fits the f32 mantissa"
            )]
            let row_pos = (i % rows) as f32;
            LayoutRect::new(
                LayoutPos::new(
                    LayoutPx::new(col_x.get(col).copied().unwrap_or(rect.origin.x.value())),
                    LayoutPx::new(rect.origin.y.value() + row_pos * (row_height + gap.value())),
                ),
                LayoutSize::new(LayoutPx::new(col_width(col)), LayoutPx::new(row_height)),
            )
        })
        .collect()
}

const FIT_EPSILON_PX: f32 = 0.5;
const POPUP_PADDING_PX: f32 = 4.0;
const POPUP_GAP_PX: f32 = 4.0;
const POPUP_LABEL_PADDING_PX: f32 = 12.0;
const POPUP_LABEL_INSET_PX: f32 = 12.0;
const POPUP_MIN_WIDTH_PX: f32 = 160.0;
const POPUP_MAX_WIDTH_PX: f32 = 320.0;
const POPUP_MIN_ROW_HEIGHT_PX: f32 = 24.0;

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

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

    use super::{Toolbar, ToolbarItem, ToolbarOverflowConfig, overflow_chevron_id, 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,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::ToolbarResponse, HitState) {
        render_with(items, rect, None, focus, snap, prev)
    }

    fn render_with(
        items: &[ToolbarItem],
        rect: LayoutRect,
        overflow: Option<ToolbarOverflowConfig>,
        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,
            );
            let toolbar = Toolbar::horizontal(
                toolbar_id(),
                rect,
                StringKey::new("test.toolbar"),
                items,
                LayoutPx::new(28.0),
                LayoutPx::new(4.0),
            );
            let toolbar = match overflow {
                Some(cfg) => toolbar.with_overflow(cfg),
                None => toolbar,
            };
            show_toolbar(&mut ctx, toolbar)
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    #[test]
    fn fits_all_items() {
        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 focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(&items, rect, &mut focus, &mut snap, &prev);
        assert_eq!(response.visible_count, 3);
        assert_eq!(response.overflow_count, 0);
    }

    #[test]
    fn grid_flows_items_column_major_across_rows() {
        let items = items(5);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(200.0), LayoutPx::new(60.0)),
        );
        let cells = super::lay_out_grid(rect, &items, LayoutPx::new(28.0), LayoutPx::new(4.0), 2);
        let near = |a: f32, b: f32| (a - b).abs() < 1e-3;
        let x = |i: usize| cells[i].origin.x.value();
        let y = |i: usize| cells[i].origin.y.value();
        assert_eq!(cells.len(), 5);
        assert!(near(x(0), 0.0) && near(y(0), 0.0));
        assert!(
            near(x(1), 0.0) && near(y(1), 32.0),
            "second item stacks under the first"
        );
        assert!(
            near(x(2), 32.0) && near(y(2), 0.0),
            "third item starts the next column"
        );
        assert!(near(x(3), 32.0) && near(y(3), 32.0));
        assert!(near(x(4), 64.0) && near(y(4), 0.0));
        assert!(
            cells.iter().all(|c| near(c.size.height.value(), 28.0)),
            "two rows split the 60 px band minus the 4 px gap",
        );
    }

    #[test]
    fn drops_items_that_dont_fit() {
        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 focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render(&items, rect, &mut focus, &mut snap, &prev);
        assert!(response.visible_count < 5);
    }

    #[test]
    fn overflow_chevron_reports_hidden_count_when_items_dont_fit() {
        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 cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow"));
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
        assert!(response.overflow_count >= 1);
        assert_eq!(response.visible_count + response.overflow_count, 5);
    }

    #[test]
    fn overflow_chevron_absent_when_all_items_fit() {
        let items = items(3);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(400.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow"));
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
        assert_eq!(response.visible_count, 3);
        assert_eq!(response.overflow_count, 0);
        assert!(!response.overflow_toggled);
    }

    #[test]
    fn clicking_chevron_sets_overflow_toggled() {
        let items = items(6);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow"));
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(86.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_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert!(response.overflow_toggled);
        let _ = overflow_chevron_id(toolbar_id());
    }

    #[test]
    fn open_popup_emits_popover_paint_for_hidden_items() {
        let items = items(6);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow")).with_open(true);
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
        assert!(!response.popover_paint.is_empty());
        assert!(response.overflow_count >= 1);
    }

    #[test]
    fn closed_popup_emits_no_popover_paint() {
        let items = items(6);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow"));
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let prev = HitState::new();
        let (response, _) = render_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
        assert!(response.popover_paint.is_empty());
    }

    #[test]
    fn clicking_disabled_popup_item_consumes_click_but_does_not_activate() {
        let items: Vec<ToolbarItem> = items(6).into_iter().map(|it| it.disabled(true)).collect();
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow")).with_open(true);
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let mut warm_snap = InputSnapshot::idle(FrameInstant::ZERO);
        let (_, warm_state) =
            render_with(&items, rect, Some(cfg), &mut focus, &mut warm_snap, &prev);
        prev = warm_state;
        let click_y = 28.0 + super::POPUP_GAP_PX + super::POPUP_PADDING_PX + 12.0;
        let click_pos = LayoutPos::new(LayoutPx::new(60.0), LayoutPx::new(click_y));
        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_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert!(response.activated.is_none());
        assert!(response.popup_consumed_click);
    }

    #[test]
    fn clicking_popup_item_activates_it() {
        let items = items(6);
        let rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(100.0), LayoutPx::new(28.0)),
        );
        let cfg = ToolbarOverflowConfig::new(StringKey::new("toolbar.overflow")).with_open(true);
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let mut warm_snap = InputSnapshot::idle(FrameInstant::ZERO);
        let (warm, warm_state) =
            render_with(&items, rect, Some(cfg), &mut focus, &mut warm_snap, &prev);
        prev = warm_state;
        let hidden_index = items.len() - warm.overflow_count;
        let hidden_item_id = items[hidden_index].id;
        let click_y = 28.0 + super::POPUP_GAP_PX + super::POPUP_PADDING_PX + 12.0;
        let click_pos = LayoutPos::new(LayoutPx::new(60.0), LayoutPx::new(click_y));
        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_with(&items, rect, Some(cfg), &mut focus, &mut snap, &prev);
                last = Some(response);
                prev = next;
            });
        let Some(response) = last else {
            panic!("response missing")
        };
        assert_eq!(response.activated, Some(hidden_item_id));
    }

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

    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 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 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 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 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 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 focus, &mut enter, &prev);
        assert_eq!(response.activated, Some(items[0].id));
    }
}
