use std::collections::{BTreeMap};
use serde::{Serialize, Deserialize};
use rand::{Rng};
use rand::distributions::{WeightedIndex, Distribution};

use crate::entity::item::tech::{Technique, TechniqueDisk};
use crate::ship::room::{Difficulty, Episode};
use crate::ship::map::MapArea;
use crate::entity::character::SectionID;
use crate::ship::drops::{ItemDropType, load_data_file};



#[derive(Debug, Serialize, Deserialize)]
struct TechniqueRateStat {
    rate: u32,
    min: i32,
    max: i32,
}

#[derive(Debug, Serialize, Deserialize)]
struct TechniqueRatesRaw {
    area1: BTreeMap<String, TechniqueRateStat>,
    area2: BTreeMap<String, TechniqueRateStat>,
    area3: BTreeMap<String, TechniqueRateStat>,
    area4: BTreeMap<String, TechniqueRateStat>,
    area5: BTreeMap<String, TechniqueRateStat>,
    area6: BTreeMap<String, TechniqueRateStat>,
    area7: BTreeMap<String, TechniqueRateStat>,
    area8: BTreeMap<String, TechniqueRateStat>,
    area9: BTreeMap<String, TechniqueRateStat>,
    area10: BTreeMap<String, TechniqueRateStat>,
}

#[derive(Debug, Serialize, Deserialize)]
struct TechniqueRates {
    area1: BTreeMap<Technique, TechniqueRateStat>,
    area2: BTreeMap<Technique, TechniqueRateStat>,
    area3: BTreeMap<Technique, TechniqueRateStat>,
    area4: BTreeMap<Technique, TechniqueRateStat>,
    area5: BTreeMap<Technique, TechniqueRateStat>,
    area6: BTreeMap<Technique, TechniqueRateStat>,
    area7: BTreeMap<Technique, TechniqueRateStat>,
    area8: BTreeMap<Technique, TechniqueRateStat>,
    area9: BTreeMap<Technique, TechniqueRateStat>,
    area10: BTreeMap<Technique, TechniqueRateStat>,
}

impl TechniqueRates {
    fn new(rates: TechniqueRatesRaw) -> TechniqueRates {
        TechniqueRates {
            area1: rates.area1.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area2: rates.area2.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area3: rates.area3.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area4: rates.area4.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area5: rates.area5.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area6: rates.area6.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area7: rates.area7.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area8: rates.area8.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area9: rates.area9.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
            area10: rates.area10.into_iter().map(|(tech, rate)| (tech.parse().unwrap(), rate)).collect(),
        }
    }
}

impl TechniqueRates {
    fn get_by_area<'a>(&'a self, map_area: &MapArea) -> &'a BTreeMap<Technique, TechniqueRateStat> {
        match map_area.drop_area_value().unwrap() {
            0 => &self.area1,
            1 => &self.area2,
            2 => &self.area3,
            3 => &self.area4,
            4 => &self.area5,
            5 => &self.area6,
            6 => &self.area7,
            7 => &self.area8,
            8 => &self.area9,
            _ => &self.area10,
        }
    }
}

pub struct TechniqueTable {
    rates: TechniqueRates
}

impl TechniqueTable {
    pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> TechniqueTable {
        let rates: TechniqueRatesRaw = load_data_file(episode, difficulty, section_id, "tech_rate.toml");

        TechniqueTable {
            rates: TechniqueRates::new(rates),
        }
    }


    pub fn get_drop<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> Option<ItemDropType> {
        let mut tech_rates = self.rates.get_by_area(map_area).iter();
        let tech_weights = WeightedIndex::new(tech_rates.clone().map(|(_, stat)| stat.rate)).unwrap();

        let (tech, stat) = tech_rates.nth(tech_weights.sample(rng)).unwrap();
        let level = rng.gen_range(stat.min, stat.max+1) + 1;

        Some(ItemDropType::TechniqueDisk(TechniqueDisk {
            tech: *tech,
            level: level as u32
        }))
    }
}


#[cfg(test)]
mod test {
    use super::*;
    use rand::{SeedableRng};
    #[test]
    fn test_tech_drops() {
        let mut rng = rand_chacha::ChaCha20Rng::from_seed([23;32]);
        let tt = TechniqueTable::new(Episode::One, Difficulty::Ultimate, SectionID::Skyly);

        let tech_tests = vec![(MapArea::Forest1, Technique::Resta, 14),
                              (MapArea::Caves3, Technique::Foie, 25),
                              (MapArea::Mines2, Technique::Gibarta, 21),
                              (MapArea::DarkFalz, Technique::Razonde, 23)];

        for (area, tech, level) in tech_tests {
            assert!(tt.get_drop(&area, &mut rng) == Some(ItemDropType::TechniqueDisk(
                TechniqueDisk {
                    tech: tech,
                    level: level
                })));
        }
    }
}