use crate::a11y::{AccessNode, Role};
use crate::frame::{FrameCtx, InteractDeclaration};
use crate::hit_test::{Interaction, Sense};
use crate::hotkey::KeyChord;
use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey};
use crate::layout::LayoutRect;
use crate::strings::StringKey;
use crate::theme::{Border, Step12, StrokeWidth};
use crate::widget_id::WidgetId;

use super::paint::{LabelText, WidgetPaint};
use super::visuals::push_focus_ring;

#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct HotkeyCaptureState {
    pub recording: bool,
    pub chord: Option<KeyChord>,
}

#[derive(Debug, PartialEq)]
pub struct HotkeyCapture<'state> {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub placeholder: StringKey,
    pub state: &'state mut HotkeyCaptureState,
    pub disabled: bool,
}

impl<'state> HotkeyCapture<'state> {
    #[must_use]
    pub fn new(
        id: WidgetId,
        rect: LayoutRect,
        placeholder: StringKey,
        state: &'state mut HotkeyCaptureState,
    ) -> Self {
        Self {
            id,
            rect,
            placeholder,
            state,
            disabled: false,
        }
    }

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

#[derive(Clone, Debug, PartialEq)]
pub struct HotkeyCaptureResponse {
    pub interaction: Interaction,
    pub captured: Option<KeyChord>,
    pub paint: Vec<WidgetPaint>,
}

#[must_use]
pub fn show_hotkey_capture(
    ctx: &mut FrameCtx<'_>,
    capture: HotkeyCapture<'_>,
) -> HotkeyCaptureResponse {
    let HotkeyCapture {
        id,
        rect,
        placeholder,
        state,
        disabled,
    } = capture;
    let interactive = !disabled;
    let interaction = ctx.interact(
        InteractDeclaration::new(id, rect, Sense::INTERACTIVE)
            .focusable(interactive)
            .disabled(!interactive)
            .active(state.recording)
            .a11y(
                AccessNode::new(Role::Button)
                    .with_label(placeholder)
                    .with_disabled(!interactive),
            ),
    );
    let click = interactive && interaction.click();
    let live_focused = ctx.is_focused(id);
    if click {
        state.recording = !state.recording;
    } else if interactive && !live_focused {
        state.recording = false;
    }
    let mut captured = None;
    if interactive && state.recording {
        let pending = core::mem::take(&mut ctx.input.keys_pressed);
        let unconsumed = pending.into_iter().fold(Vec::new(), |mut acc, event| {
            if !state.recording || !is_recordable(event) {
                acc.push(event);
                return acc;
            }
            if matches!(event.code, KeyCode::Named(NamedKey::Escape))
                && event.modifiers == ModifierMask::NONE
            {
                state.recording = false;
                return acc;
            }
            let chord = KeyChord::from(event);
            state.chord = Some(chord);
            state.recording = false;
            captured = Some(chord);
            acc
        });
        ctx.input.keys_pressed = unconsumed;
    }
    let paint = build_paint(
        ctx,
        rect,
        label_text(placeholder, state.chord, state.recording),
        state.recording,
        disabled,
        interaction,
        live_focused,
    );
    HotkeyCaptureResponse {
        interaction,
        captured,
        paint,
    }
}

fn label_text(placeholder: StringKey, chord: Option<KeyChord>, recording: bool) -> LabelText {
    if recording {
        return LabelText::Key(placeholder);
    }
    chord.map_or_else(
        || LabelText::Key(placeholder),
        |chord| LabelText::Owned(chord.to_string()),
    )
}

fn is_recordable(event: KeyEvent) -> bool {
    let plain_focus_key = event.modifiers == ModifierMask::NONE
        && matches!(
            event.code,
            KeyCode::Named(NamedKey::Tab | NamedKey::Enter | NamedKey::Space)
        );
    !plain_focus_key
}

fn build_paint(
    ctx: &FrameCtx<'_>,
    rect: LayoutRect,
    label: LabelText,
    recording: bool,
    disabled: bool,
    interaction: Interaction,
    live_focused: bool,
) -> Vec<WidgetPaint> {
    let neutral = ctx.theme().colors.neutral;
    let radius = ctx.theme().radius.sm;
    let hovered = interaction.hover();
    let fill = if disabled {
        neutral.step(Step12::SUBTLE_BG)
    } else if recording {
        ctx.theme().colors.accent.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 recording {
            Step12::HOVER_BORDER
        } else {
            Step12::BORDER
        }),
    };
    let mut paint = vec![
        WidgetPaint::Surface {
            rect,
            fill,
            border: Some(border),
            radius,
            elevation: None,
        },
        WidgetPaint::Label {
            rect,
            text: label,
            color: ctx.theme().colors.text_primary(),
            role: ctx.theme().typography.label,
        },
    ];
    push_focus_ring(ctx, &mut paint, rect, radius, live_focused);
    paint
}

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

    use super::{HotkeyCapture, HotkeyCaptureState, show_hotkey_capture};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState};
    use crate::hotkey::{HotkeyTable, KeyChord};
    use crate::input::{
        FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, NamedKey,
    };
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{StringKey, StringTable};
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};

    const PLACEHOLDER: StringKey = StringKey::new("hotkey.placeholder");

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

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

    fn run(
        state: &mut HotkeyCaptureState,
        focus: &mut FocusManager,
        snap: &mut InputSnapshot,
    ) -> super::HotkeyCaptureResponse {
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let widget = HotkeyCapture::new(id_widget(), rect(), PLACEHOLDER, state);
        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_hotkey_capture(&mut ctx, widget)
    }

    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 ctrl_s_records_chord() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Char(KeyChar::from_char('s')),
            ModifierMask::CTRL,
        ));
        let response = run(&mut state, &mut focus, &mut snap);
        let chord = KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        assert_eq!(response.captured, Some(chord));
        assert_eq!(state.chord, Some(chord));
        assert!(!state.recording, "recording stops after capture");
    }

    #[test]
    fn escape_cancels_recording_without_capture() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Escape),
            ModifierMask::NONE,
        ));
        let response = run(&mut state, &mut focus, &mut snap);
        assert!(response.captured.is_none());
        assert!(!state.recording);
    }

    #[test]
    fn plain_tab_passes_through_for_focus_traversal() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let tab = KeyEvent::new(KeyCode::Named(NamedKey::Tab), ModifierMask::NONE);
        snap.keys_pressed.push(tab);
        let response = run(&mut state, &mut focus, &mut snap);
        assert!(response.captured.is_none());
        assert_eq!(snap.keys_pressed, vec![tab], "Tab not consumed");
        assert!(state.recording, "still listening");
    }

    #[test]
    fn ctrl_tab_is_recordable() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Tab),
            ModifierMask::CTRL,
        ));
        let response = run(&mut state, &mut focus, &mut snap);
        let chord = KeyChord::new(KeyCode::Named(NamedKey::Tab), ModifierMask::CTRL);
        assert_eq!(response.captured, Some(chord));
    }

    #[test]
    fn losing_focus_stops_recording() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.end_frame();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let response = run(&mut state, &mut focus, &mut snap);
        assert!(response.captured.is_none());
        assert!(!state.recording);
    }

    #[test]
    fn shift_letter_records_with_modifier() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        snap.keys_pressed.push(KeyEvent::new(
            KeyCode::Char(KeyChar::from_char('q')),
            ModifierMask::SHIFT | ModifierMask::CTRL,
        ));
        let _ = run(&mut state, &mut focus, &mut snap);
        let chord = KeyChord::new(
            KeyCode::Char(KeyChar::from_char('q')),
            ModifierMask::SHIFT | ModifierMask::CTRL,
        );
        assert_eq!(state.chord, Some(chord));
    }

    #[test]
    fn captured_chord_paints_as_owned_label_text() {
        let chord = KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        let mut state = HotkeyCaptureState {
            recording: false,
            chord: Some(chord),
        };
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let response = run(&mut state, &mut focus, &mut snap);
        let owned = response.paint.iter().find_map(|p| match p {
            super::WidgetPaint::Label {
                text: super::LabelText::Owned(s),
                ..
            } => Some(s.clone()),
            _ => None,
        });
        assert_eq!(owned.as_deref(), Some("Ctrl+S"));
    }

    #[test]
    fn first_recordable_event_wins_when_multiple_arrive() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let ctrl_s = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        let ctrl_t = KeyEvent::new(KeyCode::Char(KeyChar::from_char('t')), ModifierMask::CTRL);
        snap.keys_pressed = vec![ctrl_s, ctrl_t];
        let response = run(&mut state, &mut focus, &mut snap);
        assert_eq!(
            response.captured,
            Some(KeyChord::from(ctrl_s)),
            "first recordable event captured",
        );
        assert_eq!(snap.keys_pressed, vec![ctrl_t], "later events preserved");
        assert!(!state.recording);
    }

    #[test]
    fn escape_does_not_capture_subsequent_event() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = focused_at_widget();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let escape = KeyEvent::new(KeyCode::Named(NamedKey::Escape), ModifierMask::NONE);
        let ctrl_s = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        snap.keys_pressed = vec![escape, ctrl_s];
        let response = run(&mut state, &mut focus, &mut snap);
        assert!(response.captured.is_none(), "Escape cancels");
        assert_eq!(
            snap.keys_pressed,
            vec![ctrl_s],
            "post-Escape event preserved"
        );
        assert!(!state.recording);
    }

    #[test]
    fn unbound_capture_paints_placeholder_key() {
        let mut state = HotkeyCaptureState::default();
        let mut focus = FocusManager::new();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let response = run(&mut state, &mut focus, &mut snap);
        let key = response.paint.iter().find_map(|p| match p {
            super::WidgetPaint::Label {
                text: super::LabelText::Key(k),
                ..
            } => Some(*k),
            _ => None,
        });
        assert_eq!(key, Some(PLACEHOLDER));
    }

    #[test]
    fn first_click_starts_recording_despite_one_frame_focus_delay() {
        use crate::hit_test::resolve;
        use crate::input::{PointerButton, PointerButtonMask, PointerSample};

        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut focus = FocusManager::new();
        let mut state = HotkeyCaptureState::default();
        let mut prev = HitState::new();

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

        [press, release, idle].into_iter().for_each(|mut snap| {
            let mut hits = HitFrame::new();
            let widget = HotkeyCapture::new(id_widget(), rect(), PLACEHOLDER, &mut state);
            {
                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,
                    &prev,
                    &mut a11y,
                    &mut shaper,
                );
                let _ = show_hotkey_capture(&mut ctx, widget);
            }
            prev = resolve(&prev, &hits, &snap, focus.focused());
        });

        assert!(
            state.recording,
            "single click on a fresh widget must start recording even though focus seats next frame",
        );
        assert_eq!(focus.focused(), Some(id_widget()));
    }

    #[test]
    fn losing_focus_after_recording_started_cancels() {
        let mut state = HotkeyCaptureState {
            recording: true,
            chord: None,
        };
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.end_frame();
        let mut snap = InputSnapshot::idle(FrameInstant::ZERO);
        let _ = run(&mut state, &mut focus, &mut snap);
        assert!(!state.recording);
    }
}
