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

use super::keys::{TakeKey, take_key};
use bone_text::{ShapeRequest, ShapedLine};

use super::paint::{GlyphMark, HorizontalAlign, LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Clone, Debug, PartialEq)]
pub enum MenuItem {
    Action {
        id: WidgetId,
        label: StringKey,
        shortcut: Option<super::paint::LabelText>,
        disabled: bool,
    },
    Submenu {
        id: WidgetId,
        label: StringKey,
        items: Vec<MenuItem>,
    },
    Separator,
}

impl MenuItem {
    #[must_use]
    pub fn id(&self) -> Option<WidgetId> {
        match self {
            Self::Action { id, .. } | Self::Submenu { id, .. } => Some(*id),
            Self::Separator => None,
        }
    }

    #[must_use]
    pub fn is_focusable(&self) -> bool {
        match self {
            Self::Action { disabled, .. } => !*disabled,
            Self::Submenu { .. } => true,
            Self::Separator => false,
        }
    }
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MenuState {
    pub open_submenu: Option<WidgetId>,
    pub highlighted: Option<usize>,
    pub submenu: Option<Box<MenuState>>,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct MenuMetrics {
    pub item_height: LayoutPx,
    pub separator_height: LayoutPx,
    pub padding_x: LayoutPx,
    pub min_width: LayoutPx,
    pub shortcut_gap: LayoutPx,
}

impl MenuMetrics {
    #[must_use]
    pub const fn standard() -> Self {
        Self {
            item_height: LayoutPx::new(24.0),
            separator_height: LayoutPx::new(7.0),
            padding_x: LayoutPx::new(10.0),
            min_width: LayoutPx::new(180.0),
            shortcut_gap: LayoutPx::new(24.0),
        }
    }
}

#[derive(Debug, PartialEq)]
pub struct Menu<'a, 'state> {
    pub id: WidgetId,
    pub origin: LayoutPos,
    pub label: StringKey,
    pub items: &'a [MenuItem],
    pub metrics: MenuMetrics,
    pub state: &'state mut MenuState,
}

impl<'a, 'state> Menu<'a, 'state> {
    #[must_use]
    pub fn new(
        id: WidgetId,
        origin: LayoutPos,
        label: StringKey,
        items: &'a [MenuItem],
        state: &'state mut MenuState,
    ) -> Self {
        Self {
            id,
            origin,
            label,
            items,
            metrics: MenuMetrics::standard(),
            state,
        }
    }

    #[must_use]
    pub fn metrics(self, metrics: MenuMetrics) -> Self {
        Self { metrics, ..self }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct MenuResponse {
    pub activated: Option<WidgetId>,
    pub close: bool,
    pub paint: Vec<WidgetPaint>,
    pub rect: LayoutRect,
}

#[must_use]
pub fn show_menu(ctx: &mut FrameCtx<'_>, menu: Menu<'_, '_>) -> MenuResponse {
    let Menu {
        id,
        origin,
        label,
        items,
        metrics,
        state,
    } = menu;
    let rect = menu_rect(origin, items, metrics);
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::Menu).with_label(label));
    let mut paint = vec![WidgetPaint::Surface {
        rect,
        fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L1),
        border: Some(Border {
            width: StrokeWidth::px(1.5),
            color: ctx.theme().colors.neutral.step(Step12::BORDER),
        }),
        radius: ctx.theme().radius.sm,
        elevation: Some(ctx.theme().elevation.level1),
    }];
    let layouts = item_rects(rect, items, metrics);
    let mut activated: Option<WidgetId> = None;
    let mut close = false;
    let prev_open = state.open_submenu;
    let snap_open = state.open_submenu;
    items
        .iter()
        .zip(layouts.iter())
        .enumerate()
        .for_each(|(idx, (item, item_rect))| {
            let is_highlighted = state.highlighted == Some(idx);
            let item_result = draw_item(
                ctx,
                ItemDrawArgs {
                    item,
                    rect: *item_rect,
                    metrics,
                    is_highlighted,
                    is_open_submenu: matches!(item, MenuItem::Submenu { id: sid, .. } if snap_open == Some(*sid)),
                },
            );
            paint.extend(item_result.paint);
            if item_result.hovered {
                state.highlighted = Some(idx);
            }
            if let Some(activate) = item_result.activated {
                if activated.is_none() {
                    activated = Some(activate);
                }
                close = true;
            }
            if let Some(sid) = item_result.opened_submenu {
                state.open_submenu = Some(sid);
            }
        });
    if state.open_submenu != prev_open {
        state.submenu = None;
    }
    render_open_submenu(
        ctx,
        SubmenuArgs {
            parent_id: id,
            items,
            layouts: &layouts,
            metrics,
            state,
        },
        &mut paint,
        &mut activated,
        &mut close,
    );
    handle_keyboard(ctx, items, state, &mut activated, &mut close);
    MenuResponse {
        activated,
        close,
        paint,
        rect,
    }
}

struct SubmenuArgs<'a, 'state> {
    parent_id: WidgetId,
    items: &'a [MenuItem],
    layouts: &'a [LayoutRect],
    metrics: MenuMetrics,
    state: &'state mut MenuState,
}

fn render_open_submenu(
    ctx: &mut FrameCtx<'_>,
    args: SubmenuArgs<'_, '_>,
    paint: &mut Vec<WidgetPaint>,
    activated: &mut Option<WidgetId>,
    close: &mut bool,
) {
    let SubmenuArgs {
        parent_id,
        items,
        layouts,
        metrics,
        state,
    } = args;
    let Some(sid) = state.open_submenu else {
        return;
    };
    let active = items.iter().zip(layouts.iter()).find_map(|(item, rect)| {
        matches!(item, MenuItem::Submenu { id: iid, .. } if *iid == sid).then(|| match item {
            MenuItem::Submenu {
                items: sub, label, ..
            } => (*rect, sub.as_slice(), *label),
            _ => unreachable!(),
        })
    });
    let Some((item_rect, subitems, sub_label)) = active else {
        state.open_submenu = None;
        state.submenu = None;
        return;
    };
    let sub_origin = LayoutPos::new(
        LayoutPx::new(item_rect.origin.x.value() + item_rect.size.width.value()),
        item_rect.origin.y,
    );
    let sub_id = parent_id.child(WidgetKey::new("submenu"));
    let sub_state = state
        .submenu
        .get_or_insert_with(|| Box::new(MenuState::default()));
    let sub_response = show_menu(
        ctx,
        Menu::new(sub_id, sub_origin, sub_label, subitems, sub_state).metrics(metrics),
    );
    paint.extend(sub_response.paint);
    if let Some(a) = sub_response.activated {
        if activated.is_none() {
            *activated = Some(a);
        }
        *close = true;
    } else if sub_response.close {
        state.open_submenu = None;
        state.submenu = None;
    }
}

#[derive(Copy, Clone)]
struct ItemDrawArgs<'a> {
    item: &'a MenuItem,
    rect: LayoutRect,
    metrics: MenuMetrics,
    is_highlighted: bool,
    is_open_submenu: bool,
}

