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

use crate::entity::item::weapon::{Weapon, WeaponType, Attribute, WeaponAttribute, WeaponSpecial};
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, Eq, PartialEq, Hash, Copy, Clone, Ord, PartialOrd)]
pub enum WeaponDropType {
    Saber,
    Sword,
    Dagger,
    Partisan,
    Slicer,
    Handgun,
    Rifle,
    Mechgun,
    Shot,
    Cane,
    Rod,
    Wand
}

#[derive(Debug, Serialize, Deserialize, Default, Copy, Clone)]
pub struct WeaponRatio {
    rate: u32,
    rank: i32,
    inc: u32,
}

#[derive(Debug, Serialize, Deserialize, Default, Clone)]
pub struct WeaponRatios {
    saber: WeaponRatio,
    sword: WeaponRatio,
    dagger: WeaponRatio,
    partisan: WeaponRatio,
    slicer: WeaponRatio,
    handgun: WeaponRatio,
    rifle: WeaponRatio,
    mechgun: WeaponRatio,
    shot: WeaponRatio,
    cane: WeaponRatio,
    rod: WeaponRatio,
    wand: WeaponRatio,
}

impl WeaponRatios {
    fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> WeaponRatios {
        load_data_file(episode, difficulty, section_id, "weapon_rate.toml")
    }
}

#[derive(Debug, Serialize, Deserialize)]
struct GrindRates {
    grind_rate: [[u32; 9]; 4]
}

impl GrindRates {
    fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GrindRates {
        load_data_file(episode, difficulty, section_id, "grind_rate.toml")
    }
}

#[derive(Debug, Serialize, Deserialize, Clone, Copy)]
struct AttributeRate {
    none: u32,
    native: u32,
    abeast: u32,
    machine: u32,
    dark: u32,
    hit: u32,
}

#[derive(Debug, Serialize, Deserialize)]
struct AttributeRates {
    area1: AttributeRate,
    area2: AttributeRate,
    area3: AttributeRate,
    area4: AttributeRate,
    area5: AttributeRate,
    area6: AttributeRate,
    area7: AttributeRate,
    area8: AttributeRate,
    area9: AttributeRate,
    area10: AttributeRate,
}

impl AttributeRates {
    fn get_by_area(&self, map_area: &MapArea) -> AttributeRate {
        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,
        }
    }
}

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct PercentRate {
    p5: u32,
    p10: u32,
    p15: u32,
    p20: u32,
    p25: u32,
    p30: u32,
    p35: u32,
    p40: u32,
    p45: u32,
    p50: u32,
    p55: u32,
    p60: u32,
    p65: u32,
    p70: u32,
    p75: u32,
    p80: u32,
    p85: u32,
    p90: u32,
}

impl PercentRate {
    fn as_array(&self) -> [u32; 18] {
        [self.p5,  self.p10, self.p15, self.p20, self.p25, self.p30, self.p35, self.p40, self.p45,
         self.p50, self.p55, self.p60, self.p65, self.p70, self.p75, self.p80, self.p85, self.p90]
    }
}


#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
enum PercentPatternType {
    #[serde(rename = "pattern1")]
    Pattern1,
    #[serde(rename = "pattern2")]
    Pattern2,
    #[serde(rename = "pattern3")]
    Pattern3,
    #[serde(rename = "pattern4")]
    Pattern4,
    #[serde(rename = "pattern5")]
    Pattern5,
    #[serde(rename = "pattern6")]
    Pattern6,
}

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct AttributePercentPattern {
    attribute1: Option<PercentPatternType>,
    attribute2: Option<PercentPatternType>,
    attribute3: Option<PercentPatternType>,
}

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct AreaPercentPatterns {
    area1: AttributePercentPattern,
    area2: AttributePercentPattern,
    area3: AttributePercentPattern,
    area4: AttributePercentPattern,
    area5: AttributePercentPattern,
    area6: AttributePercentPattern,
    area7: AttributePercentPattern,
    area8: AttributePercentPattern,
    area9: AttributePercentPattern,
    area10: AttributePercentPattern,
}

