use crate::{
    N_INF, N00, approx, compute_margin,
    drift::Drift,
    evidence,
    gaussian::Gaussian,
    message::{DiffMessage, TeamMessage},
    player::Player,
    sort_perm, tuple_gt, tuple_max,
};

#[derive(Debug)]
pub struct Game<'a, D: Drift> {
    teams: Vec<Vec<Player<D>>>,
    result: &'a [f64],
    weights: &'a [Vec<f64>],
    p_draw: f64,
    pub(crate) likelihoods: Vec<Vec<Gaussian>>,
    pub(crate) evidence: f64,
}

impl<'a, D: Drift> Game<'a, D> {
    pub fn new(
        teams: Vec<Vec<Player<D>>>,
        result: &'a [f64],
        weights: &'a [Vec<f64>],
        p_draw: f64,
    ) -> Self {
        debug_assert!(
            (result.len() == teams.len()),
            "result must have the same length as teams"
        );

        debug_assert!(
            weights
                .iter()
                .zip(teams.iter())
                .all(|(w, t)| w.len() == t.len()),
            "weights must have the same dimensions as teams"
        );

        debug_assert!(
            (0.0..1.0).contains(&p_draw),
            "draw probability.must be >= 0.0 and < 1.0"
        );

        debug_assert!(
            p_draw > 0.0 || {
                let mut r = result.to_vec();
                r.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap());
                r.windows(2).all(|w| w[0] != w[1])
            },
            "draw must be > 0.0 if there is teams with draw"
        );

        let mut this = Self {
            teams,
            result,
            weights,
            p_draw,
            likelihoods: Vec::new(),
            evidence: 0.0,
        };

        this.likelihoods();

