#![allow(dead_code, unused_must_use)]
// TODO: there is some structure duplication that occurs here:
// the rare and box tables instantiate their own copies of the
// generic drop tables as they need them to apply their modifiers
// to their drops


pub mod rare_drop_table;
mod generic_weapon;
mod generic_armor;
mod generic_shield;
mod generic_unit;
mod tool_table;
mod tech_table;
mod box_drop_table;

use std::collections::HashMap;
use std::fs::File;
use std::path::PathBuf;
use std::io::Read;
use serde::{Serialize, Deserialize};
use rand::{Rng, SeedableRng};

use maps::monster::MonsterType;
use maps::room::{Difficulty, Episode};
use maps::area::MapArea;
use entity::character::SectionID;
use crate::generic_weapon::GenericWeaponTable;
use crate::generic_armor::GenericArmorTable;
use crate::generic_shield::GenericShieldTable;
use crate::generic_unit::GenericUnitTable;
use crate::tool_table::ToolTable;
use crate::rare_drop_table::RareDropTable;
use crate::box_drop_table::BoxDropTable;
use maps::object::MapObject;
use entity::item::{ItemType, weapon, armor, shield, unit, mag, tool, tech, esweapon};


fn data_file_path(episode: Episode, difficulty: Difficulty, section_id: SectionID, filename: &str) -> PathBuf {
    let mut path = PathBuf::from("data/drops/");
    path.push(episode.to_string());
    path.push(difficulty.to_string().to_lowercase());
    path.push(section_id.to_string().to_lowercase());
    path.push(filename);
    path
}

pub fn load_data_file<T: serde::de::DeserializeOwned>(episode: Episode, difficulty: Difficulty, section_id: SectionID, filename: &str) -> T {
    let path = data_file_path(episode, difficulty, section_id, filename);
    let mut f = File::open(path).unwrap();
    let mut s = String::new();
    f.read_to_string(&mut s);

    toml::from_str::<T>(s.as_str()).unwrap()
}

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub enum MonsterDropType {
    #[serde(rename = "weapon")]
    Weapon,
    #[serde(rename = "armor")]
    Armor,
    #[serde(rename = "shield")]
    Shield,
    #[serde(rename = "unit")]
    Unit,
    #[serde(rename = "none")]
    None,
}

