use super::{ Raws, Reaction };
use crate::components::*;
use crate::gamesystem::*;
use crate::gui::Ancestry;
use crate::random_table::RandomTable;
use crate::config::CONFIG;
use crate::consts::visuals::BLOODSTAIN_COLOUR;
use crate::consts::entity::DEFAULT_VIEWSHED_STANDARD;
use bracket_lib::prelude::*;
use specs::prelude::*;
use specs::saveload::{ MarkedBuilder, SimpleMarker };
use std::collections::{ HashMap, HashSet };

/// Applies effects to the entity - e.g. "healing: 1d4+1", where
///  effects are components on the entity with varying parameters.
macro_rules! apply_effects {
    ($effects:expr, $eb:expr) => {
        for effect in $effects.iter() {
            let effect_name = effect.0.as_str();
            match effect_name {
                "heal" => {
                    let dice = parse_dice_string(effect.1.as_str()).expect("Failed to parse dice string");
                    $eb = $eb.with(ProvidesHealing { n_dice: dice.n_dice, sides: dice.die_type, modifier: dice.bonus })
                }
                "ranged" => $eb = $eb.with(Ranged { range: effect.1.parse::<i32>().unwrap() }),
                "damage" => {
                    let (damage_type, dice) = parse_damage_string(effect.1.as_str());
                    $eb = $eb.with(InflictsDamage { damage_type, n_dice: dice.n_dice, sides: dice.die_type, modifier: dice.bonus })
                }
                "aoe" => $eb = $eb.with(AOE { radius: effect.1.parse::<i32>().unwrap() }),
                "confusion" => $eb = $eb.with(Confusion { turns: effect.1.parse::<i32>().unwrap() }),
                "ac" => $eb = $eb.with(ArmourClassBonus { amount: effect.1.parse::<i32>().unwrap() }),
                "to_hit" => $eb = $eb.with(ToHitBonus { amount: effect.1.parse::<i32>().unwrap() }),
                "particle_line" => $eb = $eb.with(parse_particle_line(&effect.1)),
                "particle_burst" => $eb = $eb.with(parse_particle_burst(&effect.1)),
                "particle" => $eb = $eb.with(parse_particle(&effect.1)),
                _ => console::log(format!("Warning: effect {} not implemented.", effect_name)),
            }
        }
    };
}

/// Applies flags to the entity - e.g. "blocks_tile", where
/// flags are components that have no parameters to modify.
macro_rules! apply_flags {
    ($flags:expr, $eb:expr) => {
        let mut damage_modifiers: HashMap<DamageType, DamageModifier> = HashMap::new();
        for flag in $flags.iter() {
            match flag.as_str() {
                // --- PROP FLAGS BEGIN HERE ---
                "HIDDEN" => $eb = $eb.with(Hidden {}),
                "BLOCKS_TILE" => $eb = $eb.with(BlocksTile {}),
                "BLOCKS_VISIBILITY" => $eb = $eb.with(BlocksVisibility {}),
                "ENTRY_TRIGGER" => $eb = $eb.with(EntryTrigger {}),
                "SINGLE_ACTIVATION" => $eb = $eb.with(SingleActivation {}),
                // --- EFFECT FLAGS ---
                "FOOD" => $eb = $eb.with(ProvidesNutrition {}),
                "CONSUMABLE" => $eb = $eb.with(Consumable {}),
                "CHARGES" => $eb = $eb.with(Charges { uses: 3, max_uses: 3 }),
                "REMOVE_CURSE" => $eb = $eb.with(ProvidesRemoveCurse {}),
                "IDENTIFY" => $eb = $eb.with(ProvidesIdentify {}),
                "DIGGER" => $eb = $eb.with(Digger {}),
                "MAGICMAP" => $eb = $eb.with(MagicMapper {}),
                // CAN BE DESTROYED BY DAMAGE
                "DESTRUCTIBLE" => $eb = $eb.with(Destructible {}),
                // --- EQUIP SLOTS ---
                "EQUIP_MELEE" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Melee }),
                "EQUIP_SHIELD" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Shield }),
                "EQUIP_HEAD" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Head }),
                "EQUIP_BODY" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Body }),
                "EQUIP_FEET" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Feet }),
                "EQUIP_HANDS" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Hands }),
                "EQUIP_NECK" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Neck }),
                "EQUIP_BACK" => $eb = $eb.with(Equippable { slot: EquipmentSlot::Back }),
                // --- MOB ANCESTRIES ---
                "IS_PLAYER" => $eb = $eb.with(Player {}),
                "IS_HUMAN" => $eb = $eb.with(HasAncestry { name: Ancestry::Human }),
                "IS_DWARF" => $eb = $eb.with(HasAncestry { name: Ancestry::Dwarf }),
                "IS_ELF" => $eb = $eb.with(HasAncestry { name: Ancestry::Elf }),
                "IS_CATFOLK" => $eb = $eb.with(HasAncestry { name: Ancestry::Catfolk }),
                "IS_GNOME" => $eb = $eb.with(HasAncestry { name: Ancestry::Gnome }),
                // --- MOB FACTIONS ---
                "MINDLESS" => $eb = $eb.with(Faction { name: "mindless".to_string() }),
                "NEUTRAL" => $eb = $eb.with(Faction { name: "neutral".to_string() }),
                "HERBIVORE" => $eb = $eb.with(Faction { name: "herbivore".to_string() }),
                "CARNIVORE" => $eb = $eb.with(Faction { name: "carnivore".to_string() }),
                // --- DAMAGE MODIFIERS ---
                "PHYS_IMMUNITY" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Immune); }
                "PHYS_WEAK" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Weakness); }
                "PHYS_RES" => { damage_modifiers.insert(DamageType::Physical, DamageModifier::Resistance); }
                "MAGIC_IMMUNITY" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Immune); }
                "MAGIC_WEAK" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Weakness); }
                "MAGIC_RES" => { damage_modifiers.insert(DamageType::Magic, DamageModifier::Resistance); }
                "FIRE_IMMUNITY" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Immune); }
                "FIRE_WEAK" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Weakness); }
                "FIRE_RES" => { damage_modifiers.insert(DamageType::Fire, DamageModifier::Resistance); }
                "COLD_IMMUNITY" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Immune); }
                "COLD_WEAK" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Weakness); }
                "COLD_RES" => { damage_modifiers.insert(DamageType::Cold, DamageModifier::Resistance); }
                "POISON_IMMUNITY" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Immune); }
                "POISON_WEAK" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Weakness); }
                "POISON_RES" => { damage_modifiers.insert(DamageType::Poison, DamageModifier::Resistance); }
                // --- MOVEMENT MODES --- ( defaults to WANDER )
                "STATIC" => $eb = $eb.with(MoveMode { mode: Movement::Static }),
                "RANDOM_PATH" => $eb = $eb.with(MoveMode { mode: Movement::RandomWaypoint { path: None } }),
                // --- RANDOM MOB ATTRIBUTES ---
                "GREEN_BLOOD" => $eb = $eb.with(Bleeds { colour: RGB::named((0, 153, 0)) }),
                "BLUE_BLOOD" => $eb = $eb.with(Bleeds { colour: RGB::named((0, 0, 153)) }),
                "SMALL_GROUP" => {} // These flags are for region spawning,
                "LARGE_GROUP" => {} // and don't need to apply a component.
                "MULTIATTACK" => $eb = $eb.with(MultiAttack {}),
                "BLIND" => $eb = $eb.with(Blind {}),
                _ => console::log(format!("Unrecognised flag: {}", flag.as_str())),
            }
        }
        if damage_modifiers.len() > 0 {
            $eb = $eb.with(HasDamageModifiers { modifiers: damage_modifiers });
        }
    };
}