        this
    }

    fn likelihoods(&mut self) {
        let o = sort_perm(self.result, true);

        let mut team = o
            .iter()
            .map(|&e| {
                let performance = self.teams[e]
                    .iter()
                    .zip(self.weights[e].iter())
                    .fold(N00, |p, (player, &weight)| {
                        p + (player.performance() * weight)
                    });

                TeamMessage {
                    prior: performance,
                    ..Default::default()
                }
            })
            .collect::<Vec<_>>();

        let mut diff = team
            .windows(2)
            .map(|w| DiffMessage {
                prior: w[0].prior - w[1].prior,
                likelihood: N_INF,
            })
            .collect::<Vec<_>>();

        let tie = o
            .windows(2)
            .map(|e| self.result[e[0]] == self.result[e[1]])
            .collect::<Vec<_>>();

        let margin = if self.p_draw == 0.0 {
            vec![0.0; o.len() - 1]
        } else {
            o.windows(2)
                .map(|w| {
                    let a: f64 = self.teams[w[0]].iter().map(|a| a.beta.powi(2)).sum();
                    let b: f64 = self.teams[w[1]].iter().map(|a| a.beta.powi(2)).sum();

                    compute_margin(self.p_draw, (a + b).sqrt())
                })
                .collect::<Vec<_>>()
        };

        self.evidence = 1.0;

        let mut step = (f64::INFINITY, f64::INFINITY);
        let mut iter = 0;

        while tuple_gt(step, 1e-6) && iter < 10 {
            step = (0.0, 0.0);

            for e in 0..diff.len() - 1 {
                diff[e].prior = team[e].posterior_win() - team[e + 1].posterior_lose();

                if iter == 0 {
                    self.evidence *= evidence(&diff, &margin, &tie, e);
                }

                diff[e].likelihood = approx(diff[e].prior, margin[e], tie[e]) / diff[e].prior;
                let likelihood_lose = team[e].posterior_win() - diff[e].likelihood;
                step = tuple_max(step, team[e + 1].likelihood_lose.delta(likelihood_lose));
                team[e + 1].likelihood_lose = likelihood_lose;
            }

            for e in (1..diff.len()).rev() {
                diff[e].prior = team[e].posterior_win() - team[e + 1].posterior_lose();

                if iter == 0 && e == diff.len() - 1 {
                    self.evidence *= evidence(&diff, &margin, &tie, e);
                }

                diff[e].likelihood = approx(diff[e].prior, margin[e], tie[e]) / diff[e].prior;
                let likelihood_win = team[e + 1].posterior_lose() + diff[e].likelihood;
                step = tuple_max(step, team[e].likelihood_win.delta(likelihood_win));
                team[e].likelihood_win = likelihood_win;
            }

            iter += 1;
        }

        if diff.len() == 1 {
            self.evidence = evidence(&diff, &margin, &tie, 0);

            diff[0].prior = team[0].posterior_win() - team[1].posterior_lose();
            diff[0].likelihood = approx(diff[0].prior, margin[0], tie[0]) / diff[0].prior;
        }

        let t_end = team.len() - 1;
        let d_end = diff.len() - 1;

        team[0].likelihood_win = team[1].posterior_lose() + diff[0].likelihood;
        team[t_end].likelihood_lose = team[t_end - 1].posterior_win() - diff[d_end].likelihood;

        let m_t_ft = o.into_iter().map(|e| team[e].likelihood());

        self.likelihoods = self
            .teams
            .iter()
            .zip(self.weights.iter())
            .zip(m_t_ft)
            .map(|((p, w), m)| {
                let performance = p.iter().zip(w.iter()).fold(N00, |p, (player, &weight)| {
                    p + (player.performance() * weight)
                });

                p.iter()
                    .zip(w.iter())
                    .map(|(p, &w)| {
                        ((m - performance.exclude(p.performance() * w)) * (1.0 / w))
                            .forget(p.beta.powi(2))
                    })
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>();
    }

    pub fn posteriors(&self) -> Vec<Vec<Gaussian>> {
        self.likelihoods
            .iter()
            .zip(self.teams.iter())
            .map(|(l, t)| {
                l.iter()
                    .zip(t.iter())
                    .map(|(&l, p)| l * p.prior)
                    .collect::<Vec<_>>()
            })
            .collect::<Vec<_>>()
    }
}

#[cfg(test)]
mod tests {
    use ::approx::assert_ulps_eq;

    use super::*;
    use crate::{ConstantDrift, GAMMA, Gaussian, N_INF, Player};

    #[test]
    fn test_1vs1() {
        let t_a = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(20.794779, 7.194481), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(29.205220, 7.194481), epsilon = 1e-6);

        let t_a = Player::new(
            Gaussian::from_ms(29.0, 1.0),
            25.0 / 6.0,
            ConstantDrift(GAMMA),
        );
        let t_b = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(GAMMA),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(28.896475, 0.996604), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(32.189211, 6.062063), epsilon = 1e-6);

        let t_a = Player::new(Gaussian::from_ms(1.139, 0.531), 1.0, ConstantDrift(0.2125));
        let t_b = Player::new(Gaussian::from_ms(15.568, 0.51), 1.0, ConstantDrift(0.2125));

        let w = [vec![1.0], vec![1.0]];
        let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 1.0], &w, 0.0);

        assert_eq!(g.likelihoods[0][0], N_INF);
        assert_eq!(g.likelihoods[1][0], N_INF);
    }

    #[test]
    fn test_1vs1vs1() {
        let teams = vec![
            vec![Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
            vec![Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
            vec![Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            )],
        ];

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::new(teams.clone(), &[1.0, 2.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::new(teams.clone(), &[2.0, 1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(31.311358, 6.698818), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 6.238469), epsilon = 1e-6);

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::new(teams, &[1.0, 2.0, 0.0], &w, 0.5);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 6.092561), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(33.379314, 6.483575), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(16.620685, 6.483575), epsilon = 1e-6);
    }

    #[test]
    fn test_1vs1_draw() {
        let t_a = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, 0.25);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(24.999999, 6.469480), epsilon = 1e-6);

        let t_a = Player::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = Player::new(
            Gaussian::from_ms(29.0, 2.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0]];
        let g = Game::new(vec![vec![t_a], vec![t_b]], &[0.0, 0.0], &w, 0.25);
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.736001, 2.709956), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(28.672888, 1.916471), epsilon = 1e-6);
    }

    #[test]
    fn test_1vs1vs1_draw() {
        let t_a = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_c = Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::new(
            vec![vec![t_a], vec![t_b], vec![t_c]],
            &[0.0, 0.0, 0.0],
            &w,
            0.25,
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        assert_ulps_eq!(a, Gaussian::from_ms(24.999999, 5.729068), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.000000, 5.707423), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(24.999999, 5.729068), epsilon = 1e-6);

        let t_a = Player::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_b = Player::new(
            Gaussian::from_ms(25.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );
        let t_c = Player::new(
            Gaussian::from_ms(29.0, 2.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        );

        let w = [vec![1.0], vec![1.0], vec![1.0]];
        let g = Game::new(
            vec![vec![t_a], vec![t_b], vec![t_c]],
            &[0.0, 0.0, 0.0],
            &w,
            0.25,
        );
        let p = g.posteriors();

        let a = p[0][0];
        let b = p[1][0];
        let c = p[2][0];

        assert_ulps_eq!(a, Gaussian::from_ms(25.488507, 2.638208), epsilon = 1e-6);
        assert_ulps_eq!(b, Gaussian::from_ms(25.510671, 2.628751), epsilon = 1e-6);
        assert_ulps_eq!(c, Gaussian::from_ms(28.555920, 1.885689), epsilon = 1e-6);
    }

    #[test]
    fn test_2vs1vs2_mixed() {
        let t_a = vec![
            Player::new(
                Gaussian::from_ms(12.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
            Player::new(
                Gaussian::from_ms(18.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
        ];
        let t_b = vec![Player::new(
            Gaussian::from_ms(30.0, 3.0),
            25.0 / 6.0,
            ConstantDrift(25.0 / 300.0),
        )];
        let t_c = vec![
            Player::new(
                Gaussian::from_ms(14.0, 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
            Player::new(
                Gaussian::from_ms(16., 3.0),
                25.0 / 6.0,
                ConstantDrift(25.0 / 300.0),
            ),
        ];

        let w = [vec![1.0, 1.0], vec![1.0], vec![1.0, 1.0]];
        let g = Game::new(vec![t_a, t_b, t_c], &[1.0, 0.0, 0.0], &w, 0.25);
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], Gaussian::from_ms(13.051, 2.864), epsilon = 1e-3);
        assert_ulps_eq!(p[0][1], Gaussian::from_ms(19.051, 2.864), epsilon = 1e-3);
        assert_ulps_eq!(p[1][0], Gaussian::from_ms(29.292, 2.764), epsilon = 1e-3);
        assert_ulps_eq!(p[2][0], Gaussian::from_ms(13.658, 2.813), epsilon = 1e-3);
        assert_ulps_eq!(p[2][1], Gaussian::from_ms(15.658, 2.813), epsilon = 1e-3);
    }

    #[test]
    fn test_1vs1_weighted() {
        let w_a = vec![1.0];
        let w_b = vec![2.0];

        let t_a = vec![Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(0.0),
        )];
        let t_b = vec![Player::new(
            Gaussian::from_ms(25.0, 25.0 / 3.0),
            25.0 / 6.0,
            ConstantDrift(0.0),
        )];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(30.625173, 7.765472),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(13.749653, 5.733840),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![0.7];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(27.630080, 7.206676),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(23.158943, 7.801628),
            epsilon = 1e-6
        );

        let w_a = vec![1.6];
        let w_b = vec![0.7];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(26.142438, 7.573088),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(24.500183, 8.193278),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![0.0];

        let t_a = vec![Player::new(
            Gaussian::from_ms(2.0, 6.0),
            1.0,
            ConstantDrift(0.0),
        )];
        let t_b = vec![Player::new(
            Gaussian::from_ms(2.0, 6.0),
            1.0,
            ConstantDrift(0.0),
        )];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(5.557067, 4.052826),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(2.000000, 6.000000),
            epsilon = 1e-6
        );

        let w_a = vec![1.0];
        let w_b = vec![-1.0];

        let t_a = vec![Player::new(
            Gaussian::from_ms(2.0, 6.0),
            1.0,
            ConstantDrift(0.0),
        )];
        let t_b = vec![Player::new(
            Gaussian::from_ms(2.0, 6.0),
            1.0,
            ConstantDrift(0.0),
        )];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a, t_b], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], p[1][0], epsilon = 1e-6);
    }

    #[test]
    fn test_2vs2_weighted() {
        let t_a = vec![
            Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
            Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
        ];
        let w_a = vec![0.4, 0.8];

        let t_b = vec![
            Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
            Player::new(
                Gaussian::from_ms(25.0, 25.0 / 3.0),
                25.0 / 6.0,
                ConstantDrift(0.0),
            ),
        ];
        let w_b = vec![0.9, 0.6];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(27.539023, 8.129639),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(30.078046, 7.485372),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(19.287197, 7.243465),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(21.191465, 7.867608),
            epsilon = 1e-6
        );

        let w_a = vec![1.3, 1.5];
        let w_b = vec![0.7, 0.4];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(25.190190, 8.220511),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(25.219450, 8.182783),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(24.897589, 8.300779),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(24.941479, 8.322717),
            epsilon = 1e-6
        );

        let w_a = vec![1.6, 0.2];
        let w_b = vec![0.7, 2.4];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a.clone(), t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(
            p[0][0],
            Gaussian::from_ms(31.674697, 7.501180),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[0][1],
            Gaussian::from_ms(25.834337, 8.320970),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][0],
            Gaussian::from_ms(22.079819, 8.180607),
            epsilon = 1e-6
        );
        assert_ulps_eq!(
            p[1][1],
            Gaussian::from_ms(14.987953, 6.308469),
            epsilon = 1e-6
        );

        let w = [vec![1.0, 1.0], vec![1.0]];
        let g = Game::new(
            vec![
                t_a.clone(),
                vec![Player::new(
                    Gaussian::from_ms(25.0, 25.0 / 3.0),
                    25.0 / 6.0,
                    ConstantDrift(0.0),
                )],
            ],
            &[1.0, 0.0],
            &w,
            0.0,
        );
        let post_2vs1 = g.posteriors();

        let w_a = vec![1.0, 1.0];
        let w_b = vec![1.0, 0.0];

        let w = [w_a, w_b];
        let g = Game::new(vec![t_a, t_b.clone()], &[1.0, 0.0], &w, 0.0);
        let p = g.posteriors();

        assert_ulps_eq!(p[0][0], post_2vs1[0][0], epsilon = 1e-6);
        assert_ulps_eq!(p[0][1], post_2vs1[0][1], epsilon = 1e-6);
        assert_ulps_eq!(p[1][0], post_2vs1[1][0], epsilon = 1e-6);
        assert_ulps_eq!(p[1][1], t_b[1].prior, epsilon = 1e-6);
    }
}
