use serde::Serialize;

use crate::a11y::{AccessNode, Role, Toggled};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::{Interaction, Sense};
use crate::layout::LayoutRect;
use crate::strings::StringKey;
use crate::widget_id::WidgetId;

use super::keys::take_activation;
use super::paint::{GlyphMark, WidgetPaint};
use super::visuals::{Indicator, push_indicator};

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum CheckboxState {
    Unchecked,
    Checked,
    Indeterminate,
}

impl CheckboxState {
    #[must_use]
    pub const fn next(self) -> Self {
        match self {
            Self::Unchecked => Self::Checked,
            Self::Checked | Self::Indeterminate => Self::Unchecked,
        }
    }

    #[must_use]
    pub const fn is_active(self) -> bool {
        matches!(self, Self::Checked | Self::Indeterminate)
    }
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Checkbox {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub state: CheckboxState,
    pub disabled: bool,
}

impl Checkbox {
    #[must_use]
    pub const fn new(
        id: WidgetId,
        rect: LayoutRect,
        label: StringKey,
        state: CheckboxState,
    ) -> Self {
        Self {
            id,
            rect,
            label,
            state,
            disabled: false,
        }
    }

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

#[derive(Clone, Debug, PartialEq)]
pub struct CheckboxResponse {
    pub interaction: Interaction,
    pub state: CheckboxState,
    pub toggled: bool,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_checkbox(ctx: &mut FrameCtx<'_>, checkbox: Checkbox) -> CheckboxResponse {
    let interactive = !checkbox.disabled;
    let toggled_state = match checkbox.state {
        CheckboxState::Unchecked => Toggled::False,
        CheckboxState::Checked => Toggled::True,
        CheckboxState::Indeterminate => Toggled::Mixed,
    };
    let interaction = ctx.interact(
        InteractDeclaration::new(checkbox.id, checkbox.rect, Sense::INTERACTIVE)
            .focusable(interactive)
            .disabled(!interactive)
            .active(checkbox.state.is_active())
            .a11y(
                AccessNode::new(Role::CheckBox)
                    .with_label(checkbox.label)
                    .with_disabled(!interactive)
                    .with_toggled(toggled_state),
            ),
    );
    let live_focused = ctx.is_focused(checkbox.id);
    let toggled =
        interactive && (interaction.click() || (live_focused && take_activation(ctx.input)));
    let next_state = if toggled {
        checkbox.state.next()
    } else {
        checkbox.state
    };
    let paint = build_paint(ctx, &checkbox, interaction, live_focused, next_state);
    CheckboxResponse {
        interaction,
        state: next_state,
        toggled,
        paint,
    }
}

fn build_paint(
    ctx: &FrameCtx<'_>,
    checkbox: &Checkbox,
    interaction: Interaction,
    live_focused: bool,
    state: CheckboxState,
) -> Vec<WidgetPaint> {
    let mark = match state {
        CheckboxState::Checked => Some(GlyphMark::Checkmark),
        CheckboxState::Indeterminate => Some(GlyphMark::Indeterminate),
        CheckboxState::Unchecked => None,
    };
    let mut paint = Vec::new();
    push_indicator(
        ctx,
        &mut paint,
        Indicator {
            rect: checkbox.rect,
            label: checkbox.label,
            mark,
            active: state.is_active(),
            disabled: checkbox.disabled,
            radius: ctx.theme().radius.sm,
        },
        interaction,
        live_focused,
    );
    paint
}

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

    use super::{Checkbox, CheckboxState, show_checkbox};
    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;
    use crate::strings::StringTable;
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

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

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

    fn id_widget() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("checkbox"))
    }

    #[test]
    fn next_cycles_through_states() {
        assert_eq!(CheckboxState::Unchecked.next(), CheckboxState::Checked);
        assert_eq!(CheckboxState::Checked.next(), CheckboxState::Unchecked);
        assert_eq!(
            CheckboxState::Indeterminate.next(),
            CheckboxState::Unchecked
        );
    }

    #[test]
    fn space_key_when_focused_toggles() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Space),
            ModifierMask::NONE,
        ));
        let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked);
        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_checkbox(&mut ctx, checkbox)
        };
        assert!(response.toggled);
        assert_eq!(response.state, CheckboxState::Checked);
    }

    #[test]
    fn pointer_click_through_three_frames_flips_state() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let mut state = HitState::new();
        let mut current = CheckboxState::Unchecked;
        [press_snap(), release_snap(), idle_snap()]
            .into_iter()
            .for_each(|mut input| {
                hits.clear();
                let checkbox = Checkbox::new(id_widget(), rect(), LABEL, current);
                let response = {
                    let mut shaper = bone_text::Shaper::new();
                    let mut a11y = crate::a11y::AccessTreeBuilder::new();
                    let mut ctx = FrameCtx::new(
                        theme.clone(),
                        &mut input,
                        &mut focus,
                        &table,
                        StringTable::empty(),
                        &mut hits,
                        &state,
                        &mut a11y,
                        &mut shaper,
                    );
                    show_checkbox(&mut ctx, checkbox)
                };
                current = response.state;
                state = resolve(&state, &hits, &input, focus.focused());
            });
        assert_eq!(current, CheckboxState::Checked);
    }

    #[test]
    fn indeterminate_checkbox_paints_indeterminate_mark() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let checkbox = Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Indeterminate);
        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_checkbox(&mut ctx, checkbox)
        };
        let has_indeterminate = response.paint.iter().any(|p| {
            matches!(
                p,
                super::WidgetPaint::Mark {
                    kind: super::GlyphMark::Indeterminate,
                    ..
                }
            )
        });
        assert!(has_indeterminate);
    }

    #[test]
    fn disabled_checkbox_ignores_keys() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let state = HitState::new();
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let event = KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE);
        input.keys_pressed.push(event);
        let checkbox =
            Checkbox::new(id_widget(), rect(), LABEL, CheckboxState::Unchecked).disabled(true);
        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_checkbox(&mut ctx, checkbox)
        };
        assert!(!response.toggled);
        assert_eq!(input.keys_pressed, vec![event]);
    }

    fn press_snap() -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(10.0),
            LayoutPx::new(10.0),
        )));
        s.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn release_snap() -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(12.0),
            LayoutPx::new(12.0),
        )));
        s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn idle_snap() -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(12.0),
            LayoutPx::new(12.0),
        )));
        s
    }
}
