use serde::{Deserialize, Serialize};

use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::{Interaction, Sense};
use crate::layout::LayoutRect;
use crate::strings::StringKey;
use crate::theme::{
    Border, Color, Radius, Step12, StrokeWidth, SurfaceLevel, Theme, TypographyRole,
};
use crate::widget_id::WidgetId;

use super::keys::take_activation;
use super::paint::{ButtonPaintKind, WidgetPaint};
use super::visuals::{SurfaceVisuals, TextVisuals, push_focus_ring};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ButtonVariant {
    Primary,
    Secondary,
    Ghost,
    IconOnly,
    Destructive,
}

impl ButtonVariant {
    #[must_use]
    pub const fn paint_kind(self) -> ButtonPaintKind {
        match self {
            Self::Primary => ButtonPaintKind::Filled,
            Self::Secondary => ButtonPaintKind::Outlined,
            Self::Ghost => ButtonPaintKind::Ghost,
            Self::IconOnly => ButtonPaintKind::IconOnly,
            Self::Destructive => ButtonPaintKind::Danger,
        }
    }
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum ButtonState {
    Idle,
    Loading,
    Disabled,
}

impl ButtonState {
    #[must_use]
    pub const fn is_interactive(self) -> bool {
        matches!(self, Self::Idle)
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Button {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub variant: ButtonVariant,
    pub state: ButtonState,
}

impl Button {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        variant: ButtonVariant,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            variant,
            state: ButtonState::Idle,
        }
    }