pub enum SpawnType {
    AtPosition {
        x: i32,
        y: i32,
    },
    Equipped {
        by: Entity,
    },
    Carried {
        by: Entity,
    },
}

pub struct RawMaster {
    raws: Raws,
    item_index: HashMap<String, usize>,
    mob_index: HashMap<String, usize>,
    prop_index: HashMap<String, usize>,
    table_index: HashMap<String, usize>,
    loot_index: HashMap<String, usize>,
    faction_index: HashMap<String, HashMap<String, Reaction>>,
    ancestry_index: HashMap<String, HashSet<String>>,
}

impl RawMaster {
    pub fn empty() -> RawMaster {
        RawMaster {
            raws: Raws {
                items: Vec::new(),
                mobs: Vec::new(),
                props: Vec::new(),
                spawn_tables: Vec::new(),
                loot_tables: Vec::new(),
                factions: Vec::new(),
                ancestries: Vec::new(),
            },
            item_index: HashMap::new(),
            mob_index: HashMap::new(),
            prop_index: HashMap::new(),
            table_index: HashMap::new(),
            loot_index: HashMap::new(),
            faction_index: HashMap::new(),
            ancestry_index: HashMap::new(),
        }
    }

    pub fn load(&mut self, raws: Raws) {
        self.raws = raws;
        let mut used_names: HashSet<String> = HashSet::new();
        for (i, item) in self.raws.items.iter().enumerate() {
            check_for_duplicate_entries(&used_names, &item.id);
            self.item_index.insert(item.id.clone(), i);
            used_names.insert(item.id.clone());
        }
        for (i, mob) in self.raws.mobs.iter().enumerate() {
            check_for_duplicate_entries(&used_names, &mob.id);
            self.mob_index.insert(mob.id.clone(), i);
            used_names.insert(mob.id.clone());
        }
        for (i, prop) in self.raws.props.iter().enumerate() {
            check_for_duplicate_entries(&used_names, &prop.id);
            self.prop_index.insert(prop.id.clone(), i);
            used_names.insert(prop.id.clone());
        }
        for (i, table) in self.raws.spawn_tables.iter().enumerate() {
            check_for_duplicate_entries(&used_names, &table.id);
            self.table_index.insert(table.id.clone(), i);
            used_names.insert(table.id.clone());
            for entry in table.table.iter() {
                check_for_unspecified_entity(&used_names, &entry.id);
            }
        }
        for (i, loot_table) in self.raws.loot_tables.iter().enumerate() {
            check_for_duplicate_entries(&used_names, &loot_table.id);
            self.loot_index.insert(loot_table.id.clone(), i);
            for entry in loot_table.table.iter() {
                check_for_unspecified_entity(&used_names, &entry.id);
            }
        }
        for faction in self.raws.factions.iter() {
            let mut reactions: HashMap<String, Reaction> = HashMap::new();
            for other in faction.responses.iter() {
                reactions.insert(other.0.clone(), match other.1.as_str() {
                    "flee" => Reaction::Flee,
                    "attack" => Reaction::Attack,
                    _ => Reaction::Ignore,
                });
            }
            self.faction_index.insert(faction.id.clone(), reactions);
        }
        for ancestry in self.raws.ancestries.iter() {
            self.ancestry_index.insert(ancestry.id.clone(), ancestry.allies.clone());
        }
    }
}

