use crate::distributions::Distribution;
use num::Complex;
use statrs::function::gamma::{digamma, gamma, gamma_li};
use RustQuant_error::RustQuantError;
pub struct ChiSquared {
        k: usize,
}
impl Default for ChiSquared {
        fn default() -> Self {
        Self { k: 1 }
    }
}
impl ChiSquared {
                                                        #[must_use]
    pub fn new(k: usize) -> Self {
        assert!(k > 0);
        Self { k }
    }
}
impl Distribution for ChiSquared {
                                                        fn cf(&self, t: f64) -> Complex<f64> {
        let i: Complex<f64> = Complex::i();
        let k = self.k;
        (1.0 - 2.0 * i * t).powf(-(k as f64 / 2.0))
    }
                                            fn pdf(&self, x: f64) -> f64 {
        assert!(if self.k == 1 { x > 0.0 } else { x >= 0.0 });
        let k = self.k;
        x.powf((k as f64 / 2.0) - 1.0) * (-x / 2.0).exp()
            / (2_f64.powf(k as f64 / 2.0) * gamma(k as f64 / 2.0))
    }
                                                    fn pmf(&self, x: f64) -> f64 {
        self.pdf(x)
    }
                                            fn cdf(&self, x: f64) -> f64 {
        assert!(if self.k == 1 { x > 0.0 } else { x >= 0.0 });
        let k = self.k;
        gamma_li(k as f64 / 2.0, x / 2.0) / gamma(k as f64 / 2.0)
    }
                                            fn inv_cdf(&self, p: f64) -> f64 {
        assert!((0.0..=1.0).contains(&p));
        let k = self.k as f64;
        let mut x = 0.5 * k;
        let mut delta = 0.5 * k;
        while delta > 1e-10 {
            let cdf = self.cdf(x);
            if cdf < p {
                x += delta;
            } else {
                x -= delta;
            }
            delta *= 0.5;
        }
        x
    }
                                        fn mean(&self) -> f64 {
        self.k as f64
    }
                                            fn median(&self) -> f64 {
        self.k as f64 * (1.0 - (2.0 / (9.0 * self.k as f64))).powf(3.0)
    }
                                        fn mode(&self) -> f64 {
        0_f64.max(self.k as f64 - 2.0)
    }
                                        fn variance(&self) -> f64 {
        2.0 * self.k as f64
    }
                                            fn skewness(&self) -> f64 {
        (8.0 / self.k as f64).sqrt()
    }
                                    fn kurtosis(&self) -> f64 {
        12.0 / self.k as f64
    }
                                            fn entropy(&self) -> f64 {
        let k = self.k as f64;
        k / 2.0 + (2.0 * gamma(k / 2.0)).ln() + (1.0 - k / 2.0) * digamma(k / 2.0)
    }
                                            fn mgf(&self, t: f64) -> f64 {
        assert!(t < 0.5);
        (1.0 - 2.0 * t).powf(-(self.k as f64) / 2.0)
    }
                                                        fn sample(&self, n: usize) -> Result<Vec<f64>, RustQuantError> {
                        use rand::thread_rng;
        use rand_distr::{ChiSquared, Distribution};
        assert!(n > 0);
        let mut rng = thread_rng();
        let dist = ChiSquared::new(self.k as f64)?;
        let mut variates: Vec<f64> = Vec::with_capacity(n);
        for _ in 0..variates.capacity() {
            variates.push(dist.sample(&mut rng) as usize as f64);
        }
        Ok(variates)
    }
}
#[cfg(test)]
mod tests {
    use super::*;
    use RustQuant_utils::assert_approx_equal;
    #[test]
    fn test_chi_squared_characteristic_function() {
        let dist: ChiSquared = ChiSquared::new(1);
        let cf = dist.cf(1.0);
        assert_approx_equal!(cf.re, 0.568_864_5, 1e-7);
        assert_approx_equal!(cf.im, 0.351_577_6, 1e-7);
    }
    #[test]
    fn test_chi_squared_density_function() {
        let dist: ChiSquared = ChiSquared::new(1);
                assert_approx_equal!(dist.pdf(1.0), 0.241_970_72, 1e-8);
        assert_approx_equal!(dist.pdf(2.0), 0.103_776_87, 1e-8);
        assert_approx_equal!(dist.pdf(3.0), 0.051_393_44, 1e-8);
        assert_approx_equal!(dist.pdf(4.0), 0.026_995_48, 1e-8);
        assert_approx_equal!(dist.pdf(5.0), 0.014_644_98, 1e-8);
    }
    #[test]
    fn test_chi_squared_distribution_function() {
        let dist: ChiSquared = ChiSquared::new(1);
                assert_approx_equal!(dist.cdf(1.0), 0.682_689_5, 1e-7);
        assert_approx_equal!(dist.cdf(2.0), 0.842_700_8, 1e-7);
        assert_approx_equal!(dist.cdf(3.0), 0.916_735_5, 1e-7);
        assert_approx_equal!(dist.cdf(4.0), 0.954_499_7, 1e-7);
        assert_approx_equal!(dist.cdf(5.0), 0.974_652_7, 1e-7);
    }
}