//! Gamepad input via gilrs (item 15 of the v0.4 sweep), behind the
//! `gamepad` feature.
//!
//! Same shape as keyboard input: a polled SNAPSHOT with edge detection,
//! published fresh each frame (a resource in the ECS door, an accessor on
//! Context in the Game door). Same failure policy as audio: no controller,
//! no crash - the snapshot just stays disconnected.
//!
//! One controller in v0.4 (the first that speaks); multi-pad is a Vec of
//! these when a game needs it.

use std::collections::HashSet;

use crate::math::Vec2;

/// Mawida's button names: the positional layout (South = the bottom face
/// button: A on Xbox, Cross on PlayStation, B on Nintendo). Positional
/// beats lettered because the letters MOVE between vendors.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum GamepadButton {
    South,
    East,
    West,
    North,
    L1,
    R1,
    Select,
    Start,
    LeftStick,
    RightStick,
    DpadUp,
    DpadDown,
    DpadLeft,
    DpadRight,
}

/// This frame's controller snapshot. `Clone` so the ECS door can publish
/// it as a resource, like `InputState`.
#[derive(Debug, Clone, Default)]
pub struct GamepadState {
    pub connected: bool,
    /// Sticks in -1..1, x right. y is UP (+1 = stick pushed up): sticks
    /// report hardware direction; apply your own convention in game code.
    pub left_stick: Vec2,
    pub right_stick: Vec2,
    /// Analog triggers, 0..1.
    pub left_trigger: f32,
    pub right_trigger: f32,
    down: HashSet<GamepadButton>,
    pressed: HashSet<GamepadButton>,
    released: HashSet<GamepadButton>,
}

impl GamepadState {
    pub fn is_down(&self, b: GamepadButton) -> bool {
        self.down.contains(&b)
    }

    /// Edge-triggered: true only on the frame the button went down.
    pub fn just_pressed(&self, b: GamepadButton) -> bool {
        self.pressed.contains(&b)
    }

    pub fn just_released(&self, b: GamepadButton) -> bool {
        self.released.contains(&b)
    }

    pub(crate) fn press(&mut self, b: GamepadButton) {
        if self.down.insert(b) {
            self.pressed.insert(b);
        }
    }

    pub(crate) fn release(&mut self, b: GamepadButton) {
        if self.down.remove(&b) {
            self.released.insert(b);
        }
    }

    /// Clear the edges for the next frame (held state persists).
    pub(crate) fn begin_frame(&mut self) {
        self.pressed.clear();
        self.released.clear();
    }
}

/// Owns the gilrs context and folds its event stream into a
/// [`GamepadState`]. Runners hold one and call [`poll`](GamepadPump::poll)
/// once per frame.
pub struct GamepadPump {
    gilrs: Option<gilrs::Gilrs>, // None: no backend on this machine. Fail soft.
    state: GamepadState,
}

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

impl GamepadPump {
    pub fn new() -> Self {
        let gilrs = match gilrs::Gilrs::new() {
            Ok(g) => Some(g),
            Err(e) => {
                log::warn!("gamepad backend unavailable: {e}");
                None
            }
        };
        Self {
            gilrs,
            state: GamepadState::default(),
        }
    }

    fn map_button(b: gilrs::Button) -> Option<GamepadButton> {
        use gilrs::Button as G;
        Some(match b {
            G::South => GamepadButton::South,
            G::East => GamepadButton::East,
            G::West => GamepadButton::West,
            G::North => GamepadButton::North,
            G::LeftTrigger => GamepadButton::L1, // gilrs: LeftTrigger = the bumper
            G::RightTrigger => GamepadButton::R1,
            G::Select => GamepadButton::Select,
            G::Start => GamepadButton::Start,
            G::LeftThumb => GamepadButton::LeftStick,
            G::RightThumb => GamepadButton::RightStick,
            G::DPadUp => GamepadButton::DpadUp,
            G::DPadDown => GamepadButton::DpadDown,
            G::DPadLeft => GamepadButton::DpadLeft,
            G::DPadRight => GamepadButton::DpadRight,
            _ => return None,
        })
    }

    /// Drain pending events into the snapshot. Call once per frame,
    /// BEFORE publishing the state; edges reset on the next poll.
    pub fn poll(&mut self) -> &GamepadState {
        self.state.begin_frame();
        let Some(gilrs) = self.gilrs.as_mut() else {
            return &self.state;
        };
        while let Some(ev) = gilrs.next_event() {
            use gilrs::EventType as E;
            self.state.connected = true;
            match ev.event {
                E::ButtonPressed(b, _) => {
                    if let Some(b) = Self::map_button(b) {
                        self.state.press(b);
                    }
                }
                E::ButtonReleased(b, _) => {
                    if let Some(b) = Self::map_button(b) {
                        self.state.release(b);
                    }
                }
                E::AxisChanged(axis, v, _) => {
                    use gilrs::Axis as A;
                    match axis {
                        A::LeftStickX => self.state.left_stick.x = v,
                        A::LeftStickY => self.state.left_stick.y = v,
                        A::RightStickX => self.state.right_stick.x = v,
                        A::RightStickY => self.state.right_stick.y = v,
                        _ => {}
                    }
                }
                E::ButtonChanged(b, v, _) => {
                    // analog triggers arrive as value changes
                    use gilrs::Button as G;
                    match b {
                        G::LeftTrigger2 => self.state.left_trigger = v,
                        G::RightTrigger2 => self.state.right_trigger = v,
                        _ => {}
                    }
                }
                E::Disconnected => self.state = GamepadState::default(),
                _ => {}
            }
        }
        &self.state
    }
}

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

    #[test]
    fn edges_fire_once_and_held_persists() {
        let mut s = GamepadState::default();
        s.press(GamepadButton::South);
        assert!(s.is_down(GamepadButton::South));
        assert!(s.just_pressed(GamepadButton::South));

        s.begin_frame(); // next frame: still held, edge gone
        assert!(s.is_down(GamepadButton::South));
        assert!(!s.just_pressed(GamepadButton::South));

        s.release(GamepadButton::South);
        assert!(!s.is_down(GamepadButton::South));
        assert!(s.just_released(GamepadButton::South));
    }

    #[test]
    fn pump_without_a_device_is_silent_not_fatal() {
        // On CI or a controller-less desk this must simply do nothing.
        let mut pump = GamepadPump::new();
        let state = pump.poll();
        assert!(!state.is_down(GamepadButton::Start));
    }
}
