use core::time::Duration;
use std::sync::Arc;

use uom::si::angle::degree;
use uom::si::f64::{Angle, Length};
use uom::si::length::millimeter;

use crate::a11y::AccessTreeBuilder;
use crate::focus::FocusManager;
use crate::frame::FrameCtx;
use crate::hit_test::{HitFrame, HitState, Interaction, InteractionState};
use crate::hotkey::HotkeyTable;
use crate::input::{FrameInstant, InputSnapshot};
use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::raster::{CanvasPx, CanvasSize};
use crate::strings::{StringKey, StringTable};
use crate::theme::Theme;
use crate::widget_id::{WidgetId, WidgetKey};
use crate::widgets::{
    AlwaysValid, AngleEditor, BoolEditor, Button, ButtonState, ButtonVariant, Checkbox,
    CheckboxState, ConfirmationDialog, ContextMenu, Dialog, DialogButton, Dropdown, DropdownItem,
    DropdownState, FilePickerDialog, FilePickerEntry, FilePickerMode, FilePickerState,
    HotkeyCapture, HotkeyCaptureState, LabelText, LengthEditor, ListItem, ListView, ListViewState,
    MemoryClipboard, Menu, MenuBar, MenuBarEntry, MenuBarState, MenuItem, MenuState, Modal,
    NumericInput, Panel, PanelState, PanelTitlebar, PropertyGrid, PropertyOption, PropertyRow,
    RadioGroup, RadioOption, Ribbon, RibbonGroup, RibbonIconSize, RibbonTab, SelectionEditor,
    Slider, SliderRange, SliderStep, StatusAlign, StatusBar, StatusItem, Tab, Table, TableColumn,
    TableRow, TableState, Tabs, TabsOrientation, TextEditor, TextInput, TextInputState, Toast,
    ToastKind, ToastState, ToggleButton, Toolbar, ToolbarItem, Tooltip, TooltipPlacement,
    TooltipState, TreeNode, TreeView, TreeViewState, WidgetPaint, show_button, show_checkbox,
    show_confirmation, show_context_menu, show_dialog, show_dropdown, show_file_picker,
    show_hotkey_capture, show_list_view, show_menu, show_menu_bar, show_modal, show_panel,
    show_parsed_input, show_property_grid, show_radio_group, show_ribbon, show_slider,
    show_status_bar, show_table, show_tabs, show_text_input, show_toast, show_toggle_button,
    show_toolbar, show_tooltip, show_tree_view,
};

pub const GALLERY_LABEL: StringKey = StringKey::new("gallery.label");
pub const GALLERY_FRAME_NOW: FrameInstant = FrameInstant::from_duration(Duration::from_secs(1));
pub const GALLERY_CANVAS: CanvasSize = CanvasSize::new(CanvasPx::new(1400), CanvasPx::new(2400));
const TOOLTIP_ANCHOR_KEY: &str = "button";

pub type StoryPath = &'static [&'static str];

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Story {
    pub key: &'static str,
    pub parent: Option<&'static str>,
    pub kind: StoryKind,
    pub extras: &'static [StoryPath],
}

impl Story {
    const fn root(key: &'static str, kind: StoryKind) -> Self {
        Self {
            key,
            parent: None,
            kind,
            extras: &[],
        }
    }

    const fn root_with(key: &'static str, kind: StoryKind, extras: &'static [StoryPath]) -> Self {
        Self {
            key,
            parent: None,
            kind,
            extras,
        }
    }

