#![cfg(feature = "ndarray")]

use ndarray::{Array1, Array2};
use rand::rngs::StdRng;
use rand::SeedableRng;
use smawk::online_column_minima;

mod random_monge;
use random_monge::random_monge_matrix;

#[derive(Debug)]
struct LinRegression {
    alpha: f64,
    beta: f64,
    r_squared: f64,
}

/// Square an expression. Works equally well for floats and matrices.
macro_rules! squared {
    ($x:expr) => {
        $x * $x
    };
}

/// Compute the mean of a 1-dimensional array.
macro_rules! mean {
    ($a:expr) => {
        $a.mean().expect("Mean of empty array")
    };
}

/// Compute a simple linear regression from the list of values.
///
/// See <https://en.wikipedia.org/wiki/Simple_linear_regression>.
fn linear_regression(values: &[(usize, i32)]) -> LinRegression {
    let xs = values.iter().map(|&(x, _)| x as f64).collect::<Array1<_>>();
    let ys = values.iter().map(|&(_, y)| y as f64).collect::<Array1<_>>();

    let xs_mean = mean!(&xs);
    let ys_mean = mean!(&ys);
    let xs_ys_mean = mean!(&xs * &ys);

    let cov_xs_ys = ((&xs - xs_mean) * (&ys - ys_mean)).sum();
    let var_xs = squared!(&xs - xs_mean).sum();

    let beta = cov_xs_ys / var_xs;
    let alpha = ys_mean - beta * xs_mean;
    let r_squared = squared!(xs_ys_mean - xs_mean * ys_mean)
        / ((mean!(&xs * &xs) - squared!(xs_mean)) * (mean!(&ys * &ys) - squared!(ys_mean)));

    LinRegression {
        alpha,
        beta,
        r_squared,
    }
}

/// Check that the number of matrix accesses in `online_column_minima`
/// grows as O(*n*) for *n* × *n* matrix.
#[test]
fn online_linear_complexity() {
    let mut rng = StdRng::seed_from_u64(0);
    let mut data = vec![];

    for &size in &[1, 2, 3, 4, 5, 10, 15, 20, 30, 40, 50, 60, 70, 80, 90, 100] {
        let matrix: Array2<i32> = random_monge_matrix(size, size, &mut rng);
        let count = core::cell::RefCell::new(0);
        online_column_minima(0, size, |_, i, j| {
            *count.borrow_mut() += 1;
            matrix[[i, j]]
        });
        data.push((size, count.into_inner()));
    }

    let lin_reg = linear_regression(&data);
    println!(
        "Linear regression:\n  α  = {:6.2}\n  β  = {:6.2}\n  r² = {:6.2}",
        lin_reg.alpha, lin_reg.beta, lin_reg.r_squared
    );

    assert!(
        lin_reg.r_squared > 0.95,
        "r² = {:.4} is lower than expected for a linear fit\nData points: {:?}\n{:?}",
        lin_reg.r_squared,
        data,
        lin_reg
    );
}
