//! Haar wavelet variance method for Hurst exponent estimation
//!
//! Reference: Flandrin (1992), "Wavelet analysis and synthesis of fractional Brownian motion"
//!
//! Uses Haar DWT via lifting scheme (in-place, O(n)) to estimate H from
//! the decay of wavelet variance across scales.
//!
//! H = (beta + 1) / 2, where beta is the slope of log2(var_j) vs scale j.

use linreg::linear_regression;

/// Estimate Hurst exponent via Haar wavelet variance.
///
/// O(n) complexity via pyramidal Haar DWT.
///
/// # Arguments
/// * `x` - Input time series
///
/// # Returns
/// Hurst exponent estimate
pub fn hurst_wavelet(x: &[f64]) -> f64 {
    let n = x.len();
    if n < 8 {
        return 0.5;
    }

    // Work on a copy — the lifting scheme is in-place
    let max_j = (n as f64).log2().floor() as usize;
    // Use only the first 2^max_j samples for clean dyadic decomposition
    let usable = 1 << max_j;
    if usable < 4 {
        return 0.5;
    }

    let mut data: Vec<f64> = x[..usable].to_vec();

    let mut scales = Vec::with_capacity(max_j);
    let mut log_var = Vec::with_capacity(max_j);

    let mut current_len = usable;

    for j in 1..=max_j {
        let half = current_len / 2;
        if half == 0 {
            break;
        }

        // Haar lifting: compute detail and approximation coefficients in-place
        let mut detail_var_sum = 0.0;
        for k in 0..half {
            let a = data[2 * k];
            let b = data[2 * k + 1];
            let detail = (a - b) * std::f64::consts::FRAC_1_SQRT_2;
            let approx = (a + b) * std::f64::consts::FRAC_1_SQRT_2;
            detail_var_sum += detail * detail;
            data[k] = approx; // Store approximation for next level
        }

        let variance = detail_var_sum / half as f64;
        if variance > 0.0 {
            scales.push(j as f64);
            log_var.push(variance.log2());
        }

        current_len = half;
    }

    if scales.len() < 3 {
        return 0.5;
    }

    // Linear regression of log2(variance) vs scale j
    // Slope beta relates to H: H = (beta + 1) / 2
    // But for Haar wavelet on fGn: var_j ∝ 2^(2H-1)j, so log2(var_j) = (2H-1)*j + const
    // => slope = 2H - 1, => H = (slope + 1) / 2
    let result: Result<(f64, f64), _> = linear_regression(&scales, &log_var);
    match result {
        Ok((slope, _)) => {
            #[allow(clippy::manual_midpoint)] // Not a midpoint — H = (slope + 1) / 2
            let h: f64 = (slope + 1.0) / 2.0;
            if h.is_finite() { h } else { 0.5 }
        }
        Err(_) => 0.5,
    }
}

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

    #[test]
    fn test_wavelet_trending() {
        let prices: Vec<f64> = (0..256).map(|i| 100.0 + i as f64 * 0.5).collect();
        let h = hurst_wavelet(&prices);
        assert!(
            h > 0.5,
            "Trending series should have H > 0.5 via wavelet: {h}"
        );
    }

    #[test]
    fn test_wavelet_mean_reverting() {
        // Use a longer-period mean-reverting pattern (period 4) to give
        // wavelet variance multiple scales to work with
        let prices: Vec<f64> = (0..256)
            .map(|i| match i % 4 {
                0 => 100.0,
                1 => 101.0,
                2 => 100.5,
                _ => 99.5,
            })
            .collect();
        let h = hurst_wavelet(&prices);
        assert!(
            h <= 0.5,
            "Mean-reverting series should have H <= 0.5 via wavelet: {h}"
        );
    }

    #[test]
    fn test_wavelet_short() {
        let h = hurst_wavelet(&[1.0, 2.0, 3.0]);
        assert_eq!(h, 0.5, "Short series should return 0.5");
    }

    #[test]
    fn test_wavelet_constant() {
        let prices = vec![100.0; 128];
        let h = hurst_wavelet(&prices);
        assert!(h.is_finite(), "Constant series should be finite: {h}");
    }
}