struct ItemDrawResult {
    paint: Vec<WidgetPaint>,
    hovered: bool,
    activated: Option<WidgetId>,
    opened_submenu: Option<WidgetId>,
}

fn draw_item(ctx: &mut FrameCtx<'_>, args: ItemDrawArgs<'_>) -> ItemDrawResult {
    match args.item {
        MenuItem::Separator => ItemDrawResult {
            paint: separator_paint(ctx, args.rect, args.metrics),
            hovered: false,
            activated: None,
            opened_submenu: None,
        },
        MenuItem::Action {
            id,
            label,
            shortcut,
            disabled,
        } => {
            let interaction = ctx.interact(
                InteractDeclaration::new(*id, args.rect, Sense::INTERACTIVE)
                    .at_z(ZLayer::POPUP)
                    .focusable(false)
                    .disabled(*disabled)
                    .a11y(
                        AccessNode::new(Role::MenuItem)
                            .with_label(*label)
                            .with_disabled(*disabled),
                    ),
            );
            let activated = (!*disabled && interaction.click()).then_some(*id);
            let mut paint =
                item_surface(ctx, args.rect, args.is_highlighted || interaction.hover());
            paint.push(WidgetPaint::AlignedLabel {
                rect: label_only_rect(args.rect, args.metrics),
                text: LabelText::Key(*label),
                color: if *disabled {
                    ctx.theme().colors.text_disabled()
                } else {
                    ctx.theme().colors.text_primary()
                },
                role: ctx.theme().typography.body,
                align: HorizontalAlign::Start,
            });
            if let Some(sc) = shortcut {
                paint.push(WidgetPaint::AlignedLabel {
                    rect: shortcut_rect(args.rect, args.metrics),
                    text: sc.clone(),
                    color: ctx.theme().colors.text_secondary(),
                    role: ctx.theme().typography.caption,
                    align: HorizontalAlign::End,
                });
            }
            ItemDrawResult {
                paint,
                hovered: interaction.hover(),
                activated,
                opened_submenu: None,
            }
        }
        MenuItem::Submenu { id, label, .. } => {
            let interaction = ctx.interact(
                InteractDeclaration::new(*id, args.rect, Sense::INTERACTIVE)
                    .at_z(ZLayer::POPUP)
                    .focusable(false)
                    .active(args.is_open_submenu)
                    .a11y(
                        AccessNode::new(Role::MenuItem)
                            .with_label(*label)
                            .with_expanded(args.is_open_submenu),
                    ),
            );
            let opened = (interaction.click() || (interaction.hover() && !args.is_open_submenu))
                .then_some(*id);
            let mut paint = item_surface(
                ctx,
                args.rect,
                args.is_highlighted || interaction.hover() || args.is_open_submenu,
            );
            paint.push(WidgetPaint::AlignedLabel {
                rect: label_only_rect(args.rect, args.metrics),
                text: LabelText::Key(*label),
                color: ctx.theme().colors.text_primary(),
                role: ctx.theme().typography.body,
                align: HorizontalAlign::Start,
            });
            paint.push(WidgetPaint::Mark {
                rect: arrow_rect(args.rect, args.metrics),
                kind: GlyphMark::SubmenuArrow,
                color: ctx.theme().colors.text_secondary(),
            });
            ItemDrawResult {
                paint,
                hovered: interaction.hover(),
                activated: None,
                opened_submenu: opened,
            }
        }
    }
}