impl AreaPercentPatterns {
    fn get_by_area(&self, map_area: &MapArea) -> AttributePercentPattern {
        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,
        }
    }
}


#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct PercentRatePatterns {
    pattern1: PercentRate,
    pattern2: PercentRate,
    pattern3: PercentRate,
    pattern4: PercentRate,
    pattern5: PercentRate,
    pattern6: PercentRate,
}

impl PercentRatePatterns {
    fn get_by_pattern(&self, pattern: &PercentPatternType) -> PercentRate {
        match pattern {
            PercentPatternType::Pattern1 => self.pattern1,
            PercentPatternType::Pattern2 => self.pattern2,
            PercentPatternType::Pattern3 => self.pattern3,
            PercentPatternType::Pattern4 => self.pattern4,
            PercentPatternType::Pattern5 => self.pattern5,
            PercentPatternType::Pattern6 => self.pattern6
        }
    }
}

pub struct AttributeTable {
    attribute_rates: AttributeRates,
    percent_rates: PercentRatePatterns,
    area_percent_patterns: AreaPercentPatterns,
}


impl AttributeTable {
    pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> AttributeTable {
        // TODO: new these
        let attribute_rates: AttributeRates = load_data_file(episode, difficulty, section_id, "attribute_rate.toml");
        let percent_rates: PercentRatePatterns = load_data_file(episode, difficulty, section_id, "percent_rate.toml");
        let area_percent_patterns: AreaPercentPatterns = load_data_file(episode, difficulty, section_id, "area_percent_pattern.toml");

        AttributeTable {
            attribute_rates,
            percent_rates,
            area_percent_patterns,
        }
    }


    fn generate_attribute<R: Rng>(&self, pattern: &PercentPatternType, rates: &AttributeRate, rng: &mut R) -> Option<WeaponAttribute> {
        let attribute_weights = WeightedIndex::new([rates.none, rates.native, rates.abeast, rates.machine, rates.dark, rates.hit]).unwrap();
        let attr = match attribute_weights.sample(rng) {
            0 => return None,
            1 => Attribute::Native,
            2 => Attribute::ABeast,
            3 => Attribute::Machine,
            4 => Attribute::Dark,
            5 => Attribute::Hit,
            _ => panic!()
        };

        let percents = self.percent_rates.get_by_pattern(pattern);

        let value_weights = WeightedIndex::new(percents.as_array()).unwrap();
        let value = value_weights.sample(rng);
        let percent = ((value + 1) * 5) as i8;

        Some(WeaponAttribute {
            attr,
            value: percent
        })
    }

    fn attributes<R: Rng>(&self, percent_pattern: &AttributePercentPattern, attribute_rate: &AttributeRate, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
        let mut percents = vec![
            percent_pattern.attribute1.and_then(|pattern_type| {
                self.generate_attribute(&pattern_type, attribute_rate, rng)
            }),
            percent_pattern.attribute2.and_then(|pattern_type| {
                self.generate_attribute(&pattern_type, attribute_rate, rng)
            }),
            percent_pattern.attribute3.and_then(|pattern_type| {
                self.generate_attribute(&pattern_type, attribute_rate, rng)
            }),
        ];
        percents.sort_by_key(|p| {
            p.as_ref().map(|a| a.attr)
        });
        percents.dedup_by_key(|p| {
            p.as_ref().map(|a| a.attr)
        });
        percents.iter()
            .fold(([None; 3], 0), |(mut acc, index), p| { // one day I'll be able to collece into an array
                if  p.is_some() {
                    acc[index] = *p;
                    (acc, index + 1)
                }
                else {
                    (acc, index)
                }
            }).0
    }

    fn generate_attributes<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
        let percent_pattern = self.area_percent_patterns.get_by_area(map_area);
        let attribute_rate = self.attribute_rates.get_by_area(map_area);
        self.attributes(&percent_pattern, &attribute_rate, rng)
    }

    pub fn generate_rare_attributes<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> [Option<WeaponAttribute>; 3] {
        let percent_pattern = AttributePercentPattern {
            attribute1: Some(PercentPatternType::Pattern6),
            attribute2: Some(PercentPatternType::Pattern6),
            attribute3: Some(PercentPatternType::Pattern6),
        };
        let attribute_rate = self.attribute_rates.get_by_area(map_area);
        self.attributes(&percent_pattern, &attribute_rate, rng)
    }
}


