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

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

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

#[derive(Clone, Debug, PartialEq)]
pub struct RadioOption<T: Copy + PartialEq> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub label: StringKey,
    pub value: T,
}

#[derive(Clone, Debug, PartialEq)]
pub struct RadioGroup<T: Copy + PartialEq> {
    pub options: Vec<RadioOption<T>>,
    pub selected: T,
    pub orientation: RadioOrientation,
    pub disabled: bool,
}

impl<T: Copy + PartialEq> RadioGroup<T> {
    #[must_use]
    pub fn new(options: Vec<RadioOption<T>>, selected: T) -> Self {
        Self {
            options,
            selected,
            orientation: RadioOrientation::Vertical,
            disabled: false,
        }
    }

    #[must_use]
    pub fn orientation(self, orientation: RadioOrientation) -> Self {
        Self {
            orientation,
            ..self
        }
    }

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

#[derive(Clone, Debug, PartialEq)]
pub struct RadioGroupResponse<T: Copy + PartialEq> {
    pub selected: T,
    pub changed: bool,
    pub paint: Vec<WidgetPaint>,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq)]
enum NavAction {
    Prev,
    Next,
    First,
    Last,
}

#[must_use]
pub fn show_radio_group<T: Copy + PartialEq>(
    ctx: &mut FrameCtx<'_>,
    group: RadioGroup<T>,
) -> RadioGroupResponse<T> {
    let RadioGroup {
        options,
        selected: initial_selected,
        orientation,
        disabled,
    } = group;
    if !disabled
        && let Some(tab_stop) = options
            .iter()
            .find(|o| o.value == initial_selected)
            .or_else(|| options.first())
            .map(|o| o.id)
    {
        ctx.focus.register_tab_stop(tab_stop);
    }
    let mut paint = Vec::new();
    let mut clicked: Option<T> = None;
    options.iter().for_each(|option| {
        let active = option.value == initial_selected;
        let interaction = ctx.interact(
            InteractDeclaration::new(option.id, option.rect, Sense::INTERACTIVE)
                .focusable(false)
                .disabled(disabled)
                .active(active)
                .a11y(
                    AccessNode::new(Role::RadioButton)
                        .with_label(option.label)
                        .with_disabled(disabled)
                        .with_selected(active),
                ),
        );
        if !disabled && interaction.click() {
            clicked = Some(option.value);
            ctx.focus.request_focus(option.id);
        }
        let live_focused = ctx.is_focused(option.id);
        push_indicator(
            ctx,
            &mut paint,
            Indicator {
                rect: option.rect,
                label: option.label,
                mark: active.then_some(IndicatorMark::Glyph(GlyphMark::RadioDot)),
                active,
                disabled,
                radius: ctx.theme().radius.pill,
            },
            interaction,
            live_focused,
        );
    });
    let in_group_focus = ctx
        .focus
        .focused()
        .is_some_and(|id| options.iter().any(|o| o.id == id));
    if !disabled && in_group_focus {
        if let Some(action) = take_navigation(ctx, orientation) {
            let len = options.len();
            let current = options
                .iter()
                .position(|o| Some(o.id) == ctx.focus.focused())
                .unwrap_or(0);
            let next = navigate(action, current, len);
            if next != current
                && let Some(target) = options.get(next)
            {
                ctx.focus.request_focus(target.id);
            }
        }
        if take_activation(ctx.input)
            && let Some(focused_id) = ctx.focus.focused()
            && let Some(option) = options.iter().find(|o| o.id == focused_id)
        {
            clicked = Some(option.value);
        }
    }
    let mut selected = initial_selected;
    let changed = if let Some(value) = clicked {
        selected = value;
        value != initial_selected
    } else {
        false
    };
    RadioGroupResponse {
        selected,
        changed,
        paint,
    }
}

fn take_navigation(ctx: &mut FrameCtx<'_>, orientation: RadioOrientation) -> Option<NavAction> {
    let (prev, next) = match orientation {
        RadioOrientation::Vertical => (NamedKey::ArrowUp, NamedKey::ArrowDown),
        RadioOrientation::Horizontal => (NamedKey::ArrowLeft, NamedKey::ArrowRight),
    };
    let event = take_key(
        ctx.input,
        &[
            TakeKey::named(prev),
            TakeKey::named(next),
            TakeKey::named(NamedKey::Home),
            TakeKey::named(NamedKey::End),
        ],
    )?;
    Some(match event.code {
        KeyCode::Named(key) if key == prev => NavAction::Prev,
        KeyCode::Named(key) if key == next => NavAction::Next,
        KeyCode::Named(NamedKey::Home) => NavAction::First,
        KeyCode::Named(NamedKey::End) => NavAction::Last,
        _ => unreachable!("take_key only returns the listed candidates"),
    })
}

