use std::iter;
use uuid::Uuid;

// reclaims
use rand::prelude::*;
use rand::{thread_rng};
use rand::distributions::{WeightedIndex};

use postgres::transaction::Transaction;

use failure::Error;
use failure::err_msg;

use account::Account;
use rpc::{VboxAcceptParams, VboxDiscardParams, VboxCombineParams, VboxApplyParams, VboxReclaimParams, VboxUnequipParams};
use skill::{Skill};
use spec::{Spec};
use player::{Player, player_get, player_update};
use cryp::{Colours};

#[derive(Debug,Copy,Clone,Serialize,Deserialize,PartialEq,PartialOrd,Ord,Eq)]
pub enum Var {
    // colours
    Blue,
    Green,
    Red,

    // base skills
    Attack,
    Block,
    Stun,
    Buff,
    Debuff,

    // specs
    // Base
    Damage,
    Hp,
    Speed,

    // Shields Upgrades
    LifeI,
    RedShieldI,
    BlueShieldI,
    LRSI,
    LBSI,
    RBSI,

    // Damage Upgrades
    RedDamageI,
    BlueDamageI,
    GreenDamageI,
    GRDI,
    GBDI,
    RBDI,

    // Speed Upgrades
    RedSpeedI,
    BlueSpeedI,
    GreenSpeedI,
    GRSpeedI,
    GBSpeedI,
    RBSpeedI,

    Amplify,
    Banish,
    Blast,
    Curse,
    Decay,
    Empower,
    Hostility,
    Haste,
    Heal,
    Hex,
    Invert,
    Parry,
    Purge,
    Purify,
    Reflect,
    Recharge,
    Ruin,
    Shield,
    Silence,
    Slay,
    Slow,
    Snare,
    Strangle,
    Strike,
    Siphon,
    Clutch,
    Taunt,
    Throw,
    Corrupt,
    Triage,


    TestTouch,
    TestStun,
    TestBlock,
    TestParry,
    TestSiphon,
}

enum VarEffect {
    Skill,
    Spec,
}

impl Var {
    pub fn colours(&self, count: &mut Colours) {
        let combos = get_combos();
        let combo = combos.iter().find(|c| c.var == *self);
        match combo {
            Some(c) => c.units.iter().for_each(|unit| match unit {
                Var::Red => count.red += 1,
                Var::Blue => count.blue += 1,
                Var::Green => count.green += 1,
                _ => (),
            }),
            None => (),
        }
    }

    fn cost(&self) -> u16 {
        match self {
            Var::Red => 1,
            Var::Green => 1,
            Var::Blue => 1,

            Var::Attack => 2,
            Var::Block => 2,
            Var::Buff => 2,
            Var::Debuff => 2,
            Var::Stun => 2,

            Var::Damage => 3,
            Var::Hp => 3,
            Var::Speed => 3,

            _ => {
                let combos = get_combos();
                let combo = combos.iter().find(|c| c.var == *self)
                    .unwrap_or_else(|| panic!("unable to find components for {:?}", self));
                return combo.units.iter().fold(0, |acc, c| acc + c.cost());
            },
        }
    }

    pub fn speed(&self) -> u8 {
        match self {
            Var::Red => 3,
            Var::Green => 2,
            Var::Blue => 1,

            Var::Attack => 1,
            Var::Stun => 2,
            Var::Block => 3,
            Var::Buff => 4,
            Var::Debuff => 4,

            _ => {
                let combos = get_combos();
                let combo = combos.iter().find(|c| c.var == *self)
                    .unwrap_or_else(|| panic!("unable to find components for {:?}", self));
                return combo.units.iter().fold(0, |acc, c| acc + c.speed());
            },
        }
    }

    fn effect(&self) -> Option<VarEffect> {
        if let Some(_skill) = self.into_skill() {
            return Some(VarEffect::Skill);
        }
        if let Some(_spec) = self.into_spec() {
            return Some(VarEffect::Spec);
        }
        return None;
    }

