//! The per-frame [`InputState`] — keyboard, mouse, cursor, and scroll with
//! edge detection. The module-level documentation lives in
//! [`crate::input`](super); this file is the implementation.

use std::collections::HashSet;

use winit::event::{ElementState, MouseButton, MouseScrollDelta, WindowEvent};
use winit::keyboard::{KeyCode, PhysicalKey};

use crate::math::Vec2;

/// Pixels-per-line factor used to normalize trackpad pixel scroll deltas into
/// the same units as wheel-notch [`MouseScrollDelta::LineDelta`]. Matches the
/// convention the editor's orbit-camera zoom already uses, so behavior is
/// consistent whether the user has a mouse wheel or a touchpad.
const SCROLL_PIXELS_PER_LINE: f32 = 40.0;

/// Per-frame snapshot of keyboard, mouse, and pointer state.
///
/// Built up across the frame from raw events and queried by game / editor
/// code. All edge sets (pressed / released, mouse delta, scroll) are cleared
/// by [`end_frame`](Self::end_frame); held state and cursor position persist
/// across frames.
#[derive(Debug, Default, Clone)]
pub struct InputState {
    keys_held: HashSet<KeyCode>,
    keys_pressed: HashSet<KeyCode>,
    keys_released: HashSet<KeyCode>,

    mouse_held: HashSet<MouseButton>,
    mouse_pressed: HashSet<MouseButton>,
    mouse_released: HashSet<MouseButton>,

    cursor: Option<Vec2>,
    mouse_delta: Vec2,
    scroll: Vec2,
}

impl InputState {
    /// A new state with nothing pressed and no cursor known.
    pub fn new() -> Self {
        Self::default()
    }

    // --- Queries: keyboard -------------------------------------------------

    /// `true` if `key` became pressed this frame (edge — true for exactly the
    /// frame of the key-down, regardless of OS auto-repeat).
    pub fn pressed(&self, key: KeyCode) -> bool {
        self.keys_pressed.contains(&key)
    }

    /// `true` if `key` was released this frame (edge — true for exactly the
    /// frame of the key-up).
    pub fn released(&self, key: KeyCode) -> bool {
        self.keys_released.contains(&key)
    }

    /// `true` if `key` is currently held down (state — true every frame until
    /// the key-up arrives).
    pub fn held(&self, key: KeyCode) -> bool {
        self.keys_held.contains(&key)
    }

