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

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

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum TabsOrientation {
    Top,
    Bottom,
    Side,
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Tab {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub disabled: bool,
    pub closable: bool,
    pub glyph: Option<GlyphMark>,
}

impl Tab {
    #[must_use]
    pub const fn new(id: WidgetId, rect: LayoutRect, label: StringKey) -> Self {
        Self {
            id,
            rect,
            label,
            disabled: false,
            closable: false,
            glyph: None,
        }
    }

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

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

    #[must_use]
    pub const fn with_glyph(self, glyph: GlyphMark) -> Self {
        Self {
            glyph: Some(glyph),
            ..self
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Tabs<'a> {
    pub id: WidgetId,
    pub orientation: TabsOrientation,
    pub label: StringKey,
    pub tabs: &'a [Tab],
    pub active: WidgetId,
}

impl<'a> Tabs<'a> {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        orientation: TabsOrientation,
        label: StringKey,
        tabs: &'a [Tab],
        active: WidgetId,
    ) -> Self {
        Self {
            id,
            orientation,
            label,
            tabs,
            active,
        }
    }
}

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

const CLOSE_BUTTON_PX: f32 = 14.0;
const CLOSE_BUTTON_GAP: f32 = 6.0;

#[must_use]
pub fn show_tabs(ctx: &mut FrameCtx<'_>, tabs: Tabs<'_>) -> TabsResponse {
    let Tabs {
        id: tabs_id,
        orientation,
        label,
        tabs: items,
        active,
    } = tabs;
    let active_present = items.iter().any(|t| t.id == active && !t.disabled);
    let tab_stop = items
        .iter()
        .find(|t| t.id == active && !t.disabled)
        .or_else(|| items.iter().find(|t| !t.disabled))
        .map(|t| t.id);
    if let Some(stop) = tab_stop {
        ctx.focus.register_tab_stop(stop);
    }
    if let Some(strip_rect) = items.iter().map(|t| t.rect).reduce(LayoutRect::union) {
        ctx.a11y.push(
            tabs_id,
            strip_rect,
            AccessNode::new(Role::TabList).with_label(label),
        );
    }
    let mut paint = Vec::new();
    let folded = items
        .iter()
        .map(|tab| {
            draw_tab(
                ctx,
                tabs_id,
                tab,
                tab.id == active && active_present,
                orientation,
            )
        })
        .fold(
            (None::<WidgetId>, None::<WidgetId>),
            |(activated, closed), per_tab| {
                let new_activated = activated.or(per_tab.activated);
                let new_closed = closed.or(per_tab.closed);
                paint.extend(per_tab.paint);
                (new_activated, new_closed)
            },
        );
    let in_strip_focus = ctx
        .focus
        .focused()
        .is_some_and(|f| items.iter().any(|t| t.id == f));
    let activated_via_keys = if in_strip_focus {
        handle_keyboard(ctx, items, orientation)
    } else {
        None
    };
    let activated = folded.0.or(activated_via_keys);
    TabsResponse {
        activated,
        closed: folded.1,
        paint,
    }
}

struct PerTab {
    activated: Option<WidgetId>,
    closed: Option<WidgetId>,
    paint: Vec<WidgetPaint>,
}

fn draw_tab(
    ctx: &mut FrameCtx<'_>,
    tabs_id: WidgetId,
    tab: &Tab,
    is_active: bool,
    orientation: TabsOrientation,
) -> PerTab {
    let interactive = !tab.disabled;
    let interaction = ctx.interact(
        InteractDeclaration::new(tab.id, tab.rect, Sense::INTERACTIVE)
            .focusable(false)
            .disabled(!interactive)
            .active(is_active)
            .a11y(
                AccessNode::new(Role::Tab)
                    .with_label(tab.label)
                    .with_disabled(!interactive)
                    .with_selected(is_active),
            ),
    );
    if interactive && interaction.click() {
        ctx.focus.request_focus(tab.id);
    }
    let live_focused = ctx.is_focused(tab.id);
    let mut paint = Vec::new();
    paint.extend(tab_surface_paint(
        ctx,
        tab.rect,
        is_active,
        interaction,
        orientation,
    ));
    let label_color = tab_label_color(ctx, is_active, tab.disabled);
    if let Some(glyph) = tab.glyph {
        paint.push(WidgetPaint::Mark {
            rect: label_rect(tab.rect, tab.closable),
            kind: glyph,
            color: label_color,
        });
    } else {
        paint.push(WidgetPaint::Label {
            rect: label_rect(tab.rect, tab.closable),
            text: LabelText::Key(tab.label),
            color: label_color,
            role: ctx.theme().typography.label,
        });
    }
    let mut closed = None;
    let close_id = tabs_id.child_indexed(WidgetKey::new("close"), tab_close_index(tab.id));
    if tab.closable {
        let close_rect = close_button_rect(tab.rect);
        let close_interaction = ctx.interact(
            InteractDeclaration::new(close_id, close_rect, Sense::INTERACTIVE)
                .focusable(false)
                .disabled(!interactive)
                .a11y(
                    AccessNode::new(Role::Button)
                        .with_label(StringKey::new("tabs.close"))
                        .with_disabled(!interactive),
                ),
        );
        if interactive && close_interaction.click() {
            closed = Some(tab.id);
        }
        paint.push(WidgetPaint::Surface {
            rect: close_rect,
            fill: if close_interaction.hover() && interactive {
                ctx.theme().colors.neutral.step(Step12::HOVER_BG)
            } else {
                Color::TRANSPARENT
            },
            border: None,
            radius: ctx.theme().radius.sm,
            elevation: None,
        });
        paint.push(WidgetPaint::Mark {
            rect: close_rect,
            kind: GlyphMark::Close,
            color: tab_label_color(ctx, is_active, tab.disabled),
        });
    }
    push_focus_ring(
        ctx,
        &mut paint,
        tab.rect,
        ctx.theme().radius.sm,
        live_focused,
    );
    let activated = (interactive && !is_active && interaction.click()).then_some(tab.id);
    PerTab {
        activated,
        closed,
        paint,
    }
}

fn tab_close_index(tab_id: WidgetId) -> u64 {
    tab_id.raw().get()
}

fn label_rect(tab: LayoutRect, closable: bool) -> LayoutRect {
    if !closable {
        return tab;
    }
    let trim = LayoutPx::new(CLOSE_BUTTON_PX + CLOSE_BUTTON_GAP);
    let width = (tab.size.width.value() - trim.value()).max(0.0);
    LayoutRect::new(
        tab.origin,
        LayoutSize::new(LayoutPx::new(width), tab.size.height),
    )
}

fn close_button_rect(tab: LayoutRect) -> LayoutRect {
    let pad = (tab.size.height.value() - CLOSE_BUTTON_PX).max(0.0) / 2.0;
    let x = tab.origin.x.value() + tab.size.width.value() - CLOSE_BUTTON_PX - pad;
    let y = tab.origin.y.value() + pad;
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(x), LayoutPx::new(y)),
        LayoutSize::new(
            LayoutPx::new(CLOSE_BUTTON_PX),
            LayoutPx::new(CLOSE_BUTTON_PX),
        ),
    )
}