/// Checks a string against a HashSet, logging if a duplicate is found.
fn check_for_duplicate_entries(used_names: &HashSet<String>, id: &String) {
    if used_names.contains(id) {
        console::log(format!("DEBUGINFO: Duplicate ID found in raws [{}]", id));
    }
}
/// Checks a string against a HashSet, logging if the string isn't found.
fn check_for_unspecified_entity(used_names: &HashSet<String>, id: &String) {
    if !used_names.contains(id) {
        console::log(format!("DEBUGINFO: Table references unspecified entity [{}]", id));
    }
}

pub fn spawn_named_entity(
    raws: &RawMaster,
    ecs: &mut World,
    key: &str,
    buc: Option<BUC>,
    pos: SpawnType,
    map_difficulty: i32
) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        return spawn_named_item(raws, ecs, key, buc, pos);
    } else if raws.mob_index.contains_key(key) {
        return spawn_named_mob(raws, ecs, key, pos, map_difficulty);
    } else if raws.prop_index.contains_key(key) {
        return spawn_named_prop(raws, ecs, key, pos);
    }
    None
}

pub fn spawn_named_item(
    raws: &RawMaster,
    ecs: &mut World,
    key: &str,
    buc: Option<BUC>,
    pos: SpawnType
) -> Option<Entity> {
    if raws.item_index.contains_key(key) {
        let item_template = &raws.raws.items[raws.item_index[key]];
        let mut dm = ecs.fetch_mut::<crate::map::MasterDungeonMap>();
        let scroll_names = dm.scroll_map.clone();
        let potion_names = dm.potion_map.clone();
        let wand_names = dm.wand_map.clone();
        let identified_items = dm.identified_items.clone();
        // --- WE GET ALL THE VALUES FROM THE ECS WE NEED HERE, BEFORE ---
        // --- WE CREATE THE EB, TO AVOID BORROW CHECKER COMPLAINTS    ---
        let roll = ecs.write_resource::<RandomNumberGenerator>().roll_dice(1, 6);
        let player_entity = ecs.fetch::<Entity>();
        let known_beatitude = match pos {
            SpawnType::Equipped { by } => {
                if by == *player_entity { true } else { false }
            }
            SpawnType::Carried { by } => {
                if by == *player_entity { true } else { false }
            }
            _ => false,
        };
        if known_beatitude && !identified_items.contains(&item_template.name.name) {
            dm.identified_items.insert(item_template.name.name.clone());
        }
        std::mem::drop(player_entity);
        std::mem::drop(dm);
        // -- DROP EVERYTHING THAT INVOLVES THE ECS BEFORE THIS POINT ---
        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();

        eb = eb.with(Name {
            name: item_template.name.name.clone(),
            plural: item_template.name.plural.clone(),
        });
        eb = eb.with(Item {
            weight: item_template.weight.unwrap_or(0.0),
            value: item_template.value.unwrap_or(0.0),
        });
        eb = spawn_position(pos, eb, key, raws);

        if let Some(renderable) = &item_template.renderable {
            eb = eb.with(get_renderable_component(renderable));
        }
        // BEATITUDE
        let buc = if let Some(buc_status) = buc {
            buc_status
        } else {
            match roll {
                1 => BUC::Cursed,
                2 => BUC::Blessed,
                _ => BUC::Uncursed,
            }
        };
        eb = eb.with(Beatitude { buc, known: known_beatitude });

        if let Some(flags) = &item_template.flags {
            apply_flags!(flags, eb);
        }
        if let Some(effects_list) = &item_template.effects {
            apply_effects!(effects_list, eb);
        }

        if let Some(magic_item) = &item_template.magic {
            let item_class = match magic_item.class.as_str() {
                "common" => MagicItemClass::Common,
                "uncommon" => MagicItemClass::Uncommon,
                "rare" => MagicItemClass::Rare,
                "veryrare" => MagicItemClass::VeryRare,
                _ => MagicItemClass::Legendary,
            };
            eb = eb.with(MagicItem { class: item_class });
            if !identified_items.contains(&item_template.name.name) {
                #[allow(clippy::single_match)]
                match magic_item.naming.as_str() {
                    "scroll" => {
                        let singular = scroll_names[&item_template.name.name].clone();
                        let mut plural = singular.clone();
                        plural += "s";
                        eb = eb.with(ObfuscatedName { name: singular, plural: plural });
                    }
                    "potion" => {
                        let singular = potion_names[&item_template.name.name].clone();
                        let mut plural = singular.clone();
                        plural += "s";
                        eb = eb.with(ObfuscatedName { name: singular, plural: plural });
                    }
                    "wand" => {
                        let singular = wand_names[&item_template.name.name].clone();
                        let mut plural = singular.clone();
                        plural += "s";
                        eb = eb.with(ObfuscatedName { name: singular, plural: plural });
                    }
                    _ => {
                        let singular = magic_item.naming.clone();
                        let mut plural = singular.clone();
                        plural += "s";
                        eb = eb.with(ObfuscatedName { name: singular, plural: plural });
                    }
                }
            }
        }

        if let Some(weapon) = &item_template.equip {
            let (damage_type, dice) = parse_damage_string(weapon.damage.as_str());
            let weapon_attribute = match weapon.flag.as_str() {
                "DEXTERITY" => WeaponAttribute::Dexterity,
                "FINESSE" => WeaponAttribute::Finesse,
                _ => WeaponAttribute::Strength,
            };
            let wpn = MeleeWeapon {
                damage_type,
                attribute: weapon_attribute,
                damage_n_dice: dice.n_dice,
                damage_die_type: dice.die_type,
                damage_bonus: dice.bonus,
                hit_bonus: weapon.to_hit.unwrap_or(0),
            };
            eb = eb.with(wpn);
        }

        return Some(eb.build());
    }
    console::log(format!("DEBUGINFO: Tried to spawn named item [{}] but failed", key));
    None
}