    fn into_skill(&self) -> Option<Skill> {
        match self {
            Var::Attack     => Some(Skill::Attack),
            Var::Amplify    => Some(Skill::Amplify),
            Var::Banish     => Some(Skill::Banish),
            Var::Blast      => Some(Skill::Blast),
            Var::Block      => Some(Skill::Block),
            Var::Curse      => Some(Skill::Curse),
            Var::Decay      => Some(Skill::Decay),
            Var::Empower    => Some(Skill::Empower),
            Var::Haste      => Some(Skill::Haste),
            Var::Heal       => Some(Skill::Heal),
            Var::Hex        => Some(Skill::Hex),
            Var::Hostility  => Some(Skill::Hostility),
            Var::Invert     => Some(Skill::Invert),
            Var::Parry      => Some(Skill::Parry),
            Var::Purge      => Some(Skill::Purge),
            Var::Purify     => Some(Skill::Purify),
            Var::Recharge   => Some(Skill::Recharge),
            Var::Reflect    => Some(Skill::Reflect),
            Var::Ruin       => Some(Skill::Ruin),
            Var::Shield     => Some(Skill::Shield),
            Var::Silence    => Some(Skill::Silence),
            // Var::Slay       => Some(Skill::Slay),
            Var::Slow       => Some(Skill::Slow),
            Var::Siphon     => Some(Skill::Siphon),
            Var::Snare      => Some(Skill::Snare),
            Var::Strangle   => Some(Skill::Strangle),
            Var::Strike     => Some(Skill::Strike),
            Var::Clutch     => Some(Skill::Clutch),
            Var::Taunt      => Some(Skill::Taunt),
            Var::Throw      => Some(Skill::Throw),
            Var::Corrupt    => Some(Skill::Corrupt),
            Var::Triage     => Some(Skill::Triage),
            _               => None,
        }
    }

    fn into_spec(&self) -> Option<Spec> {
        match *self {
            Var::Speed          => Some(Spec::Speed),
            Var::RedSpeedI      => Some(Spec::RedSpeedI),
            Var::BlueSpeedI     => Some(Spec::BlueSpeedI),
            Var::GreenSpeedI    => Some(Spec::GreenSpeedI),
            Var::GRSpeedI       => Some(Spec::GRSpeedI),
            Var::GBSpeedI       => Some(Spec::GBSpeedI),
            Var::RBSpeedI       => Some(Spec::RBSpeedI),

            Var::Damage         => Some(Spec::Damage),
            Var::RedDamageI     => Some(Spec::RedDamageI),
            Var::BlueDamageI    => Some(Spec::BlueDamageI),
            Var::GreenDamageI   => Some(Spec::GreenDamageI),
            Var::GRDI           => Some(Spec::GRDI),
            Var::GBDI           => Some(Spec::GBDI),
            Var::RBDI           => Some(Spec::RBDI),

            Var::Hp             => Some(Spec::Hp),
            Var::LifeI          => Some(Spec::LifeI),
            Var::LRSI           => Some(Spec::LRSI),
            Var::LBSI           => Some(Spec::LBSI),
            Var::RBSI           => Some(Spec::RBSI),
            Var::RedShieldI     => Some(Spec::RedShieldI),
            Var::BlueShieldI    => Some(Spec::BlueShieldI),

            _                   => None,
        }
    }
}

