//! Listener-side stereo pan law.
//!
//! Capture, Opus, and the network stay mono. These helpers are used only after a
//! peer has been decoded locally, just before the playout mix is written to the
//! stereo playback bus.

/// Clamp and compute constant-power pan gains for `pan` in `[-1.0, 1.0]`.
///
/// - `-1.0` is hard left `(1, 0)`
/// - `0.0` is center `(sqrt(1/2), sqrt(1/2))`
/// - `1.0` is hard right `(0, 1)`
pub fn pan_gains(pan: f32) -> (f32, f32) {
    let pan = pan.clamp(-1.0, 1.0);
    let theta = (pan + 1.0) * std::f32::consts::FRAC_PI_4;
    (theta.cos(), theta.sin())
}

/// Gains used by the legacy-compatible playback mixer.
///
/// The pure law above is constant-power. The existing application, however, was
/// mono and users heard the full old mono signal in both ears. Scaling by sqrt(2)
/// makes `pan = 0` exactly dual-mono `(1, 1)`, preserving the default sound while
/// still following the same equal-power curve as a peer is moved away from center.
pub fn playback_pan_gains(pan: f32) -> (f32, f32) {
    let (left, right) = pan_gains(pan);
    (
        left * std::f32::consts::SQRT_2,
        right * std::f32::consts::SQRT_2,
    )
}

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

    const EPS: f32 = 1.0e-6;

    #[test]
    fn hard_left_and_right_are_endpoints() {
        assert_eq!(pan_gains(-1.0), (1.0, 0.0));
        let (l, r) = pan_gains(1.0);
        assert!(
            l.abs() < EPS,
            "left at hard-right should be zero-ish, got {l}"
        );
        assert!(
            (r - 1.0).abs() < EPS,
            "right at hard-right should be one, got {r}"
        );
    }

    #[test]
    fn center_is_equal_and_power_preserving() {
        let (l, r) = pan_gains(0.0);
        assert!((l - r).abs() < EPS);
        assert!((l - std::f32::consts::FRAC_1_SQRT_2).abs() < EPS);
        assert!(((l * l + r * r) - 1.0).abs() < EPS);
    }

    #[test]
    fn gains_move_monotonically() {
        let pans = [-1.0, -0.5, 0.0, 0.5, 1.0];
        let mut prev_l = f32::INFINITY;
        let mut prev_r = f32::NEG_INFINITY;
        for pan in pans {
            let (l, r) = pan_gains(pan);
            assert!(
                l <= prev_l + EPS,
                "left gain must not rise as pan moves right"
            );
            assert!(
                r >= prev_r - EPS,
                "right gain must not fall as pan moves right"
            );
            prev_l = l;
            prev_r = r;
        }
    }

    #[test]
    fn playback_center_preserves_legacy_dual_mono() {
        let (l, r) = playback_pan_gains(0.0);
        assert!((l - 1.0).abs() < EPS);
        assert!((r - 1.0).abs() < EPS);
    }

    #[test]
    fn input_is_clamped() {
        assert_eq!(pan_gains(-9.0), pan_gains(-1.0));
        assert_eq!(pan_gains(9.0), pan_gains(1.0));
    }
}