pub fn spawn_named_mob(
    raws: &RawMaster,
    ecs: &mut World,
    key: &str,
    pos: SpawnType,
    map_difficulty: i32
) -> Option<Entity> {
    if raws.mob_index.contains_key(key) {
        let mob_template = &raws.raws.mobs[raws.mob_index[key]];
        let mut player_level = 1;
        {
            let pools = ecs.read_storage::<Pools>();
            let player_entity = ecs.fetch::<Entity>();
            let player_pool = pools.get(*player_entity);
            if let Some(pool) = player_pool {
                player_level = pool.level;
            }
        }

        let mut eb;
        // New entity with a position, name, combatstats, and viewshed
        eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        eb = spawn_position(pos, eb, key, raws);
        eb = eb.with(Name { name: mob_template.name.clone(), plural: mob_template.name.clone() });
        eb = eb.with(Viewshed {
            visible_tiles: Vec::new(),
            range: if let Some(range) = mob_template.vision_range {
                range
            } else {
                DEFAULT_VIEWSHED_STANDARD
            },
            dirty: true,
        });
        if let Some(telepath) = &mob_template.telepathy_range {
            eb = eb.with(Telepath { telepath_tiles: Vec::new(), range: *telepath, dirty: true });
        }
        if let Some(renderable) = &mob_template.renderable {
            eb = eb.with(get_renderable_component(renderable));
        }
        eb = eb.with(BlocksTile {});
        eb = eb.with(Faction { name: "hostile".to_string() });
        eb = eb.with(MoveMode { mode: Movement::Random });
        eb = eb.with(Bleeds { colour: RGB::named(BLOODSTAIN_COLOUR) });
        let mut xp_value = 1;
        let mut has_mind = true;
        if let Some(flags) = &mob_template.flags {
            apply_flags!(flags, eb);
            if flags.contains(&"MULTIATTACK".to_string()) {
                xp_value += 3;
            }
            if flags.contains(&"MINDLESS".to_string()) {
                has_mind = false;
            }
        }
        if has_mind {
            eb = eb.with(Mind {});
        }
        // Add quips, if we have some listed.
        if let Some(quips) = &mob_template.quips {
            eb = eb.with(Quips { available: quips.clone() });
        }

        // Setup combat stats
        let mut attr = Attributes {
            strength: Attribute { base: 10, modifiers: 0, bonus: 0 },
            dexterity: Attribute { base: 10, modifiers: 0, bonus: 0 },
            constitution: Attribute { base: 10, modifiers: 0, bonus: 0 },
            intelligence: Attribute { base: 10, modifiers: 0, bonus: 0 },
            wisdom: Attribute { base: 10, modifiers: 0, bonus: 0 },
            charisma: Attribute { base: 10, modifiers: 0, bonus: 0 },
        };
        let mut mob_con = 10;
        let mut mob_int = 10;
        if let Some(attributes) = &mob_template.attributes {
            if let Some(str) = attributes.str {
                attr.strength = Attribute { base: str, modifiers: 0, bonus: attr_bonus(str) };
            }
            if let Some(dex) = attributes.dex {
                attr.strength = Attribute { base: dex, modifiers: 0, bonus: attr_bonus(dex) };
            }
            if let Some(con) = attributes.con {
                attr.constitution = Attribute { base: con, modifiers: 0, bonus: attr_bonus(con) };
                mob_con = con;
            }
            if let Some(int) = attributes.int {
                attr.intelligence = Attribute { base: int, modifiers: 0, bonus: attr_bonus(int) };
                mob_int = int;
            }
            if let Some(wis) = attributes.wis {
                attr.wisdom = Attribute { base: wis, modifiers: 0, bonus: attr_bonus(wis) };
            }
            if let Some(cha) = attributes.cha {
                attr.charisma = Attribute { base: cha, modifiers: 0, bonus: attr_bonus(cha) };
            }
        }
        eb = eb.with(attr);

        let speed = if mob_template.speed.is_some() { mob_template.speed.unwrap() } else { 12 };
        eb = eb.with(Energy { current: 0, speed: speed });

        let base_mob_level = if mob_template.level.is_some() {
            mob_template.level.unwrap()
        } else {
            0
        };
        let mut mob_level = base_mob_level;
        // If the level difficulty is smaller than the mob's base level, subtract 1;
        // else, if the level difficulty is larger, add one-fifth of the difference
        if base_mob_level > map_difficulty {
            mob_level -= 1;
        } else if base_mob_level < map_difficulty {
            mob_level += (map_difficulty - base_mob_level) / 5;
        }
        // If the player is a higher level than the mob, add one-fifth of the difference
        if base_mob_level < player_level {
            mob_level += (player_level - base_mob_level) / 4;
        }
        // If the resulting mob level is more than 1.5x the base, lower it to that number
        mob_level = i32::min(mob_level, (1.5 * (base_mob_level as f32)).trunc() as i32);

        // Should really use existing RNG here
        let mut rng = RandomNumberGenerator::new();
        let mob_hp = npc_hp_at_level(&mut rng, mob_con, mob_level);
        let mob_mana = mana_at_level(&mut rng, mob_int, mob_level);
        let mob_bac = if mob_template.bac.is_some() { mob_template.bac.unwrap() } else { 10 };

        let pools = Pools {
            level: mob_level,
            xp: 0,
            bac: mob_bac,
            hit_points: Pool { current: mob_hp, max: mob_hp },
            mana: Pool { current: mob_mana, max: mob_mana },
            weight: 0.0,
            god: false,
        };
        eb = eb.with(pools);
        eb = eb.with(EquipmentChanged {});

        let mut skills = Skills { skills: HashMap::new() };
        skills.skills.insert(Skill::Melee, 0);
        skills.skills.insert(Skill::Defence, 0);
        if let Some(mobskills) = &mob_template.skills {
            for sk in mobskills.iter() {
                match sk.0.as_str() {
                    "melee" => {
                        skills.skills.insert(Skill::Melee, *sk.1);
                    }
                    "defence" => {
                        skills.skills.insert(Skill::Defence, *sk.1);
                    }
                    "magic" => {
                        skills.skills.insert(Skill::Magic, *sk.1);
                    }
                    _ => {
                        console::log(format!("Unknown skill referenced: [{}]", sk.0));
                    }
                }
            }
        }
        eb = eb.with(skills);

        if let Some(natural_attacks) = &mob_template.attacks {
            let mut natural = NaturalAttacks { attacks: Vec::new() };
            for na in natural_attacks.iter() {
                let (damage_type, dice) = parse_damage_string(&na.damage);
                let attack = NaturalAttack {
                    name: na.name.clone(),
                    damage_type,
                    hit_bonus: na.hit_bonus,
                    damage_n_dice: dice.n_dice,
                    damage_die_type: dice.die_type,
                    damage_bonus: dice.bonus,
                };
                natural.attacks.push(attack);
            }
            eb = eb.with(natural);
        }

        xp_value += mob_level * mob_level;
        if speed > 18 {
            xp_value += 5;
        } else if speed > 12 {
            xp_value += 3;
        }
        if mob_bac < 0 {
            xp_value += 14 + 2 * mob_bac;
        } else if mob_bac == 0 {
            xp_value += 7;
        } else if mob_bac == 1 {
            xp_value += 6;
        } else if mob_bac == 2 {
            xp_value += 5;
        }
        if mob_level > 9 {
            xp_value += 50;
        }
        // Final xp value = 1 + level^2 + bonus for low ac + bonus for speed + bonus for multiattack + bonus for level>9

        eb = eb.with(GrantsXP { amount: xp_value });

        // Setup loot table
        if let Some(loot) = &mob_template.loot {
            eb = eb.with(LootTable { table: loot.table.clone(), chance: loot.chance });
        }

        if CONFIG.logging.log_spawning {
            console::log(
                format!(
                    "SPAWNLOG: {} ({}HP, {}MANA, {}BAC) spawned at level {} ({}[base], {}[map difficulty], {}[player level]), worth {} XP",
                    &mob_template.name,
                    mob_hp,
                    mob_mana,
                    mob_bac,
                    mob_level,
                    base_mob_level,
                    map_difficulty,
                    player_level,
                    xp_value
                )
            );
        }

        let new_mob = eb.build();

        // Build entity, then check for anything they're wearing
        if let Some(wielding) = &mob_template.equipped {
            for tag in wielding.iter() {
                spawn_named_entity(
                    raws,
                    ecs,
                    tag,
                    None,
                    SpawnType::Equipped { by: new_mob },
                    map_difficulty
                );
            }
        }

        return Some(new_mob);
    }
    None
}