impl From<Skill> for Var {
    fn from(skill: Skill) -> Var {
        match skill {
            Skill::Amplify => Var::Amplify,
            Skill::Attack => Var::Attack,
            Skill::Banish => Var::Banish,
            Skill::Blast => Var::Blast,
            Skill::Block => Var::Block,
            Skill::Curse => Var::Curse,
            Skill::Decay => Var::Decay,
            Skill::Empower => Var::Empower,
            Skill::Haste => Var::Haste,
            Skill::Hostility => Var::Hostility,
            Skill::Heal => Var::Heal,
            Skill::Hex => Var::Hex,
            Skill::Invert => Var::Invert,
            Skill::Parry => Var::Parry,
            Skill::Purge => Var::Purge,
            Skill::Purify => Var::Purify,
            Skill::Recharge => Var::Recharge,
            Skill::Reflect => Var::Reflect,
            Skill::Ruin => Var::Ruin,
            Skill::Shield => Var::Shield,
            Skill::Silence => Var::Silence,
            Skill::Siphon => Var::Siphon,
            Skill::Slow => Var::Slow,
            Skill::Snare => Var::Snare,
            Skill::Strangle => Var::Strangle,
            Skill::Strike => Var::Strike,
            Skill::Stun => Var::Stun,
            Skill::Taunt => Var::Taunt,
            Skill::Throw => Var::Throw,
            Skill::Triage => Var::Triage,
            Skill::Corrupt => Var::Corrupt,

            Skill::TestTouch => Var::TestTouch,
            Skill::TestStun => Var::TestStun,
            Skill::TestBlock => Var::TestBlock,
            Skill::TestParry => Var::TestParry,
            Skill::TestSiphon => Var::TestSiphon,
            _ => panic!("{:?} not implemented as a var", skill),
        }
    }
}

impl From<Spec> for Var {
    fn from(spec: Spec) -> Var {
        match spec {
            Spec::Speed          => Var::Speed,
            Spec::RedSpeedI      => Var::RedSpeedI,
            Spec::BlueSpeedI     => Var::BlueSpeedI,
            Spec::GreenSpeedI    => Var::GreenSpeedI,
            Spec::GRSpeedI       => Var::GRSpeedI,
            Spec::GBSpeedI       => Var::GBSpeedI,
            Spec::RBSpeedI       => Var::RBSpeedI,

            Spec::Damage         => Var::Damage,
            Spec::RedDamageI     => Var::RedDamageI,
            Spec::BlueDamageI    => Var::BlueDamageI,
            Spec::GreenDamageI   => Var::GreenDamageI,
            Spec::GRDI           => Var::GRDI,
            Spec::GBDI           => Var::GBDI,
            Spec::RBDI           => Var::RBDI,

            Spec::Hp             => Var::Hp,
            Spec::LifeI          => Var::LifeI,
            Spec::LRSI           => Var::LRSI,
            Spec::LBSI           => Var::LBSI,
            Spec::RBSI           => Var::RBSI,
            Spec::RedShieldI     => Var::RedShieldI,
            Spec::BlueShieldI    => Var::BlueShieldI,
            // _ => panic!("{:?} not implemented as a var", spec),
        }
    }
}


struct Combo {
    var: Var,
    units: Vec<Var>,
}