    pub fn ids(self) -> impl Iterator<Item = WidgetId> {
        core::iter::once(story_id(self)).chain(self.extras.iter().copied().map(path_id))
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum StoryKind {
    InputPrimitive,
    Composite,
    Overlay,
}

pub const STORIES: &[Story] = &[
    Story::root(TOOLTIP_ANCHOR_KEY, StoryKind::InputPrimitive),
    Story::root("button_secondary", StoryKind::InputPrimitive),
    Story::root("button_destructive", StoryKind::InputPrimitive),
    Story::root("button_ghost", StoryKind::InputPrimitive),
    Story::root("button_icon", StoryKind::InputPrimitive),
    Story::root("button_disabled", StoryKind::InputPrimitive),
    Story::root("button_loading", StoryKind::InputPrimitive),
    Story::root("check", StoryKind::InputPrimitive),
    Story::root("check_unchecked", StoryKind::InputPrimitive),
    Story::root("check_indeterminate", StoryKind::InputPrimitive),
    Story::root("toggle", StoryKind::InputPrimitive),
    Story::root("toggle_off", StoryKind::InputPrimitive),
    Story::root_with("radio_a", StoryKind::InputPrimitive, &[&["radio_b"]]),
    Story::root("slider", StoryKind::InputPrimitive),
    Story::root_with("tabs", StoryKind::InputPrimitive, &[&["tab_a"], &["tab_b"]]),
    Story::root_with("status_bar", StoryKind::Composite, &[&["status_item"]]),
    Story::root_with("panel", StoryKind::Composite, &[&["panel", "titlebar"]]),
    Story::root_with(
        "panel_collapsed",
        StoryKind::Composite,
        &[&["panel_collapsed", "titlebar"]],
    ),
    Story::root("dropdown", StoryKind::InputPrimitive),
    Story::root("text_input", StoryKind::InputPrimitive),
    Story::root("numeric_input", StoryKind::InputPrimitive),
    Story::root("hotkey_capture", StoryKind::InputPrimitive),
    Story::root_with(
        "list_view",
        StoryKind::Composite,
        &[&["list_a"], &["list_b"]],
    ),
    Story::root_with(
        "table",
        StoryKind::Composite,
        &[
            &["col_a"],
            &["col_b"],
            &["row_a"],
            &["row_b"],
            &["col_a", "resize"],
            &["col_b", "resize"],
        ],
    ),
    Story::root_with(
        "tree_view",
        StoryKind::Composite,
        &[
            &["tree_root"],
            &["tree_child"],
            &["tree_root", "disclosure"],
        ],
    ),
    Story::root_with(
        "property_grid",
        StoryKind::Composite,
        &[
            &["prop_text"],
            &["prop_bool"],
            &["prop_select"],
            &["prop_length"],
            &["prop_angle"],
        ],
    ),
    Story::root_with(
        "toolbar",
        StoryKind::Composite,
        &[&["toolbar_a"], &["toolbar_b"]],
    ),
    Story::root_with(
        "ribbon",
        StoryKind::Composite,
        &[
            &["ribbon_tab"],
            &["ribbon_tool_a"],
            &["ribbon_tool_b"],
            &["ribbon_group"],
            &["ribbon", "tabs"],
            &["ribbon_group", "toolbar"],
        ],
    ),
    Story::root_with(
        "menu",
        StoryKind::Composite,
        &[&["menu_action"], &["menu_submenu"]],
    ),
    Story::root_with("context_menu", StoryKind::Composite, &[&["context_action"]]),
    Story::root_with("menu_bar", StoryKind::Composite, &[&["menubar_entry"]]),
    Story::root_with("toast", StoryKind::Overlay, &[&["toast", "close"]]),
    Story::root_with(
        "toast_success",
        StoryKind::Overlay,
        &[&["toast_success", "close"]],
    ),
    Story::root_with(
        "toast_warning",
        StoryKind::Overlay,
        &[&["toast_warning", "close"]],
    ),
    Story::root_with(
        "toast_danger",
        StoryKind::Overlay,
        &[&["toast_danger", "close"]],
    ),
    Story::root("modal", StoryKind::Overlay),
    Story::root_with(
        "dialog",
        StoryKind::Overlay,
        &[&["dialog_ok"], &["dialog_cancel"]],
    ),
    Story::root_with(
        "confirmation",
        StoryKind::Overlay,
        &[&["confirmation", "confirm"], &["confirmation", "cancel"]],
    ),
    Story::root_with(
        "file_picker",
        StoryKind::Overlay,
        &[
            &["picker_entry"],
            &["file_picker", "confirm"],
            &["file_picker", "cancel"],
            &["file_picker", "list"],
            &["file_picker", "filename"],
        ],
    ),
    Story::root_with(
        "file_picker_open",
        StoryKind::Overlay,
        &[
            &["picker_entry_open"],
            &["file_picker_open", "confirm"],
            &["file_picker_open", "cancel"],
            &["file_picker_open", "list"],
        ],
    ),
    Story {
        key: "tooltip",
        parent: Some(TOOLTIP_ANCHOR_KEY),
        kind: StoryKind::Overlay,
        extras: &[],
    },
];

#[must_use]
pub fn path_id(path: &[&'static str]) -> WidgetId {
    path.iter()
        .fold(WidgetId::ROOT, |acc, seg| acc.child(WidgetKey::new(seg)))
}

#[must_use]
pub fn story_id(story: Story) -> WidgetId {
    let base = match story.parent {
        Some(parent) => WidgetId::ROOT.child(WidgetKey::new(parent)),
        None => WidgetId::ROOT,
    };
    base.child(WidgetKey::new(story.key))
}

#[must_use]
pub fn declared_ids() -> std::collections::BTreeSet<WidgetId> {
    STORIES.iter().copied().flat_map(Story::ids).collect()
}

#[must_use]
pub fn id(name: &'static str) -> WidgetId {
    path_id(&[name])
}

fn rect(x: f32, y: f32, w: f32, h: f32) -> LayoutRect {
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
        LayoutSize::new(LayoutPx::new(w), LayoutPx::new(h)),
    )
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum Choice {
    A,
    B,
}

pub struct GalleryState {
    pub panel: PanelState,
    pub panel_collapsed: PanelState,
    pub dropdown: DropdownState,
    pub text_input: TextInputState,
    pub numeric_input: TextInputState,
    pub clipboard: MemoryClipboard,
    pub list: ListViewState,
    pub table: TableState,
    pub tree: TreeViewState,
    pub menu: MenuState,
    pub context_menu: MenuState,
    pub menu_bar: MenuBarState,
    pub tooltip: TooltipState,
    pub toast_info: ToastState,
    pub toast_success: ToastState,
    pub toast_warning: ToastState,
    pub toast_danger: ToastState,
    pub file_picker: FilePickerState,
    pub file_picker_open: FilePickerState,
    pub property_text: TextEditor,
    pub property_bool: BoolEditor,
    pub property_select: SelectionEditor,
    pub property_length: LengthEditor,
    pub property_angle: AngleEditor,
    pub hotkey: HotkeyCaptureState,
}

impl Default for GalleryState {
    fn default() -> Self {
        Self::new()
    }
}

impl GalleryState {
    #[must_use]
    pub fn new() -> Self {
        let mut tree = TreeViewState::default();
        tree.expanded.insert(id("tree_root"));
        Self {
            panel: PanelState::open(),
            panel_collapsed: PanelState::collapsed(),
            dropdown: DropdownState::closed(),
            text_input: TextInputState::from_text("Hello"),
            numeric_input: TextInputState::from_text("42"),
            clipboard: MemoryClipboard::default(),
            list: ListViewState::default(),
            table: TableState::default(),
            tree,
            menu: MenuState::default(),
            context_menu: MenuState::default(),
            menu_bar: MenuBarState::default(),
            tooltip: TooltipState {
                hover_began: Some(FrameInstant::ZERO),
                ..TooltipState::default()
            },
            toast_info: ToastState::fresh(),
            toast_success: ToastState::fresh(),
            toast_warning: ToastState::fresh(),
            toast_danger: ToastState::fresh(),
            file_picker: FilePickerState::default(),
            file_picker_open: FilePickerState::default(),
            property_text: TextEditor::new("name"),
            property_bool: BoolEditor::new(true),
            property_select: SelectionEditor::new(
                vec![
                    PropertyOption {
                        label: StringKey::new("gallery.option_a"),
                    },
                    PropertyOption {
                        label: StringKey::new("gallery.option_b"),
                    },
                ],
                Some(0),
            ),
            property_length: LengthEditor::new(Length::new::<millimeter>(10.0)),
            property_angle: AngleEditor::new(Angle::new::<degree>(45.0)),
            hotkey: HotkeyCaptureState::default(),
        }
    }
}

#[must_use]
pub fn render(theme: Arc<Theme>, state: &mut GalleryState) -> Vec<WidgetPaint> {
    let mut focus = FocusManager::new();
    let mut a11y = AccessTreeBuilder::new();
    render_with(theme, state, &mut focus, &mut a11y, StringTable::empty())
}

#[must_use]
pub fn render_with(
    theme: Arc<Theme>,
    state: &mut GalleryState,
    focus: &mut FocusManager,
    a11y: &mut AccessTreeBuilder,
    strings: &StringTable,
) -> Vec<WidgetPaint> {
    let table = HotkeyTable::new();
    let mut hits = HitFrame::new();
    let prev = prev_hit_state();
    let mut input = InputSnapshot::idle(GALLERY_FRAME_NOW);
    let mut paint = Vec::new();
    let mut shaper = bone_text::Shaper::new();

    let mut ctx = FrameCtx::new(
        theme,
        &mut input,
        focus,
        &table,
        strings,
        &mut hits,
        &prev,
        a11y,
        &mut shaper,
    );
    render_basics(&mut ctx, state, &mut paint);
    render_inputs(&mut ctx, state, &mut paint);
    render_collections(&mut ctx, state, &mut paint);
    render_chrome(&mut ctx, state, &mut paint);
    render_state_variants(&mut ctx, state, &mut paint);
    render_overlays(&mut ctx, state, &mut paint);
    paint
}

fn prev_hit_state() -> HitState {
    let mut state = HitState::new();
    let hover = Interaction {
        state: InteractionState::NONE.with(InteractionState::HOVER, true),
        ..Interaction::idle()
    };
    let anchor = id(TOOLTIP_ANCHOR_KEY);
    state.interactions.insert(anchor, hover);
    state.hovered = Some(anchor);
    state
}

#[allow(
    clippy::too_many_lines,
    reason = "gallery renders many widgets in one frame"
)]
fn render_basics(ctx: &mut FrameCtx<'_>, state: &mut GalleryState, paint: &mut Vec<WidgetPaint>) {
    let response = show_button(
        ctx,
        Button::new(
            id("button"),
            rect(0.0, 0.0, 80.0, 24.0),
            GALLERY_LABEL,
            ButtonVariant::Primary,
        ),
    );
    paint.extend(response.paint);
    let response = show_checkbox(
        ctx,
        Checkbox::new(
            id("check"),
            rect(0.0, 28.0, 80.0, 20.0),
            GALLERY_LABEL,
            CheckboxState::Checked,
        ),
    );
    paint.extend(response.paint);
    let response = show_toggle_button(
        ctx,
        ToggleButton::new(
            id("toggle"),
            rect(0.0, 52.0, 80.0, 20.0),
            GALLERY_LABEL,
            true,
        ),
    );
    paint.extend(response.paint);
    let response = show_radio_group(
        ctx,
        RadioGroup::new(
            vec![
                RadioOption {
                    id: id("radio_a"),
                    rect: rect(0.0, 76.0, 80.0, 20.0),
                    label: GALLERY_LABEL,
                    value: Choice::A,
                },
                RadioOption {
                    id: id("radio_b"),
                    rect: rect(0.0, 100.0, 80.0, 20.0),
                    label: GALLERY_LABEL,
                    value: Choice::B,
                },
            ],
            Choice::A,
        ),
    );
    paint.extend(response.paint);
    let Ok(slider_range) = SliderRange::try_new(0.0_f64, 10.0) else {
        unreachable!("slider range");
    };
    let Ok(slider_step) = SliderStep::try_new(1.0_f64) else {
        unreachable!("slider step");
    };
    let response = show_slider(
        ctx,
        Slider::new(
            id("slider"),
            rect(0.0, 124.0, 200.0, 18.0),
            GALLERY_LABEL,
            5.0_f64,
            slider_range,
            slider_step,
        ),
    );
    paint.extend(response.paint);
    let tab_items = [
        Tab::new(id("tab_a"), rect(0.0, 144.0, 80.0, 24.0), GALLERY_LABEL),
        Tab::new(id("tab_b"), rect(80.0, 144.0, 80.0, 24.0), GALLERY_LABEL),
    ];
    let response = show_tabs(
        ctx,
        Tabs::new(
            id("tabs"),
            TabsOrientation::Top,
            GALLERY_LABEL,
            &tab_items,
            id("tab_a"),
        ),
    );
    paint.extend(response.paint);
    let status_items = [StatusItem::new(
        id("status_item"),
        GALLERY_LABEL,
        StatusAlign::Start,
        LayoutPx::new(80.0),
    )
    .interactive(true)];
    let response = show_status_bar(
        ctx,
        StatusBar::new(
            id("status_bar"),
            rect(0.0, 168.0, 200.0, 22.0),
            GALLERY_LABEL,
            &status_items,
        ),
    );
    paint.extend(response.paint);
    let response = show_panel(
        ctx,
        Panel::new(
            id("panel"),
            rect(0.0, 192.0, 200.0, 100.0),
            &mut state.panel,
        )
        .titlebar(PanelTitlebar {
            label: GALLERY_LABEL,
            height: LayoutPx::new(22.0),
            collapsible: true,
        }),
    );
    paint.extend(response.paint);
    let response = show_dropdown(
        ctx,
        Dropdown::new(
            id("dropdown"),
            rect(0.0, 296.0, 160.0, 24.0),
            LayoutPx::new(20.0),
            vec![
                DropdownItem {
                    value: Choice::A,
                    label: GALLERY_LABEL,
                },
                DropdownItem {
                    value: Choice::B,
                    label: GALLERY_LABEL,
                },
            ],
            None,
            GALLERY_LABEL,
            &mut state.dropdown,
        ),
    );
    paint.extend(response.paint);
}

fn render_inputs(ctx: &mut FrameCtx<'_>, state: &mut GalleryState, paint: &mut Vec<WidgetPaint>) {
    let response = show_text_input(
        ctx,
        TextInput {
            id: id("text_input"),
            rect: rect(0.0, 324.0, 200.0, 24.0),
            placeholder: GALLERY_LABEL,
            state: &mut state.text_input,
            disabled: false,
            validator: AlwaysValid,
        },
        &mut state.clipboard,
    );
    paint.extend(response.paint);
    let response = show_parsed_input::<i32, _>(
        ctx,
        NumericInput::<i32>::new(
            id("numeric_input"),
            rect(0.0, 352.0, 200.0, 24.0),
            GALLERY_LABEL,
            &mut state.numeric_input,
        ),
        &mut state.clipboard,
    );
    paint.extend(response.paint);
    let response = show_hotkey_capture(
        ctx,
        HotkeyCapture::new(
            id("hotkey_capture"),
            rect(0.0, 380.0, 200.0, 24.0),
            GALLERY_LABEL,
            &mut state.hotkey,
        ),
    );
    paint.extend(response.paint);
}

#[allow(
    clippy::too_many_lines,
    reason = "gallery renders many widgets in one frame"
)]
fn render_collections(
    ctx: &mut FrameCtx<'_>,
    state: &mut GalleryState,
    paint: &mut Vec<WidgetPaint>,
) {
    let list_items = [
        ListItem {
            id: id("list_a"),
            label: LabelText::Key(GALLERY_LABEL),
        },
        ListItem {
            id: id("list_b"),
            label: LabelText::Key(GALLERY_LABEL),
        },
    ];
    let response = show_list_view(
        ctx,
        ListView::new(
            id("list_view"),
            rect(220.0, 0.0, 180.0, 60.0),
            GALLERY_LABEL,
            &list_items,
            &mut state.list,
        ),
    );
    paint.extend(response.paint);
    let columns = [
        TableColumn::new(id("col_a"), GALLERY_LABEL, LayoutPx::new(80.0)),
        TableColumn::new(id("col_b"), GALLERY_LABEL, LayoutPx::new(80.0)),
    ];
    let rows = [
        TableRow {
            id: id("row_a"),
            cells: [GALLERY_LABEL, GALLERY_LABEL],
        },
        TableRow {
            id: id("row_b"),
            cells: [GALLERY_LABEL, GALLERY_LABEL],
        },
    ];
    let response = show_table(
        ctx,
        Table::new(
            id("table"),
            rect(220.0, 64.0, 180.0, 80.0),
            GALLERY_LABEL,
            &columns,
            &rows,
            &mut state.table,
        ),
    );
    paint.extend(response.paint);
    let tree_roots = [TreeNode::parent(
        id("tree_root"),
        GALLERY_LABEL,
        vec![TreeNode::leaf(id("tree_child"), GALLERY_LABEL)],
    )];
    let response = show_tree_view(
        ctx,
        TreeView::new(
            id("tree_view"),
            rect(220.0, 152.0, 180.0, 60.0),
            GALLERY_LABEL,
            &tree_roots,
            &mut state.tree,
        ),
    );
    paint.extend(response.paint);
    let mut rows: [PropertyRow<'_>; 5] = [
        PropertyRow {
            id: id("prop_text"),
            label: StringKey::new("gallery.prop.text"),
            editor: &mut state.property_text,
            read_only: false,
        },
        PropertyRow {
            id: id("prop_bool"),
            label: StringKey::new("gallery.prop.bool"),
            editor: &mut state.property_bool,
            read_only: false,
        },
        PropertyRow {
            id: id("prop_select"),
            label: StringKey::new("gallery.prop.select"),
            editor: &mut state.property_select,
            read_only: false,
        },
        PropertyRow {
            id: id("prop_length"),
            label: StringKey::new("gallery.prop.length"),
            editor: &mut state.property_length,
            read_only: false,
        },
        PropertyRow {
            id: id("prop_angle"),
            label: StringKey::new("gallery.prop.angle"),
            editor: &mut state.property_angle,
            read_only: false,
        },
    ];
    let response = show_property_grid(
        ctx,
        PropertyGrid::new(
            id("property_grid"),
            rect(220.0, 220.0, 240.0, 160.0),
            GALLERY_LABEL,
            &mut rows,
        ),
        &mut state.clipboard,
    );
    paint.extend(response.paint);
}