fn tab_surface_paint(
    ctx: &FrameCtx<'_>,
    rect: LayoutRect,
    active: bool,
    interaction: Interaction,
    orientation: TabsOrientation,
) -> Vec<WidgetPaint> {
    let neutral = ctx.theme().colors.neutral;
    let accent = ctx.theme().colors.accent;
    let fill = if interaction.disabled() {
        Color::TRANSPARENT
    } else if interaction.pressed() {
        neutral.step(Step12::SELECTED_BG)
    } else if interaction.hover() {
        neutral.step(Step12::HOVER_BG)
    } else {
        Color::TRANSPARENT
    };
    let mut paints = vec![WidgetPaint::Surface {
        rect,
        fill,
        border: None,
        radius: ctx.theme().radius.none,
        elevation: None,
    }];
    if active {
        paints.push(WidgetPaint::Surface {
            rect: tab_underline_rect(rect, orientation),
            fill: accent.step(Step12::SOLID),
            border: None,
            radius: ctx.theme().radius.none,
            elevation: None,
        });
    }
    paints
}

const TAB_UNDERLINE_THICKNESS_PX: f32 = 2.0;

fn tab_underline_rect(rect: LayoutRect, orientation: TabsOrientation) -> LayoutRect {
    let thickness = LayoutPx::new(TAB_UNDERLINE_THICKNESS_PX);
    match orientation {
        TabsOrientation::Top => LayoutRect::new(
            LayoutPos::new(
                rect.origin.x,
                LayoutPx::new(rect.origin.y.value() + rect.size.height.value() - thickness.value()),
            ),
            LayoutSize::new(rect.size.width, thickness),
        ),
        TabsOrientation::Bottom => {
            LayoutRect::new(rect.origin, LayoutSize::new(rect.size.width, thickness))
        }
        TabsOrientation::Side => LayoutRect::new(
            LayoutPos::new(
                LayoutPx::new(rect.origin.x.value() + rect.size.width.value() - thickness.value()),
                rect.origin.y,
            ),
            LayoutSize::new(thickness, rect.size.height),
        ),
    }
}