    /// All currently-held keys. Useful for debug overlays.
    pub fn keys_held(&self) -> impl Iterator<Item = KeyCode> + '_ {
        self.keys_held.iter().copied()
    }

    // --- Queries: mouse ----------------------------------------------------

    /// `true` if `button` became pressed this frame (edge).
    pub fn mouse_pressed(&self, button: MouseButton) -> bool {
        self.mouse_pressed.contains(&button)
    }

    /// `true` if `button` was released this frame (edge).
    pub fn mouse_released(&self, button: MouseButton) -> bool {
        self.mouse_released.contains(&button)
    }

    /// `true` if `button` is currently held down (state).
    pub fn mouse_held(&self, button: MouseButton) -> bool {
        self.mouse_held.contains(&button)
    }

    /// All currently-held mouse buttons.
    pub fn mouse_buttons_held(&self) -> impl Iterator<Item = MouseButton> + '_ {
        self.mouse_held.iter().copied()
    }

    /// Current cursor position in physical pixels, or `None` if the cursor
    /// has not entered the window yet (or just left it).
    pub fn cursor(&self) -> Option<Vec2> {
        self.cursor
    }

    /// Cursor movement since the last [`end_frame`](Self::end_frame), in
    /// physical pixels. The first cursor event of a session (or after a
    /// [`CursorLeft`](WindowEvent::CursorLeft)) seeds the position **without**
    /// producing a delta, so consumers never see a phantom jump on the first
    /// frame the cursor appears.
    pub fn mouse_delta(&self) -> Vec2 {
        self.mouse_delta
    }

    /// Scroll accumulated since the last [`end_frame`](Self::end_frame), in
    /// line-equivalent units (pixel deltas are divided by a fixed pixels-per-
    /// line constant so wheels and touchpads report on the same scale).
    pub fn scroll(&self) -> Vec2 {
        self.scroll
    }

    // --- Event pump --------------------------------------------------------

    /// Folds one raw [`WindowEvent`] into the state.
    ///
    /// Non-input events (resize, redraw, focus, …) are ignored, so the runner
    /// can pump every event without filtering. Auto-repeat key-down events
    /// from the OS do not re-fire the [`pressed`](Self::pressed) edge: a held
    /// key only produces an edge on the first down.
    pub fn handle_event(&mut self, event: &WindowEvent) {
        match event {
            WindowEvent::KeyboardInput { event, .. } => {
                if let PhysicalKey::Code(code) = event.physical_key {
                    match event.state {
                        ElementState::Pressed => self.press_key(code),
                        ElementState::Released => self.release_key(code),
                    }
                }
            }
            WindowEvent::MouseInput { state, button, .. } => match state {
                ElementState::Pressed => self.press_mouse(*button),
                ElementState::Released => self.release_mouse(*button),
            },
            WindowEvent::CursorMoved { position, .. } => {
                self.set_cursor(Vec2::new(position.x as f32, position.y as f32));
            }
            WindowEvent::CursorLeft { .. } => self.forget_cursor(),
            WindowEvent::MouseWheel { delta, .. } => match delta {
                MouseScrollDelta::LineDelta(x, y) => self.add_scroll(*x, *y),
                MouseScrollDelta::PixelDelta(p) => self.add_scroll(
                    p.x as f32 / SCROLL_PIXELS_PER_LINE,
                    p.y as f32 / SCROLL_PIXELS_PER_LINE,
                ),
            },
            WindowEvent::Focused(false) => self.release_all_held(),
            _ => {}
        }
    }

    // --- Synthesized mutators (used by both handle_event and tests) --------

    /// Records that `key` was pressed. The [`pressed`](Self::pressed) edge
    /// fires only when the key was not already held, so OS auto-repeat does
    /// not retrigger one-shot actions.
    pub fn press_key(&mut self, key: KeyCode) {
        if self.keys_held.insert(key) {
            self.keys_pressed.insert(key);
        }
    }

    /// Records that `key` was released. The [`released`](Self::released)
    /// edge fires whether or not the key was previously tracked as held —
    /// the OS occasionally sends a release without a matching press (e.g.
    /// the window gained focus mid-press).
    pub fn release_key(&mut self, key: KeyCode) {
        self.keys_held.remove(&key);
        self.keys_released.insert(key);
    }

    /// Records that `button` was pressed (with the same edge semantics as
    /// [`press_key`]).
    pub fn press_mouse(&mut self, button: MouseButton) {
        if self.mouse_held.insert(button) {
            self.mouse_pressed.insert(button);
        }
    }

    /// Records that `button` was released.
    pub fn release_mouse(&mut self, button: MouseButton) {
        self.mouse_held.remove(&button);
        self.mouse_released.insert(button);
    }

    /// Sets the cursor position. The delta is accumulated **only** relative
    /// to a previously-known cursor; the very first set (or the first set
    /// after a [`CursorLeft`](WindowEvent::CursorLeft) event) seeds the
    /// position without contributing to [`mouse_delta`](Self::mouse_delta).
    pub fn set_cursor(&mut self, position: Vec2) {
        if let Some(prev) = self.cursor {
            self.mouse_delta += position - prev;
        }
        self.cursor = Some(position);
    }

    /// Adds a raw mouse delta in physical pixels. Useful for relative-motion
    /// sources (`DeviceEvent::MouseMotion`, future pointer-lock) and for tests.
    pub fn add_mouse_delta(&mut self, dx: f32, dy: f32) {
        self.mouse_delta += Vec2::new(dx, dy);
    }

    /// Adds a scroll increment in line-equivalent units.
    pub fn add_scroll(&mut self, x: f32, y: f32) {
        self.scroll += Vec2::new(x, y);
    }

    // --- Frame boundary ----------------------------------------------------

    /// Clears per-frame edge state and accumulated deltas; held state and
    /// cursor position persist. The runner calls this after game logic has
    /// read the edges for the current frame.
    pub fn end_frame(&mut self) {
        self.keys_pressed.clear();
        self.keys_released.clear();
        self.mouse_pressed.clear();
        self.mouse_released.clear();
        self.mouse_delta = Vec2::ZERO;
        self.scroll = Vec2::ZERO;
    }

    /// Forgets the cursor anchor so the next [`set_cursor`](Self::set_cursor)
    /// re-seeds without producing a phantom delta. The event pump calls this
    /// on [`CursorLeft`](WindowEvent::CursorLeft); the public exposure lets
    /// hosts that drive `InputState` directly (e.g. tests, or a future
    /// pointer-lock toggle) re-anchor without simulating a window event.
    pub fn forget_cursor(&mut self) {
        self.cursor = None;
    }

    /// Releases every currently-held key and mouse button (firing each
    /// `released` edge once). The event pump calls this when the window
    /// loses focus, since the OS will never deliver the matching releases
    /// for keys held at that moment, and stuck-key bugs would otherwise
    /// follow the window across alt-tab cycles.
    pub fn release_all_held(&mut self) {
        for key in self.keys_held.drain() {
            self.keys_released.insert(key);
        }
        for button in self.mouse_held.drain() {
            self.mouse_released.insert(button);
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn key_press_sets_edge_and_state() {
        let mut input = InputState::new();
        input.press_key(KeyCode::Space);

        assert!(input.pressed(KeyCode::Space));
        assert!(input.held(KeyCode::Space));
        assert!(!input.released(KeyCode::Space));
    }

    #[test]
    fn end_frame_clears_edges_but_not_held() {
        let mut input = InputState::new();
        input.press_key(KeyCode::Space);
        input.end_frame();

        assert!(!input.pressed(KeyCode::Space), "edge must clear");
        assert!(input.held(KeyCode::Space), "state must persist");
    }

    #[test]
    fn key_release_sets_edge_and_clears_held() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyA);
        input.end_frame();

        input.release_key(KeyCode::KeyA);
        assert!(input.released(KeyCode::KeyA));
        assert!(!input.held(KeyCode::KeyA));
        assert!(!input.pressed(KeyCode::KeyA));
    }

    #[test]
    fn os_auto_repeat_does_not_refire_pressed_edge() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyW);
        input.end_frame(); // pressed edge consumed

        // The OS resends Pressed for the same key while it's held.
        input.press_key(KeyCode::KeyW);
        assert!(
            !input.pressed(KeyCode::KeyW),
            "auto-repeat must not retrigger pressed"
        );
        assert!(input.held(KeyCode::KeyW));
    }

    #[test]
    fn release_without_prior_press_still_emits_edge() {
        // The OS occasionally delivers a release with no matching press (e.g.
        // window focused mid-press). The released edge still fires so consumers
        // can react.
        let mut input = InputState::new();
        input.release_key(KeyCode::Escape);
        assert!(input.released(KeyCode::Escape));
        assert!(!input.held(KeyCode::Escape));
    }

    #[test]
    fn pressed_and_released_in_same_frame_both_fire() {
        // Within a single frame a quick tap should register both edges so
        // logic that wants a "click on release" pattern is reachable from
        // the synthesized input path.
        let mut input = InputState::new();
        input.press_key(KeyCode::Enter);
        input.release_key(KeyCode::Enter);

        assert!(input.pressed(KeyCode::Enter));
        assert!(input.released(KeyCode::Enter));
        assert!(!input.held(KeyCode::Enter));
    }

    #[test]
    fn mouse_button_edges_parallel_keyboard() {
        let mut input = InputState::new();
        input.press_mouse(MouseButton::Left);
        assert!(input.mouse_pressed(MouseButton::Left));
        assert!(input.mouse_held(MouseButton::Left));

        input.end_frame();
        assert!(!input.mouse_pressed(MouseButton::Left));
        assert!(input.mouse_held(MouseButton::Left));

        input.release_mouse(MouseButton::Left);
        assert!(input.mouse_released(MouseButton::Left));
        assert!(!input.mouse_held(MouseButton::Left));
    }

    #[test]
    fn first_cursor_move_produces_no_delta() {
        let mut input = InputState::new();
        input.set_cursor(Vec2::new(100.0, 200.0));
        assert_eq!(input.mouse_delta(), Vec2::ZERO);
        assert_eq!(input.cursor(), Some(Vec2::new(100.0, 200.0)));
    }

    #[test]
    fn subsequent_cursor_moves_accumulate_delta() {
        let mut input = InputState::new();
        input.set_cursor(Vec2::new(100.0, 200.0));
        input.set_cursor(Vec2::new(110.0, 195.0));
        input.set_cursor(Vec2::new(115.0, 190.0));

        // (110-100) + (115-110), (195-200) + (190-195) = (15, -10)
        assert_eq!(input.mouse_delta(), Vec2::new(15.0, -10.0));
    }

    #[test]
    fn end_frame_resets_delta_but_preserves_cursor() {
        let mut input = InputState::new();
        input.set_cursor(Vec2::new(0.0, 0.0));
        input.set_cursor(Vec2::new(10.0, 10.0));
        input.end_frame();

        assert_eq!(input.mouse_delta(), Vec2::ZERO);
        assert_eq!(input.cursor(), Some(Vec2::new(10.0, 10.0)));

        // Next move accumulates from the persisted cursor, not from zero.
        input.set_cursor(Vec2::new(13.0, 11.0));
        assert_eq!(input.mouse_delta(), Vec2::new(3.0, 1.0));
    }

    #[test]
    fn add_mouse_delta_layers_on_top_of_cursor_motion() {
        let mut input = InputState::new();
        input.set_cursor(Vec2::new(0.0, 0.0));
        input.set_cursor(Vec2::new(5.0, 0.0));
        input.add_mouse_delta(2.0, 3.0); // e.g. raw DeviceEvent motion

        assert_eq!(input.mouse_delta(), Vec2::new(7.0, 3.0));
    }

    #[test]
    fn scroll_accumulates_and_resets() {
        let mut input = InputState::new();
        input.add_scroll(0.0, 1.0);
        input.add_scroll(0.0, 2.5);
        assert_eq!(input.scroll(), Vec2::new(0.0, 3.5));

        input.end_frame();
        assert_eq!(input.scroll(), Vec2::ZERO);
    }

    #[test]
    fn focus_loss_via_handle_event_releases_held() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyW);
        input.press_mouse(MouseButton::Left);
        input.end_frame();

        // Focused(false) is one of the WindowEvent variants with no DeviceId,
        // so the routing through handle_event itself is exercised here.
        input.handle_event(&WindowEvent::Focused(false));

        assert!(!input.held(KeyCode::KeyW), "key must not stay stuck");
        assert!(!input.mouse_held(MouseButton::Left));
        assert!(input.released(KeyCode::KeyW));
        assert!(input.mouse_released(MouseButton::Left));
    }

    #[test]
    fn release_all_held_drops_state_and_fires_edges() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyW);
        input.press_key(KeyCode::ShiftLeft);
        input.press_mouse(MouseButton::Right);
        input.end_frame();

        input.release_all_held();

        assert!(!input.held(KeyCode::KeyW));
        assert!(!input.held(KeyCode::ShiftLeft));
        assert!(!input.mouse_held(MouseButton::Right));
        assert!(input.released(KeyCode::KeyW));
        assert!(input.released(KeyCode::ShiftLeft));
        assert!(input.mouse_released(MouseButton::Right));
    }

    #[test]
    fn forget_cursor_resets_anchor_so_next_move_has_no_delta() {
        let mut input = InputState::new();
        input.set_cursor(Vec2::new(0.0, 0.0));
        input.set_cursor(Vec2::new(10.0, 10.0));
        input.end_frame();

        input.forget_cursor();
        assert!(input.cursor().is_none());

        // First move back in reseeds without contributing a delta.
        input.set_cursor(Vec2::new(200.0, 50.0));
        assert_eq!(input.mouse_delta(), Vec2::ZERO);
        assert_eq!(input.cursor(), Some(Vec2::new(200.0, 50.0)));
    }

    #[test]
    fn handle_event_ignores_unrelated_window_events() {
        // These three WindowEvent variants don't carry a DeviceId, so they
        // can be constructed in tests — the routing through handle_event is
        // exercised end-to-end here.
        let mut input = InputState::new();
        input.press_key(KeyCode::Space);

        input.handle_event(&WindowEvent::Resized(winit::dpi::PhysicalSize::new(
            800, 600,
        )));
        input.handle_event(&WindowEvent::CloseRequested);
        input.handle_event(&WindowEvent::RedrawRequested);

        assert!(input.pressed(KeyCode::Space));
        assert!(input.held(KeyCode::Space));
    }

    #[test]
    fn keys_held_iterates_currently_held_keys() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyW);
        input.press_key(KeyCode::KeyA);
        input.release_key(KeyCode::KeyA);

        let held: HashSet<KeyCode> = input.keys_held().collect();
        assert_eq!(held, HashSet::from([KeyCode::KeyW]));
    }
}