fn item_surface(ctx: &FrameCtx<'_>, rect: LayoutRect, highlighted: bool) -> Vec<WidgetPaint> {
    if !highlighted {
        return Vec::new();
    }
    vec![WidgetPaint::Surface {
        rect,
        fill: ctx.theme().colors.accent.step(Step12::HOVER_BG),
        border: None,
        radius: ctx.theme().radius.sm,
        elevation: None,
    }]
}

fn separator_paint(ctx: &FrameCtx<'_>, rect: LayoutRect, metrics: MenuMetrics) -> Vec<WidgetPaint> {
    let line_y = rect.origin.y.value() + metrics.separator_height.value() / 2.0 - 0.5;
    let line_rect = LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(rect.origin.x.value() + metrics.padding_x.value()),
            LayoutPx::new(line_y),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(rect.size.width.value() - 2.0 * metrics.padding_x.value()),
            LayoutPx::new(1.0),
        ),
    );
    vec![WidgetPaint::Surface {
        rect: line_rect,
        fill: ctx.theme().colors.neutral.step(Step12::SUBTLE_BORDER),
        border: None,
        radius: ctx.theme().radius.none,
        elevation: None,
    }]
}

fn label_only_rect(item: LayoutRect, metrics: MenuMetrics) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(item.origin.x.value() + metrics.padding_x.value()),
            item.origin.y,
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(item.size.width.value() - 2.0 * metrics.padding_x.value()),
            item.size.height,
        ),
    )
}

fn shortcut_rect(item: LayoutRect, metrics: MenuMetrics) -> LayoutRect {
    let half = item.size.width.value() / 2.0;
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(item.origin.x.value() + half), item.origin.y),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(half - metrics.padding_x.value()),
            item.size.height,
        ),
    )
}

fn arrow_rect(item: LayoutRect, metrics: MenuMetrics) -> LayoutRect {
    let arrow_px = 12.0;
    let pad = (item.size.height.value() - arrow_px).max(0.0) / 2.0;
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::new(
                item.origin.x.value() + item.size.width.value()
                    - arrow_px
                    - metrics.padding_x.value(),
            ),
            LayoutPx::new(item.origin.y.value() + pad),
        ),
        LayoutSize::new(LayoutPx::new(arrow_px), LayoutPx::new(arrow_px)),
    )
}