fn tab_label_color(ctx: &FrameCtx<'_>, active: bool, disabled: bool) -> Color {
    if disabled {
        ctx.theme().colors.text_disabled()
    } else if active {
        ctx.theme().colors.text_primary()
    } else {
        ctx.theme().colors.text_secondary()
    }
}

fn handle_keyboard(
    ctx: &mut FrameCtx<'_>,
    items: &[Tab],
    orientation: TabsOrientation,
) -> Option<WidgetId> {
    let (prev, next) = match orientation {
        TabsOrientation::Top | TabsOrientation::Bottom => {
            (NamedKey::ArrowLeft, NamedKey::ArrowRight)
        }
        TabsOrientation::Side => (NamedKey::ArrowUp, NamedKey::ArrowDown),
    };
    let event = take_key(
        ctx.input,
        &[
            TakeKey::named(prev),
            TakeKey::named(next),
            TakeKey::named(NamedKey::Home),
            TakeKey::named(NamedKey::End),
            TakeKey::named(NamedKey::Enter),
            TakeKey::named(NamedKey::Space),
        ],
    )?;
    let focused = ctx.focus.focused()?;
    let current = items.iter().position(|t| t.id == focused)?;
    match event.code {
        KeyCode::Named(NamedKey::Enter | NamedKey::Space) => {
            items.get(current).filter(|t| !t.disabled).map(|t| t.id)
        }
        KeyCode::Named(key) => {
            let target = step_to(items, current, key, prev, next)?;
            ctx.focus.request_focus(items[target].id);
            None
        }
        KeyCode::Char(_) => None,
    }
}