pub fn spawn_named_prop(
    raws: &RawMaster,
    ecs: &mut World,
    key: &str,
    pos: SpawnType
) -> Option<Entity> {
    if raws.prop_index.contains_key(key) {
        // ENTITY BUILDER PREP
        let prop_template = &raws.raws.props[raws.prop_index[key]];
        let mut eb = ecs.create_entity().marked::<SimpleMarker<SerializeMe>>();
        eb = spawn_position(pos, eb, key, raws);
        // APPLY MANDATORY COMPONENTS FOR A PROP:
        //  - Name
        //  - Prop {}
        eb = eb.with(Name { name: prop_template.name.clone(), plural: prop_template.name.clone() });
        eb = eb.with(Prop {});
        // APPLY OPTIONAL COMPONENTS FOR A PROP:
        // - Renderable
        // - Flags
        // - Effects
        if let Some(renderable) = &prop_template.renderable {
            eb = eb.with(get_renderable_component(renderable));
        }

        if let Some(flags) = &prop_template.flags {
            apply_flags!(flags, eb);
        }

        if let Some(effects_list) = &prop_template.effects {
            apply_effects!(effects_list, eb);
        }

        if let Some(door) = &prop_template.door {
            eb = eb.with(Door {
                open: door.open,
                locked: door.locked,
                blocks_vis: door.blocks_vis,
                blocks_move: door.blocks_move,
            });
            if !door.open {
                if door.blocks_vis {
                    eb = eb.with(BlocksVisibility {});
                }
                if door.blocks_move {
                    eb = eb.with(BlocksTile {});
                }
            }
        }
        // BUILD THE ENTITY
        return Some(eb.build());
    }
    None
}