fn menu_rect(origin: LayoutPos, items: &[MenuItem], metrics: MenuMetrics) -> LayoutRect {
    let height: f32 = items
        .iter()
        .map(|i| match i {
            MenuItem::Separator => metrics.separator_height.value(),
            _ => metrics.item_height.value(),
        })
        .sum();
    LayoutRect::new(
        origin,
        LayoutSize::new(metrics.min_width, LayoutPx::new(height)),
    )
}

fn item_rects(menu: LayoutRect, items: &[MenuItem], metrics: MenuMetrics) -> Vec<LayoutRect> {
    items
        .iter()
        .scan(menu.origin.y.value(), |y, item| {
            let h = match item {
                MenuItem::Separator => metrics.separator_height.value(),
                _ => metrics.item_height.value(),
            };
            let rect = LayoutRect::new(
                LayoutPos::new(menu.origin.x, LayoutPx::new(*y)),
                LayoutSize::new(menu.size.width, LayoutPx::new(h)),
            );
            *y += h;
            Some(rect)
        })
        .collect()
}

fn handle_keyboard(
    ctx: &mut FrameCtx<'_>,
    items: &[MenuItem],
    state: &mut MenuState,
    activated: &mut Option<WidgetId>,
    close: &mut bool,
) {
    let event = take_key(
        ctx.input,
        &[
            TakeKey::named(NamedKey::ArrowUp),
            TakeKey::named(NamedKey::ArrowDown),
            TakeKey::named(NamedKey::Enter),
            TakeKey::named(NamedKey::Space),
            TakeKey::named(NamedKey::Escape),
            TakeKey::named(NamedKey::Home),
            TakeKey::named(NamedKey::End),
        ],
    );
    let Some(event) = event else { return };
    match event.code {
        KeyCode::Named(NamedKey::Escape) => {
            *close = true;
        }
        KeyCode::Named(NamedKey::ArrowDown) => {
            state.highlighted = next_focusable(items, state.highlighted, false);
        }
        KeyCode::Named(NamedKey::ArrowUp) => {
            state.highlighted = next_focusable(items, state.highlighted, true);
        }
        KeyCode::Named(NamedKey::Home) => {
            state.highlighted = first_focusable(items);
        }
        KeyCode::Named(NamedKey::End) => {
            state.highlighted = last_focusable(items);
        }
        KeyCode::Named(NamedKey::Enter | NamedKey::Space) => {
            if let Some(idx) = state.highlighted
                && let Some(MenuItem::Action { id, disabled, .. }) = items.get(idx)
                && !*disabled
            {
                *activated = Some(*id);
                *close = true;
            }
        }
        KeyCode::Named(_) | KeyCode::Char(_) => {}
    }
}

fn first_focusable(items: &[MenuItem]) -> Option<usize> {
    items.iter().position(MenuItem::is_focusable)
}

fn last_focusable(items: &[MenuItem]) -> Option<usize> {
    items
        .iter()
        .enumerate()
        .rev()
        .find(|(_, i)| i.is_focusable())
        .map(|(idx, _)| idx)
}

fn next_focusable(items: &[MenuItem], from: Option<usize>, reverse: bool) -> Option<usize> {
    if items.is_empty() {
        return None;
    }
    let len = items.len();
    let start = from.unwrap_or(if reverse { 0 } else { len - 1 });
    (1..=len)
        .find_map(|delta| {
            let idx = if reverse {
                (start + len - delta) % len
            } else {
                (start + delta) % len
            };
            items[idx].is_focusable().then_some(idx)
        })
        .or(from)
}

#[derive(Debug, PartialEq)]
pub struct ContextMenu<'a, 'state> {
    pub id: WidgetId,
    pub anchor: LayoutPos,
    pub label: StringKey,
    pub items: &'a [MenuItem],
    pub state: &'state mut MenuState,
    pub metrics: MenuMetrics,
}

impl<'a, 'state> ContextMenu<'a, 'state> {
    #[must_use]
    pub fn at_cursor(
        id: WidgetId,
        anchor: LayoutPos,
        label: StringKey,
        items: &'a [MenuItem],
        state: &'state mut MenuState,
    ) -> Self {
        Self {
            id,
            anchor,
            label,
            items,
            state,
            metrics: MenuMetrics::standard(),
        }
    }
}

#[must_use]
pub fn show_context_menu(ctx: &mut FrameCtx<'_>, menu: ContextMenu<'_, '_>) -> MenuResponse {
    let ContextMenu {
        id,
        anchor,
        label,
        items,
        state,
        metrics,
    } = menu;
    show_menu(
        ctx,
        Menu {
            id,
            origin: anchor,
            label,
            items,
            metrics,
            state,
        },
    )
}