    #[must_use]
    pub const fn with_state(self, state: ButtonState) -> Self {
        Self { state, ..self }
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct ButtonResponse {
    pub interaction: Interaction,
    pub activated: bool,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_button(ctx: &mut FrameCtx<'_>, button: Button) -> ButtonResponse {
    let interactive = button.state.is_interactive();
    let interaction = ctx.interact(
        InteractDeclaration::new(button.id, button.rect, Sense::INTERACTIVE)
            .focusable(interactive)
            .disabled(!interactive)
            .a11y(
                AccessNode::new(Role::Button)
                    .with_label(button.label)
                    .with_disabled(!interactive),
            ),
    );
    let live_focused = ctx.is_focused(button.id);
    let activated_via_pointer = interactive && interaction.click();
    let activated_via_key = interactive && live_focused && take_activation(ctx.input);
    let visuals = button_visuals(ctx.theme(), button.variant, button.state, interaction);
    let paint = build_paint(ctx, &button, &visuals, live_focused);
    ButtonResponse {
        interaction,
        activated: activated_via_pointer || activated_via_key,
        paint,
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ButtonVisuals {
    pub surface: SurfaceVisuals,
    pub text: TextVisuals,
    pub kind: ButtonPaintKind,
}

#[must_use]
pub fn button_visuals(
    theme: &Theme,
    variant: ButtonVariant,
    state: ButtonState,
    interaction: Interaction,
) -> ButtonVisuals {
    let radius = theme.radius.sm;
    let surface = surface_for(theme, variant, state, interaction, radius);
    let text = text_for(theme, variant, state, surface.fill);
    ButtonVisuals {
        surface,
        text,
        kind: variant.paint_kind(),
    }
}

fn surface_for(
    theme: &Theme,
    variant: ButtonVariant,
    state: ButtonState,
    interaction: Interaction,
    radius: Radius,
) -> SurfaceVisuals {
    let neutral = theme.colors.neutral;
    let accent = theme.colors.accent;
    let danger = theme.colors.danger;
    let disabled = matches!(state, ButtonState::Disabled);
    let pressed = interaction.pressed();
    let hovered = interaction.hover();
    let (fill, border) = match variant {
        ButtonVariant::Primary => {
            let base = if disabled {
                neutral.step(Step12::SUBTLE_BG)
            } else if pressed || hovered {
                accent.step(Step12::HOVER_SOLID)
            } else {
                accent.step(Step12::SOLID)
            };
            (base, None)
        }
        ButtonVariant::Secondary | ButtonVariant::IconOnly => {
            let base = if disabled {
                neutral.step(Step12::APP_BG)
            } else if pressed {
                neutral.step(Step12::SELECTED_BG)
            } else if hovered {
                neutral.step(Step12::HOVER_BG)
            } else {
                neutral.step(Step12::ELEMENT_BG)
            };
            let border = Border {
                width: StrokeWidth::HAIRLINE,
                color: neutral.step(if hovered {
                    Step12::HOVER_BORDER
                } else {
                    Step12::BORDER
                }),
            };
            (base, Some(border))
        }
        ButtonVariant::Ghost => {
            let base = if pressed && !disabled {
                neutral.step(Step12::SELECTED_BG)
            } else if hovered && !disabled {
                neutral.step(Step12::HOVER_BG)
            } else {
                Color::TRANSPARENT
            };
            (base, None)
        }
        ButtonVariant::Destructive => {
            let base = if disabled {
                neutral.step(Step12::SUBTLE_BG)
            } else if pressed || hovered {
                danger.step(Step12::HOVER_SOLID)
            } else {
                danger.step(Step12::SOLID)
            };
            (base, None)
        }
    };
    SurfaceVisuals {
        fill,
        border,
        radius,
        elevation: None,
    }
}

fn text_for(
    theme: &Theme,
    variant: ButtonVariant,
    state: ButtonState,
    surface_fill: Color,
) -> TextVisuals {
    let role: TypographyRole = theme.typography.label;
    let color = if matches!(state, ButtonState::Disabled) {
        theme.colors.text_disabled()
    } else {
        match variant {
            ButtonVariant::Primary | ButtonVariant::Destructive => {
                theme.colors.contrast_text(surface_fill)
            }
            ButtonVariant::Secondary | ButtonVariant::Ghost | ButtonVariant::IconOnly => {
                theme.colors.text_primary()
            }
        }
    };
    TextVisuals { color, role }
}

fn build_paint(
    ctx: &FrameCtx<'_>,
    button: &Button,
    visuals: &ButtonVisuals,
    live_focused: bool,
) -> Vec<WidgetPaint> {
    let mut paint = vec![
        WidgetPaint::Surface {
            rect: button.rect,
            fill: visuals.surface.fill,
            border: visuals.surface.border,
            radius: visuals.surface.radius,
            elevation: visuals.surface.elevation,
        },
        WidgetPaint::Label {
            rect: button.rect,
            text: super::paint::LabelText::Key(button.label),
            color: visuals.text.color,
            role: visuals.text.role,
        },
    ];
    if matches!(button.state, ButtonState::Loading) {
        paint.push(WidgetPaint::Mark {
            rect: button.rect,
            kind: super::paint::GlyphMark::Spinner,
            color: ctx.theme().colors.surface(SurfaceLevel::L1),
        });
    }
    push_focus_ring(
        ctx,
        &mut paint,
        button.rect,
        visuals.surface.radius,
        live_focused,
    );
    paint
}

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

    use super::{Button, ButtonState, ButtonVariant, show_button};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState, resolve};
    use crate::hotkey::HotkeyTable;
    use crate::input::{
        FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey,
        PointerButton, PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::StringKey;
    use crate::strings::StringTable;
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

    const LABEL: StringKey = StringKey::new("button.label");

    fn rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(80.0), LayoutPx::new(28.0)),
        )
    }

    fn id(key: &'static str) -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new(key))
    }

    fn cycle_press_release(button: Button) -> (HitState, Vec<bool>) {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let mut state = HitState::new();
        let mut activations = Vec::new();

        let frame = |snap: &mut InputSnapshot,
                     focus: &mut FocusManager,
                     hits: &mut HitFrame,
                     state: &mut HitState,
                     activations: &mut Vec<bool>| {
            hits.clear();
            {
                let mut shaper = bone_text::Shaper::new();
                let mut a11y = crate::a11y::AccessTreeBuilder::new();
                let mut ctx = FrameCtx::new(
                    theme.clone(),
                    snap,
                    focus,
                    &table,
                    StringTable::empty(),
                    hits,
                    state,
                    &mut a11y,
                    &mut shaper,
                );
                let response = show_button(&mut ctx, button);
                activations.push(response.activated);
            }
            *state = resolve(state, hits, snap, focus.focused());
        };

        let mut press = InputSnapshot::idle(FrameInstant::ZERO);
        press.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(10.0),
            LayoutPx::new(10.0),
        )));
        press.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        frame(
            &mut press,
            &mut focus,
            &mut hits,
            &mut state,
            &mut activations,
        );

