//! Fractional Brownian motion (fBm) generator for benchmark ground truth.
//!
//! Two methods available:
//! - `generate_fbm_davies_harte`: O(n log n) via circulant embedding + FFT (preferred)
//! - `generate_fbm`: O(n²) Cholesky decomposition (fallback)
//!
//! Davies-Harte is ~100x faster for n=1024 and produces statistically
//! identical output. Only used in benchmarks, not production.

use rustfft::{num_complex::Complex, FftPlanner};

/// Generate fractional Brownian motion via Davies-Harte method (O(n log n)).
///
/// Uses circulant embedding of the fGn autocovariance + FFT for exact sampling.
/// This is the recommended method — ~100x faster than Cholesky for n=1024.
///
/// # Arguments
/// * `n` - Length of output series
/// * `h` - Target Hurst exponent (0 < h < 1)
/// * `seed` - Random seed for reproducibility
///
/// # Returns
/// Vec of length `n` representing fBm sample path
pub fn generate_fbm_davies_harte(n: usize, h: f64, seed: u64) -> Vec<f64> {
    if n == 0 {
        return vec![];
    }
    if n == 1 {
        return vec![0.0];
    }

    let fgn = generate_fgn_davies_harte(n, h, seed);

    // Cumulative sum to get fBm
    let mut fbm = Vec::with_capacity(n);
    let mut cumsum = 0.0;
    for &x in &fgn {
        cumsum += x;
        fbm.push(cumsum);
    }
    fbm
}

/// Generate fractional Gaussian noise via Davies-Harte (circulant embedding + FFT).
///
/// Algorithm:
/// 1. Compute autocovariance row of length 2n
/// 2. FFT to get eigenvalues of circulant matrix
/// 3. Check all eigenvalues are non-negative (guaranteed for H ∈ (0,1))
/// 4. Generate random complex Gaussian, multiply by sqrt(eigenvalues)
/// 5. IFFT, take first n real values
#[allow(clippy::many_single_char_names)]
fn generate_fgn_davies_harte(n: usize, h: f64, seed: u64) -> Vec<f64> {
    let two_h = 2.0 * h;
    let m = 2 * n; // Circulant embedding size

    // Build first row of circulant matrix (autocovariance)
    // c[k] = gamma(k) for k=0..n, then c[m-k] = gamma(k) for symmetry
    let mut c = vec![0.0; m];
    for (k, c_val) in c.iter_mut().enumerate().take(n + 1) {
        *c_val = fgn_autocovariance(k, two_h);
    }
    for k in 1..n {
        c[m - k] = c[k];
    }

    // FFT of circulant row → eigenvalues
    let mut planner = FftPlanner::new();
    let fft = planner.plan_fft_forward(m);
    let ifft = planner.plan_fft_inverse(m);

    let mut c_fft: Vec<Complex<f64>> = c.iter().map(|&v| Complex::new(v, 0.0)).collect();
    fft.process(&mut c_fft);

    // Eigenvalues should be real and non-negative for valid fGn
    // Take real part and sqrt
    let sqrt_eigenvalues: Vec<f64> = c_fft
        .iter()
        .map(|c| c.re.max(0.0).sqrt())
        .collect();

    // Generate random complex Gaussian: Z = (N1 + i*N2) / sqrt(2)
    let normals = generate_standard_normal(2 * m, seed);
    let inv_sqrt2 = 1.0 / 2.0_f64.sqrt();
    let mut z: Vec<Complex<f64>> = (0..m)
        .map(|k| {
            Complex::new(
                normals[2 * k] * inv_sqrt2,
                normals[2 * k + 1] * inv_sqrt2,
            )
        })
        .collect();

    // Multiply by sqrt(eigenvalues)
    for (zk, &sv) in z.iter_mut().zip(sqrt_eigenvalues.iter()) {
        *zk *= sv;
    }

    // IFFT
    ifft.process(&mut z);
    let inv_m = 1.0 / m as f64;

    // Take first n real values, scale by sqrt(m) for correct variance
    let sqrt_m = (m as f64).sqrt();
    z.iter().take(n).map(|c| c.re * inv_m * sqrt_m).collect()
}