#[derive(Clone, Debug, PartialEq)]
pub struct MenuBarEntry {
    pub id: WidgetId,
    pub label: StringKey,
    pub items: Vec<MenuItem>,
}

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct MenuBarState {
    pub open: Option<WidgetId>,
    pub menu: MenuState,
}

#[derive(Debug, PartialEq)]
pub struct MenuBar<'a, 'state> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub entries: &'a [MenuBarEntry],
    pub state: &'state mut MenuBarState,
    pub min_item_width: LayoutPx,
    pub item_padding: LayoutPx,
    pub document_label: Option<LabelText>,
}

impl<'a, 'state> MenuBar<'a, 'state> {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        entries: &'a [MenuBarEntry],
        state: &'state mut MenuBarState,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            entries,
            state,
            min_item_width: LayoutPx::new(36.0),
            item_padding: LayoutPx::new(10.0),
            document_label: None,
        }
    }

    #[must_use]
    pub fn with_document_label(mut self, label: LabelText) -> Self {
        self.document_label = Some(label);
        self
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct MenuBarResponse {
    pub activated: Option<WidgetId>,
    pub paint: Vec<WidgetPaint>,
    pub popover_paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_menu_bar(ctx: &mut FrameCtx<'_>, bar: MenuBar<'_, '_>) -> MenuBarResponse {
    let MenuBar {
        id,
        rect,
        label,
        entries,
        state,
        min_item_width,
        item_padding,
        document_label,
    } = bar;
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::MenuBar).with_label(label));
    let mut paint = vec![WidgetPaint::Surface {
        rect,
        fill: ctx.theme().colors.surface(crate::theme::SurfaceLevel::L0),
        border: Some(Border {
            width: StrokeWidth::HAIRLINE,
            color: ctx.theme().colors.neutral.step(Step12::SUBTLE_BORDER),
        }),
        radius: ctx.theme().radius.none,
        elevation: None,
    }];
    let role = ctx.theme().typography.label;
    let request = ShapeRequest {
        face: role.face,
        size_px: role.size.as_px_f32(),
        weight: role.weight,
        line_height_px: 0.0,
        letter_spacing_px: 0.0,
        max_width: None,
    };
    let widths: Vec<LayoutPx> = entries
        .iter()
        .map(|e| {
            let resolved = ctx.strings.resolve(e.label);
            let advance = ctx
                .shaper
                .shape(resolved, request)
                .lines
                .first()
                .map_or(0.0, ShapedLine::visible_advance_px);
            let total = advance + 2.0 * item_padding.value();
            LayoutPx::new(total.max(min_item_width.value()))
        })
        .collect();
    let direction = ctx.direction();
    let raw_entry_layouts = entry_rects(rect, &widths);
    let entry_layouts: Vec<LayoutRect> = raw_entry_layouts
        .iter()
        .map(|r| r.mirror_horizontally_within(rect, direction))
        .collect();
    entries
        .iter()
        .zip(entry_layouts.iter())
        .for_each(|(entry, entry_rect)| {
            paint.extend(draw_menu_bar_entry(
                ctx,
                entry,
                *entry_rect,
                item_padding,
                state,
            ));
        });
    if let Some(label_text) = document_label {
        paint.push(document_label_paint(
            ctx,
            label_text,
            rect,
            request,
            raw_entry_layouts.as_slice(),
            direction,
        ));
    }
    let mut popover_paint = Vec::new();
    let activated = open_menu_bar_dropdown(ctx, entries, &entry_layouts, state, &mut popover_paint);
    MenuBarResponse {
        activated,
        paint,
        popover_paint,
    }
}