fn spawn_position<'a>(
    pos: SpawnType,
    new_entity: EntityBuilder<'a>,
    tag: &str,
    raws: &RawMaster
) -> EntityBuilder<'a> {
    let mut eb = new_entity;

    match pos {
        SpawnType::AtPosition { x, y } => {
            eb = eb.with(Position { x, y });
        }
        SpawnType::Carried { by } => {
            eb = eb.with(InBackpack { owner: by });
        }
        SpawnType::Equipped { by } => {
            let slot = find_slot_for_equippable_item(tag, raws);
            eb = eb.with(Equipped { owner: by, slot });
        }
    }

    eb
}

fn get_renderable_component(
    renderable: &super::item_structs::Renderable
) -> crate::components::Renderable {
    crate::components::Renderable {
        glyph: to_cp437(renderable.glyph.chars().next().unwrap()),
        sprite: if let Some(spriteinfo) = &renderable.sprite {
            let x = spriteinfo.x.unwrap_or(0.0);
            let y = spriteinfo.y.unwrap_or(0.0);
            let alt_x = spriteinfo.alt_x.unwrap_or(0.0);
            let alt_y = spriteinfo.alt_y.unwrap_or(0.0);
            Some(SpriteInfo {
                id: spriteinfo.id.clone(),
                recolour: if let Some(colour) = spriteinfo.colour {
                    colour
                } else {
                    true
                },
                alt: spriteinfo.alt.clone(),
                offset: (x, -y), // Invert y so that positive y = move upwards.
                alt_offset: (alt_x, -alt_y),
            })
        } else {
            None
        },
        fg: RGB::from_hex(&renderable.fg).expect("Invalid RGB"),
        bg: RGB::from_hex(&renderable.bg).expect("Invalid RGB"),
        render_order: renderable.order,
        alt_render_order: if let Some(alt_order) = renderable.alt_order {
            Some(alt_order)
        } else {
            None
        },
    }
}

pub fn table_by_name(raws: &RawMaster, key: &str, optional_difficulty: Option<i32>) -> RandomTable {
    let (difficulty, upper_bound, lower_bound);
    if optional_difficulty.is_some() {
        difficulty = optional_difficulty.unwrap();
        upper_bound = difficulty;
        lower_bound = if key != "mobs" { 0 } else { difficulty / 6 };
    } else {
        difficulty = -1;
        upper_bound = 100;
        lower_bound = 0;
    }
    if raws.table_index.contains_key(key) {
        let spawn_table = &raws.raws.spawn_tables[raws.table_index[key]];

        use super::SpawnTableEntry;

        let available_options: Vec<&SpawnTableEntry> = spawn_table.table
            .iter()
            .filter(|entry| entry.difficulty >= lower_bound && entry.difficulty <= upper_bound)
            .collect();

        let mut rt = RandomTable::new();
        if !available_options.is_empty() {
            for e in available_options.iter() {
                rt = rt.add(e.id.clone(), e.weight);
            }

            return rt;
        }
    }
    unreachable!(
        "Something went wrong when trying to spawn {} @ map difficulty {} [upper bound: {}, lower bound: {}].",
        key,
        difficulty,
        upper_bound,
        lower_bound
    );
}

/*pub fn parse_dice_string(dice: &str) -> (i32, i32, i32) {
    lazy_static! {
        static ref DICE_RE: Regex = Regex::new(r"(\d+)d(\d+)([\+\-]\d+)?").unwrap();
    }
    let mut n_dice = 1;
    let mut die_type = 1;
    let mut die_bonus = 0;
    for cap in DICE_RE.captures_iter(dice) {
        if let Some(group) = cap.get(1) {
            n_dice = group.as_str().parse::<i32>().expect("Not a digit");
        }
        if let Some(group) = cap.get(2) {
            die_type = group.as_str().parse::<i32>().expect("Not a digit");
        }
        if let Some(group) = cap.get(3) {
            die_bonus = group.as_str().parse::<i32>().expect("Not a digit");
        }
    }
    (n_dice, die_type, die_bonus)
}*/