#[allow(
    clippy::too_many_lines,
    reason = "gallery renders many widgets in one frame"
)]
fn render_chrome(ctx: &mut FrameCtx<'_>, state: &mut GalleryState, paint: &mut Vec<WidgetPaint>) {
    let toolbar_items = [
        ToolbarItem::new(id("toolbar_a"), GALLERY_LABEL),
        ToolbarItem::new(id("toolbar_b"), GALLERY_LABEL),
    ];
    let response = show_toolbar(
        ctx,
        Toolbar::horizontal(
            id("toolbar"),
            rect(480.0, 0.0, 160.0, 32.0),
            GALLERY_LABEL,
            &toolbar_items,
            LayoutPx::new(28.0),
            LayoutPx::new(4.0),
        ),
    );
    paint.extend(response.paint);
    let ribbon_toolbar_items = [
        ToolbarItem::new(id("ribbon_tool_a"), GALLERY_LABEL),
        ToolbarItem::new(id("ribbon_tool_b"), GALLERY_LABEL),
    ];
    let ribbon_tabs = [RibbonTab::new(
        id("ribbon_tab"),
        GALLERY_LABEL,
        vec![RibbonGroup {
            id: id("ribbon_group"),
            label: GALLERY_LABEL,
            items: ribbon_toolbar_items.to_vec(),
            icon_size: RibbonIconSize::Large,
            min_width: LayoutPx::new(80.0),
            width: LayoutPx::new(140.0),
        }],
    )];
    let response = show_ribbon(
        ctx,
        Ribbon::new(
            id("ribbon"),
            rect(480.0, 36.0, 200.0, 96.0),
            GALLERY_LABEL,
            &ribbon_tabs,
            id("ribbon_tab"),
        ),
    );
    paint.extend(response.paint);
    let menu_items = vec![
        MenuItem::Action {
            id: id("menu_action"),
            label: GALLERY_LABEL,
            shortcut: None,
            disabled: false,
        },
        MenuItem::Separator,
        MenuItem::Submenu {
            id: id("menu_submenu"),
            label: GALLERY_LABEL,
            items: vec![MenuItem::Action {
                id: id("menu_subaction"),
                label: GALLERY_LABEL,
                shortcut: None,
                disabled: false,
            }],
        },
    ];
    let response = show_menu(
        ctx,
        Menu::new(
            id("menu"),
            LayoutPos::new(LayoutPx::new(480.0), LayoutPx::new(140.0)),
            GALLERY_LABEL,
            &menu_items,
            &mut state.menu,
        ),
    );
    paint.extend(response.paint);
    let context_items = vec![MenuItem::Action {
        id: id("context_action"),
        label: GALLERY_LABEL,
        shortcut: None,
        disabled: false,
    }];
    let response = show_context_menu(
        ctx,
        ContextMenu::at_cursor(
            id("context_menu"),
            LayoutPos::new(LayoutPx::new(480.0), LayoutPx::new(220.0)),
            GALLERY_LABEL,
            &context_items,
            &mut state.context_menu,
        ),
    );
    paint.extend(response.paint);
    let menu_bar_entries = [MenuBarEntry {
        id: id("menubar_entry"),
        label: GALLERY_LABEL,
        items: vec![MenuItem::Action {
            id: id("menubar_action"),
            label: GALLERY_LABEL,
            shortcut: None,
            disabled: false,
        }],
    }];
    let response = show_menu_bar(
        ctx,
        MenuBar::new(
            id("menu_bar"),
            rect(480.0, 280.0, 200.0, 24.0),
            GALLERY_LABEL,
            &menu_bar_entries,
            &mut state.menu_bar,
        ),
    );
    paint.extend(response.paint);
}