#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
pub struct MonsterDropStats {
    pub dar: u32,
    pub drop_type: MonsterDropType,
    pub min_meseta: u32,
    pub max_meseta: u32,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ItemDropType {
    Weapon(weapon::Weapon),
    Armor(armor::Armor),
    Shield(shield::Shield),
    Unit(unit::Unit),
    Tool(tool::Tool),
    //Tools(Vec<tool::Tool>),
    TechniqueDisk(tech::TechniqueDisk),
    Mag(mag::Mag),
    Meseta(u32),
}

impl ItemDropType {
    pub fn parse_item_from_bytes(data: [u8; 16]) -> Option<ItemDropType> {
        let item_type = weapon::WeaponType::parse_type([data[0],data[1],data[2]]).map(ItemType::Weapon)
            .or_else(|_| armor::ArmorType::parse_type([data[0],data[1],data[2]]).map(ItemType::Armor))
            .or_else(|_| shield::ShieldType::parse_type([data[0],data[1],data[2]]).map(ItemType::Shield))
            .or_else(|_| unit::UnitType::parse_type([data[0],data[1],data[2]]).map(ItemType::Unit))
            .or_else(|_| mag::MagType::parse_type([data[0],data[1],data[2]]).map(ItemType::Mag))
            .or_else(|_| tool::ToolType::parse_type([data[0],data[1],data[2]]).map(ItemType::Tool))
            .or_else(|_| esweapon::ESWeaponType::parse_type([data[0],data[1],data[2]]).map(ItemType::ESWeapon)).ok()?;

        match item_type {
            ItemType::Weapon(_w) => Some(ItemDropType::Weapon(weapon::Weapon::from_bytes(data).ok()?)),
            ItemType::Armor(_a) => Some(ItemDropType::Armor(armor::Armor::from_bytes(data).ok()?)),
            ItemType::Shield(_s) => Some(ItemDropType::Shield(shield::Shield::from_bytes(data).ok()?)),
            ItemType::Unit(_u) => Some(ItemDropType::Unit(unit::Unit::from_bytes(data).ok()?)),
            ItemType::Mag(_m) => Some(ItemDropType::Mag(mag::Mag::from_bytes(data).ok()?)),
            ItemType::Tool(_t) => Some(ItemDropType::Tool(tool::Tool::from_bytes(data).ok()?)),
            _ => None,
        }
    }
}

#[derive(Clone, Debug)]
pub struct ItemDrop {
    pub map_area: MapArea,
    pub x: f32,
    pub y: f32,
    pub z: f32,
    pub item: ItemDropType,
}


pub trait DropTable {
    fn get_drop(&mut self, map_area: &MapArea, monster: &MonsterType) -> Option<ItemDropType>;
    fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option<ItemDropType>;
}

pub struct StandardDropTable {
    monster_stats: HashMap<MonsterType, MonsterDropStats>,
    rare_table: RareDropTable,
    weapon_table: GenericWeaponTable,
    armor_table: GenericArmorTable,
    shield_table: GenericShieldTable,
    unit_table: GenericUnitTable,
    tool_table: ToolTable,
    box_table: BoxDropTable,
    rng: rand_chacha::ChaCha20Rng,
}

impl StandardDropTable {
    #[allow(clippy::new_ret_no_self)]
    pub fn new(episode: Episode, difficulty: Difficulty, section_id: SectionID) -> Box<dyn DropTable + Send + Sync> {
        let monster_stats: HashMap<String, MonsterDropStats> = load_data_file(episode, difficulty, section_id, "monster_dar.toml");

        Box::new(StandardDropTable {
            monster_stats: monster_stats.into_iter().map(|(m, s)| (m.parse().unwrap(), s)).collect(),
            rare_table: RareDropTable::new(episode, difficulty, section_id),
            weapon_table: GenericWeaponTable::new(episode, difficulty, section_id),
            armor_table: GenericArmorTable::new(episode, difficulty, section_id),
            shield_table: GenericShieldTable::new(episode, difficulty, section_id),
            unit_table: GenericUnitTable::new(episode, difficulty, section_id),
            tool_table: ToolTable::new(episode, difficulty, section_id),
            box_table: BoxDropTable::new(episode, difficulty, section_id),
            rng: rand_chacha::ChaCha20Rng::from_entropy(),
        })
    }

    pub fn builder() -> DropTableBuilder {
        DropTableBuilder {
            monster_stats: None,
            rare_table: None,
            weapon_table: None,
            armor_table: None,
            shield_table: None,
            unit_table: None,
            tool_table: None,
            box_table: None,
            rng: None,
        }
    }

    fn generate_meseta(&mut self, monster: &MonsterDropStats) -> Option<ItemDropType> {
        Some(ItemDropType::Meseta(self.rng.gen_range(monster.min_meseta, monster.max_meseta + 1)))
    }

    fn generate_typed_drop(&mut self, map_area: &MapArea, monster: &MonsterDropStats) -> Option<ItemDropType> {
        match monster.drop_type {
            MonsterDropType::Weapon => self.weapon_table.get_drop(map_area, &mut self.rng),
            MonsterDropType::Armor => self.armor_table.get_drop(map_area, &mut self.rng),
            MonsterDropType::Shield => self.shield_table.get_drop(map_area, &mut self.rng),
            MonsterDropType::Unit => self.unit_table.get_drop(map_area, &mut self.rng),
            MonsterDropType::None => None,
        }
    }
}

impl DropTable for StandardDropTable {
    fn get_drop(&mut self, map_area: &MapArea, monster: &MonsterType) -> Option<ItemDropType> {
        let monster_stat = *self.monster_stats.get(monster)?;

        let drop_anything = self.rng.gen_range(0, 100);
        if drop_anything > monster_stat.dar {
            return None;
        }

        if let Some(item) = self.rare_table.get_drop(map_area, monster, &mut self.rng) {
            return Some(item);
        }

        let drop_type = self.rng.gen_range(0, 3);

        match drop_type {
            0 => {
                self.generate_meseta(&monster_stat)
            },
            1 => {
                self.tool_table.get_drop(map_area, &mut self.rng)
            },
            2 => {
                self.generate_typed_drop(map_area, &monster_stat)
            },
            _ => panic!()
        }
    }

