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

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

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

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

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

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

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

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

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

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

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

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

#[derive(Clone, Debug, PartialEq)]
pub struct ToolbarResponse {
    pub activated: Option<WidgetId>,
    pub visible_count: usize,
    pub 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,
    } = toolbar;
    let layout = layout_items(rect, items, item_size, item_gap, orientation);
    let visible_count = layout.len();
    ctx.a11y
        .push(id, rect, AccessNode::new(Role::Toolbar).with_label(label));
    let mut paint = Vec::new();
    let mut activated: Option<WidgetId> = None;
    items
        .iter()
        .take(visible_count)
        .zip(layout.iter())
        .for_each(|(item, item_rect)| {
            let result = draw_item(ctx, *item_rect, item);
            paint.extend(result.paint);
            if result.activated && activated.is_none() {
                activated = Some(item.id);
            }
        });
    ToolbarResponse {
        activated,
        visible_count,
        paint,
    }
}

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

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

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

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

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

fn layout_items(
    rect: LayoutRect,
    items: &[ToolbarItem],
    item_size: LayoutPx,
    gap: LayoutPx,
    orientation: ToolbarOrientation,
) -> Vec<LayoutRect> {
    let available = match orientation {
        ToolbarOrientation::Horizontal => rect.size.width.value(),
        ToolbarOrientation::Vertical => rect.size.height.value(),
    };
    let 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()
}

const FIT_EPSILON_PX: f32 = 0.5;

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

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

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

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

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

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

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