//! A synthetic acoustic echo path — the model the AEC test harness uses in place
//! of real speakers + a room + a microphone.
//!
//! When you're on a speakerphone call, the far-end voice comes out of your
//! speaker, bounces around the room, and re-enters your mic delayed, coloured,
//! and attenuated. That transformation is a linear filter: an **impulse
//! response** (IR). Convolving the far-end signal with an IR reproduces the echo
//! a mic would capture — fully deterministically, so the canceller can be
//! measured headless against ground truth.
//!
//! The IR here is `delay` samples of silence (bulk acoustic propagation /
//! buffering latency) followed by an exponentially-decaying diffuse tail (the
//! room reverb), scaled so the whole echo sits `~attenuation` below the far-end.

use super::generators;

/// A linear echo path expressed as an impulse response.
pub struct EchoPath {
    /// The impulse response; `echo[n] = Σ_k ir[k]·far[n-k]`.
    pub ir: Vec<f32>,
}

impl EchoPath {
    /// Builds the path directly from a caller-supplied impulse response.
    pub fn from_ir(ir: Vec<f32>) -> Self {
        EchoPath { ir }
    }

    /// A synthetic room echo: `delay` samples of pure delay, then a `tail`-sample
    /// exponentially-decaying diffuse reflection cluster. `attenuation` is the
    /// peak linear gain of the path (e.g. `0.5` ≈ -6 dB echo). `seed` makes the
    /// diffuse tail reproducible.
    ///
    /// A strong early reflection is placed at the delay (the dominant first bounce)
    /// followed by a decaying noisy tail, then the whole IR is normalized so its
    /// energy gain matches `attenuation` — a realistic, non-trivial path for the
    /// adaptive filter to identify.
    pub fn synthetic(delay: usize, tail: usize, attenuation: f32, seed: u64) -> Self {
        let tail = tail.max(1);
        let mut ir = vec![0.0f32; delay + tail];
        let noise = generators::white_noise(1.0, tail, seed);
        // Time constant: decay to ~e^-4 (≈1.8%) by the end of the tail.
        let tau = tail as f32 / 4.0;
        for i in 0..tail {
            let env = (-(i as f32) / tau).exp();
            // Dominant direct reflection at i=0, diffuse decaying noise after.
            let direct = if i == 0 { 1.0 } else { 0.4 * noise[i] };
            ir[delay + i] = env * direct;
        }
        // Normalize so the path's RMS gain equals `attenuation` (energy-based, so
        // ERLE targets are predictable regardless of delay/tail choices).
        let energy: f32 = ir.iter().map(|&x| x * x).sum::<f32>().sqrt();
        if energy > 1e-9 {
            let scale = attenuation / energy;
            for x in &mut ir {
                *x *= scale;
            }
        }
        EchoPath { ir }
    }

    /// Number of taps in the impulse response (its total length).
    pub fn len(&self) -> usize {
        self.ir.len()
    }

    pub fn is_empty(&self) -> bool {
        self.ir.is_empty()
    }

    /// Convolves `far` with the impulse response, returning the echo signal the
    /// mic would capture. Output length matches `far` (the convolution tail past
    /// the input end is truncated — the far-end keeps going in a real call).
    pub fn apply(&self, far: &[f32]) -> Vec<f32> {
        let n = far.len();
        let m = self.ir.len();
        let mut echo = vec![0.0f32; n];
        for (k, &h) in self.ir.iter().enumerate() {
            if h == 0.0 {
                continue;
            }
            // echo[i] += h * far[i-k] for all valid i.
            for i in k..n {
                echo[i] += h * far[i - k];
            }
        }
        let _ = m;
        echo
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::dsp::metrics::rms;

    #[test]
    fn pure_delay_shifts_the_signal() {
        // IR = single unit tap at index 3 -> echo is far delayed by 3 samples.
        let path = EchoPath::from_ir({
            let mut ir = vec![0.0; 4];
            ir[3] = 1.0;
            ir
        });
        let far = vec![1.0, 2.0, 3.0, 4.0, 5.0];
        let echo = path.apply(&far);
        assert_eq!(echo, vec![0.0, 0.0, 0.0, 1.0, 2.0]);
    }

    #[test]
    fn synthetic_path_attenuates_to_target() {
        // A white-noise far-end through a path normalized to 0.5 RMS gain should
        // produce an echo roughly 0.5x the far-end RMS (≈ -6 dB).
        let far = generators::white_noise(0.5, 48_000, 1);
        let path = EchoPath::synthetic(480, 480, 0.5, 99);
        let echo = path.apply(&far);
        let ratio = rms(&echo) / rms(&far);
        assert!(
            (0.3..0.7).contains(&ratio),
            "echo/far rms ratio {ratio} off target"
        );
    }

    #[test]
    fn synthetic_path_has_leading_delay() {
        let path = EchoPath::synthetic(100, 200, 0.5, 5);
        // The first `delay` IR taps are silent (pure propagation delay).
        assert!(path.ir[..100].iter().all(|&x| x == 0.0));
        assert!(path.ir[100..].iter().any(|&x| x != 0.0));
        assert_eq!(path.len(), 300);
    }
}
