//! Raw OS keyboard-state backend for terminals without the Kitty protocol.
//! Polls global key state each frame and diffs it into per-action Down/Up edges,
//! giving true concurrent holds + real DAS/ARR + clean release on any terminal.

use std::collections::HashSet;

use device_query::{DeviceQuery, DeviceState, Keycode};
use ratatui::crossterm::event::KeyCode;

use crate::config::keymap::KeyMap;
use crate::sim::input::{Action, FrameInput, InputKind};

/// Map a `device_query` keycode to the equivalent crossterm `KeyCode`, so the
/// config `KeyMap` (which is crossterm-keyed) can be reused. Letters map to the
/// lowercase `Char` the KeyMap uses.
pub fn device_to_crossterm(k: Keycode) -> Option<KeyCode> {
    Some(match k {
        Keycode::Left => KeyCode::Left,
        Keycode::Right => KeyCode::Right,
        Keycode::Up => KeyCode::Up,
        Keycode::Down => KeyCode::Down,
        Keycode::Space => KeyCode::Char(' '),
        Keycode::Escape => KeyCode::Esc,
        Keycode::Enter => KeyCode::Enter,
        Keycode::Tab => KeyCode::Tab,
        Keycode::A => KeyCode::Char('a'),
        Keycode::B => KeyCode::Char('b'),
        Keycode::C => KeyCode::Char('c'),
        Keycode::D => KeyCode::Char('d'),
        Keycode::E => KeyCode::Char('e'),
        Keycode::F => KeyCode::Char('f'),
        Keycode::G => KeyCode::Char('g'),
        Keycode::H => KeyCode::Char('h'),
        Keycode::I => KeyCode::Char('i'),
        Keycode::J => KeyCode::Char('j'),
        Keycode::K => KeyCode::Char('k'),
        Keycode::L => KeyCode::Char('l'),
        Keycode::M => KeyCode::Char('m'),
        Keycode::N => KeyCode::Char('n'),
        Keycode::O => KeyCode::Char('o'),
        Keycode::P => KeyCode::Char('p'),
        Keycode::Q => KeyCode::Char('q'),
        Keycode::R => KeyCode::Char('r'),
        Keycode::S => KeyCode::Char('s'),
        Keycode::T => KeyCode::Char('t'),
        Keycode::U => KeyCode::Char('u'),
        Keycode::V => KeyCode::Char('v'),
        Keycode::W => KeyCode::Char('w'),
        Keycode::X => KeyCode::Char('x'),
        Keycode::Y => KeyCode::Char('y'),
        Keycode::Z => KeyCode::Char('z'),
        _ => return None,
    })
}

/// Pure edge diff: Down for actions newly active, Up for actions newly inactive.
pub fn diff_active(prev: &HashSet<Action>, now: &HashSet<Action>) -> Vec<FrameInput> {
    let mut events = Vec::new();
    for action in now {
        if !prev.contains(action) {
            events.push(FrameInput {
                subframe: 0.0,
                kind: InputKind::Down,
                action: *action,
                hoisted: false,
            });
        }
    }
    for action in prev {
        if !now.contains(action) {
            events.push(FrameInput {
                subframe: 0.0,
                kind: InputKind::Up,
                action: *action,
                hoisted: false,
            });
        }
    }
    events
}

/// Tracks which game actions are currently held (via OS key state) and emits edges.
pub struct RawInput {
    device: DeviceState,
    active: HashSet<Action>,
}

impl RawInput {
    pub fn new() -> RawInput {
        RawInput {
            device: DeviceState::new(),
            active: HashSet::new(),
        }
    }

    /// Read current OS key state, translate via the keymap, and return the
    /// Down/Up edges since the last poll. Held actions stay active (no repeat
    /// Down) — the engine's DAS/ARR drives repeats.
    pub fn poll(&mut self, keymap: &KeyMap) -> Vec<FrameInput> {
        let mut now: HashSet<Action> = HashSet::new();
        for key in self.device.get_keys() {
            if let Some(code) = device_to_crossterm(key) {
                if let Some(action) = keymap.action(code) {
                    now.insert(action);
                }
            }
        }
        let events = diff_active(&self.active, &now);
        self.active = now;
        events
    }

    /// Forget the tracked active set (e.g. across a pause, so resume re-acquires
    /// currently-held keys as fresh Down edges).
    pub fn reset(&mut self) {
        self.active.clear();
    }
}

impl Default for RawInput {
    fn default() -> Self {
        RawInput::new()
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sim::input::{Action, InputKind};
    use ratatui::crossterm::event::KeyCode;
    use std::collections::HashSet;

    #[test]
    fn device_keys_map_to_crossterm() {
        use device_query::Keycode;
        assert_eq!(device_to_crossterm(Keycode::Left), Some(KeyCode::Left));
        assert_eq!(
            device_to_crossterm(Keycode::Space),
            Some(KeyCode::Char(' '))
        );
        assert_eq!(device_to_crossterm(Keycode::X), Some(KeyCode::Char('x')));
        assert_eq!(device_to_crossterm(Keycode::Z), Some(KeyCode::Char('z')));
        assert_eq!(device_to_crossterm(Keycode::Escape), Some(KeyCode::Esc));
    }

    #[test]
    fn diff_emits_down_for_new_and_up_for_released() {
        let prev: HashSet<Action> = [Action::MoveLeft].into_iter().collect();
        let now: HashSet<Action> = [Action::MoveLeft, Action::RotateCW].into_iter().collect();
        // MoveLeft stays held (no event); RotateCW newly down.
        let events = diff_active(&prev, &now);
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].action, Action::RotateCW);
        assert_eq!(events[0].kind, InputKind::Down);

        // Now release RotateCW, keep MoveLeft.
        let later: HashSet<Action> = [Action::MoveLeft].into_iter().collect();
        let events = diff_active(&now, &later);
        assert_eq!(events.len(), 1);
        assert_eq!(events[0].action, Action::RotateCW);
        assert_eq!(events[0].kind, InputKind::Up);
    }

    #[test]
    fn diff_concurrent_down_and_up_in_one_step() {
        let prev: HashSet<Action> = [Action::SoftDrop].into_iter().collect();
        let now: HashSet<Action> = [Action::MoveRight].into_iter().collect();
        let events = diff_active(&prev, &now);
        // SoftDrop up + MoveRight down (order not asserted).
        assert_eq!(events.len(), 2);
        assert!(
            events
                .iter()
                .any(|e| e.action == Action::MoveRight && e.kind == InputKind::Down)
        );
        assert!(
            events
                .iter()
                .any(|e| e.action == Action::SoftDrop && e.kind == InputKind::Up)
        );
    }
}