    fn get_box_drop(&mut self, map_area: &MapArea, object: &MapObject) -> Option<ItemDropType> {
        self.box_table.get_drop(map_area, object, &mut self.rng)
    }
}


pub struct DropTableBuilder {
    monster_stats: Option<HashMap<MonsterType, MonsterDropStats>>,
    rare_table: Option<RareDropTable>,
    weapon_table: Option<GenericWeaponTable>,
    armor_table: Option<GenericArmorTable>,
    shield_table: Option<GenericShieldTable>,
    unit_table: Option<GenericUnitTable>,
    tool_table: Option<ToolTable>,
    box_table: Option<BoxDropTable>,
    rng: Option<rand_chacha::ChaCha20Rng>,
}

// TODO: add the rest of these later I just need these ones right now
impl DropTableBuilder {
    #[must_use]
    pub fn monster_stats(mut self, monster_stats: HashMap<MonsterType, MonsterDropStats>) -> DropTableBuilder {
        self.monster_stats = Some(monster_stats);
        self
    }

    #[must_use]
    pub fn monster_stat(mut self, monster_type: MonsterType, drop_stats: MonsterDropStats) -> DropTableBuilder {
        match &mut self.monster_stats {
            Some(monster_stats) => {
                monster_stats.insert(monster_type, drop_stats);
            },
            None => {
                let mut monster_stats = HashMap::default();
                monster_stats.insert(monster_type, drop_stats);
                self.monster_stats = Some(monster_stats);
            }
        }
        self
    }

    #[must_use]
    pub fn rare_table(mut self, rare_table: RareDropTable) -> DropTableBuilder {
        self.rare_table = Some(rare_table);
        self
    }

    pub fn build(self, episode: Episode, difficulty: Difficulty, section_id: SectionID) -> Box<dyn DropTable + Send + Sync> {
        Box::new(StandardDropTable {
            monster_stats: self.monster_stats.unwrap_or_else(|| {
                let monster_stats: HashMap<String, MonsterDropStats> = load_data_file(episode, difficulty, section_id, "monster_dar.toml");
                monster_stats.into_iter().map(|(m, s)| (m.parse().unwrap(), s)).collect()
            }),
            rare_table: self.rare_table.unwrap_or_else(|| RareDropTable::new(episode, difficulty, section_id)),
            weapon_table: self.weapon_table.unwrap_or_else(|| GenericWeaponTable::new(episode, difficulty, section_id)),
            armor_table: self.armor_table.unwrap_or_else(|| GenericArmorTable::new(episode, difficulty, section_id)),
            shield_table: self.shield_table.unwrap_or_else(|| GenericShieldTable::new(episode, difficulty, section_id)),
            unit_table: self.unit_table.unwrap_or_else(|| GenericUnitTable::new(episode, difficulty, section_id)),
            tool_table: self.tool_table.unwrap_or_else(|| ToolTable::new(episode, difficulty, section_id)),
            box_table: self.box_table.unwrap_or_else(|| BoxDropTable::new(episode, difficulty, section_id)),
            rng: self.rng.unwrap_or_else(rand_chacha::ChaCha20Rng::from_entropy),
        })
    }
}

struct NullDropTable;

impl DropTable for NullDropTable {
    fn get_drop(&mut self, _map_area: &MapArea, _monster: &MonsterType) -> Option<ItemDropType> {
        None
    }
    fn get_box_drop(&mut self, _map_area: &MapArea, _object: &MapObject) -> Option<ItemDropType> {
        None
    }
}

pub fn null_drop_table(_episode: Episode, _difficult: Difficulty, _section_id: SectionID) -> Box<dyn DropTable + Send + Sync> {
    Box::new(NullDropTable)
}

#[cfg(test)]
mod test {
    use super::*;
    use rand::seq::IteratorRandom;

    #[test]
    fn test_initializing_drop_table() {
        let mut rng = rand_chacha::ChaCha20Rng::from_entropy();
        let episode = vec![Episode::One, Episode::Two].into_iter().choose(&mut rng).unwrap();
        let difficulty = vec![Difficulty::Normal, Difficulty::Hard, Difficulty::VeryHard, Difficulty::Ultimate]
            .into_iter().choose(&mut rng).unwrap();
        let section_id = vec![SectionID::Viridia, SectionID::Greenill, SectionID::Skyly, SectionID::Bluefull, SectionID::Purplenum,
                              SectionID::Pinkal, SectionID::Redria, SectionID::Oran, SectionID::Yellowboze, SectionID::Whitill]
            .into_iter().choose(&mut rng).unwrap();
        DropTable::new(episode, difficulty, section_id);
    }
}