use std::sync::Arc;

use bone_text::Shaper;

use crate::a11y::{AccessNode, AccessTreeBuilder, Role};
use crate::focus::FocusManager;
use crate::hit_test::{HitFrame, HitItem, HitState, Interaction, Sense, ZLayer};
use crate::hotkey::{ActionId, HotkeyScopes, HotkeyTable, KeyChord};
use crate::input::{InputSnapshot, KeyEvent};
use crate::layout::{LayoutDirection, LayoutRect};
use crate::strings::{Locale, StringTable};
use crate::theme::Theme;
use crate::widget_id::WidgetId;

#[derive(Clone, Debug, PartialEq)]
pub struct InteractDeclaration {
    pub id: WidgetId,
    pub rect: LayoutRect,
    pub sense: Sense,
    pub z: ZLayer,
    pub disabled: bool,
    pub focusable: bool,
    pub active: bool,
    pub a11y: Option<AccessNode>,
}

impl InteractDeclaration {
    #[must_use]
    pub const fn new(id: WidgetId, rect: LayoutRect, sense: Sense) -> Self {
        Self {
            id,
            rect,
            sense,
            z: ZLayer::BASE,
            disabled: false,
            focusable: false,
            active: false,
            a11y: None,
        }
    }

    #[must_use]
    pub fn at_z(self, z: ZLayer) -> Self {
        Self { z, ..self }
    }

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

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

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

    #[must_use]
    pub fn a11y(self, node: AccessNode) -> Self {
        Self {
            a11y: Some(node),
            ..self
        }
    }
}

pub struct FrameCtx<'a> {
    theme: Arc<Theme>,
    pub input: &'a mut InputSnapshot,
    pub focus: &'a mut FocusManager,
    pub hotkeys: &'a HotkeyTable,
    pub strings: &'a StringTable,
    pub hits: &'a mut HitFrame,
    pub previous: &'a HitState,
    pub a11y: &'a mut AccessTreeBuilder,
    pub shaper: &'a mut Shaper,
}

impl<'a> FrameCtx<'a> {
    #[must_use]
    #[allow(
        clippy::too_many_arguments,
        reason = "FrameCtx threads every per-frame subsystem; bundling them obscures lifetimes"
    )]
    pub fn new(
        theme: Arc<Theme>,
        input: &'a mut InputSnapshot,
        focus: &'a mut FocusManager,
        hotkeys: &'a HotkeyTable,
        strings: &'a StringTable,
        hits: &'a mut HitFrame,
        previous: &'a HitState,
        a11y: &'a mut AccessTreeBuilder,
        shaper: &'a mut Shaper,
    ) -> Self {
        focus.begin_frame();
        focus.observe_input(input);
        a11y.begin_frame();
        Self {
            theme,
            input,
            focus,
            hotkeys,
            strings,
            hits,
            previous,
            a11y,
            shaper,
        }
    }

    #[must_use]
    pub fn theme(&self) -> &Theme {
        &self.theme
    }

    #[must_use]
    pub fn locale(&self) -> Locale {
        self.strings.locale()
    }

    #[must_use]
    pub fn direction(&self) -> LayoutDirection {
        self.strings.direction()
    }

    #[must_use]
    pub fn is_focused(&self, id: WidgetId) -> bool {
        self.focus.focused() == Some(id)
    }

    pub fn request_focus(&mut self, id: WidgetId) {
        self.focus.request_focus(id);
    }

    pub fn interact(&mut self, declaration: InteractDeclaration) -> Interaction {
        if !declaration.disabled {
            self.focus.register_focusable(declaration.id);
            if declaration.focusable {
                self.focus.register_tab_stop(declaration.id);
            }
            if declaration
                .a11y
                .as_ref()
                .is_some_and(|node| node.role == Role::TextInput)
            {
                self.focus.register_text_input(declaration.id);
            }
        }
        self.hits.push(HitItem {
            id: declaration.id,
            rect: declaration.rect,
            sense: declaration.sense,
            z: declaration.z,
            disabled: declaration.disabled,
            active: declaration.active,
        });
        if let Some(node) = declaration.a11y {
            self.a11y.push(declaration.id, declaration.rect, node);
        }
        let interaction = self.previous.interaction(declaration.id);
        if interaction.click() && declaration.focusable && !declaration.disabled {
            self.focus.request_focus(declaration.id);
        }
        interaction
    }

    pub fn block_pointer(&mut self, id: WidgetId, rect: LayoutRect) {
        self.hits.push(HitItem {
            id,
            rect,
            sense: Sense::INTERACTIVE,
            z: ZLayer::BASE,
            disabled: false,
            active: false,
        });
    }

    pub fn theme_scope<R>(
        &mut self,
        modify: impl FnOnce(&Theme) -> Theme,
        body: impl FnOnce(&mut FrameCtx<'a>) -> R,
    ) -> R {
        let outer = Arc::clone(&self.theme);
        self.theme = Arc::new(modify(&self.theme));
        let result = body(self);
        self.theme = outer;
        result
    }

    pub fn dispatch_hotkeys(&mut self, scopes: &HotkeyScopes) -> Vec<ActionId> {
        let table = self.hotkeys;
        let pending = std::mem::take(&mut self.input.keys_pressed);
        let (matched, remaining) = pending.into_iter().fold(
            (Vec::new(), Vec::new()),
            |(mut matched, mut remaining), event: KeyEvent| {
                match table.dispatch(KeyChord::from(event), scopes) {
                    Some(action) => matched.push(action),
                    None => remaining.push(event),
                }
                (matched, remaining)
            },
        );
        self.input.keys_pressed = remaining;
        matched
    }
}