fn get_combos() -> Vec<Combo> {
    let mut combinations = vec![
        Combo { units: vec![Var::Buff, Var::Red,    Var::Red],      var: Var::Empower   },
        Combo { units: vec![Var::Buff, Var::Green,  Var::Green],    var: Var::Triage    },
        Combo { units: vec![Var::Buff, Var::Blue,   Var::Blue],     var: Var::Hostility },
        Combo { units: vec![Var::Buff, Var::Red,    Var::Green],    var: Var::Clutch    },
        Combo { units: vec![Var::Buff, Var::Green,  Var::Blue],     var: Var::Amplify   },
        Combo { units: vec![Var::Buff, Var::Red,    Var::Blue],     var: Var::Haste     },

        Combo { units: vec![Var::Debuff, Var::Red,    Var::Red],    var: Var::Snare     },
        Combo { units: vec![Var::Debuff, Var::Green,  Var::Green],  var: Var::Purge     },
        Combo { units: vec![Var::Debuff, Var::Blue,   Var::Blue],   var: Var::Curse     },
        Combo { units: vec![Var::Debuff, Var::Red,    Var::Green],  var: Var::Slow      },
        Combo { units: vec![Var::Debuff, Var::Green,  Var::Blue],   var: Var::Siphon    },
        Combo { units: vec![Var::Debuff, Var::Red,    Var::Blue],   var: Var::Invert    },

        Combo { units: vec![Var::Block, Var::Red,     Var::Red],    var: Var::Parry     },
        Combo { units: vec![Var::Block, Var::Green,   Var::Green],  var: Var::Reflect   },
        Combo { units: vec![Var::Block, Var::Blue,    Var::Blue],   var: Var::Corrupt   },
        Combo { units: vec![Var::Block, Var::Red,     Var::Green],  var: Var::Taunt     },
        Combo { units: vec![Var::Block, Var::Green,   Var::Blue],   var: Var::Shield    },
        Combo { units: vec![Var::Block, Var::Red,     Var::Blue],   var: Var::Recharge  },

        Combo { units: vec![Var::Stun, Var::Red,    Var::Red],      var: Var::Strangle  },
        Combo { units: vec![Var::Stun, Var::Green,  Var::Green],    var: Var::Throw     },
        Combo { units: vec![Var::Stun, Var::Blue,   Var::Blue],     var: Var::Ruin      },
        Combo { units: vec![Var::Stun, Var::Red,    Var::Green],    var: Var::Banish    },
        Combo { units: vec![Var::Stun, Var::Green,  Var::Blue],     var: Var::Silence   },
        Combo { units: vec![Var::Stun, Var::Red,    Var::Blue],     var: Var::Hex       },

        Combo { units: vec![Var::Attack, Var::Red,    Var::Red],    var: Var::Strike    },
        Combo { units: vec![Var::Attack, Var::Green,  Var::Green],  var: Var::Heal      },
        Combo { units: vec![Var::Attack, Var::Blue,   Var::Blue],   var: Var::Blast     },
        Combo { units: vec![Var::Attack, Var::Red,    Var::Green],  var: Var::Purify    },
        Combo { units: vec![Var::Attack, Var::Green,  Var::Blue],   var: Var::Decay     },
        // Combo { units: vec![Var::Attack, Var::Red,    Var::Blue],   var: Var::Blast    },

        Combo { units: vec![Var::Damage, Var::Red,    Var::Red],    var: Var::RedDamageI    },
        Combo { units: vec![Var::Damage, Var::Green,  Var::Green],  var: Var::GreenDamageI  },
        Combo { units: vec![Var::Damage, Var::Blue,   Var::Blue],   var: Var::BlueDamageI   },
        Combo { units: vec![Var::Damage, Var::Red,    Var::Green],  var: Var::GRDI          },
        Combo { units: vec![Var::Damage, Var::Green,  Var::Blue],   var: Var::GBDI          },
        Combo { units: vec![Var::Damage, Var::Red,    Var::Blue],   var: Var::RBDI          },

        Combo { units: vec![Var::Hp, Var::Red,    Var::Red],    var: Var::RedShieldI    },
        Combo { units: vec![Var::Hp, Var::Green,  Var::Green],  var: Var::LifeI         },
        Combo { units: vec![Var::Hp, Var::Blue,   Var::Blue],   var: Var::BlueShieldI   },
        Combo { units: vec![Var::Hp, Var::Red,    Var::Green],  var: Var::LRSI          },
        Combo { units: vec![Var::Hp, Var::Green,  Var::Blue],   var: Var::LBSI          },
        Combo { units: vec![Var::Hp, Var::Red,    Var::Blue],   var: Var::RBSI          },

        Combo { units: vec![Var::Speed, Var::Red,    Var::Red],     var: Var::RedSpeedI     },
        Combo { units: vec![Var::Speed, Var::Green,  Var::Green],   var: Var::GreenSpeedI   },
        Combo { units: vec![Var::Speed, Var::Blue,   Var::Blue],    var: Var::BlueSpeedI    },
        Combo { units: vec![Var::Speed, Var::Red,    Var::Green],   var: Var::GRSpeedI      },
        Combo { units: vec![Var::Speed, Var::Green,  Var::Blue],    var: Var::GBSpeedI      },
        Combo { units: vec![Var::Speed, Var::Red,    Var::Blue],    var: Var::RBSpeedI      },

    ];

    combinations.iter_mut().for_each(|set| set.units.sort_unstable());

    return combinations;
}

#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Vbox {
    pub id: Uuid,
    pub bits: u16,
    pub free: Vec<Vec<Var>>,
    pub bound: Vec<Var>,
    pub instance: Uuid,
    pub account: Uuid,
}