fn draw_menu_bar_entry(
    ctx: &mut FrameCtx<'_>,
    entry: &MenuBarEntry,
    entry_rect: LayoutRect,
    item_padding: LayoutPx,
    state: &mut MenuBarState,
) -> Vec<WidgetPaint> {
    let is_open = state.open == Some(entry.id);
    let interaction = ctx.interact(
        InteractDeclaration::new(entry.id, entry_rect, Sense::INTERACTIVE)
            .focusable(true)
            .active(is_open)
            .a11y(
                AccessNode::new(Role::MenuItem)
                    .with_label(entry.label)
                    .with_expanded(is_open),
            ),
    );
    let live_focused = ctx.is_focused(entry.id);
    let pointer_toggled = interaction.click();
    let key_toggled = live_focused
        && take_key(
            ctx.input,
            &[
                TakeKey::named(NamedKey::Enter),
                TakeKey::named(NamedKey::Space),
                TakeKey::named(NamedKey::ArrowDown),
            ],
        )
        .is_some();
    if pointer_toggled || key_toggled {
        state.open = if is_open { None } else { Some(entry.id) };
        state.menu = MenuState::default();
    } else if state.open.is_some() && !is_open && interaction.hover() {
        state.open = Some(entry.id);
        state.menu = MenuState::default();
    }
    let mut paint = vec![
        WidgetPaint::Surface {
            rect: entry_rect,
            fill: if is_open {
                ctx.theme().colors.neutral.step(Step12::SELECTED_BG)
            } else if interaction.hover() {
                ctx.theme().colors.neutral.step(Step12::HOVER_BG)
            } else {
                Color::TRANSPARENT
            },
            border: None,
            radius: ctx.theme().radius.sm,
            elevation: None,
        },
        WidgetPaint::AlignedLabel {
            rect: LayoutRect::new(
                LayoutPos::new(
                    LayoutPx::new(entry_rect.origin.x.value() + item_padding.value()),
                    entry_rect.origin.y,
                ),
                LayoutSize::new(
                    LayoutPx::saturating_nonneg(
                        entry_rect.size.width.value() - 2.0 * item_padding.value(),
                    ),
                    entry_rect.size.height,
                ),
            ),
            text: LabelText::Key(entry.label),
            color: ctx.theme().colors.text_primary(),
            role: ctx.theme().typography.label,
            align: HorizontalAlign::Start,
        },
    ];
    push_focus_ring(
        ctx,
        &mut paint,
        entry_rect,
        ctx.theme().radius.sm,
        live_focused,
    );
    paint
}

fn open_menu_bar_dropdown(
    ctx: &mut FrameCtx<'_>,
    entries: &[MenuBarEntry],
    entry_layouts: &[LayoutRect],
    state: &mut MenuBarState,
    paint: &mut Vec<WidgetPaint>,
) -> Option<WidgetId> {
    let open_id = state.open?;
    let (entry, entry_rect) = entries
        .iter()
        .zip(entry_layouts.iter())
        .find(|(e, _)| e.id == open_id)?;
    let menu_origin = LayoutPos::new(
        entry_rect.origin.x,
        LayoutPx::new(entry_rect.origin.y.value() + entry_rect.size.height.value()),
    );
    let response = show_menu(
        ctx,
        Menu::new(
            entry.id.child(WidgetKey::new("menu")),
            menu_origin,
            entry.label,
            &entry.items,
            &mut state.menu,
        ),
    );
    paint.extend(response.paint);
    if response.close {
        state.open = None;
        state.menu = MenuState::default();
    }
    response.activated
}

fn document_label_paint(
    ctx: &mut FrameCtx<'_>,
    label_text: LabelText,
    bar_rect: LayoutRect,
    request: ShapeRequest,
    entry_layouts: &[LayoutRect],
    direction: LayoutDirection,
) -> WidgetPaint {
    let resolved = label_text.resolve(ctx.strings);
    let advance = ctx
        .shaper
        .shape(resolved, request)
        .lines
        .first()
        .map_or(0.0, ShapedLine::visible_advance_px);
    let bar_min_x = bar_rect.origin.x.value();
    let bar_max_x = bar_min_x + bar_rect.size.width.value();
    let width = advance.min(bar_rect.size.width.value()).max(0.0);
    let entries_end = entry_layouts
        .last()
        .map_or(bar_min_x, |r| r.origin.x.value() + r.size.width.value());
    let centered_x = bar_min_x + (bar_rect.size.width.value() - width) * 0.5;
    let max_x = (bar_max_x - width).max(bar_min_x);
    let label_x = centered_x.clamp(entries_end.min(max_x), max_x);
    let label_rect = LayoutRect::new(
        LayoutPos::new(LayoutPx::saturating(label_x), bar_rect.origin.y),
        LayoutSize::new(LayoutPx::saturating_nonneg(width), bar_rect.size.height),
    );
    WidgetPaint::AlignedLabel {
        rect: label_rect.mirror_horizontally_within(bar_rect, direction),
        text: label_text,
        color: ctx.theme().colors.text_primary(),
        role: ctx.theme().typography.label,
        align: HorizontalAlign::Center,
    }
}