        let mut release = InputSnapshot::idle(FrameInstant::ZERO);
        release.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(15.0),
            LayoutPx::new(15.0),
        )));
        release.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        frame(
            &mut release,
            &mut focus,
            &mut hits,
            &mut state,
            &mut activations,
        );

        let mut idle = InputSnapshot::idle(FrameInstant::ZERO);
        idle.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(15.0),
            LayoutPx::new(15.0),
        )));
        frame(
            &mut idle,
            &mut focus,
            &mut hits,
            &mut state,
            &mut activations,
        );

        (state, activations)
    }

    #[test]
    fn primary_button_click_sets_activated() {
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let (_, activations) = cycle_press_release(button);
        assert_eq!(activations, vec![false, false, true]);
    }

    #[test]
    fn disabled_button_never_activates() {
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary)
            .with_state(ButtonState::Disabled);
        let (state, activations) = cycle_press_release(button);
        assert!(activations.iter().all(|a| !a));
        assert!(state.interaction(button.id).disabled());
        assert!(!state.interaction(button.id).click());
    }

    fn focused_input_with(events: Vec<KeyEvent>) -> (InputSnapshot, FocusManager) {
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed = events;
        let mut focus = FocusManager::new();
        focus.register_focusable(id("ok"));
        focus.request_focus(id("ok"));
        focus.end_frame();
        (input, focus)
    }

    #[test]
    fn enter_key_on_focused_button_activates() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let (mut input, mut focus) = focused_input_with(vec![KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        )]);
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert!(response.activated);
        assert!(input.keys_pressed.is_empty(), "consumed Enter");
    }

    #[test]
    fn space_key_on_focused_button_activates() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let (mut input, mut focus) = focused_input_with(vec![KeyEvent::new(
            KeyCode::Named(NamedKey::Space),
            ModifierMask::NONE,
        )]);
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert!(response.activated);
    }

    #[test]
    fn unfocused_button_does_not_consume_keys() {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let event = KeyEvent::new(KeyCode::Named(NamedKey::Enter), ModifierMask::NONE);
        input.keys_pressed.push(event);
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert!(!response.activated);
        assert_eq!(input.keys_pressed, vec![event]);
    }

    #[test]
    fn loading_button_blocks_activation_and_keys() {
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary)
            .with_state(ButtonState::Loading);
        let (_, activations) = cycle_press_release(button);
        assert!(!activations[0]);
        assert!(!activations[1]);
    }

    #[test]
    fn other_keys_pass_through() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let other = KeyEvent::new(KeyCode::Char(KeyChar::from_char('x')), ModifierMask::NONE);
        let (mut input, mut focus) = focused_input_with(vec![other]);
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let _ = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert_eq!(
            input.keys_pressed,
            vec![other],
            "non-activation keys remain"
        );
    }

    #[test]
    fn ghost_variant_is_transparent_when_idle() {
        let theme = Theme::light();
        let visuals = super::button_visuals(
            &theme,
            ButtonVariant::Ghost,
            ButtonState::Idle,
            crate::hit_test::Interaction::idle(),
        );
        assert_eq!(visuals.surface.fill, crate::theme::Color::TRANSPARENT);
    }

    #[test]
    fn destructive_variant_uses_danger_solid() {
        let theme = Theme::light();
        let visuals = super::button_visuals(
            &theme,
            ButtonVariant::Destructive,
            ButtonState::Idle,
            crate::hit_test::Interaction::idle(),
        );
        assert_eq!(visuals.surface.fill, theme.colors.danger_solid());
    }

    #[test]
    fn paint_includes_focus_ring_when_focused_via_keyboard() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let tab = KeyEvent::new(KeyCode::Named(NamedKey::Tab), ModifierMask::NONE);
        let (mut input, mut focus) = focused_input_with(vec![tab]);
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert!(
            response
                .paint
                .iter()
                .any(|p| matches!(p, super::WidgetPaint::FocusRing { .. })),
            "focused button paints focus ring",
        );
    }

    #[test]
    fn focus_ring_hidden_when_pointer_modality() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut focus = FocusManager::new();
        focus.register_focusable(id("ok"));
        focus.request_focus(id("ok"));
        focus.end_frame();
        let button = Button::new(id("ok"), rect(), LABEL, ButtonVariant::Primary);
        let response = {
            let mut shaper = bone_text::Shaper::new();
            let mut a11y = crate::a11y::AccessTreeBuilder::new();
            let mut ctx = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            show_button(&mut ctx, button)
        };
        assert!(
            !response
                .paint
                .iter()
                .any(|p| matches!(p, super::WidgetPaint::FocusRing { .. })),
            "pointer-modality focus must not draw ring",
        );
    }
}
