//! gilrs → [`xenia_hid::GamepadState`] adapter.

use gilrs::{Axis, Button, EventType, Gilrs, GilrsBuilder};
use xenia_hid::{GamepadState, buttons};

/// Simulated-pad policy: if no physical controller is attached, advertise
/// a connected pad (with all-zero state) so games don't early-exit on
/// `XamInputGetState → ERROR_DEVICE_NOT_CONNECTED`. Set
/// `XENIA_FAKE_PAD=0` to disable and report the true no-controller status.
fn fake_pad_policy() -> bool {
    match std::env::var("XENIA_FAKE_PAD").as_deref() {
        Ok("0") | Ok("false") | Ok("off") => false,
        _ => true,
    }
}

pub struct HostInput {
    ctx: Gilrs,
    /// Currently-active gilrs pad id. `None` until the first Connected event.
    active: Option<gilrs::GamepadId>,
    /// `true` if the pad is really physical; `false` for the zeroed simulated
    /// pad we advertise when no hardware is attached (see `fake_pad_policy`).
    pub physical_attached: bool,
    fake_allowed: bool,
}

impl HostInput {
    pub fn new() -> anyhow::Result<Self> {
        let ctx = GilrsBuilder::new()
            .add_included_mappings(true)
            .build()
            .map_err(|e| anyhow::anyhow!("gilrs init failed: {e}"))?;
        let active = ctx.gamepads().next().map(|(id, _)| id);
        let physical_attached = active.is_some();
        let fake_allowed = fake_pad_policy();
        tracing::info!(
            physical = physical_attached,
            fake_allowed,
            "input: init (pad simulation: {})",
            if fake_allowed && !physical_attached {
                "simulated (no physical pad)"
            } else if physical_attached {
                "physical"
            } else {
                "disabled"
            }
        );
        Ok(Self {
            ctx,
            active,
            physical_attached,
            fake_allowed,
        })
    }

    /// True iff a gamepad is "available" for user index 0 — either the
    /// actual host pad, or a simulated one when `XENIA_FAKE_PAD != 0`.
    pub fn is_connected(&self) -> bool {
        if self.physical_attached
            && self
                .active
                .and_then(|id| self.ctx.connected_gamepad(id))
                .is_some()
        {
            return true;
        }
        self.fake_allowed
    }

    /// Pump gilrs events until empty. Updates the internal "active pad"
    /// registration; pad state is sampled by [`snapshot`].
    pub fn poll(&mut self) {
        while let Some(event) = self.ctx.next_event() {
            match event.event {
                EventType::Connected => {
                    if self.active.is_none() {
                        self.active = Some(event.id);
                        self.physical_attached = true;
                        tracing::info!("gilrs: pad connected (id={:?})", event.id);
                    }
                }
                EventType::Disconnected => {
                    if self.active == Some(event.id) {
                        self.active = None;
                        tracing::info!("gilrs: pad disconnected (id={:?})", event.id);
                        self.active = self.ctx.gamepads().next().map(|(id, _)| id);
                        self.physical_attached = self.active.is_some();
                    }
                }
                _ => {}
            }
        }
    }

    /// Build an Xbox-360-layout `GamepadState` from the active gilrs pad.
    /// Returns `GamepadState::default()` (all-zero) when no pad is present.
    pub fn snapshot(&self) -> GamepadState {
        let Some(id) = self.active else {
            return GamepadState::default();
        };
        let Some(pad) = self.ctx.connected_gamepad(id) else {
            return GamepadState::default();
        };
        let mut state = GamepadState::default();

        let pressed = |btn: Button| pad.is_pressed(btn);
        let set = |state: &mut GamepadState, btn: Button, mask: u16| {
            if pressed(btn) {
                state.buttons |= mask;
            }
        };
        set(&mut state, Button::DPadUp, buttons::DPAD_UP);
        set(&mut state, Button::DPadDown, buttons::DPAD_DOWN);
        set(&mut state, Button::DPadLeft, buttons::DPAD_LEFT);
        set(&mut state, Button::DPadRight, buttons::DPAD_RIGHT);
        set(&mut state, Button::Start, buttons::START);
        set(&mut state, Button::Select, buttons::BACK);
        set(&mut state, Button::LeftThumb, buttons::LEFT_THUMB);
        set(&mut state, Button::RightThumb, buttons::RIGHT_THUMB);
        set(&mut state, Button::LeftTrigger, buttons::LEFT_SHOULDER);
        set(&mut state, Button::RightTrigger, buttons::RIGHT_SHOULDER);
        set(&mut state, Button::Mode, buttons::GUIDE);
        set(&mut state, Button::South, buttons::A);
        set(&mut state, Button::East, buttons::B);
        set(&mut state, Button::West, buttons::X);
        set(&mut state, Button::North, buttons::Y);

        // Triggers: gilrs reports LeftTrigger2 / RightTrigger2 as axes in
        // range [0.0, 1.0]. Map to u8 0..=255.
        let trig = |axis: Axis| -> u8 {
            pad.axis_data(axis)
                .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
                .unwrap_or(0)
        };
        state.left_trigger = pad
            .button_data(Button::LeftTrigger2)
            .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
            .unwrap_or_else(|| trig(Axis::LeftZ));
        state.right_trigger = pad
            .button_data(Button::RightTrigger2)
            .map(|d| (d.value().clamp(0.0, 1.0) * 255.0) as u8)
            .unwrap_or_else(|| trig(Axis::RightZ));

        // Sticks: gilrs reports [-1.0, 1.0]. Xbox uses i16 [-32768, 32767].
        let stick = |axis: Axis| -> i16 {
            let v = pad
                .axis_data(axis)
                .map(|d| d.value().clamp(-1.0, 1.0))
                .unwrap_or(0.0);
            (v * 32767.0) as i16
        };
        state.left_stick_x = stick(Axis::LeftStickX);
        state.left_stick_y = stick(Axis::LeftStickY);
        state.right_stick_x = stick(Axis::RightStickX);
        state.right_stick_y = stick(Axis::RightStickY);

        state
    }
}