#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct SpecialRate {
    rank: u32,
    rate: u32,
}


#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
struct SpecialRates {
    area1: SpecialRate,
    area2: SpecialRate,
    area3: SpecialRate,
    area4: SpecialRate,
    area5: SpecialRate,
    area6: SpecialRate,
    area7: SpecialRate,
    area8: SpecialRate,
    area9: SpecialRate,
    area10: SpecialRate,
}

impl SpecialRates {
    fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> SpecialRates {
        load_data_file(episode, difficulty, section_id, "weapon_special_rate.toml")
    }

    fn rate_by_area(&self, map_area: &MapArea) -> SpecialRate {
        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,
        }
    }

    fn random_special_by_rank<R: Rng>(&self, rank: u32, rng: &mut R) -> WeaponSpecial {
        let specials = match rank {
            1 => vec![WeaponSpecial::Draw, WeaponSpecial::Heart, WeaponSpecial::Ice, WeaponSpecial::Bind, WeaponSpecial::Heat, WeaponSpecial::Shock,
                      WeaponSpecial::Dim, WeaponSpecial::Panic],
            2 => vec![WeaponSpecial::Drain, WeaponSpecial::Mind, WeaponSpecial::Frost, WeaponSpecial::Hold, WeaponSpecial::Fire, WeaponSpecial::Thunder,
                      WeaponSpecial::Shadow, WeaponSpecial::Riot, WeaponSpecial::Masters,  WeaponSpecial::Charge],
            3 => vec![WeaponSpecial::Fill, WeaponSpecial::Soul, WeaponSpecial::Freeze, WeaponSpecial::Seize, WeaponSpecial::Flame, WeaponSpecial::Storm,
                      WeaponSpecial::Dark, WeaponSpecial::Havoc, WeaponSpecial::Lords, WeaponSpecial::Charge, WeaponSpecial::Spirit,  WeaponSpecial::Devils],
            4 => vec![WeaponSpecial::Gush, WeaponSpecial::Geist, WeaponSpecial::Blizzard, WeaponSpecial::Arrest, WeaponSpecial::Burning, WeaponSpecial::Tempest,
                      WeaponSpecial::Hell, WeaponSpecial::Chaos, WeaponSpecial::Kings, WeaponSpecial::Charge, WeaponSpecial::Berserk,  WeaponSpecial::Demons],
            _ => panic!(),
        };

        *specials.choose(rng).unwrap()
    }

    fn get_special<R: Rng>(&self, map_area:  &MapArea, rng: &mut R) -> Option<WeaponSpecial> {
        let rate = self.rate_by_area(map_area);
        if rng.gen_range(0, 100) < rate.rate {
            Some(self.random_special_by_rank(rate.rank, rng))
        }
        else {
            None
        }
    }
}


pub struct GenericWeaponTable {
    rank_table:  HashMap<WeaponDropType, Vec<WeaponType>>,
    weapon_ratio: WeaponRatios,
    grind_rates: GrindRates,
    attribute_table: AttributeTable,
    special_table: SpecialRates,
}