fn find_slot_for_equippable_item(tag: &str, raws: &RawMaster) -> EquipmentSlot {
    if !raws.item_index.contains_key(tag) {
        unreachable!("Tried to equip an unknown item: {}", tag);
    }
    let item_index = raws.item_index[tag];
    let item = &raws.raws.items[item_index];
    if let Some(flags) = &item.flags {
        for flag in flags {
            match flag.as_str() {
                "EQUIP_MELEE" => {
                    return EquipmentSlot::Melee;
                }
                "EQUIP_SHIELD" => {
                    return EquipmentSlot::Shield;
                }
                "EQUIP_BODY" => {
                    return EquipmentSlot::Body;
                }
                "EQUIP_HEAD" => {
                    return EquipmentSlot::Head;
                }
                "EQUIP_FEET" => {
                    return EquipmentSlot::Feet;
                }
                "EQUIP_NECK" => {
                    return EquipmentSlot::Neck;
                }
                "EQUIP_BACK" => {
                    return EquipmentSlot::Back;
                }
                "EQUIP_HANDS" => {
                    return EquipmentSlot::Hands;
                }
                _ => {}
            }
        }
    }
    unreachable!("Tried to equip {}, but it has no slot tag.", tag);
}

pub fn roll_on_loot_table(
    raws: &RawMaster,
    rng: &mut RandomNumberGenerator,
    key: &str
) -> Option<String> {
    if raws.loot_index.contains_key(key) {
        console::log(format!("DEBUGINFO: Rolling on loot table: {}", key));
        let mut rt = RandomTable::new();
        let available_options = &raws.raws.loot_tables[raws.loot_index[key]];
        for item in available_options.table.iter() {
            rt = rt.add(item.id.clone(), item.weight);
        }
        return Some(rt.roll(rng));
    } else if raws.table_index.contains_key(key) {
        console::log(format!("DEBUGINFO: No loot table found, so using spawn table: {}", key));
        let mut rt = RandomTable::new();
        let available_options = &raws.raws.spawn_tables[raws.table_index[key]];
        for item in available_options.table.iter() {
            rt = rt.add(item.id.clone(), item.weight);
        }
        return Some(rt.roll(rng));
    }
    console::log(format!("DEBUGINFO: Unknown loot table {}", key));
    return None;
}

#[derive(PartialEq, Copy, Clone)]
pub enum SpawnsAs {
    Single,
    SmallGroup,
    LargeGroup,
}

/// Looks up a key in the mob index, and if a corresponding mob is found, searches the
/// entry for a group spawn flag. i.e. SMALL_GROUP/LARGE_GROUP.
pub fn get_mob_spawn_type(raws: &RawMaster, key: &str) -> SpawnsAs {
    if raws.mob_index.contains_key(key) {
        let mob_template = &raws.raws.mobs[raws.mob_index[key]];
        if let Some(flags) = &mob_template.flags {
            for flag in flags {
                match flag.as_str() {
                    "SMALL_GROUP" => {
                        return SpawnsAs::SmallGroup;
                    }
                    "LARGE_GROUP" => {
                        return SpawnsAs::LargeGroup;
                    }
                    _ => {}
                }
            }
        }
    }
    return SpawnsAs::Single;
}

pub fn get_mob_spawn_amount(
    rng: &mut RandomNumberGenerator,
    spawn_type: &SpawnsAs,
    player_level: i32
) -> i32 {
    let n = match spawn_type {
        // Single mobs always spawn alone.
        SpawnsAs::Single => 1,
        // Small groups either spawn alone or as a small group (2-4).
        SpawnsAs::SmallGroup => {
            if rng.roll_dice(1, 2) == 1 { 1 } else { 4 }
        }
        // Large groups either spawn in a small group or as a large group (2-11).
        SpawnsAs::LargeGroup => {
            if rng.roll_dice(1, 2) == 1 { 4 } else { 11 }
        }
    };
    let roll = if n == 1 { 1 } else { rng.roll_dice(2, n) };
    // We want to constrain group sizes depending on player's level, so
    // we don't get large groups of mobs when the player is unequipped.
    match player_level {
        0..=2 => {
            return i32::max(1, roll / 4);
        }
        3..=4 => {
            return i32::max(1, roll / 2);
        }
        _ => {
            return roll;
        }
    }
}

pub fn get_scroll_tags() -> Vec<String> {
    let raws = &super::RAWS.lock().unwrap();
    let mut result = Vec::new();
    for item in raws.raws.items.iter() {
        if let Some(magic) = &item.magic {
            if &magic.naming == "scroll" {
                result.push(item.name.name.clone());
            }
        }
    }
    return result;
}

pub fn get_potion_tags() -> Vec<String> {
    let raws = &super::RAWS.lock().unwrap();
    let mut result = Vec::new();
    for item in raws.raws.items.iter() {
        if let Some(magic) = &item.magic {
            if &magic.naming == "potion" {
                result.push(item.name.name.clone());
            }
        }
    }
    return result;
}

pub fn get_wand_tags() -> Vec<String> {
    let raws = &super::RAWS.lock().unwrap();
    let mut result = Vec::new();
    for item in raws.raws.items.iter() {
        if let Some(magic) = &item.magic {
            if &magic.naming == "wand" {
                result.push(item.name.name.clone());
            }
        }
    }
    return result;
}

pub fn get_id_from_name(name: String) -> String {
    let raws = &super::RAWS.lock().unwrap();
    for item in &raws.raws.items {
        if item.name.name == name {
            return item.id.clone();
        }
    }
    return "null".to_string();
}