fn step_to(
    items: &[Tab],
    current: usize,
    key: NamedKey,
    prev: NamedKey,
    next: NamedKey,
) -> Option<usize> {
    let len = items.len();
    if len == 0 {
        return None;
    }
    let candidates: Vec<usize> = if key == prev {
        (1..=len)
            .map(|delta| (current + len - delta) % len)
            .collect()
    } else if key == next {
        (1..=len).map(|delta| (current + delta) % len).collect()
    } else if matches!(key, NamedKey::Home) {
        (0..len).collect()
    } else if matches!(key, NamedKey::End) {
        (0..len).rev().collect()
    } else {
        return None;
    };
    candidates.into_iter().find(|&idx| !items[idx].disabled)
}

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

    use super::{Tab, Tabs, TabsOrientation, show_tabs};
    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 tabs_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("tabs"))
    }

    fn make_tabs() -> Vec<Tab> {
        let label_keys = ["tabs.first", "tabs.second", "tabs.third"];
        label_keys
            .iter()
            .enumerate()
            .map(|(idx, key)| {
                #[allow(clippy::cast_precision_loss, reason = "small index fits f32 mantissa")]
                let i_f32 = idx as f32;
                let id = tabs_id().child_indexed(WidgetKey::new("t"), idx as u64);
                let rect = LayoutRect::new(
                    LayoutPos::new(LayoutPx::new(i_f32 * 80.0), LayoutPx::ZERO),
                    LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(28.0)),
                );
                Tab::new(id, rect, StringKey::new(key)).closable(true)
            })
            .collect()
    }

    fn render_with(
        items: &[Tab],
        active: WidgetId,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
        prev: &HitState,
    ) -> (super::TabsResponse, 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_tabs(
                &mut ctx,
                Tabs::new(
                    tabs_id(),
                    TabsOrientation::Top,
                    StringKey::new("test.tabs"),
                    items,
                    active,
                ),
            )
        };
        let next_state = resolve(prev, &hits, snap, focus.focused());
        (response, next_state)
    }

    fn pointer_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 pointer_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 click_at(
        items: &[Tab],
        active: WidgetId,
        focus: &mut FocusManager,
        prev: &mut HitState,
        pos: LayoutPos,
    ) -> super::TabsResponse {
        let mut last_response: Option<super::TabsResponse> = None;
        [pointer_press(pos), pointer_release(pos), pointer_idle(pos)]
            .into_iter()
            .for_each(|mut snap| {
                let (response, next) = render_with(items, active, focus, &mut snap, prev);
                last_response = Some(response);
                *prev = next;
            });
        let Some(response) = last_response else {
            panic!("three snapshots produced a response");
        };
        response
    }

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

    #[test]
    fn click_on_inactive_tab_activates_it() {
        let items = make_tabs();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let response = click_at(
            &items,
            items[0].id,
            &mut focus,
            &mut prev,
            LayoutPos::new(LayoutPx::new(100.0), LayoutPx::new(10.0)),
        );
        assert_eq!(response.activated, Some(items[1].id));
        assert!(response.closed.is_none());
    }

    #[test]
    fn click_on_close_button_emits_closed_not_activated() {
        let items = make_tabs();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let close_pos = LayoutPos::new(LayoutPx::new(160.0 + 64.0), LayoutPx::new(14.0));
        let response = click_at(&items, items[0].id, &mut focus, &mut prev, close_pos);
        assert_eq!(response.closed, Some(items[2].id));
        assert!(response.activated.is_none());
    }

    #[test]
    fn click_on_active_tab_does_not_re_activate() {
        let items = make_tabs();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let response = click_at(
            &items,
            items[1].id,
            &mut focus,
            &mut prev,
            LayoutPos::new(LayoutPx::new(100.0), LayoutPx::new(10.0)),
        );
        assert!(response.activated.is_none());
    }

    fn focused_setup(target: WidgetId) -> FocusManager {
        let mut focus = FocusManager::new();
        focus.register_focusable(target);
        focus.request_focus(target);
        focus.end_frame();
        focus
    }

    #[test]
    fn arrow_right_roves_focus_skipping_disabled() {
        let mut items = make_tabs();
        items[1] = items[1].disabled(true);
        let first_id = items[0].id;
        let third_id = items[2].id;
        let mut focus = focused_setup(first_id);
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::ArrowRight),
            ModifierMask::NONE,
        ));
        let _ = render_with(&items, first_id, &mut focus, &mut snap, &prev);
        assert_eq!(focus.focused(), Some(third_id));
    }

    #[test]
    fn enter_on_focused_tab_activates_it() {
        let items = make_tabs();
        let second_id = items[1].id;
        let mut focus = focused_setup(second_id);
        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_with(&items, items[0].id, &mut focus, &mut snap, &prev);
        assert_eq!(response.activated, Some(second_id));
    }

    #[test]
    fn arrow_wraps_at_end() {
        let items = make_tabs();
        let last_id = items[2].id;
        let first_id = items[0].id;
        let mut focus = focused_setup(last_id);
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::ArrowRight),
            ModifierMask::NONE,
        ));
        let _ = render_with(&items, items[0].id, &mut focus, &mut snap, &prev);
        assert_eq!(focus.focused(), Some(first_id));
    }

    #[test]
    fn home_jumps_to_first_enabled() {
        let mut items = make_tabs();
        items[0] = items[0].disabled(true);
        let last_id = items[2].id;
        let second_id = items[1].id;
        let mut focus = focused_setup(last_id);
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Home),
            ModifierMask::NONE,
        ));
        let _ = render_with(&items, last_id, &mut focus, &mut snap, &prev);
        assert_eq!(focus.focused(), Some(second_id));
    }

    #[test]
    fn click_on_inactive_tab_moves_focus_to_clicked_tab() {
        let items = make_tabs();
        let mut focus = FocusManager::new();
        let mut prev = HitState::new();
        let _ = click_at(
            &items,
            items[0].id,
            &mut focus,
            &mut prev,
            LayoutPos::new(LayoutPx::new(100.0), LayoutPx::new(10.0)),
        );
        assert_eq!(focus.focused(), Some(items[1].id));
    }

    #[test]
    fn only_active_tab_is_in_tab_order() {
        let items = make_tabs();
        let mut focus = FocusManager::new();
        let prev = HitState::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let _ = render_with(&items, items[1].id, &mut focus, &mut snap, &prev);
        let stops: Vec<WidgetId> = focus.tab_stops().iter().map(|(id, _)| *id).collect();
        assert_eq!(stops, vec![items[1].id]);
    }
}