impl GenericWeaponTable {
    pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> GenericWeaponTable {
        let mut rank_table = HashMap::new();
        rank_table.insert(WeaponDropType::Saber, vec![WeaponType::Saber, WeaponType::Brand, WeaponType::Buster, WeaponType::Pallasch, WeaponType::Gladius]);
        rank_table.insert(WeaponDropType::Sword, vec![WeaponType::Sword, WeaponType::Gigush, WeaponType::Breaker, WeaponType::Claymore, WeaponType::Calibur]);
        rank_table.insert(WeaponDropType::Dagger, vec![WeaponType::Dagger, WeaponType::Knife, WeaponType::Blade, WeaponType::Edge, WeaponType::Ripper]);
        rank_table.insert(WeaponDropType::Partisan, vec![WeaponType::Partisan, WeaponType::Halbert, WeaponType::Glaive, WeaponType::Berdys, WeaponType::Gungnir]);
        rank_table.insert(WeaponDropType::Slicer, vec![WeaponType::Slicer, WeaponType::Spinner, WeaponType::Cutter, WeaponType::Sawcer, WeaponType::Diska]);
        rank_table.insert(WeaponDropType::Handgun, vec![WeaponType::Handgun, WeaponType::Autogun, WeaponType::Lockgun, WeaponType::Railgun, WeaponType::Raygun]);
        rank_table.insert(WeaponDropType::Rifle, vec![WeaponType::Rifle, WeaponType::Sniper, WeaponType::Blaster, WeaponType::Beam, WeaponType::Laser]);
        rank_table.insert(WeaponDropType::Mechgun, vec![WeaponType::Mechgun, WeaponType::Assault, WeaponType::Repeater, WeaponType::Gatling, WeaponType::Vulcan]);
        rank_table.insert(WeaponDropType::Shot, vec![WeaponType::Shot, WeaponType::Spread, WeaponType::Cannon, WeaponType::Launcher, WeaponType::Arms]);
        rank_table.insert(WeaponDropType::Cane, vec![WeaponType::Cane, WeaponType::Stick, WeaponType::Mace, WeaponType::Club]);
        rank_table.insert(WeaponDropType::Rod, vec![WeaponType::Rod, WeaponType::Pole, WeaponType::Pillar, WeaponType::Striker]);
        rank_table.insert(WeaponDropType::Wand, vec![WeaponType::Wand, WeaponType::Staff, WeaponType::Baton, WeaponType::Scepter]);

        GenericWeaponTable {
            rank_table,
            weapon_ratio: WeaponRatios::new(episode, difficulty, section_id),
            grind_rates: GrindRates::new(episode, difficulty, section_id),
            attribute_table: AttributeTable::new(episode, difficulty, section_id),
            special_table: SpecialRates::new(episode, difficulty, section_id),
        }
    }

    fn area_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> (u32, u32) {
        if ratio.rank >= 0 {
            (map_area.drop_area_value().unwrap_or(0), ratio.rank as u32)
        }
        else {
            ((map_area.drop_area_value().unwrap_or(0) as i32 + ratio.rank) as u32, 0)
        }
    }

    fn get_possible_weapon_types(&self, map_area: &MapArea) -> BTreeMap<WeaponDropType, WeaponRatio> {
        let mut valid_weapons = BTreeMap::new();

        let item_rate_pairs = vec![
            (WeaponDropType::Saber, self.weapon_ratio.saber),
            (WeaponDropType::Sword, self.weapon_ratio.sword),
            (WeaponDropType::Dagger, self.weapon_ratio.dagger),
            (WeaponDropType::Partisan, self.weapon_ratio.partisan),
            (WeaponDropType::Slicer, self.weapon_ratio.slicer),
            (WeaponDropType::Handgun, self.weapon_ratio.handgun),
            (WeaponDropType::Rifle, self.weapon_ratio.rifle),
            (WeaponDropType::Mechgun, self.weapon_ratio.mechgun),
            (WeaponDropType::Shot, self.weapon_ratio.shot),
            (WeaponDropType::Cane, self.weapon_ratio.cane),
            (WeaponDropType::Rod, self.weapon_ratio.rod),
            (WeaponDropType::Wand, self.weapon_ratio.wand),
        ];

        for (item_type, ratio) in item_rate_pairs.into_iter() {
            if ratio.rate > 0 && map_area.drop_area_value().map(|k| k as i32 + ratio.rank).unwrap_or(-1) >= 0 {
                valid_weapons.insert(item_type, ratio);
            }
        }

        valid_weapons
    }

    fn weapon_type<R: Rng>(&self, possible_weapon_types: &BTreeMap<WeaponDropType, WeaponRatio>, _map_area: &MapArea, rng: &mut R) -> WeaponDropType {
        let mut weapon_rates = possible_weapon_types.iter()
            .map(|(weapon, stat)| {
                (weapon, stat.rate)
            });
        let weapon_choice = WeightedIndex::new(weapon_rates.clone().map(|wr| wr.1)).unwrap();
        *weapon_rates.nth(weapon_choice.sample(rng)).unwrap().0
    }