/// Generate fractional Brownian motion with known Hurst exponent.
///
/// Uses Cholesky decomposition of the fGn (fractional Gaussian noise) covariance matrix,
/// then cumulative sums to produce fBm. O(n²) — use `generate_fbm_davies_harte` for large n.
///
/// # Arguments
/// * `n` - Length of output series
/// * `h` - Target Hurst exponent (0 < h < 1)
/// * `seed` - Random seed for reproducibility
///
/// # Returns
/// Vec of length `n` representing fBm sample path
pub fn generate_fbm(n: usize, h: f64, seed: u64) -> Vec<f64> {
    if n == 0 {
        return vec![];
    }
    if n == 1 {
        return vec![0.0];
    }

    // Generate fGn (fractional Gaussian noise) via Cholesky method
    let fgn = generate_fgn_cholesky(n, h, seed);

    // Cumulative sum to get fBm
    let mut fbm = Vec::with_capacity(n);
    let mut cumsum = 0.0;
    for &x in &fgn {
        cumsum += x;
        fbm.push(cumsum);
    }

    fbm
}

/// Generate fractional Gaussian noise via Cholesky decomposition.
///
/// The autocovariance of fGn is:
///   gamma(k) = 0.5 * (|k-1|^(2H) + |k+1|^(2H) - 2*|k|^(2H))
/// For k=0: gamma(0) = 1
fn generate_fgn_cholesky(n: usize, h: f64, seed: u64) -> Vec<f64> {
    let two_h = 2.0 * h;

    // Build covariance matrix
    let mut cov = vec![0.0; n * n];
    for i in 0..n {
        for j in 0..n {
            let k = i.abs_diff(j);
            cov[i * n + j] = fgn_autocovariance(k, two_h);
        }
    }

    // Cholesky decomposition: cov = L * L^T
    let l = cholesky_decompose(&cov, n);

    // Generate standard normal samples using Box-Muller transform with LCG
    let z = generate_standard_normal(n, seed);

    // fGn = L * z
    let mut fgn = vec![0.0; n];
    for i in 0..n {
        let mut sum = 0.0;
        for j in 0..=i {
            sum += l[i * n + j] * z[j];
        }
        fgn[i] = sum;
    }

    fgn
}

/// fGn autocovariance: gamma(k) = 0.5 * (|k-1|^(2H) + |k+1|^(2H) - 2*|k|^(2H))
#[inline]
fn fgn_autocovariance(k: usize, two_h: f64) -> f64 {
    if k == 0 {
        return 1.0;
    }
    let kf = k as f64;
    0.5 * ((kf - 1.0).abs().powf(two_h) + (kf + 1.0).powf(two_h) - 2.0 * kf.powf(two_h))
}

/// Cholesky decomposition of a positive-definite matrix.
/// Returns lower-triangular L such that A = L * L^T.
fn cholesky_decompose(a: &[f64], n: usize) -> Vec<f64> {
    let mut l = vec![0.0; n * n];

    for j in 0..n {
        let mut sum = 0.0;
        for k in 0..j {
            sum += l[j * n + k] * l[j * n + k];
        }
        let diag = a[j * n + j] - sum;
        // Numerical safety: clamp to small positive value
        l[j * n + j] = diag.max(1e-15).sqrt();

        for i in (j + 1)..n {
            let mut sum = 0.0;
            for k in 0..j {
                sum += l[i * n + k] * l[j * n + k];
            }
            l[i * n + j] = (a[i * n + j] - sum) / l[j * n + j];
        }
    }

    l
}

/// Generate n standard normal random variates using Box-Muller + LCG.
fn generate_standard_normal(n: usize, seed: u64) -> Vec<f64> {
    let mut result = Vec::with_capacity(n);
    let mut state = seed;

    // Generate pairs using Box-Muller
    let pairs = n.div_ceil(2);
    for _ in 0..pairs {
        let u1 = lcg_uniform(&mut state);
        let u2 = lcg_uniform(&mut state);

        let r = (-2.0 * u1.ln()).sqrt();
        let theta = 2.0 * std::f64::consts::PI * u2;

        result.push(r * theta.cos());
        if result.len() < n {
            result.push(r * theta.sin());
        }
    }

    result.truncate(n);
    result
}

