/// Diagnostics returned by render downmix helpers.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub(crate) struct RenderDownmixStats {
    /// Peak absolute sample magnitude after i16 conversion.
    pub peak_i16: i16,
    /// Samples clipped while applying output gain.
    pub clipped_samples: u64,
}

/// Downmix interleaved stereo f32 samples into mono i16 samples.
///
/// The helper is allocation-free and safe for realtime render callbacks.
/// If the stereo source is shorter than expected, the remainder of `out`
/// is filled with silence.
#[cfg(any(target_os = "ios", test))]
pub(crate) fn downmix_stereo_f32_to_mono_i16(
    stereo: &[f32],
    out: &mut [i16],
    gain: f32,
    muted: bool,
) -> RenderDownmixStats {
    if muted {
        out.fill(0);
        return RenderDownmixStats::default();
    }

    let available_frames = stereo.len() / 2;
    if available_frames < out.len() {
        out.fill(0);
    }

    let mut peak = 0_u16;
    let mut clipped_samples = 0_u64;
    for (dst, lr) in out.iter_mut().zip(stereo.chunks_exact(2)) {
        let mono = (lr[0] + lr[1]) * 0.5 * gain;
        let clamped = mono.clamp(-1.0, 1.0);
        if (mono - clamped).abs() > f32::EPSILON {
            clipped_samples = clipped_samples.saturating_add(1);
        }
        let sample = (clamped * i16::MAX as f32) as i16;
        *dst = sample;
        peak = peak.max(sample.unsigned_abs());
    }

    RenderDownmixStats {
        peak_i16: peak.min(i16::MAX as u16) as i16,
        clipped_samples,
    }
}

pub(crate) fn downmix_stereo_f32_to_interleaved_i16(
    stereo: &[f32],
    out: &mut [i16],
    out_channels: usize,
    gain: f32,
    muted: bool,
) -> RenderDownmixStats {
    if out_channels == 0 {
        out.fill(0);
        return RenderDownmixStats::default();
    }

    if muted {
        out.fill(0);
        return RenderDownmixStats::default();
    }

    let available_frames = stereo.len() / 2;
    let requested_frames = out.len() / out_channels;
    if available_frames < requested_frames {
        out.fill(0);
    }

    let mut peak = 0_u16;
    let mut clipped_samples = 0_u64;
    for (dst_frame, lr) in out
        .chunks_exact_mut(out_channels)
        .zip(stereo.chunks_exact(2))
    {
        let mono = (lr[0] + lr[1]) * 0.5 * gain;
        let clamped = mono.clamp(-1.0, 1.0);
        if (mono - clamped).abs() > f32::EPSILON {
            clipped_samples = clipped_samples.saturating_add(1);
        }
        let sample = (clamped * i16::MAX as f32) as i16;
        for dst in dst_frame.iter_mut() {
            *dst = sample;
        }
        peak = peak.max(sample.unsigned_abs());
    }

    RenderDownmixStats {
        peak_i16: peak.min(i16::MAX as u16) as i16,
        clipped_samples,
    }
}

/// Downmix interleaved stereo f32 samples into mono f32 samples.
///
/// Used for software-AEC render references and debug WAV taps.
#[cfg(any(target_os = "ios", test))]
pub(crate) fn downmix_stereo_f32_to_mono_f32(stereo: &[f32], out: &mut [f32]) {
    let available_frames = stereo.len() / 2;
    if available_frames < out.len() {
        out.fill(0.0);
    }

    for (dst, lr) in out.iter_mut().zip(stereo.chunks_exact(2)) {
        *dst = (lr[0] + lr[1]) * 0.5;
    }
}