fn entry_rects(bar: LayoutRect, widths: &[LayoutPx]) -> Vec<LayoutRect> {
    widths
        .iter()
        .scan(bar.origin.x.value(), |x, w| {
            let rect = LayoutRect::new(
                LayoutPos::new(LayoutPx::new(*x), bar.origin.y),
                LayoutSize::new(*w, bar.size.height),
            );
            *x += w.value();
            Some(rect)
        })
        .collect()
}

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

    use super::{
        Menu, MenuBar, MenuBarEntry, MenuBarState, MenuItem, MenuState, show_menu, show_menu_bar,
    };
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        FrameInstant, InputSnapshot, KeyCode, KeyEvent, ModifierMask, NamedKey, 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 menu_root() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("menu"))
    }

    fn menu_bar_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("menu_bar"))
    }

    fn action(name: &'static str) -> MenuItem {
        MenuItem::Action {
            id: menu_root().child(WidgetKey::new(name)),
            label: StringKey::new("menu.action"),
            shortcut: Some(super::super::paint::LabelText::Key(StringKey::new(
                "menu.shortcut",
            ))),
            disabled: false,
        }
    }

    fn disabled_action(name: &'static str) -> MenuItem {
        match action(name) {
            MenuItem::Action {
                id,
                label,
                shortcut,
                ..
            } => MenuItem::Action {
                id,
                label,
                shortcut,
                disabled: true,
            },
            _ => unreachable!(),
        }
    }

    fn render(
        items: &[MenuItem],
        state: &mut MenuState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::MenuResponse, 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_menu(
                &mut ctx,
                Menu::new(
                    menu_root(),
                    LayoutPos::ORIGIN,
                    StringKey::new("test.menu"),
                    items,
                    state,
                ),
            )
        };
        let next = resolve(prev, &hits, snap, focus.focused());
        (response, next)
    }

    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 click_action_item_activates() {
        let items = vec![action("save"), action("save_as")];
        let mut state = MenuState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(36.0));
        let mut last_response: Option<super::MenuResponse> = None;
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(&items, &mut state, &mut focus, &mut snap, &prev);
                last_response = Some(response);
                prev = next;
            });
        let Some(r) = last_response else {
            panic!("response missing")
        };
        let MenuItem::Action { id: target_id, .. } = &items[1] else {
            panic!("expected action");
        };
        assert_eq!(r.activated, Some(*target_id));
        assert!(r.close);
    }

    #[test]
    fn disabled_action_does_not_activate() {
        let items = vec![disabled_action("save")];
        let mut state = MenuState::default();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(40.0), LayoutPx::new(12.0));
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render(&items, &mut state, &mut focus, &mut snap, &prev);
                assert!(response.activated.is_none());
                assert!(!response.close);
                prev = next;
            });
    }

    #[test]
    fn arrow_down_skips_separator_and_disabled() {
        let items = vec![
            disabled_action("a"),
            MenuItem::Separator,
            action("b"),
            action("c"),
        ];
        let mut state = MenuState::default();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::ArrowDown),
            ModifierMask::NONE,
        ));
        let _ = render(&items, &mut state, &mut focus, &mut snap, &prev);
        assert_eq!(state.highlighted, Some(2));
    }

    #[test]
    fn enter_with_highlighted_action_activates_it() {
        let items = vec![action("a"), action("b")];
        let mut state = MenuState {
            highlighted: Some(1),
            ..MenuState::default()
        };
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let (response, _) = render(&items, &mut state, &mut focus, &mut snap, &prev);
        let MenuItem::Action { id, .. } = &items[1] else {
            panic!()
        };
        assert_eq!(response.activated, Some(*id));
        assert!(response.close);
    }

    #[test]
    fn escape_closes_without_activating() {
        let items = vec![action("a")];
        let mut state = MenuState::default();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Escape),
            ModifierMask::NONE,
        ));
        let (response, _) = render(&items, &mut state, &mut focus, &mut snap, &prev);
        assert!(response.activated.is_none());
        assert!(response.close);
    }

    fn bar_state() -> MenuBarState {
        MenuBarState::default()
    }

    fn entry(name: &'static str) -> MenuBarEntry {
        let entry_id = menu_root().child(WidgetKey::new(name));
        MenuBarEntry {
            id: entry_id,
            label: StringKey::new("menubar.entry"),
            items: vec![MenuItem::Action {
                id: entry_id.child(WidgetKey::new("first")),
                label: StringKey::new("menu.action"),
                shortcut: None,
                disabled: false,
            }],
        }
    }

    #[test]
    fn click_menubar_entry_opens_its_menu() {
        let entries = vec![entry("file"), entry("edit")];
        let mut state = bar_state();
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let click_pos = LayoutPos::new(LayoutPx::new(200.0), LayoutPx::new(10.0));
        let bar_rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(600.0), LayoutPx::new(24.0)),
        );
        [press(click_pos), release(click_pos), idle(click_pos)]
            .into_iter()
            .for_each(|mut snap| {
                let mut hits = HitFrame::new();
                {
                    let mut shaper = bone_text::Shaper::new();
                    let mut a11y = crate::a11y::AccessTreeBuilder::new();
                    let mut ctx = FrameCtx::new(
                        theme.clone(),
                        &mut snap,
                        &mut focus,
                        &table,
                        StringTable::empty(),
                        &mut hits,
                        &prev,
                        &mut a11y,
                        &mut shaper,
                    );
                    let _ = show_menu_bar(
                        &mut ctx,
                        MenuBar::new(
                            menu_bar_id(),
                            bar_rect,
                            StringKey::new("test.menu_bar"),
                            &entries,
                            &mut state,
                        ),
                    );
                }
                prev = resolve(&prev, &hits, &snap, focus.focused());
            });
        assert_eq!(state.open, Some(entries[1].id));
    }

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

        let entries = vec![entry("file"), entry("edit")];
        let mut state = bar_state();
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let bar_rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(400.0), LayoutPx::new(24.0)),
        );
        focus.request_focus(entries[0].id);
        let prev = HitState::new();

        [InputSnapshot::idle(FrameInstant::ZERO), {
            let mut s = InputSnapshot::idle(FrameInstant::ZERO);
            s.keys_pressed.push(KeyEvent::new(
                KeyCode::Named(NamedKey::ArrowDown),
                ModifierMask::NONE,
            ));
            s
        }]
        .into_iter()
        .for_each(|mut snap| {
            let mut hits = HitFrame::new();
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme.clone(),
                &mut snap,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            let _ = show_menu_bar(
                &mut ctx,
                MenuBar::new(
                    menu_bar_id(),
                    bar_rect,
                    StringKey::new("test.menu_bar"),
                    &entries,
                    &mut state,
                ),
            );
        });
        assert_eq!(state.open, Some(entries[0].id));
    }

    #[test]
    fn document_label_centers_in_the_bar() {
        use super::super::paint::{HorizontalAlign, LabelText, WidgetPaint};

        let entries = vec![entry("file"), entry("edit"), entry("view")];
        let mut state = bar_state();
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let bar_rect = LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(1920.0), LayoutPx::new(24.0)),
        );
        let doc = "Part117 *";
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let mut hits = HitFrame::new();
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = crate::a11y::AccessTreeBuilder::new();
        let response = {
            let mut ctx = FrameCtx::new(
                theme.clone(),
                &mut snap,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            show_menu_bar(
                &mut ctx,
                MenuBar::new(
                    menu_bar_id(),
                    bar_rect,
                    StringKey::new("test.menu_bar"),
                    &entries,
                    &mut state,
                )
                .with_document_label(LabelText::Owned(doc.to_owned())),
            )
        };
        let found = response.paint.iter().find_map(|p| match p {
            WidgetPaint::AlignedLabel {
                rect,
                text: LabelText::Owned(t),
                align,
                ..
            } if t.as_str() == doc => Some((*rect, *align)),
            _ => None,
        });
        let Some((rect, align)) = found else {
            panic!("document label is painted");
        };
        assert_eq!(align, HorizontalAlign::Center);
        let label_center = rect.origin.x.value() + rect.size.width.value() * 0.5;
        let bar_center = bar_rect.origin.x.value() + bar_rect.size.width.value() * 0.5;
        assert!(
            (label_center - bar_center).abs() <= 0.5,
            "document label center {label_center} expected bar center {bar_center}",
        );
    }
}