impl Vbox {
    pub fn new(account_id: Uuid, instance_id: Uuid) -> Vbox {
        // line them up with the columns of the vbox
        let starting_items = vec![
            Var::Red,
            Var::Attack,
            Var::Hp,
            Var::Green,
            Var::Attack,
            Var::Speed,
            Var::Blue,
            Var::Attack,
            Var::Damage,
        ];

        let mut vbox = Vbox {
            id: Uuid::new_v4(),
            account: account_id,
            instance: instance_id,
            free: vec![],
            bound: starting_items,
            bits: 18,
        };

        vbox.fill();

        return vbox;
    }

    pub fn balance_sub(&mut self, amount: u16) -> Result<&mut Vbox, Error> {
        let new_balance = self.bits
            .checked_sub(amount)
            .ok_or(format_err!("insufficient balance: {:?}", self.bits))?;

        self.bits = new_balance;

        Ok(self)
    }

    pub fn balance_add(&mut self, amount: u16) -> &mut Vbox {
        self.bits = self.bits.saturating_add(amount);
        self
    }

    pub fn fill(&mut self) -> &mut Vbox {
        let colours = vec![
            (Var::Red, 1),
            (Var::Green, 1),
            (Var::Blue, 1),
        ];

        let skills = vec![
            (Var::Attack, 1),
            (Var::Block, 1),
            (Var::Buff, 1),
            (Var::Debuff, 1),
            (Var::Stun, 1),
        ];

        let specs = vec![
            (Var::Damage, 1),
            (Var::Hp, 1),
            (Var::Speed, 1),
        ];

        let mut rng = thread_rng();

        self.free = [&colours, &skills, &specs].iter()
            .map(|vars| {
                let dist = WeightedIndex::new(vars.iter().map(|item| item.1)).unwrap();

                iter::repeat_with(|| {
                    vars[dist.sample(&mut rng)].0
                })
                .take(6)
                .collect::<Vec<Var>>()
            })
            .collect::<Vec<Vec<Var>>>();

        self
    }

    pub fn accept(&mut self, i: usize, j: usize) -> Result<&mut Vbox, Error> {
        if self.bound.len() >= 9 {
            return Err(err_msg("too many vars bound"));
        }

        // check item exists
        self.free
            .get(i).ok_or(format_err!("no var group at index {:?}", i))?
            .get(j).ok_or(format_err!("no var at index {:?}", j))?;

        // check can purchase
        let cost = self.free[i][j].cost();
        self.balance_sub(cost)?;

        // actually move
        self.bound.push(self.free[i].remove(j));

        Ok(self)
    }

    pub fn reclaim(&mut self, i: usize) -> Result<&mut Vbox, Error> {
        self.bound.get(i).ok_or(format_err!("no var at index {:?}", i))?;
        let reclaimed = self.bound.remove(i);
        let refund = reclaimed.cost();
        // println!("reclaiming {:?} for {:?}", refund, reclaimed);
        self.balance_add(refund);
        Ok(self)
    }

    pub fn combine(&mut self, mut indices: Vec<usize>) -> Result<&mut Vbox, Error> {
        if indices.len() != 3 {
            return Err(err_msg("exactly 3 indices required"));
        }

        if !indices.iter().all(|i| self.bound.get(*i).is_some()) {
            return Err(err_msg("var missing index"));
        }

        // have to sort the indices and keep track of the iteration
        // because when removing the elements the array shifts
        indices.sort_unstable();
        let mut input = indices
            .iter()
            .enumerate()
            .map(|(i, index)| {
                self.bound.remove(*index - i)
            })
            .collect::<Vec<Var>>();

        // sort the input to align with the combinations
        // combos are sorted when created
        input.sort_unstable();
        let combos = get_combos();
        let combo = combos.iter().find(|c| c.units == input).ok_or(err_msg("not a combo"))?;

        self.bound.push(combo.var);

        Ok(self)
    }
}

const DISCARD_COST: u16 = 5;