pub fn is_tag_magic(tag: &str) -> bool {
    let raws = &super::RAWS.lock().unwrap();
    if raws.item_index.contains_key(tag) {
        let item_template = &raws.raws.items[raws.item_index[tag]];
        return item_template.magic.is_some();
    } else {
        return false;
    }
}

/// Queries the faction index to obtain one faction's reaction to another faction.
pub fn faction_reaction(this_faction: &str, other_faction: &str, raws: &RawMaster) -> Reaction {
    if raws.faction_index.contains_key(this_faction) {
        let mine = &raws.faction_index[this_faction];
        if mine.contains_key(other_faction) {
            return mine[other_faction];
        } else if mine.contains_key("default") {
            return mine["default"];
        }
    }
    return Reaction::Ignore;
}

pub fn ancestry_reaction(
    this_ancestry: Ancestry,
    other_ancestry: Ancestry,
    raws: &RawMaster
) -> Option<Reaction> {
    if this_ancestry == other_ancestry {
        return Some(Reaction::Ignore);
    } else {
        let this_ancestry = get_ancestry_string(this_ancestry);
        let other_ancestry = get_ancestry_string(other_ancestry);
        if raws.ancestry_index.contains_key(this_ancestry) {
            let mine = &raws.ancestry_index[this_ancestry];
            if mine.contains(other_ancestry) {
                return Some(Reaction::Ignore);
            }
        }
    }
    return None;
}

pub fn get_reactions(
    this_entity: Entity,
    other_entity: Entity,
    factions: &ReadStorage<Faction>,
    ancestries: &ReadStorage<HasAncestry>,
    raws: &RawMaster
) -> Reaction {
    if let Some(this_ancestry) = ancestries.get(this_entity) {
        if let Some(other_ancestry) = ancestries.get(other_entity) {
            let result = ancestry_reaction(this_ancestry.name, other_ancestry.name, raws);
            if result.is_some() {
                return result.unwrap();
            }
        }
    }
    if let Some(this_faction) = factions.get(this_entity) {
        if let Some(other_faction) = factions.get(other_entity) {
            return faction_reaction(&this_faction.name, &other_faction.name, raws);
        }
    }
    return Reaction::Ignore;
}

fn get_ancestry_string(ancestry: Ancestry) -> &'static str {
    match ancestry {
        Ancestry::Human => {
            return "human";
        }
        Ancestry::Elf => {
            return "elf";
        }
        Ancestry::Dwarf => {
            return "dwarf";
        }
        Ancestry::Catfolk => {
            return "catfolk";
        }
        Ancestry::Gnome => {
            return "gnome";
        }
        Ancestry::Unset => {
            return "NULL";
        }
    }
}

fn parse_particle_line(n: &str) -> SpawnParticleLine {
    let tokens: Vec<_> = n.split(';').collect();
    SpawnParticleLine {
        glyph: to_cp437(tokens[0].chars().next().unwrap()),
        tail_glyph: to_cp437(tokens[1].chars().next().unwrap()),
        colour: RGB::from_hex(tokens[2]).expect("Invalid RGB"),
        lifetime_ms: tokens[3].parse::<f32>().unwrap(),
        trail_colour: RGB::from_hex(tokens[4]).expect("Invalid trail RGB"),
        trail_lifetime_ms: tokens[5].parse::<f32>().unwrap(),
    }
}

fn parse_particle(n: &str) -> SpawnParticleSimple {
    let tokens: Vec<_> = n.split(';').collect();
    SpawnParticleSimple {
        glyph: to_cp437(tokens[0].chars().next().unwrap()),
        colour: RGB::from_hex(tokens[1]).expect("Invalid RGB"),
        lifetime_ms: tokens[2].parse::<f32>().unwrap(),
    }
}

fn parse_particle_burst(n: &str) -> SpawnParticleBurst {
    let tokens: Vec<_> = n.split(';').collect();
    SpawnParticleBurst {
        glyph: to_cp437(tokens[0].chars().next().unwrap()),
        head_glyph: to_cp437(tokens[1].chars().next().unwrap()),
        tail_glyph: to_cp437(tokens[2].chars().next().unwrap()),
        colour: RGB::from_hex(tokens[3]).expect("Invalid RGB"),
        lerp: RGB::from_hex(tokens[4]).expect("Invalid LERP RGB"),
        lifetime_ms: tokens[5].parse::<f32>().unwrap(),
        trail_colour: RGB::from_hex(tokens[6]).expect("Invalid trail RGB"),
        trail_lifetime_ms: tokens[7].parse::<f32>().unwrap(),
    }
}

fn parse_damage_string(n: &str) -> (DamageType, DiceType) {
    let tokens: Vec<_> = n.split(';').collect();
    let damage_type = if tokens.len() > 1 {
        match tokens[1] {
            "physical" => DamageType::Physical,
            "magic" => DamageType::Magic,
            "fire" => DamageType::Fire,
            "cold" => DamageType::Cold,
            "poison" => DamageType::Poison,
            _ => unreachable!("Unrecognised damage type in raws: {}", tokens[1]),
        }
    } else {
        DamageType::Physical
    };
    let dice = parse_dice_string(tokens[0]).expect("Failed to parse dice string");
    return (damage_type, dice);
}