impl Drop for FrameCtx<'_> {
    fn drop(&mut self) {
        self.focus.end_frame();
    }
}

#[cfg(test)]
mod tests {
    use core::num::NonZeroU32;
    use core::time::Duration;
    use std::sync::Arc;

    use super::{FrameCtx, InteractDeclaration};
    use crate::a11y::AccessTreeBuilder;
    use crate::focus::FocusManager;
    use crate::hit_test::{HitFrame, HitState, Sense, resolve};
    use crate::hotkey::{
        ActionId, HotkeyBinding, HotkeyScope, HotkeyScopes, HotkeyTable, KeyChord,
    };
    use crate::input::{
        FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, PointerButton,
        PointerButtonMask, PointerSample,
    };
    use crate::layout::{LayoutDirection, LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::{Locale, StringTable};
    use crate::theme::{Theme, ThemeMode};
    use crate::widget_id::{WidgetId, WidgetKey};

    fn global_scope() -> HotkeyScopes {
        HotkeyScopes::from_outer_to_inner([HotkeyScope::Global])
    }

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

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

    fn action(n: u32) -> ActionId {
        let Some(nz) = NonZeroU32::new(n) else {
            panic!("test action id must be non-zero");
        };
        ActionId::new(nz)
    }

    #[test]
    fn interact_registers_tab_stop_when_focusable() {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        {
            let mut frame = FrameCtx::new(
                theme,
                &mut input,
                &mut focus,
                &hotkeys,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            let _ = frame.interact(
                InteractDeclaration::new(id("button"), rect(), Sense::INTERACTIVE).focusable(true),
            );
        }
        assert_eq!(focus.tab_stops().len(), 1);
        assert!(hits.items().iter().any(|item| item.id == id("button")));
    }

    #[test]
    fn dispatch_hotkeys_consumes_pressed_chord() {
        let chord = KeyChord::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        let Ok(table) = HotkeyTable::try_from_bindings(vec![HotkeyBinding::new(
            chord,
            HotkeyScope::Global,
            action(42),
        )]) else {
            panic!("registration must succeed");
        };
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        input.keys_pressed.push(KeyEvent::new(
            KeyCode::Char(KeyChar::from_char('s')),
            ModifierMask::CTRL,
        ));
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let actions = {
            let mut frame = FrameCtx::new(
                Arc::new(Theme::light()),
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            frame.dispatch_hotkeys(&global_scope())
        };
        assert_eq!(actions, vec![action(42)]);
        assert!(input.keys_pressed.is_empty());
    }

    #[test]
    fn dispatch_hotkeys_leaves_unmatched_keys() {
        let table = HotkeyTable::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let event = KeyEvent::new(KeyCode::Char(KeyChar::from_char('s')), ModifierMask::CTRL);
        input.keys_pressed.push(event);
        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let actions = {
            let mut frame = FrameCtx::new(
                Arc::new(Theme::light()),
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            frame.dispatch_hotkeys(&global_scope())
        };
        assert!(actions.is_empty());
        assert_eq!(input.keys_pressed, vec![event]);
    }

    #[test]
    fn end_to_end_press_release_routes_through_resolve() {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let mut state = HitState::new();
        let mut press = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(0)));
        press.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(10.0),
            LayoutPx::new(10.0),
        )));
        press.buttons_pressed = PointerButtonMask::just(PointerButton::Primary);

        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        {
            let mut frame = FrameCtx::new(
                theme.clone(),
                &mut press,
                &mut focus,
                &hotkeys,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            let _ = frame.interact(
                InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true),
            );
        }
        state = resolve(&state, &hits, &press, focus.focused());
        assert!(state.interaction(id("btn")).pressed());