const BUTTON_VARIANTS: [(&str, ButtonVariant, ButtonState); 6] = [
    (
        "button_secondary",
        ButtonVariant::Secondary,
        ButtonState::Idle,
    ),
    (
        "button_destructive",
        ButtonVariant::Destructive,
        ButtonState::Idle,
    ),
    ("button_ghost", ButtonVariant::Ghost, ButtonState::Idle),
    ("button_icon", ButtonVariant::IconOnly, ButtonState::Idle),
    (
        "button_disabled",
        ButtonVariant::Primary,
        ButtonState::Disabled,
    ),
    (
        "button_loading",
        ButtonVariant::Primary,
        ButtonState::Loading,
    ),
];

const CHECKBOX_VARIANTS: [(&str, CheckboxState); 2] = [
    ("check_unchecked", CheckboxState::Unchecked),
    ("check_indeterminate", CheckboxState::Indeterminate),
];

const TOAST_VARIANTS: [(&str, ToastKind, f32); 3] = [
    ("toast_success", ToastKind::Success, 620.0),
    ("toast_warning", ToastKind::Warning, 672.0),
    ("toast_danger", ToastKind::Danger, 724.0),
];

#[allow(
    clippy::cast_precision_loss,
    reason = "i bounded by small const arrays, far below f32 precision boundary"
)]
fn variant_x(i: usize) -> f32 {
    (i as f32) * 84.0
}

