//! A small, dependency-free radix-2 Cooley-Tukey FFT.
//!
//! We hand-roll this rather than pull in `rustfft`/`realfft` because (a) the
//! whole DSP toolkit is a developer/measurement aid, not a hot real-time path,
//! and (b) it keeps the supply-chain surface at zero new crates. The transform
//! is the textbook iterative in-place algorithm (bit-reversal permutation +
//! log2(N) butterfly stages), computed in `f64` for headroom even though the
//! audio it analyses is `f32`.
//!
//! Only power-of-two lengths are supported; the STFT layer always pads frames
//! up to a power of two before calling in.

use std::f64::consts::PI;

/// A minimal complex number for the transform. Kept local (rather than pulling a
/// `num-complex` dependency) since the FFT is the only thing that needs it.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Complex {
    pub re: f64,
    pub im: f64,
}

impl Complex {
    pub const ZERO: Complex = Complex { re: 0.0, im: 0.0 };

    pub fn new(re: f64, im: f64) -> Self {
        Complex { re, im }
    }

    /// Magnitude `sqrt(re^2 + im^2)`.
    pub fn magnitude(self) -> f64 {
        self.re.hypot(self.im)
    }

    fn add(self, o: Complex) -> Complex {
        Complex::new(self.re + o.re, self.im + o.im)
    }

    fn sub(self, o: Complex) -> Complex {
        Complex::new(self.re - o.re, self.im - o.im)
    }

    fn mul(self, o: Complex) -> Complex {
        Complex::new(
            self.re * o.re - self.im * o.im,
            self.re * o.im + self.im * o.re,
        )
    }
}

/// In-place forward FFT. `buf.len()` must be a power of two.
///
/// Uses the standard sign convention `X[k] = sum_n x[n] * exp(-2πi·kn/N)`.
pub fn fft(buf: &mut [Complex]) {
    transform(buf, false);
}

/// In-place inverse FFT (normalized by `1/N`), the exact inverse of [`fft`].
pub fn ifft(buf: &mut [Complex]) {
    transform(buf, true);
    let n = buf.len() as f64;
    for c in buf.iter_mut() {
        c.re /= n;
        c.im /= n;
    }
}

fn transform(buf: &mut [Complex], inverse: bool) {
    let n = buf.len();
    assert!(n.is_power_of_two(), "FFT length {n} must be a power of two");
    if n <= 1 {
        return;
    }

    // Bit-reversal permutation: reorder so the iterative butterflies can run
    // bottom-up in place.
    let mut j = 0usize;
    for i in 1..n {
        let mut bit = n >> 1;
        while j & bit != 0 {
            j ^= bit;
            bit >>= 1;
        }
        j ^= bit;
        if i < j {
            buf.swap(i, j);
        }
    }

    // Butterfly stages: combine length-`len` DFTs from length-`len/2` halves,
    // doubling `len` each pass.
    let sign = if inverse { 1.0 } else { -1.0 };
    let mut len = 2;
    while len <= n {
        let ang = sign * 2.0 * PI / len as f64;
        let wlen = Complex::new(ang.cos(), ang.sin());
        let mut i = 0;
        while i < n {
            let mut w = Complex::new(1.0, 0.0);
            for k in 0..len / 2 {
                let u = buf[i + k];
                let v = buf[i + k + len / 2].mul(w);
                buf[i + k] = u.add(v);
                buf[i + k + len / 2] = u.sub(v);
                w = w.mul(wlen);
            }
            i += len;
        }
        len <<= 1;
    }
}

/// Forward FFT of a real signal, returning the **one-sided** magnitude spectrum:
/// bins `0..=N/2` (DC through Nyquist), where `N` is the next power of two ≥
/// `samples.len()`. The input is zero-padded up to `N`.
///
/// Magnitudes are raw (un-normalized) linear amplitudes; callers convert to dB
/// or normalize as needed.
pub fn real_magnitude_spectrum(samples: &[f32]) -> Vec<f32> {
    let n = samples.len().next_power_of_two().max(2);
    let mut buf = vec![Complex::ZERO; n];
    for (i, &s) in samples.iter().enumerate() {
        buf[i].re = s as f64;
    }
    fft(&mut buf);
    buf[..=n / 2].iter().map(|c| c.magnitude() as f32).collect()
}

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

    fn approx(a: f64, b: f64, eps: f64) -> bool {
        (a - b).abs() <= eps
    }

    #[test]
    fn impulse_transforms_to_flat_spectrum() {
        // FFT of a unit impulse at n=0 is all-ones (flat spectrum).
        let mut buf = vec![Complex::ZERO; 8];
        buf[0] = Complex::new(1.0, 0.0);
        fft(&mut buf);
        for c in &buf {
            assert!(
                approx(c.magnitude(), 1.0, 1e-9),
                "expected flat 1.0, got {c:?}"
            );
        }
    }

    #[test]
    fn single_bin_sine_peaks_in_that_bin() {
        // A cosine at exactly bin k=2 over N=16 should put all energy in bin 2
        // (and its mirror N-2). Check the one-sided spectrum peaks at bin 2.
        let n = 16;
        let k = 2;
        let samples: Vec<f32> = (0..n)
            .map(|i| (2.0 * PI * k as f64 * i as f64 / n as f64).cos() as f32)
            .collect();
        let mag = real_magnitude_spectrum(&samples);
        let peak_bin = mag
            .iter()
            .enumerate()
            .max_by(|a, b| a.1.partial_cmp(b.1).unwrap())
            .unwrap()
            .0;
        assert_eq!(peak_bin, k, "energy should land in bin {k}, got {peak_bin}");
    }

    #[test]
    fn ifft_inverts_fft() {
        let original: Vec<Complex> = (0..32)
            .map(|i| Complex::new((i as f64 * 0.3).sin(), (i as f64 * 0.1).cos()))
            .collect();
        let mut buf = original.clone();
        fft(&mut buf);
        ifft(&mut buf);
        for (a, b) in original.iter().zip(&buf) {
            assert!(approx(a.re, b.re, 1e-9) && approx(a.im, b.im, 1e-9));
        }
    }

    #[test]
    #[should_panic(expected = "power of two")]
    fn non_power_of_two_panics() {
        let mut buf = vec![Complex::ZERO; 6];
        fft(&mut buf);
    }
}