        hits.clear();
        let mut release =
            InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(40)));
        release.pointer = Some(PointerSample::new(LayoutPos::new(
            LayoutPx::new(15.0),
            LayoutPx::new(15.0),
        )));
        release.buttons_released = PointerButtonMask::just(PointerButton::Primary);

        {
            let mut frame = FrameCtx::new(
                theme.clone(),
                &mut release,
                &mut focus,
                &hotkeys,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            let _ = frame.interact(
                InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true),
            );
        }
        state = resolve(&state, &hits, &release, focus.focused());
        assert!(state.interaction(id("btn")).click());

        hits.clear();
        let mut idle = InputSnapshot::idle(FrameInstant::from_duration(Duration::from_millis(80)));
        {
            let mut frame = FrameCtx::new(
                theme,
                &mut idle,
                &mut focus,
                &hotkeys,
                StringTable::empty(),
                &mut hits,
                &state,
                &mut a11y,
                &mut shaper,
            );
            let _ = frame.interact(
                InteractDeclaration::new(id("btn"), rect(), Sense::INTERACTIVE).focusable(true),
            );
        }
        assert_eq!(
            focus.focused(),
            Some(id("btn")),
            "click on focusable widget auto-focuses next frame",
        );
    }

    #[test]
    fn theme_scope_swaps_theme_for_body_and_restores_after() {
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut frame = FrameCtx::new(
            Arc::new(Theme::light()),
            &mut input,
            &mut focus,
            &hotkeys,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        assert_eq!(frame.theme().mode, ThemeMode::Light);
        let inner_mode = frame.theme_scope(|_| Theme::dark(), |frame| frame.theme().mode);
        assert_eq!(inner_mode, ThemeMode::Dark);
        assert_eq!(frame.theme().mode, ThemeMode::Light);
    }

    #[test]
    fn nested_theme_scopes_unwind_lifo() {
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut frame = FrameCtx::new(
            Arc::new(Theme::light()),
            &mut input,
            &mut focus,
            &hotkeys,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let modes = frame.theme_scope(
            |_| Theme::dark(),
            |frame| {
                let outer_mode = frame.theme().mode;
                let inner_mode = frame.theme_scope(|_| Theme::light(), |frame| frame.theme().mode);
                let after_inner = frame.theme().mode;
                (outer_mode, inner_mode, after_inner)
            },
        );
        assert_eq!(modes, (ThemeMode::Dark, ThemeMode::Light, ThemeMode::Dark));
        assert_eq!(frame.theme().mode, ThemeMode::Light);
    }

    #[test]
    fn theme_scope_observes_modified_accent_via_clone() {
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let mut frame = FrameCtx::new(
            Arc::new(Theme::light()),
            &mut input,
            &mut focus,
            &hotkeys,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        let outer_accent = frame.theme().colors.accent_solid();
        let outer_ring = frame.theme().colors.focus_ring();
        let (inner_accent, inner_ring) = frame.theme_scope(
            |t| {
                let mut next = t.clone();
                next.colors.accent = t.colors.danger;
                next
            },
            |frame| {
                (
                    frame.theme().colors.accent_solid(),
                    frame.theme().colors.focus_ring(),
                )
            },
        );
        assert_ne!(inner_accent, outer_accent);
        assert_eq!(inner_ring, inner_accent);
        assert_eq!(frame.theme().colors.accent_solid(), outer_accent);
        assert_eq!(frame.theme().colors.focus_ring(), outer_ring);
    }

    #[test]
    fn hot_swap_between_frames_rebinds_token_reads() {
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let strings = StringTable::empty();
        let prev = HitState::new();
        let read_tokens = |theme: Arc<Theme>,
                           focus: &mut FocusManager,
                           hits: &mut HitFrame,
                           input: &mut InputSnapshot,
                           a11y: &mut AccessTreeBuilder,
                           shaper: &mut bone_text::Shaper| {
            let frame = FrameCtx::new(
                theme, input, focus, &hotkeys, strings, hits, &prev, a11y, shaper,
            );
            (
                frame.theme().mode,
                frame.theme().colors.text_primary(),
                frame.theme().colors.surface(crate::theme::SurfaceLevel::L0),
            )
        };

        let mut hits_a = HitFrame::new();
        let mut input_a = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper_a = bone_text::Shaper::new();
        let mut a11y_a = AccessTreeBuilder::new();
        let (mode_a, text_a, surface_a) = read_tokens(
            Arc::new(Theme::light()),
            &mut focus,
            &mut hits_a,
            &mut input_a,
            &mut a11y_a,
            &mut shaper_a,
        );

        let mut hits_b = HitFrame::new();
        let mut input_b = InputSnapshot::idle(FrameInstant::ZERO);
        let mut shaper_b = bone_text::Shaper::new();
        let mut a11y_b = AccessTreeBuilder::new();
        let (mode_b, text_b, surface_b) = read_tokens(
            Arc::new(Theme::dark()),
            &mut focus,
            &mut hits_b,
            &mut input_b,
            &mut a11y_b,
            &mut shaper_b,
        );

        assert_eq!(mode_a, ThemeMode::Light);
        assert_eq!(mode_b, ThemeMode::Dark);
        assert_ne!(text_a, text_b);
        assert_ne!(surface_a, surface_b);
        assert!(surface_a.relative_luminance() > surface_b.relative_luminance());
        assert!(text_a.relative_luminance() < text_b.relative_luminance());
    }

    #[test]
    fn locale_and_direction_follow_string_table() {
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        let hotkeys = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let strings = StringTable::for_locale(Locale::ArXb);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = AccessTreeBuilder::new();
        let frame = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &hotkeys,
            &strings,
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        assert_eq!(frame.locale(), Locale::ArXb);
        assert_eq!(frame.direction(), LayoutDirection::Rtl);
    }
}