fn render_state_variants(
    ctx: &mut FrameCtx<'_>,
    state: &mut GalleryState,
    paint: &mut Vec<WidgetPaint>,
) {
    BUTTON_VARIANTS
        .iter()
        .enumerate()
        .for_each(|(i, (key, variant, button_state))| {
            let response = show_button(
                ctx,
                Button::new(
                    id(key),
                    rect(variant_x(i), 412.0, 80.0, 24.0),
                    GALLERY_LABEL,
                    *variant,
                )
                .with_state(*button_state),
            );
            paint.extend(response.paint);
        });
    CHECKBOX_VARIANTS
        .iter()
        .enumerate()
        .for_each(|(i, (key, cb_state))| {
            let response = show_checkbox(
                ctx,
                Checkbox::new(
                    id(key),
                    rect(variant_x(i), 440.0, 80.0, 20.0),
                    GALLERY_LABEL,
                    *cb_state,
                ),
            );
            paint.extend(response.paint);
        });
    let response = show_toggle_button(
        ctx,
        ToggleButton::new(
            id("toggle_off"),
            rect(0.0, 468.0, 80.0, 20.0),
            GALLERY_LABEL,
            false,
        ),
    );
    paint.extend(response.paint);
    let response = show_panel(
        ctx,
        Panel::new(
            id("panel_collapsed"),
            rect(0.0, 492.0, 200.0, 100.0),
            &mut state.panel_collapsed,
        )
        .titlebar(PanelTitlebar {
            label: GALLERY_LABEL,
            height: LayoutPx::new(22.0),
            collapsible: true,
        }),
    );
    paint.extend(response.paint);
    let toast_states = [
        &mut state.toast_success,
        &mut state.toast_warning,
        &mut state.toast_danger,
    ];
    TOAST_VARIANTS
        .iter()
        .zip(toast_states)
        .for_each(|((key, kind, y), toast_state)| {
            let response = show_toast(
                ctx,
                Toast::new(
                    id(key),
                    rect(20.0, *y, 360.0, 48.0),
                    *kind,
                    GALLERY_LABEL,
                    toast_state,
                ),
            );
            paint.extend(response.paint);
        });
}