fn navigate(action: NavAction, current: usize, len: usize) -> usize {
    if len == 0 {
        return 0;
    }
    match action {
        NavAction::Next => (current + 1) % len,
        NavAction::Prev => (current + len - 1) % len,
        NavAction::First => 0,
        NavAction::Last => len - 1,
    }
}

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

    use super::{RadioGroup, RadioOption, RadioOrientation, show_radio_group};
    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};

    #[derive(Copy, Clone, Debug, PartialEq, Eq)]
    enum Pick {
        A,
        B,
        C,
    }

    fn group_id() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("group"))
    }

    fn opt_rect(index: f32) -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::new(index * 80.0), LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(72.0), LayoutPx::new(28.0)),
        )
    }

    fn three_options() -> Vec<RadioOption<Pick>> {
        vec![
            RadioOption {
                id: group_id().child(WidgetKey::new("a")),
                rect: opt_rect(0.0),
                label: StringKey::new("opt.a"),
                value: Pick::A,
            },
            RadioOption {
                id: group_id().child(WidgetKey::new("b")),
                rect: opt_rect(1.0),
                label: StringKey::new("opt.b"),
                value: Pick::B,
            },
            RadioOption {
                id: group_id().child(WidgetKey::new("c")),
                rect: opt_rect(2.0),
                label: StringKey::new("opt.c"),
                value: Pick::C,
            },
        ]
    }

    fn at(option: usize) -> LayoutPos {
        #[allow(clippy::cast_precision_loss, reason = "test option index < 16")]
        let option_f32 = option as f32;
        LayoutPos::new(LayoutPx::new(option_f32 * 80.0 + 10.0), LayoutPx::new(10.0))
    }

    fn press_at(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_at(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_at(pos: LayoutPos) -> InputSnapshot {
        let mut s = InputSnapshot::idle(FrameInstant::ZERO);
        s.pointer = Some(PointerSample::new(pos));
        s
    }

    fn run_pick(target: usize) -> Pick {
        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 selected = Pick::A;
        [
            press_at(at(target)),
            release_at(at(target)),
            idle_at(at(target)),
        ]
        .into_iter()
        .for_each(|mut input| {
            hits.clear();
            let group = RadioGroup::new(three_options(), selected);
            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_radio_group(&mut ctx, group)
            };
            selected = response.selected;
            state = resolve(&state, &hits, &input, focus.focused());
        });
        selected
    }

    #[test]
    fn click_picks_target_option() {
        assert_eq!(run_pick(1), Pick::B);
        assert_eq!(run_pick(2), Pick::C);
    }

    fn run_with_focus_and_keys(
        options: Vec<RadioOption<Pick>>,
        selected: Pick,
        orientation: RadioOrientation,
        focus_target: WidgetId,
        events: Vec<KeyEvent>,
    ) -> (super::RadioGroupResponse<Pick>, FocusManager) {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        focus.register_focusable(focus_target);
        focus.request_focus(focus_target);
        focus.end_frame();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed = events;
        let group = RadioGroup::new(options, selected).orientation(orientation);
        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,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            show_radio_group(&mut ctx, group)
        };
        (response, focus)
    }

    #[test]
    fn vertical_arrow_down_roves_focus_within_group() {
        let options = three_options();
        let first_id = options[0].id;
        let second_id = options[1].id;
        let (response, focus) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Vertical,
            first_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::ArrowDown),
                ModifierMask::NONE,
            )],
        );
        assert!(!response.changed);
        assert_eq!(focus.focused(), Some(second_id));
    }

    #[test]
    fn horizontal_arrow_right_roves_focus() {
        let options = three_options();
        let first_id = options[0].id;
        let second_id = options[1].id;
        let (_, focus) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Horizontal,
            first_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::ArrowRight),
                ModifierMask::NONE,
            )],
        );
        assert_eq!(focus.focused(), Some(second_id));
    }

    #[test]
    fn vertical_ignores_horizontal_arrow() {
        let options = three_options();
        let first_id = options[0].id;
        let arrow = KeyEvent::new(KeyCode::Named(NamedKey::ArrowRight), ModifierMask::NONE);
        let (_, focus) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Vertical,
            first_id,
            vec![arrow],
        );
        assert_eq!(
            focus.focused(),
            Some(first_id),
            "vertical orientation ignores ArrowRight"
        );
    }

    #[test]
    fn horizontal_ignores_vertical_arrow() {
        let options = three_options();
        let first_id = options[0].id;
        let (_, focus) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Horizontal,
            first_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::ArrowDown),
                ModifierMask::NONE,
            )],
        );
        assert_eq!(focus.focused(), Some(first_id));
    }

    #[test]
    fn home_jumps_to_first_option() {
        let options = three_options();
        let third_id = options[2].id;
        let first_id = options[0].id;
        let (_, focus) = run_with_focus_and_keys(
            options,
            Pick::C,
            RadioOrientation::Vertical,
            third_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::Home),
                ModifierMask::NONE,
            )],
        );
        assert_eq!(focus.focused(), Some(first_id));
    }

    #[test]
    fn end_jumps_to_last_option() {
        let options = three_options();
        let first_id = options[0].id;
        let third_id = options[2].id;
        let (_, focus) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Vertical,
            first_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::End),
                ModifierMask::NONE,
            )],
        );
        assert_eq!(focus.focused(), Some(third_id));
    }

    #[test]
    fn space_picks_focused_option() {
        let options = three_options();
        let second_id = options[1].id;
        let (response, _) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Vertical,
            second_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::Space),
                ModifierMask::NONE,
            )],
        );
        assert!(response.changed);
        assert_eq!(response.selected, Pick::B);
    }

    #[test]
    fn enter_picks_focused_option() {
        let options = three_options();
        let second_id = options[1].id;
        let (response, _) = run_with_focus_and_keys(
            options,
            Pick::A,
            RadioOrientation::Vertical,
            second_id,
            vec![KeyEvent::new(
                KeyCode::Named(NamedKey::Enter),
                ModifierMask::NONE,
            )],
        );
        assert!(response.changed);
        assert_eq!(response.selected, Pick::B);
    }

    #[test]
    fn selected_outside_options_falls_back_to_first_tab_stop() {
        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 two = vec![three_options()[0].clone(), three_options()[1].clone()];
        let first_id = two[0].id;
        let group = RadioGroup::new(two, Pick::C);
        {
            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,
            );
            let _ = show_radio_group(&mut ctx, group);
        }
        assert_eq!(
            focus.tab_stops().iter().map(|(id, _)| *id).next(),
            Some(first_id),
            "selection not in options still leaves the group Tab-reachable",
        );
    }

    #[test]
    fn selected_option_is_only_parent_tab_stop() {
        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 options = three_options();
        let group = RadioGroup::new(options.clone(), Pick::B);
        {
            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,
            );
            let _ = show_radio_group(&mut ctx, group);
        }
        let stop_ids: Vec<WidgetId> = focus.tab_stops().iter().map(|(id, _)| *id).collect();
        assert_eq!(
            stop_ids,
            vec![options[1].id],
            "only selected option enters tab order"
        );
    }

    #[test]
    fn keys_pass_through_when_focus_outside_group() {
        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 arrow = KeyEvent::new(KeyCode::Named(NamedKey::ArrowDown), ModifierMask::NONE);
        let space = KeyEvent::new(KeyCode::Named(NamedKey::Space), ModifierMask::NONE);
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed = vec![arrow, space];
        let group = RadioGroup::new(three_options(), Pick::A);
        {
            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,
            );
            let _ = show_radio_group(&mut ctx, group);
        }
        assert_eq!(
            input.keys_pressed,
            vec![arrow, space],
            "no in-group focus, keys preserved"
        );
    }

    #[test]
    fn disabled_group_does_not_change_selection_via_pointer() {
        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 selected = Pick::A;
        [press_at(at(1)), release_at(at(1)), idle_at(at(1))]
            .into_iter()
            .for_each(|mut input| {
                hits.clear();
                let group = RadioGroup::new(three_options(), selected).disabled(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_radio_group(&mut ctx, group)
                };
                selected = response.selected;
                state = resolve(&state, &hits, &input, focus.focused());
            });
        assert_eq!(selected, Pick::A);
    }
}