    fn weapon_rank(&self, ratio: &WeaponRatio, map_area: &MapArea) -> u32 {
        let (area, rank) = self.area_rank(ratio, map_area);
        let weapon_rank = rank + area / ratio.inc;

        std::cmp::max(weapon_rank, 0)
    }

    fn actual_weapon(&self, weapon_type: &WeaponDropType, weapon_rank: u32) -> WeaponType {
        let weapons = self.rank_table.get(weapon_type).unwrap();
        weapons[std::cmp::min(weapons.len() - 1, weapon_rank as usize)]
    }

    fn get_grind<R: Rng>(&self, ratio: &WeaponRatio, map_area: &MapArea, rng: &mut R) -> usize {
        let (area, _) = self.area_rank(ratio, map_area);
        let pattern = std::cmp::min(area % ratio.inc, 3);

        let weights = self.grind_rates.grind_rate[pattern as usize];
        let grind_choice = WeightedIndex::new(weights).unwrap();
        grind_choice.sample(rng)
    }

    pub fn get_drop<R: Rng>(&self, map_area: &MapArea, rng: &mut R) -> Option<ItemDropType> {
        let possible_weapon_types = self.get_possible_weapon_types(map_area);
        let weapon_type = self.weapon_type(&possible_weapon_types, map_area, rng);
        let ratio = possible_weapon_types.get(&weapon_type).unwrap();
        let weapon_rank = self.weapon_rank(ratio, map_area);
        let weapon_grind = self.get_grind(ratio, map_area, rng);
        let weapon_attributes = self.attribute_table.generate_attributes(map_area, rng);
        let weapon_special = self.special_table.get_special(map_area, rng);
        let actual_weapon = self.actual_weapon(&weapon_type, weapon_rank);

        Some(ItemDropType::Weapon(Weapon {
            weapon: actual_weapon,
            special: weapon_special,
            grind: weapon_grind as u8,
            attrs: weapon_attributes,
            tekked: weapon_special.is_none(),
        }))
    }
}



#[cfg(test)]
mod test {
    use super::*;
    use rand::{SeedableRng};
    #[test]
    fn test_weapon_generation() {
        let mut rng = rand_chacha::ChaCha20Rng::from_seed([23;32]);

        let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Normal, SectionID::Skyly);
        assert!(gwt.get_drop(&MapArea::Forest1, &mut rng) == Some(ItemDropType::Weapon(Weapon {
            weapon: WeaponType::Cane,
            special: None,
            grind: 0,
            attrs: [None, None, None],
            tekked: true,
        })));

        let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Hard, SectionID::Skyly);
        assert!(gwt.get_drop(&MapArea::Caves2, &mut rng) == Some(ItemDropType::Weapon(Weapon {
            weapon: WeaponType::Sniper,
            special: None,
            grind: 2,
            attrs: [None, None, None],
            tekked: true,
        })));

        let gwt = GenericWeaponTable::new(Episode::One, Difficulty::VeryHard, SectionID::Skyly);
        assert!(gwt.get_drop(&MapArea::Mines1, &mut rng) == Some(ItemDropType::Weapon(Weapon {
            weapon: WeaponType::Club,
            special: Some(WeaponSpecial::Berserk),
            grind: 0,
            attrs: [None, None, None],
            tekked: false,
        })));

        let gwt = GenericWeaponTable::new(Episode::One, Difficulty::Ultimate, SectionID::Skyly);
        assert!(gwt.get_drop(&MapArea::DarkFalz, &mut rng) == Some(ItemDropType::Weapon(Weapon {
            weapon: WeaponType::Vulcan,
            special: None,
            grind: 0,
            attrs: [Some(WeaponAttribute {attr: Attribute::ABeast, value: 30}), Some(WeaponAttribute {attr: Attribute::Dark, value: 30}), None],
            tekked: true,
        })));
    }
}