//! Generalized Hurst Exponent (GHE) estimation
//!
//! Reference: Barabási & Vicsek (1991), "Multifractality of self-affine fractals"
//! Implementation follows PTRRupprecht/GenHurst and MQL5 article conventions.
//!
//! GHE computes H(q) from the scaling of q-th order moments of increments:
//!   K_q(tau) = E[|x(t+tau) - x(t)|^q] ∝ tau^(q*H(q))
//!
//! For monofractal signals, H(q) is constant for all q.

use linreg::linear_regression;

/// GHE with q=1 (absolute moment method).
///
/// More robust to outliers than q=2. Recommended for financial data.
pub fn ghe_q1(x: &[f64]) -> f64 {
    ghe(x, 1.0)
}

/// GHE with q=2 (variance method).
///
/// Equivalent to classical variogram analysis.
pub fn ghe_q2(x: &[f64]) -> f64 {
    ghe(x, 2.0)
}

/// Generalized Hurst Exponent for arbitrary q.
///
/// # Arguments
/// * `x` - Input time series
/// * `q` - Moment order (q=1 for absolute, q=2 for variance)
///
/// # Returns
/// H(q) — generalized Hurst exponent for moment order q
pub fn ghe(x: &[f64], q: f64) -> f64 {
    let n = x.len();
    if n < 8 {
        return 0.5;
    }

    let max_tau = n / 4;
    if max_tau < 2 {
        return 0.5;
    }

    // Geometric spacing of tau values for balanced regression
    let mut taus = Vec::with_capacity(16);
    let mut tau_f = 2.0_f64;
    while (tau_f as usize) <= max_tau {
        let tau = tau_f as usize;
        if taus.last().is_none_or(|&last| last != tau) {
            taus.push(tau);
        }
        tau_f *= 1.25; // ~4-5 taus per octave
    }

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

    let mut log_tau = Vec::with_capacity(taus.len());
    let mut log_kq = Vec::with_capacity(taus.len());

    for &tau in &taus {
        // K_q(tau) = mean(|x[i+tau] - x[i]|^q) for all valid i
        let count = n - tau;
        if count == 0 {
            continue;
        }

        let mut sum = 0.0;
        for i in 0..count {
            let diff = (x[i + tau] - x[i]).abs();
            sum += diff.powf(q);
        }
        let kq = sum / count as f64;

        if kq > 0.0 {
            log_tau.push((tau as f64).ln());
            log_kq.push(kq.ln());
        }
    }

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

    // H(q) = slope / q from log(K_q) vs log(tau)
    let result: Result<(f64, f64), _> = linear_regression(&log_tau, &log_kq);
    match result {
        Ok((slope, _)) => {
            let h: f64 = slope / q;
            if h.is_finite() { h } else { 0.5 }
        }
        Err(_) => 0.5,
    }
}

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

    #[test]
    fn test_ghe_trending() {
        let prices: Vec<f64> = (0..256).map(|i| 100.0 + i as f64 * 0.5).collect();
        let h1 = ghe_q1(&prices);
        let h2 = ghe_q2(&prices);
        assert!(h1 > 0.7, "GHE q=1 trending should be high: {h1}");
        assert!(h2 > 0.7, "GHE q=2 trending should be high: {h2}");
    }

    #[test]
    fn test_ghe_mean_reverting() {
        let prices: Vec<f64> = (0..256)
            .map(|i| if i % 2 == 0 { 100.0 } else { 101.0 })
            .collect();
        let h1 = ghe_q1(&prices);
        assert!(h1 < 0.4, "GHE q=1 mean-reverting should be low: {h1}");
    }

    #[test]
    fn test_ghe_short() {
        assert_eq!(ghe_q1(&[1.0, 2.0]), 0.5);
    }

    #[test]
    fn test_ghe_q1_q2_consistency() {
        // For monofractal (trending), q=1 and q=2 should give similar H
        let prices: Vec<f64> = (0..256).map(|i| 100.0 + i as f64 * 0.1).collect();
        let h1 = ghe_q1(&prices);
        let h2 = ghe_q2(&prices);
        assert!(
            (h1 - h2).abs() < 0.15,
            "GHE q=1 ({h1}) and q=2 ({h2}) should be close for monofractal"
        );
    }
}
