//! [`AxisBinding`] and [`Axis2DBinding`] — directional inputs composed from
//! [`Binding`]s into floats and [`Vec2`]s.
//!
//! A 1D axis pairs a "positive" binding set with a "negative" binding set;
//! each direction held contributes ±1. If both directions are held the
//! contributions cancel and the axis reads 0 — a "soft brake" any third-
//! person camera or twin-stick character controller needs out of the box.
//! Each direction supports several bindings (a WASD axis can also accept
//! arrow keys), and the same physical key can appear in many axes' direction
//! sets.
//!
//! A 2D axis is just a pair of 1D axes (X then Y). Diagonals are
//! intentionally **not** normalized at this layer — some games want
//! Quake-style diagonal speedup, others want unit-length input. Whichever
//! convention a game wants, applying it once at the call site is clearer
//! than having to undo a default at every site that disagrees.

use serde::{Deserialize, Serialize};

use crate::math::Vec2;

use super::{Binding, InputState};

/// One direction of an axis — typically positive (right / forward / up) or
/// negative (left / back / down) — bound to one or more physical inputs.
/// Any binding held contributes a full unit; multiple held bindings on the
/// same direction do not stack.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AxisBinding {
    /// Bindings that pull the axis toward +1.
    pub positive: Vec<Binding>,
    /// Bindings that pull the axis toward -1.
    pub negative: Vec<Binding>,
}

impl AxisBinding {
    /// A new axis with the given direction binding lists.
    pub fn new(
        positive: impl IntoIterator<Item = Binding>,
        negative: impl IntoIterator<Item = Binding>,
    ) -> Self {
        Self {
            positive: positive.into_iter().collect(),
            negative: negative.into_iter().collect(),
        }
    }

    /// Evaluates the axis against `input`. Returns -1, 0, or +1 (the
    /// directions OR'd together — multiple held bindings on the same side
    /// don't stack).
    pub fn value(&self, input: &InputState) -> f32 {
        let pos = self.positive.iter().any(|b| b.held(input));
        let neg = self.negative.iter().any(|b| b.held(input));
        match (pos, neg) {
            (true, false) => 1.0,
            (false, true) => -1.0,
            // Both held → mutual cancel; neither → idle. Same result.
            _ => 0.0,
        }
    }
}

/// A 2D axis composed of two [`AxisBinding`]s (X and Y).
///
/// Output is the unmodified vector `(x.value, y.value)` — diagonals are
/// `(±1, ±1)`, magnitude √2. Normalize at the call site if your game wants
/// unit-length movement.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Axis2DBinding {
    /// The X (right − left) axis.
    pub x: AxisBinding,
    /// The Y (up − down) axis.
    pub y: AxisBinding,
}

impl Axis2DBinding {
    /// A 2D axis from four direction binding lists in the usual order
    /// (`right`, `left`, `up`, `down`).
    pub fn new(
        right: impl IntoIterator<Item = Binding>,
        left: impl IntoIterator<Item = Binding>,
        up: impl IntoIterator<Item = Binding>,
        down: impl IntoIterator<Item = Binding>,
    ) -> Self {
        Self {
            x: AxisBinding::new(right, left),
            y: AxisBinding::new(up, down),
        }
    }

    /// Evaluates the axis against `input`, returning the raw `(x, y)` value
    /// without normalization.
    pub fn value(&self, input: &InputState) -> Vec2 {
        Vec2::new(self.x.value(input), self.y.value(input))
    }
}

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

    fn ad_axis() -> AxisBinding {
        AxisBinding::new([Binding::Key(KeyCode::KeyD)], [Binding::Key(KeyCode::KeyA)])
    }

    #[test]
    fn idle_axis_is_zero() {
        let input = InputState::new();
        assert_eq!(ad_axis().value(&input), 0.0);
    }

    #[test]
    fn positive_direction_returns_plus_one() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyD);
        assert_eq!(ad_axis().value(&input), 1.0);
    }

    #[test]
    fn negative_direction_returns_minus_one() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyA);
        assert_eq!(ad_axis().value(&input), -1.0);
    }

    #[test]
    fn both_directions_held_cancel_to_zero() {
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyA);
        input.press_key(KeyCode::KeyD);
        assert_eq!(
            ad_axis().value(&input),
            0.0,
            "left+right held simultaneously must read as idle"
        );
    }

    #[test]
    fn multi_bindings_on_same_direction_do_not_stack() {
        // WASD + arrow keys both contribute, but holding two positives is
        // still +1 (not +2). The axis is a directional indicator, not an
        // accumulator.
        let axis = AxisBinding::new(
            [
                Binding::Key(KeyCode::KeyD),
                Binding::Key(KeyCode::ArrowRight),
            ],
            [
                Binding::Key(KeyCode::KeyA),
                Binding::Key(KeyCode::ArrowLeft),
            ],
        );
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyD);
        input.press_key(KeyCode::ArrowRight);
        assert_eq!(axis.value(&input), 1.0);
    }

    #[test]
    fn axis_2d_returns_vector_components_independently() {
        let axis = Axis2DBinding::new(
            [Binding::Key(KeyCode::KeyD)],
            [Binding::Key(KeyCode::KeyA)],
            [Binding::Key(KeyCode::KeyW)],
            [Binding::Key(KeyCode::KeyS)],
        );
        let mut input = InputState::new();

        input.press_key(KeyCode::KeyD);
        input.press_key(KeyCode::KeyW);
        assert_eq!(axis.value(&input), Vec2::new(1.0, 1.0));

        input.release_key(KeyCode::KeyD);
        input.press_key(KeyCode::KeyA);
        // Now A + W held.
        assert_eq!(axis.value(&input), Vec2::new(-1.0, 1.0));
    }

    #[test]
    fn axis_2d_diagonal_is_unnormalized() {
        // Diagonals are (±1, ±1) — caller normalizes if it cares.
        let axis = Axis2DBinding::new(
            [Binding::Key(KeyCode::KeyD)],
            [Binding::Key(KeyCode::KeyA)],
            [Binding::Key(KeyCode::KeyW)],
            [Binding::Key(KeyCode::KeyS)],
        );
        let mut input = InputState::new();
        input.press_key(KeyCode::KeyD);
        input.press_key(KeyCode::KeyW);
        let v = axis.value(&input);
        assert!(
            (v.length() - 2_f32.sqrt()).abs() < 1e-6,
            "diagonal must be sqrt(2), got {}",
            v.length()
        );
    }

    #[test]
    fn axis_ron_round_trip() {
        let axis = Axis2DBinding::new(
            [Binding::Key(KeyCode::KeyD)],
            [Binding::Key(KeyCode::KeyA)],
            [Binding::Key(KeyCode::KeyW)],
            [Binding::Key(KeyCode::KeyS)],
        );
        let s = ron::to_string(&axis).unwrap();
        let parsed: Axis2DBinding = ron::from_str(&s).unwrap();
        assert_eq!(parsed, axis);
    }
}
