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::button::ButtonState;
use super::keys::take_activation;
use super::paint::WidgetPaint;
use super::visuals::{Indicator, push_indicator};

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

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

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

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

#[must_use]
pub fn show_toggle_button(ctx: &mut FrameCtx<'_>, toggle: ToggleButton) -> ToggleButtonResponse {
    let interactive = toggle.state.is_interactive();
    let interaction = ctx.interact(
        InteractDeclaration::new(toggle.id, toggle.rect, Sense::INTERACTIVE)
            .focusable(interactive)
            .disabled(!interactive)
            .active(toggle.on)
            .a11y(
                AccessNode::new(Role::Switch)
                    .with_label(toggle.label)
                    .with_disabled(!interactive)
                    .with_toggled(if toggle.on {
                        Toggled::True
                    } else {
                        Toggled::False
                    }),
            ),
    );
    let live_focused = ctx.is_focused(toggle.id);
    let toggled =
        interactive && (interaction.click() || (live_focused && take_activation(ctx.input)));
    let next_on = if toggled { !toggle.on } else { toggle.on };
    let disabled = matches!(toggle.state, ButtonState::Disabled);
    let mut paint = Vec::new();
    push_indicator(
        ctx,
        &mut paint,
        Indicator {
            rect: toggle.rect,
            label: toggle.label,
            mark: None,
            active: next_on,
            disabled,
            radius: ctx.theme().radius.sm,
        },
        interaction,
        live_focused,
    );
    ToggleButtonResponse {
        interaction,
        on: next_on,
        toggled,
        paint,
    }
}

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

    use super::{ToggleButton, show_toggle_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, 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("toggle.snap_to_grid");

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

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

    fn cycle_pointer_click(start_on: bool) -> 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 on = start_on;
        let mut history = Vec::new();
        let step = |snap: &mut InputSnapshot,
                    focus: &mut FocusManager,
                    hits: &mut HitFrame,
                    state: &mut HitState,
                    on: &mut bool,
                    history: &mut Vec<bool>| {
            hits.clear();
            let toggle = ToggleButton::new(id_widget(), rect(), LABEL, *on);
            let response = {
                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,
                );
                show_toggle_button(&mut ctx, toggle)
            };
            *on = response.on;
            history.push(*on);
            *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);
        step(
            &mut press,
            &mut focus,
            &mut hits,
            &mut state,
            &mut on,
            &mut history,
        );

        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);
        step(
            &mut release,
            &mut focus,
            &mut hits,
            &mut state,
            &mut on,
            &mut history,
        );

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

    #[test]
    fn pointer_click_flips_off_to_on() {
        let history = cycle_pointer_click(false);
        assert_eq!(history, vec![false, false, true]);
    }

    #[test]
    fn pointer_click_flips_on_to_off() {
        let history = cycle_pointer_click(true);
        assert_eq!(history, vec![true, true, false]);
    }

    #[test]
    fn space_key_toggles_when_focused() {
        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 toggle = ToggleButton::new(id_widget(), rect(), LABEL, false);
        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_toggle_button(&mut ctx, toggle)
        };
        assert!(response.toggled);
        assert!(response.on);
    }

    #[test]
    fn disabled_toggle_does_not_flip() {
        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 toggle = ToggleButton::new(id_widget(), rect(), LABEL, false)
            .with_state(super::ButtonState::Disabled);
        let history: Vec<bool> = [
            press_snap(),
            release_snap(),
            InputSnapshot::idle(FrameInstant::ZERO),
        ]
        .into_iter()
        .map(|mut snap| {
            hits.clear();
            let response = {
                let mut shaper = bone_text::Shaper::new();
                let mut a11y = crate::a11y::AccessTreeBuilder::new();
                let mut ctx = FrameCtx::new(
                    theme.clone(),
                    &mut snap,
                    &mut focus,
                    &table,
                    StringTable::empty(),
                    &mut hits,
                    &state,
                    &mut a11y,
                    &mut shaper,
                );
                show_toggle_button(&mut ctx, toggle)
            };
            state = resolve(&state, &hits, &snap, focus.focused());
            response.on
        })
        .collect();
        assert!(history.iter().all(|on| !on), "disabled never flips");
    }

    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(15.0),
            LayoutPx::new(15.0),
        )));
        s.buttons_released = PointerButtonMask::just(PointerButton::Primary);
        s
    }

    fn surface_fill(paint: &[super::WidgetPaint]) -> Option<crate::theme::Color> {
        paint.iter().find_map(|p| match p {
            super::WidgetPaint::Surface { fill, .. } => Some(*fill),
            _ => None,
        })
    }

    fn focused_at_widget() -> FocusManager {
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();
        focus
    }

    #[test]
    fn paint_reflects_next_on_not_initial() {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let prelit = {
            let mut focus = focused_at_widget();
            let mut hits = HitFrame::new();
            let state = HitState::new();
            let mut input = InputSnapshot::idle(FrameInstant::ZERO);
            let toggle = ToggleButton::new(id_widget(), rect(), LABEL, true);
            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_toggle_button(&mut ctx, toggle)
            };
            surface_fill(&response.paint)
        };
        let toggled = {
            let mut focus = focused_at_widget();
            let mut hits = HitFrame::new();
            let state = HitState::new();
            let mut input = InputSnapshot::idle(FrameInstant::ZERO);
            input.keys_pressed.push(KeyEvent::new(
                KeyCode::Named(NamedKey::Space),
                ModifierMask::NONE,
            ));
            let toggle = ToggleButton::new(id_widget(), rect(), LABEL, false);
            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_toggle_button(&mut ctx, toggle)
            };
            assert!(response.on, "Space flipped toggle on");
            surface_fill(&response.paint)
        };
        assert_eq!(
            toggled, prelit,
            "activation frame paints active surface, not stale off-state",
        );
    }
}