fn render_overlays(ctx: &mut FrameCtx<'_>, state: &mut GalleryState, paint: &mut Vec<WidgetPaint>) {
    let tooltip_paint = show_tooltip(
        ctx,
        Tooltip::new(
            id(TOOLTIP_ANCHOR_KEY),
            rect(0.0, 0.0, 80.0, 24.0),
            GALLERY_LABEL,
            TooltipPlacement::Below,
            LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(20.0)),
        ),
        &mut state.tooltip,
    );
    paint.extend(tooltip_paint);
    let response = show_toast(
        ctx,
        Toast::new(
            id("toast"),
            rect(20.0, 560.0, 360.0, 48.0),
            ToastKind::Info,
            GALLERY_LABEL,
            &mut state.toast_info,
        ),
    );
    paint.extend(response.paint);
    let (modal_response, ()) = show_modal(
        ctx,
        Modal::new(
            id("modal"),
            rect(700.0, 0.0, 600.0, 400.0),
            LayoutSize::new(LayoutPx::new(300.0), LayoutPx::new(200.0)),
            GALLERY_LABEL,
        ),
        |_ctx, _body, _paint| {},
    );
    paint.extend(modal_response.paint);
    let dialog_buttons = [
        DialogButton::primary(id("dialog_ok"), GALLERY_LABEL),
        DialogButton::secondary(id("dialog_cancel"), GALLERY_LABEL),
    ];
    let (dialog_response, ()) = show_dialog(
        ctx,
        Dialog::new(
            id("dialog"),
            rect(700.0, 410.0, 600.0, 400.0),
            LayoutSize::new(LayoutPx::new(360.0), LayoutPx::new(200.0)),
            GALLERY_LABEL,
            &dialog_buttons,
        ),
        |_ctx, _body, _paint| {},
    );
    paint.extend(dialog_response.paint);
    let response = show_confirmation(
        ctx,
        ConfirmationDialog {
            id: id("confirmation"),
            viewport: rect(700.0, 820.0, 600.0, 400.0),
            size: LayoutSize::new(LayoutPx::new(360.0), LayoutPx::new(180.0)),
            title: GALLERY_LABEL,
            message: GALLERY_LABEL,
            confirm_label: GALLERY_LABEL,
            cancel_label: GALLERY_LABEL,
            destructive: false,
        },
    );
    paint.extend(response.paint);
    let picker_entries = [FilePickerEntry {
        id: id("picker_entry"),
        label: LabelText::Key(GALLERY_LABEL),
    }];
    let response = show_file_picker(
        ctx,
        FilePickerDialog::new(
            id("file_picker"),
            rect(700.0, 1240.0, 600.0, 500.0),
            FilePickerMode::Save,
            GALLERY_LABEL,
            &picker_entries,
            GALLERY_LABEL,
            &mut state.file_picker,
        ),
    );
    paint.extend(response.paint);
    let open_entries = [FilePickerEntry {
        id: id("picker_entry_open"),
        label: LabelText::Key(GALLERY_LABEL),
    }];
    let response = show_file_picker(
        ctx,
        FilePickerDialog::new(
            id("file_picker_open"),
            rect(700.0, 1820.0, 600.0, 500.0),
            FilePickerMode::Open,
            GALLERY_LABEL,
            &open_entries,
            GALLERY_LABEL,
            &mut state.file_picker_open,
        ),
    );
    paint.extend(response.paint);
}