/// Linear congruential generator → uniform (0, 1)
#[inline]
fn lcg_uniform(state: &mut u64) -> f64 {
    *state = state
        .wrapping_mul(6_364_136_223_846_793_005)
        .wrapping_add(1_442_695_040_888_963_407);
    // Map to (0, 1) — avoid exact 0 for log safety
    ((*state >> 11) as f64 + 0.5) / ((1u64 << 53) as f64)
}

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

    #[test]
    fn test_fbm_length() {
        let fbm = generate_fbm(100, 0.5, 42);
        assert_eq!(fbm.len(), 100);
    }

    #[test]
    fn test_fbm_empty() {
        assert!(generate_fbm(0, 0.5, 42).is_empty());
    }

    #[test]
    fn test_fbm_single() {
        let fbm = generate_fbm(1, 0.5, 42);
        assert_eq!(fbm.len(), 1);
        assert_eq!(fbm[0], 0.0);
    }

    #[test]
    fn test_fbm_finite() {
        for &h in &[0.2, 0.3, 0.5, 0.7, 0.8] {
            let fbm = generate_fbm(128, h, 42);
            for (i, &val) in fbm.iter().enumerate() {
                assert!(val.is_finite(), "fBm({h}) index {i} is not finite: {val}");
            }
        }
    }

    #[test]
    fn test_fbm_reproducible() {
        let a = generate_fbm(64, 0.7, 123);
        let b = generate_fbm(64, 0.7, 123);
        assert_eq!(a, b, "Same seed must produce identical output");
    }

    #[test]
    fn test_fbm_different_seeds() {
        let a = generate_fbm(64, 0.7, 1);
        let b = generate_fbm(64, 0.7, 2);
        assert_ne!(a, b, "Different seeds should produce different output");
    }

    // Davies-Harte tests
    #[test]
    fn test_davies_harte_length() {
        let fbm = generate_fbm_davies_harte(100, 0.5, 42);
        assert_eq!(fbm.len(), 100);
    }

    #[test]
    fn test_davies_harte_empty() {
        assert!(generate_fbm_davies_harte(0, 0.5, 42).is_empty());
    }

    #[test]
    fn test_davies_harte_single() {
        let fbm = generate_fbm_davies_harte(1, 0.5, 42);
        assert_eq!(fbm.len(), 1);
        assert_eq!(fbm[0], 0.0);
    }

    #[test]
    fn test_davies_harte_finite() {
        for &h in &[0.2, 0.3, 0.5, 0.7, 0.8] {
            let fbm = generate_fbm_davies_harte(128, h, 42);
            for (i, &val) in fbm.iter().enumerate() {
                assert!(
                    val.is_finite(),
                    "DH fBm({h}) index {i} is not finite: {val}"
                );
            }
        }
    }

    #[test]
    fn test_davies_harte_reproducible() {
        let a = generate_fbm_davies_harte(64, 0.7, 123);
        let b = generate_fbm_davies_harte(64, 0.7, 123);
        assert_eq!(a, b, "Same seed must produce identical output");
    }

    #[test]
    fn test_davies_harte_produces_valid_fbm() {
        // Verify DH generates finite, non-constant fBm for various H values.
        // Note: R/S Simple has known ~0.3 bias on raw fBm at n=512
        // (documented in #244 benchmark). We verify monotonicity ordering instead.
        use crate::rssimple;

        let mut mean_by_h: Vec<(f64, f64)> = Vec::new();
        for &h_true in &[0.3, 0.5, 0.7] {
            let mut h_estimates = Vec::new();
            for seed in 0..50 {
                let fbm = generate_fbm_davies_harte(512, h_true, seed);
                assert!(
                    fbm.iter().all(|v| v.is_finite()),
                    "DH fBm H={h_true} has non-finite values"
                );
                h_estimates.push(rssimple(&fbm));
            }
            let mean_h = h_estimates.iter().sum::<f64>() / h_estimates.len() as f64;
            mean_by_h.push((h_true, mean_h));
        }

        // Monotonicity: higher true H should produce higher estimated H
        assert!(
            mean_by_h[2].1 > mean_by_h[0].1,
            "DH fBm H=0.7 ({:.3}) should estimate higher than H=0.3 ({:.3})",
            mean_by_h[2].1,
            mean_by_h[0].1
        );
    }
}