pub fn vbox_discard(params: VboxDiscardParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;
    player.vbox.balance_sub(DISCARD_COST)?;
    player.vbox.fill();
    return player_update(tx, player, false);
}

pub fn vbox_accept(params: VboxAcceptParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;
    player.vbox.accept(params.group, params.index)?;
    return player_update(tx, player, false);
}

pub fn vbox_combine(params: VboxCombineParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;
    player.vbox.combine(params.indices)?;
    return player_update(tx, player, false);
}

pub fn vbox_reclaim(params: VboxReclaimParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;
    player.vbox.reclaim(params.index)?;
    return player_update(tx, player, false);
}

pub fn vbox_apply(params: VboxApplyParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;
    let var = player.vbox.bound.remove(params.index);

    match var.effect() {
        Some(VarEffect::Skill) => {
            let skill = var.into_skill().ok_or(format_err!("var {:?} has no associated skill", var))?;
            let cryp = player.cryp_get(params.cryp_id)?;
            // done here because i teach them a tonne of skills for tests
            let max_skills = 4;
            if cryp.skills.len() >= max_skills {
                return Err(format_err!("cryp at max skills ({:?})", max_skills));
            }

            cryp.learn_mut(skill);
        },
        Some(VarEffect::Spec) => {
            let spec = var.into_spec().ok_or(format_err!("var {:?} has no associated spec", var))?;
            let cryp = player.cryp_get(params.cryp_id)?;
            cryp.spec_add(spec)?;

        },
        None => return Err(err_msg("var has no effect on cryps")),
    }

    // now the var has been applied
    // recalculate the stats of the whole team
    let team_colours = player.cryps.iter().fold(Colours::new(), |tc, c| {
        Colours {
            red: tc.red + c.colours.red,
            green: tc.green + c.colours.green,
            blue: tc.blue + c.colours.blue
        }
    });

    for cryp in player.cryps.iter_mut() {
        cryp.apply_modifiers(&team_colours);
    }

    return player_update(tx, player, false);
}

pub fn vbox_unequip(params: VboxUnequipParams, tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
    let mut player = player_get(tx, account.id, params.instance_id)?;

    if player.vbox.bound.len() >= 9 {
        return Err(err_msg("too many vars bound"));
    }

    println!("{:?}", params);

    match params.target.effect() {
        Some(VarEffect::Skill) => {
            let skill = params.target.into_skill().ok_or(format_err!("var {:?} has no associated skill", params.target))?;
            let cryp = player.cryp_get(params.cryp_id)?;
            cryp.forget(skill)?;
        },
        Some(VarEffect::Spec) => {
            let spec = params.target.into_spec().ok_or(format_err!("var {:?} has no associated spec", params.target))?;
            let cryp = player.cryp_get(params.cryp_id)?;
            cryp.spec_remove(spec)?;
        },
        None => return Err(err_msg("var has no effect on cryps")),
    }

    player.vbox.bound.push(params.target);
    return player_update(tx, player, false);
}

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

    #[test]
    fn combine_test() {
        let mut vbox = Vbox::new(Uuid::new_v4(), Uuid::new_v4());
        vbox.bound = vec![Var::Attack, Var::Green, Var::Green];
        vbox.combine(vec![1,2,0]).unwrap();
        assert_eq!(vbox.bound[0], Var::Heal);
    }

    #[test]
    fn combos_test() {
        let mut input = vec![Var::Green, Var::Attack, Var::Green];
        let combos = get_combos();

        // sort input so they align
        input.sort_unstable();

        let combo = combos.iter().find(|c| c.units == input);
        assert!(combo.is_some());
    }

    #[test]
    fn reclaim_test() {
        let mut vbox = Vbox::new(Uuid::new_v4(), Uuid::new_v4());
        vbox.bound = vec![Var::Strike];
        vbox.reclaim(0).unwrap();
        assert_eq!(vbox.bits, 22);
    }

    #[test]
    fn colours_count_test() {
        let strike = Var::Strike;

        let mut count = Colours::new();
        strike.colours(&mut count);
        assert_eq!(count.red, 2);
    }

}