/// In-place per-frame peak limiter. Scales the entire buffer so the
/// absolute peak equals `threshold`; returns the applied gain (1.0 =
/// no reduction). Used on the render path between `AudioHandler` and
/// the i16 downmix to prevent hard clipping when a multi-client mix
/// exceeds 0 dBFS. Per-frame scaling is sub-millisecond at 48 kHz, so
/// the pumping risk is negligible for speech; a look-ahead design was
/// rejected because it would add latency on top of the existing
/// jitter buffer.
pub(crate) fn limit_peak_inplace(samples: &mut [f32], threshold: f32) -> f32 {
    if threshold <= 0.0 || !threshold.is_finite() {
        return 1.0;
    }
    let peak = samples
        .iter()
        .map(|s| s.abs())
        .fold(0.0_f32, f32::max);
    if peak <= threshold {
        return 1.0;
    }
    let gain = threshold / peak;
    for s in samples.iter_mut() {
        *s *= gain;
    }
    gain
}

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

    #[test]
    fn downmix_i16_applies_gain_and_reports_clipping() {
        let stereo = [1.0_f32, 1.0, 0.25, -0.25, -2.0, -2.0];
        let mut out = [0_i16; 3];

        let stats = downmix_stereo_f32_to_mono_i16(&stereo, &mut out, 2.0, false);

        assert_eq!(out[0], i16::MAX);
        assert_eq!(out[1], 0);
        assert_eq!(out[2], -i16::MAX);
        assert_eq!(stats.peak_i16, i16::MAX);
        assert_eq!(stats.clipped_samples, 2);
    }

    #[test]
    fn downmix_i16_mutes_output() {
        let stereo = [1.0_f32, 1.0, -1.0, -1.0];
        let mut out = [123_i16; 2];

        let stats = downmix_stereo_f32_to_mono_i16(&stereo, &mut out, 1.0, true);

        assert_eq!(out, [0, 0]);
        assert_eq!(stats, RenderDownmixStats::default());
    }

    #[test]
    fn downmix_interleaved_i16_copies_mono_to_each_channel() {
        let stereo = [1.0_f32, -1.0, 0.25, 0.25];
        let mut out = [0_i16; 4];

        let stats = downmix_stereo_f32_to_interleaved_i16(&stereo, &mut out, 2, 1.0, false);

        assert_eq!(out, [0, 0, 8191, 8191]);
        assert_eq!(stats.peak_i16, 8191);
        assert_eq!(stats.clipped_samples, 0);
    }

    #[test]
    fn downmix_interleaved_i16_mutes_all_channels() {
        let stereo = [1.0_f32, 1.0, -1.0, -1.0];
        let mut out = [123_i16; 6];

        let stats = downmix_stereo_f32_to_interleaved_i16(&stereo, &mut out, 3, 1.0, true);

        assert_eq!(out, [0, 0, 0, 0, 0, 0]);
        assert_eq!(stats, RenderDownmixStats::default());
    }

    #[test]
    fn downmix_f32_fills_missing_tail_with_silence() {
        let stereo = [1.0_f32, -1.0];
        let mut out = [9.0_f32; 2];

        downmix_stereo_f32_to_mono_f32(&stereo, &mut out);

        assert_eq!(out, [0.0, 0.0]);
    }

    #[test]
    fn limit_peak_is_noop_below_threshold() {
        let mut samples = [0.1_f32, -0.2, 0.3, -0.4];
        let gain = limit_peak_inplace(&mut samples, 0.95);
        assert_eq!(gain, 1.0);
        assert_eq!(samples, [0.1, -0.2, 0.3, -0.4]);
    }

    #[test]
    fn limit_peak_scales_above_threshold() {
        let mut samples = [0.5_f32, 1.0, 2.0, -1.5];
        let gain = limit_peak_inplace(&mut samples, 0.95);
        assert!((gain - 0.475).abs() < 1e-6, "gain = {gain}");
        assert!((samples[0] - 0.2375).abs() < 1e-6);
        assert!((samples[1] - 0.475).abs() < 1e-6);
        assert!((samples[2] - 0.95).abs() < 1e-6);
        assert!((samples[3] - (-0.7125)).abs() < 1e-6);
    }

    #[test]
    fn limit_peak_handles_zero_and_invalid_thresholds() {
        let mut samples = [0.5_f32, 1.0];
        assert_eq!(limit_peak_inplace(&mut samples, 0.0), 1.0);
        assert_eq!(samples, [0.5, 1.0]);
        assert_eq!(limit_peak_inplace(&mut samples, -1.0), 1.0);
        assert_eq!(samples, [0.5, 1.0]);
        assert_eq!(limit_peak_inplace(&mut samples, f32::NAN), 1.0);
        assert_eq!(samples, [0.5, 1.0]);
    }

    #[test]
    fn limit_peak_then_downmix_produces_no_clipping() {
        // Regression: multi-client mix previously hard-clamped to i16::MAX.
        let mut scratch = [1.0_f32, 1.0, -0.5, -0.5, 0.8, 0.8];
        limit_peak_inplace(&mut scratch, 0.95);
        let mut out = [0_i16; 3];
        let stats = downmix_stereo_f32_to_mono_i16(&scratch, &mut out, 1.0, false);
        assert_eq!(stats.clipped_samples, 0);
        assert!(stats.peak_i16 < i16::MAX);
    }
}
