#![allow(dead_code)]
use std::convert::Into;
use std::path::PathBuf;
use std::io::{Read};
use std::fs::File;

use byteorder::{LittleEndian, ReadBytesExt};
use rand::Rng;
use thiserror::Error;

use crate::ship::monster::MonsterType;
use crate::ship::room::{Episode, RoomMode};

#[derive(Debug, Copy, Clone)]
struct RawMapEnemy {
    id: u32,
    _unknown1: u16,
    children: u16,
    map_area: u16,
    _unknown4: u16,
    section: u16,
    wave_idd: u16,
    wave_id: u32,
    x: f32,
    y: f32,
    z: f32,
    xrot: u32,
    yrot: u32,
    zrot: u32,
    _field1: u32,
    field2: u32,
    _field3: u32,
    _field4: u32,
    _field5: u32,
    skin: u32,
    _field6: u32
}

impl RawMapEnemy {
    fn from_byte_stream<R: Read>(cursor: &mut R) -> Result<RawMapEnemy, std::io::Error> {
        Ok(RawMapEnemy {
            id: cursor.read_u32::<LittleEndian>()?,
            _unknown1: cursor.read_u16::<LittleEndian>()?,
            children: cursor.read_u16::<LittleEndian>()?,
            map_area: cursor.read_u16::<LittleEndian>()?,
            _unknown4: cursor.read_u16::<LittleEndian>()?,
            section: cursor.read_u16::<LittleEndian>()?,
            wave_idd: cursor.read_u16::<LittleEndian>()?,
            wave_id: cursor.read_u32::<LittleEndian>()?,
            x: cursor.read_f32::<LittleEndian>()?,
            y: cursor.read_f32::<LittleEndian>()?,
            z: cursor.read_f32::<LittleEndian>()?,
            xrot: cursor.read_u32::<LittleEndian>()?,
            yrot: cursor.read_u32::<LittleEndian>()?,
            zrot: cursor.read_u32::<LittleEndian>()?,
            _field1: cursor.read_u32::<LittleEndian>()?,
            field2: cursor.read_u32::<LittleEndian>()?,
            _field3: cursor.read_u32::<LittleEndian>()?,
            _field4: cursor.read_u32::<LittleEndian>()?,
            _field5: cursor.read_u32::<LittleEndian>()?,
            skin: cursor.read_u32::<LittleEndian>()?,
            _field6: cursor.read_u32::<LittleEndian>()?,
        })
    }
}


#[derive(Error, Debug)]
#[error("")]
enum MapEnemyError {
    UnknownEnemyId(u32),
    MapAreaError(#[from] MapAreaError),
}


#[derive(Debug, Copy, Clone)]
pub struct MapEnemy {
    pub monster: MonsterType,
    pub map_area: MapArea,
    hp: u32,
    // TODO: other stats from battleparam
    pub dropped_item: bool,
    pub gave_exp: bool,
}

impl MapEnemy {
    fn from_raw(enemy: RawMapEnemy, episode: &Episode, map_area: &MapArea /*, battleparam */) -> Result<MapEnemy, MapEnemyError> {
        // TODO: rare enemies ep1-4, tower lilys, ult variants?
        let monster = match map_area {
            MapArea::Forest1 | MapArea::Forest2 | MapArea::Dragon | 
            MapArea::Caves1  | MapArea::Caves2  | MapArea::Caves3 | MapArea::DeRolLe | 
            MapArea::Mines1  | MapArea::Mines2  | MapArea::VolOpt | 
            MapArea::Ruins1  | MapArea::Ruins2  | MapArea::Ruins3 | MapArea::DarkFalz => {
                match (enemy, episode) {
                    (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildebear,
                    // (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildeblue,
                    (RawMapEnemy {id: 65, ..}, _) => MonsterType::RagRappy,
                    // (RawMapEnemy {id: 65, ..}, _) => MonsterType::AlRappy,
                    (RawMapEnemy {id: 66, ..}, _) => MonsterType::Monest,
                    (RawMapEnemy {id: 67, field2: 0, ..}, _) => MonsterType::SavageWolf,
                    (RawMapEnemy {id: 67, ..}, _) => MonsterType::BarbarousWolf,
                    (RawMapEnemy {id: 68, skin: 0, ..}, _) => MonsterType::Booma,
                    (RawMapEnemy {id: 68, skin: 1, ..}, _) => MonsterType::Gobooma,
                    (RawMapEnemy {id: 68, skin: 2, ..}, _) => MonsterType::Gigobooma,
                    (RawMapEnemy {id: 96, ..}, _) => MonsterType::GrassAssassin,
                    (RawMapEnemy {id: 97, ..}, _) => MonsterType::PoisonLily,
                    // (RawMapEnemy {id: 97, ..}, _) => MonsterType::NarLily,
                    (RawMapEnemy {id: 98, ..}, _) => MonsterType::NanoDragon,
                    (RawMapEnemy {id: 99, skin: 0, ..}, _) => MonsterType::EvilShark,
                    (RawMapEnemy {id: 99, skin: 1, ..}, _) => MonsterType::PalShark,
                    (RawMapEnemy {id: 99, skin: 2, ..}, _) => MonsterType::GuilShark,
                    (RawMapEnemy {id: 100, ..}, _) => MonsterType::PofuillySlime,
                    // (RawMapEnemy {id: 100, ..}, _) => MonsterType::PouillySlime,
                    (RawMapEnemy {id: 101, ..}, _) => MonsterType::PanArms,
                    (RawMapEnemy {id: 128, skin: 0, ..}, _) => MonsterType::Dubchic,
                    (RawMapEnemy {id: 128, skin: 1, ..}, _) => MonsterType::Gillchic,
                    (RawMapEnemy {id: 129, ..}, _) => MonsterType::Garanz,
                    (RawMapEnemy {id: 130, field2: 0, ..}, _) => MonsterType::SinowBeat,
                    (RawMapEnemy {id: 130, ..}, _) => MonsterType::SinowGold,
                    (RawMapEnemy {id: 131, ..}, _) => MonsterType::Canadine,
                    (RawMapEnemy {id: 132, ..}, _) => MonsterType::Canane,
                    (RawMapEnemy {id: 133, ..}, _) => MonsterType::Dubwitch,
                    (RawMapEnemy {id: 160, ..}, _) => MonsterType::Delsaber,
                    (RawMapEnemy {id: 161, ..}, _) => MonsterType::ChaosSorcerer,
                    (RawMapEnemy {id: 162, ..}, _) => MonsterType::DarkGunner,
                    (RawMapEnemy {id: 164, ..}, _) => MonsterType::ChaosBringer,
                    (RawMapEnemy {id: 165, ..}, _) => MonsterType::DarkBelra,
                    (RawMapEnemy {id: 166, skin: 0, ..}, _) => MonsterType::Dimenian,
                    (RawMapEnemy {id: 166, skin: 1, ..}, _) => MonsterType::LaDimenian,
                    (RawMapEnemy {id: 166, skin: 2, ..}, _) => MonsterType::SoDimenian,
                    (RawMapEnemy {id: 167, ..}, _) => MonsterType::Bulclaw,
                    (RawMapEnemy {id: 168, ..}, _) => MonsterType::Claw,
                    (RawMapEnemy {id: 192, ..}, Episode::One) => MonsterType::Dragon,
                    (RawMapEnemy {id: 193, ..}, _) => MonsterType::DeRolLe,
                    (RawMapEnemy {id: 194, ..}, _) => MonsterType::VolOptPartA,
                    (RawMapEnemy {id: 197, ..}, _) => MonsterType::VolOpt,
                    (RawMapEnemy {id: 200, ..}, _) => MonsterType::DarkFalz,
                    _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id))
                }
            },

            MapArea::VrTempleAlpha | MapArea::VrTempleBeta | MapArea::BarbaRay | 
            MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta | MapArea::GolDragon | 
            MapArea::JungleAreaNorth | MapArea::JungleAreaEast | MapArea::Mountain | MapArea::Seaside | MapArea::Cca | MapArea::GalGryphon |
            MapArea::SeabedUpper | MapArea::SeabedLower | MapArea::OlgaFlow => {
                match (enemy, episode) {
                    (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildebear,
                    // (RawMapEnemy {id: 64, ..}, _) => MonsterType::Hildeblue,
                    (RawMapEnemy {id: 65, ..}, _) => MonsterType::RagRappy,
                    // (RawMapEnemy {id: 65, ..}, _) => MonsterType::EventRappy,
                    (RawMapEnemy {id: 66, ..}, _) => MonsterType::Monest,
                    (RawMapEnemy {id: 67, field2: 0, ..}, _) => MonsterType::SavageWolf,
                    (RawMapEnemy {id: 67, ..}, _) => MonsterType::BarbarousWolf,
                    (RawMapEnemy {id: 96, ..}, _) => MonsterType::GrassAssassin,
                    (RawMapEnemy {id: 97, ..}, _) => MonsterType::PoisonLily,
                    // (RawMapEnemy {id: 97, ..}, _) => MonsterType::NarLily,
                    // (RawMapEnemy {id: 97, ..}, _) => MonsterType::DelLily,
                    (RawMapEnemy {id: 101, ..}, _) => MonsterType::PanArms,
                    (RawMapEnemy {id: 128, skin: 0, ..}, _) => MonsterType::Dubchic,
                    (RawMapEnemy {id: 128, skin: 1, ..}, _) => MonsterType::Gillchic,
                    (RawMapEnemy {id: 129, ..}, _) => MonsterType::Garanz,
                    (RawMapEnemy {id: 133, ..}, _) => MonsterType::Dubwitch,
                    (RawMapEnemy {id: 160, ..}, _) => MonsterType::Delsaber,
                    (RawMapEnemy {id: 161, ..}, _) => MonsterType::ChaosSorcerer,
                    (RawMapEnemy {id: 165, ..}, _) => MonsterType::DarkBelra,
                    (RawMapEnemy {id: 166, skin: 0, ..}, _) => MonsterType::Dimenian,
                    (RawMapEnemy {id: 166, skin: 1, ..}, _) => MonsterType::LaDimenian,
                    (RawMapEnemy {id: 166, skin: 2, ..}, _) => MonsterType::SoDimenian,
                    (RawMapEnemy {id: 192, ..}, Episode::Two) => MonsterType::GalGryphon,
                    (RawMapEnemy {id: 202, ..}, _) => MonsterType::OlgaFlow,
                    (RawMapEnemy {id: 203, ..}, _) => MonsterType::BarbaRay,
                    (RawMapEnemy {id: 204, ..}, _) => MonsterType::GolDragon,
                    (RawMapEnemy {id: 212, skin: 0, ..}, _) => MonsterType::SinowBerill,
                    (RawMapEnemy {id: 212, skin: 1, ..}, _) => MonsterType::SinowSpigell,
                    (RawMapEnemy {id: 213, skin: 0, ..}, _) => MonsterType::Merillia,
                    (RawMapEnemy {id: 213, skin: 1, ..}, _) => MonsterType::Meriltas,
                    (RawMapEnemy {id: 214, skin: 0, ..}, _) => MonsterType::Mericarol,
                    (RawMapEnemy {id: 214, skin: 1, ..}, _) => MonsterType::Merikle,
                    (RawMapEnemy {id: 214, skin: 2, ..}, _) => MonsterType::Mericus,
                    (RawMapEnemy {id: 215, skin: 0, ..}, _) => MonsterType::UlGibbon,
                    (RawMapEnemy {id: 215, skin: 1, ..}, _) => MonsterType::ZolGibbon,
                    (RawMapEnemy {id: 216, ..}, _) => MonsterType::Gibbles,
                    (RawMapEnemy {id: 217, ..}, _) => MonsterType::Gee,
                    (RawMapEnemy {id: 218, ..}, _) => MonsterType::GiGue,
                    (RawMapEnemy {id: 219, ..}, _) => MonsterType::Deldepth,
                    (RawMapEnemy {id: 220, ..}, _) => MonsterType::Delbiter,
                    (RawMapEnemy {id: 221, skin: 0, ..}, _) => MonsterType::Dolmolm,
                    (RawMapEnemy {id: 221, skin: 1, ..}, _) => MonsterType::Dolmdarl,
                    (RawMapEnemy {id: 222, ..}, _) => MonsterType::Morfos,
                    (RawMapEnemy {id: 223, ..}, _) => MonsterType::Recobox,
                    (RawMapEnemy {id: 224, skin: 0, ..}, _) => MonsterType::SinowZoa,
                    (RawMapEnemy {id: 224, skin: 1, ..}, _) => MonsterType::SinowZele,
                    (RawMapEnemy {id: 224, ..}, _) => MonsterType::Epsilon,
                    (RawMapEnemy {id: 225, ..}, _) => MonsterType::IllGill,
                    _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id))
                }
            },

            MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => {
                match (enemy, episode) {
                    (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::SandRappyCrater,
                    // (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::DelRappyCrater,
                    (RawMapEnemy {id: 272, ..}, _) => MonsterType::Astark,
                    (RawMapEnemy {id: 273, field2: 0, ..}, _) => MonsterType::SatelliteLizardCrater,
                    (RawMapEnemy {id: 273, ..}, _) => MonsterType::YowieCrater,
                    (RawMapEnemy {id: 276, ..}, _) => MonsterType::ZuCrater,
                    // (RawMapEnemy {id: 276, ..}, _) => MonsterType::PazuzuCrater,
                    (RawMapEnemy {id: 277, skin: 0, ..}, _) => MonsterType::Boota,
                    (RawMapEnemy {id: 277, skin: 1, ..}, _) => MonsterType::ZeBoota,
                    (RawMapEnemy {id: 277, skin: 2, ..}, _) => MonsterType::BaBoota,
                    (RawMapEnemy {id: 278, ..}, _) => MonsterType::Dorphon,
                    // (RawMapEnemy {id: 278, ..}, _) => MonsterType::DorphonEclair,
                    _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id))
                }
            },
            MapArea::SubDesert1 | MapArea::SubDesert2 | MapArea::SubDesert3 | MapArea::SaintMillion => {
                match (enemy, episode) {
                    (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::SandRappyDesert,
                    // (RawMapEnemy {id: 65, ..}, Episode::Four) => MonsterType::DelRappyDesert,
                    (RawMapEnemy {id: 273, field2: 0, ..}, _) => MonsterType::SatelliteLizardDesert,
                    (RawMapEnemy {id: 273, ..}, _) => MonsterType::YowieDesert,
                    (RawMapEnemy {id: 274, ..}, _) => MonsterType::MerissaA,
                    // (RawMapEnemy {id: 274, ..}, _) => MonsterType::MerissaAA,
                    (RawMapEnemy {id: 275, ..}, _) => MonsterType::Girtablulu,
                    (RawMapEnemy {id: 276, ..}, _) => MonsterType::ZuDesert,
                    // (RawMapEnemy {id: 276, ..}, _) => MonsterType::PazuzuDesert,
                    (RawMapEnemy {id: 279, skin: 0, ..}, _) => MonsterType::Goran,
                    (RawMapEnemy {id: 279, skin: 1, ..}, _) => MonsterType::PyroGoran,
                    (RawMapEnemy {id: 279, skin: 2, ..}, _) => MonsterType::GoranDetonator,
                    (RawMapEnemy {id: 281, skin: 0, ..}, _) => MonsterType::SaintMillion,
                    (RawMapEnemy {id: 281, skin: 1, ..}, _) => MonsterType::Shambertin,
                    // (RawMapEnemy {id: 281, skin: 1, ..}, _) => MonsterType::Kondrieu,
                    _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id))
                }
            },
            _ => return Err(MapEnemyError::UnknownEnemyId(enemy.id))
        };

        Ok(MapEnemy {
            monster: monster,
            map_area: map_area.clone(),
            hp: 0,
            dropped_item: false,
            gave_exp: false,
        })
    }

    fn new(monster: MonsterType, map_area: MapArea) -> MapEnemy {
        MapEnemy {
            monster: monster,
            map_area: map_area,
            hp: 0,
            dropped_item: false,
            gave_exp: false,
        }
    }
}

#[derive(Debug, Copy, Clone)]
struct RawMapObject {
    otype: u16,
    unknown1: u16,
    unknown2:  u32,
    id: u16,
    group: u16,
    section: u16,
    unknown3: u16,
    x: f32,
    y: f32,
    z: f32,
    xrot: u32,
    yrot: u32,
    zrot: u32,
    field1: f32,
    field2: f32,
    field3: f32,
    field4: u32,
    field5: u32,
    field6: u32,
    field7: u32,
}

impl RawMapObject {
    fn from_byte_stream<R: Read>(cursor: &mut R) -> Result<RawMapObject, std::io::Error> {
        Ok(RawMapObject {
            otype: cursor.read_u16::<LittleEndian>()?,
            unknown1: cursor.read_u16::<LittleEndian>()?,
            unknown2: cursor.read_u32::<LittleEndian>()?,
            id: cursor.read_u16::<LittleEndian>()?,
            group: cursor.read_u16::<LittleEndian>()?,
            section: cursor.read_u16::<LittleEndian>()?,
            unknown3: cursor.read_u16::<LittleEndian>()?,
            x: cursor.read_f32::<LittleEndian>()?,
            y: cursor.read_f32::<LittleEndian>()?,
            z: cursor.read_f32::<LittleEndian>()?,
            xrot: cursor.read_u32::<LittleEndian>()?,
            yrot: cursor.read_u32::<LittleEndian>()?,
            zrot: cursor.read_u32::<LittleEndian>()?,
            field1: cursor.read_f32::<LittleEndian>()?,
            field2: cursor.read_f32::<LittleEndian>()?,
            field3: cursor.read_f32::<LittleEndian>()?,
            field4: cursor.read_u32::<LittleEndian>()?,
            field5: cursor.read_u32::<LittleEndian>()?,
            field6: cursor.read_u32::<LittleEndian>()?,
            field7: cursor.read_u32::<LittleEndian>()?,
        })
    }
}


#[derive(Debug, Copy, Clone)]
pub enum FixedBoxDropType {
    Weapon,
    Armor,
    Tool,
    Meseta,
    Random,
    Specific(u32), // TODO: ItemDropType
}

impl FixedBoxDropType {
    fn from_object(field1: f32, field2: f32, field3: f32, field4: u32) -> FixedBoxDropType {
        match (field1.round() as i32, field2.round() as i32, field3.round() as i32, field4) {
            (0, 1, 1, 0) => {
                FixedBoxDropType::Random
            },
            (0, 1, _, 0x4000000) => {
                FixedBoxDropType::Meseta
            },
            (0, 1, 1, _) => {
                FixedBoxDropType::Specific(field4)
            },
            (-1, 1, 1, _) => { // ???????
                FixedBoxDropType::Specific(field4)
            },
            (0, 1, 0, 0) => {
                FixedBoxDropType::Weapon
            },
            (0, 1, 0, 0x1000000) => {
                FixedBoxDropType::Armor
            },
            (0, 1, 0, 0x3000000) => {
                FixedBoxDropType::Tool
            },
            (1, _, _, _) => {
                FixedBoxDropType::Random
            },
            _ => {
                println!("this box state should not occur? {} {} {} {}", field1.round() as i32, field2.round() as i32, field3.round() as i32, field4);
                FixedBoxDropType::Random
            }
        }
    }
}


#[derive(Debug, Copy, Clone)]
pub enum MapObjectType {
    Box,
    FixedBox(FixedBoxDropType),
    EnemyBox,
    EnemyFixedBox(FixedBoxDropType),
    RuinsBox,
    RuinsFixedBox(FixedBoxDropType),
    RuinsEnemyBox,
    RuinsEnemyFixedBox(FixedBoxDropType),
    CcaBox,
    CcaFixedBox(FixedBoxDropType),
    EmptyBox,
    EmptyFixedBox(FixedBoxDropType),
    RuinsEmptyBox,
    RuinsEmptyFixedBox,
}

#[derive(Debug, Copy, Clone)]
pub struct MapObject {
    pub object: MapObjectType,
    pub map: MapArea,
    pub dropped_item: bool,
    //id: u32,
}

#[derive(Debug, Copy, Clone)]
enum MapObjectError {
    UnknownObjectType(u16, RawMapObject),
}


impl MapObject {
    fn from_raw(raw: RawMapObject, episode: Episode, map_area: &MapArea) -> Result<MapObject, MapObjectError> {
        let object = match (raw, episode) {
            (RawMapObject {otype: 136, ..}, _) => MapObjectType::Box,
            (RawMapObject {otype: 145, ..}, _) => MapObjectType::EnemyBox,
            (RawMapObject {otype: 146, ..}, _) => MapObjectType::FixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            (RawMapObject {otype: 147, ..}, _) => MapObjectType::EnemyFixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            (RawMapObject {otype: 149, ..}, _) => MapObjectType::EmptyFixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            (RawMapObject {otype: 353, ..}, _) => MapObjectType::RuinsFixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            (RawMapObject {otype: 354, ..}, _) => MapObjectType::RuinsBox,
            (RawMapObject {otype: 355, ..}, _) => MapObjectType::RuinsEnemyFixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            (RawMapObject {otype: 356, ..}, _) => MapObjectType::RuinsEnemyBox,
            (RawMapObject {otype: 357, ..}, _) => MapObjectType::RuinsEmptyBox,
            (RawMapObject {otype: 512, ..}, _) => MapObjectType::CcaBox,
            (RawMapObject {otype: 515, ..}, _) => MapObjectType::CcaFixedBox(FixedBoxDropType::from_object(raw.field1, raw.field2, raw.field3, raw.field4)),
            _ => return Err(MapObjectError::UnknownObjectType(raw.otype, raw))
        };

        Ok(MapObject {
            object: object,
            map: map_area.clone(),
            dropped_item: false,
        })
    }
}


#[derive(Debug, PartialEq)]
enum MapVariantMode {
    Online,
    Offline,
}

#[derive(Debug, Copy, Clone, PartialEq)]
pub enum MapArea {
    Pioneer2Ep1,
    Forest1,
    Forest2,
    Caves1,
    Caves2,
    Caves3,
    Mines1,
    Mines2,
    Ruins1,
    Ruins2,
    Ruins3,
    Dragon,
    DeRolLe,
    VolOpt,
    DarkFalz,
    Pioneer2Ep2,
    VrTempleAlpha,
    VrTempleBeta,
    VrSpaceshipAlpha,
    VrSpaceshipBeta,
    Cca,
    JungleAreaNorth,
    JungleAreaEast,
    Mountain,
    Seaside,
    SeabedUpper,
    SeabedLower,
    GalGryphon,
    OlgaFlow,
    BarbaRay,
    GolDragon,
    // Seaside2,
    // Tower,
    Pioneer2Ep4,
    CraterEast,
    CraterWest,
    CraterSouth,
    CraterNorth,
    CraterInterior,
    SubDesert1,
    SubDesert2,
    SubDesert3,
    SaintMillion,
    // TestMapEp4,
}

#[derive(Error, Debug)]
#[error("")]
pub enum MapAreaError {
    UnknownMapArea(u32),
}

impl MapArea {
    pub fn from_value(episode: &Episode, area: u32) -> Result<MapArea, MapAreaError> {
        match (episode, area) {
            (Episode::One, 0) => Ok(MapArea::Pioneer2Ep1),
            (Episode::One, 1) => Ok(MapArea::Forest1),
            (Episode::One, 2) => Ok(MapArea::Forest2),
            (Episode::One, 3) => Ok(MapArea::Caves1),
            (Episode::One, 4) => Ok(MapArea::Caves2),
            (Episode::One, 5) => Ok(MapArea::Caves3),
            (Episode::One, 6) => Ok(MapArea::Mines1),
            (Episode::One, 7) => Ok(MapArea::Mines2),
            (Episode::One, 8) => Ok(MapArea::Ruins1),
            (Episode::One, 9) => Ok(MapArea::Ruins2),
            (Episode::One, 10) => Ok(MapArea::Ruins3),
            (Episode::One, 11) => Ok(MapArea::Dragon),
            (Episode::One, 12) => Ok(MapArea::DeRolLe),
            (Episode::One, 13) => Ok(MapArea::VolOpt),
            (Episode::One, 14) => Ok(MapArea::DarkFalz),
            (Episode::Two, 0) => Ok(MapArea::Pioneer2Ep2),
            (Episode::Two, 1) => Ok(MapArea::VrTempleAlpha),
            (Episode::Two, 2) => Ok(MapArea::VrTempleBeta),
            (Episode::Two, 3) => Ok(MapArea::VrSpaceshipAlpha),
            (Episode::Two, 4) => Ok(MapArea::VrSpaceshipBeta),
            (Episode::Two, 5) => Ok(MapArea::Cca),
            (Episode::Two, 6) => Ok(MapArea::JungleAreaNorth),
            (Episode::Two, 7) => Ok(MapArea::JungleAreaEast),
            (Episode::Two, 8) => Ok(MapArea::Mountain),
            (Episode::Two, 9) => Ok(MapArea::Seaside),
            (Episode::Two, 10) => Ok(MapArea::SeabedUpper),
            (Episode::Two, 11) => Ok(MapArea::SeabedLower),
            (Episode::Two, 12) => Ok(MapArea::GalGryphon),
            (Episode::Two, 13) => Ok(MapArea::OlgaFlow),
            (Episode::Two, 14) => Ok(MapArea::BarbaRay),
            (Episode::Two, 15) => Ok(MapArea::GolDragon),
            // (Episode::Two, 16) => Ok(MapArea::Seaside2),
            // (Episode::Two, 17) => Ok(MapArea::Tower),
            (Episode::Four, 0) => Ok(MapArea::Pioneer2Ep4),
            (Episode::Four, 1) => Ok(MapArea::CraterEast),
            (Episode::Four, 2) => Ok(MapArea::CraterWest),
            (Episode::Four, 3) => Ok(MapArea::CraterSouth),
            (Episode::Four, 4) => Ok(MapArea::CraterNorth),
            (Episode::Four, 5) => Ok(MapArea::CraterInterior),
            (Episode::Four, 6) => Ok(MapArea::SubDesert1),
            (Episode::Four, 7) => Ok(MapArea::SubDesert2),
            (Episode::Four, 8) => Ok(MapArea::SubDesert3),
            (Episode::Four, 9) => Ok(MapArea::SaintMillion),
            // (Episode::Four, 10) => Ok(MapArea::TestMapEp4),
            _ => Err(MapAreaError::UnknownMapArea(area))
        }
    }

    pub fn drop_area_value(&self) -> Option<u32> {
        match self {
            MapArea::Forest1 => Some(0),
            MapArea::Forest2 => Some(1),
            MapArea::Caves1 => Some(2),
            MapArea::Caves2 => Some(3),
            MapArea::Caves3 => Some(4),
            MapArea::Mines1 => Some(5),
            MapArea::Mines2 => Some(6),
            MapArea::Ruins1 => Some(7),
            MapArea::Ruins2 => Some(8),
            MapArea::Ruins3 => Some(9),
            MapArea::Dragon => Some(2),
            MapArea::DeRolLe => Some(5),
            MapArea::VolOpt => Some(7),
            MapArea::DarkFalz => Some(9),
            MapArea::VrTempleAlpha => Some(0),
            MapArea::VrTempleBeta => Some(1),
            MapArea::VrSpaceshipAlpha => Some(2),
            MapArea::VrSpaceshipBeta => Some(3),
            MapArea::Cca => Some(4),
            MapArea::JungleAreaNorth => Some(5),
            MapArea::JungleAreaEast => Some(5),
            MapArea::Mountain => Some(6),
            MapArea::Seaside => Some(7),
            MapArea::SeabedUpper => Some(8),
            MapArea::SeabedLower => Some(9),
            MapArea::GalGryphon => Some(8),
            MapArea::OlgaFlow => Some(9),
            MapArea::BarbaRay => Some(2),
            MapArea::GolDragon => Some(5),
            // MapArea::Seaside2 => Some(0),
            // MapArea::Tower => Some(0),
            MapArea::CraterEast => Some(2),
            MapArea::CraterWest => Some(3),
            MapArea::CraterSouth => Some(4),
            MapArea::CraterNorth => Some(5),
            MapArea::CraterInterior => Some(6),
            MapArea::SubDesert1 => Some(7),
            MapArea::SubDesert2 => Some(8),
            MapArea::SubDesert3 => Some(9),
            MapArea::SaintMillion => Some(9),
            // MapArea::TestMapEp4 => Some(0),
            _ => None
        }
    }

    pub fn area_value(&self) -> u8 {
        match self {
            MapArea::Pioneer2Ep1 => 0,
            MapArea::Forest1 => 1,
            MapArea::Forest2 => 2,
            MapArea::Caves1 => 3,
            MapArea::Caves2 => 4,
            MapArea::Caves3 => 5,
            MapArea::Mines1 => 6,
            MapArea::Mines2 => 7,
            MapArea::Ruins1 => 8,
            MapArea::Ruins2 => 9,
            MapArea::Ruins3 => 10,
            MapArea::Dragon => 11,
            MapArea::DeRolLe => 12,
            MapArea::VolOpt => 13,
            MapArea::DarkFalz => 14,
            MapArea::Pioneer2Ep2 => 0,
            MapArea::VrTempleAlpha => 1,
            MapArea::VrTempleBeta => 2,
            MapArea::VrSpaceshipAlpha => 3,
            MapArea::VrSpaceshipBeta => 4,
            MapArea::Cca => 5,
            MapArea::JungleAreaNorth => 6,
            MapArea::JungleAreaEast => 7,
            MapArea::Mountain => 8,
            MapArea::Seaside => 9,
            MapArea::SeabedUpper => 10,
            MapArea::SeabedLower => 11,
            MapArea::GalGryphon => 12,
            MapArea::OlgaFlow => 13,
            MapArea::BarbaRay => 14,
            MapArea::GolDragon => 15,
            // MapArea::Seaside2 => 16,
            // MapArea::Tower => 17,
            MapArea::Pioneer2Ep4 => 0,
            MapArea::CraterEast => 1,
            MapArea::CraterWest => 2,
            MapArea::CraterSouth => 3,
            MapArea::CraterNorth => 4,
            MapArea::CraterInterior => 5,
            MapArea::SubDesert1 => 6,
            MapArea::SubDesert2 => 7,
            MapArea::SubDesert3 => 8,
            MapArea::SaintMillion => 9,
            // MapArea::TestMapEp4 => 10,
        }
    }
}


#[derive(Debug)]
struct MapVariant {
    map: MapArea,
    mode: MapVariantMode,
    major: u8,
    minor: u8,
}

impl MapVariant {
    fn new(map: MapArea, mode: MapVariantMode) -> MapVariant {
        if mode == MapVariantMode::Online {
            let major = match map {
                MapArea::Pioneer2Ep1 => 0,
                MapArea::Forest1 | MapArea::Forest2 => 0,
                MapArea::Caves1 | MapArea::Caves2 | MapArea::Caves3 => rand::thread_rng().gen_range(0, 3),
                MapArea::Mines1 | MapArea::Mines2 => rand::thread_rng().gen_range(0, 3),
                MapArea::Ruins1 | MapArea::Ruins2 | MapArea::Ruins3 => rand::thread_rng().gen_range(0, 3),
                MapArea::Dragon | MapArea::DeRolLe | MapArea::VolOpt | MapArea::DarkFalz => 0,

                MapArea::Pioneer2Ep2 => 0,
                MapArea::VrTempleAlpha | MapArea::VrTempleBeta => rand::thread_rng().gen_range(0, 2),
                MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta => rand::thread_rng().gen_range(0, 2),
                MapArea::Cca => 0,
                MapArea::JungleAreaNorth => 0,
                MapArea::JungleAreaEast => 0,
                MapArea::Mountain => rand::thread_rng().gen_range(0, 2),
                MapArea::Seaside => 0,
                MapArea::SeabedUpper | MapArea::SeabedLower => rand::thread_rng().gen_range(0, 2),
                MapArea::BarbaRay | MapArea::GolDragon | MapArea::GalGryphon | MapArea::OlgaFlow => 0,

                MapArea::Pioneer2Ep4 => 0,
                MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => 0,
                MapArea::SubDesert1 | MapArea::SubDesert3 => rand::thread_rng().gen_range(0, 2),
                MapArea::SubDesert2 => 0,
                MapArea::SaintMillion => 0,
            };

            let minor = match map {
                MapArea::Pioneer2Ep1 => 0,
                MapArea::Forest1 => rand::thread_rng().gen_range(0, 5),
                MapArea::Forest2 => rand::thread_rng().gen_range(0, 5),
                MapArea::Caves1 | MapArea::Caves2 | MapArea::Caves3 => rand::thread_rng().gen_range(0, 2),
                MapArea::Mines1 | MapArea::Mines2 => rand::thread_rng().gen_range(0, 2),
                MapArea::Ruins1 | MapArea::Ruins2 | MapArea::Ruins3 => rand::thread_rng().gen_range(0, 2),
                MapArea::Dragon | MapArea::DeRolLe | MapArea::VolOpt | MapArea::DarkFalz => 0,

                MapArea::Pioneer2Ep2 => 0,
                MapArea::VrTempleAlpha | MapArea::VrTempleBeta => 0,
                MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta => 0,
                MapArea::Cca => rand::thread_rng().gen_range(0, 3),
                MapArea::JungleAreaNorth => rand::thread_rng().gen_range(0, 3),
                MapArea::JungleAreaEast => rand::thread_rng().gen_range(0, 3),
                MapArea::Mountain => rand::thread_rng().gen_range(0, 2),
                MapArea::Seaside => rand::thread_rng().gen_range(0, 3),
                MapArea::SeabedUpper | MapArea::SeabedLower => rand::thread_rng().gen_range(0, 2),
                MapArea::GalGryphon => 0,
                MapArea::OlgaFlow => 0,
                MapArea::BarbaRay => 0,
                MapArea::GolDragon => 0,

                MapArea::Pioneer2Ep4 => 0,
                MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => rand::thread_rng().gen_range(0, 3),
                MapArea::SubDesert1 | MapArea::SubDesert3 => 0,
                MapArea::SubDesert2 => rand::thread_rng().gen_range(0, 2),
                MapArea::SaintMillion => 0,
            };
            
            MapVariant {
                map: map,
                mode: mode,
                major: major,
                minor: minor,
            }
        }
        else {
            let major = match map {
                MapArea::Pioneer2Ep1 => 0,
                MapArea::Forest1 => 0, | MapArea::Forest2 => 0,
                MapArea::Caves1 | MapArea::Caves2 | MapArea::Caves3 => rand::thread_rng().gen_range(0, 3),
                MapArea::Mines1 | MapArea::Mines2 => rand::thread_rng().gen_range(0, 3), // no offline-specific maps
                MapArea::Ruins1 | MapArea::Ruins2 | MapArea::Ruins3 => rand::thread_rng().gen_range(0, 3), // no offline-specific maps
                MapArea::Dragon | MapArea::DeRolLe | MapArea::VolOpt | MapArea::DarkFalz => 0,

                MapArea::Pioneer2Ep2 => 0,
                MapArea::VrTempleAlpha | MapArea::VrTempleBeta | MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta => rand::thread_rng().gen_range(0, 2),
                MapArea::Cca => 0,
                MapArea::JungleAreaNorth => 0,
                MapArea::JungleAreaEast => 0,
                MapArea::Mountain => rand::thread_rng().gen_range(0, 2),
                MapArea::Seaside => 0,
                MapArea::SeabedUpper | MapArea::SeabedLower => rand::thread_rng().gen_range(0, 2),
                MapArea::BarbaRay | MapArea::GolDragon | MapArea::GalGryphon | MapArea::OlgaFlow => 0,

                MapArea::Pioneer2Ep4 => 0,
                MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => 0,
                MapArea::SubDesert1 | MapArea::SubDesert3 => rand::thread_rng().gen_range(0, 2),
                MapArea::SubDesert2 => 0,
                MapArea::SaintMillion => 0,
            };

            let minor = match map {
                MapArea::Pioneer2Ep1 => 0,
                MapArea::Forest1 => rand::thread_rng().gen_range(0, 3),
                MapArea::Forest2 => rand::thread_rng().gen_range(0, 3),
                MapArea::Caves1 | MapArea::Caves2 | MapArea::Caves3 => 0,
                MapArea::Mines1 | MapArea::Mines2 => rand::thread_rng().gen_range(0, 2), // no offline-specific maps
                MapArea::Ruins1 | MapArea::Ruins2 | MapArea::Ruins3 => rand::thread_rng().gen_range(0, 2),// no offline-specific maps
                MapArea::Dragon | MapArea::DeRolLe | MapArea::VolOpt | MapArea::DarkFalz => 0,

                MapArea::Pioneer2Ep2 => 0,
                MapArea::VrTempleAlpha | MapArea::VrTempleBeta | MapArea::VrSpaceshipAlpha | MapArea::VrSpaceshipBeta => 0,
                MapArea::Cca => rand::thread_rng().gen_range(0, 3),
                MapArea::JungleAreaNorth => rand::thread_rng().gen_range(0, 3),
                MapArea::JungleAreaEast => rand::thread_rng().gen_range(0, 3),
                MapArea::Mountain => rand::thread_rng().gen_range(0, 2),
                MapArea::Seaside => rand::thread_rng().gen_range(0, 3),
                MapArea::SeabedUpper | MapArea::SeabedLower => 0,
                MapArea::GalGryphon => 0,
                MapArea::OlgaFlow => 0,
                MapArea::BarbaRay => 0,
                MapArea::GolDragon => 0,

                MapArea::Pioneer2Ep4 => 0,
                MapArea::CraterEast | MapArea::CraterWest | MapArea::CraterSouth | MapArea::CraterNorth | MapArea::CraterInterior => rand::thread_rng().gen_range(0, 3),
                MapArea::SubDesert1 | MapArea::SubDesert3 => 0,
                MapArea::SubDesert2 => rand::thread_rng().gen_range(0, 2),
                MapArea::SaintMillion => 0,

            };

            MapVariant {
                map: map,
                mode: mode,
                major: major,
                minor: minor,
            }
        }
    }
    // TODO: rename to npc_file
    fn dat_file(&self) -> String {
        if self.mode == MapVariantMode::Online {
            match self.map {
                MapArea::Pioneer2Ep1 => "data/maps/map_city00_00e.dat".into(),
                MapArea::Forest1 => format!("data/maps/map_forest01_0{}e.dat", self.minor),
                MapArea::Forest2 => format!("data/maps/map_forest02_0{}e.dat", self.minor),
                MapArea::Caves1 => format!("data/maps/map_cave01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Caves2 => format!("data/maps/map_cave02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Caves3 => format!("data/maps/map_cave03_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Mines1 => format!("data/maps/map_machine01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Mines2 => format!("data/maps/map_machine02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins1 => format!("data/maps/map_ancient01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins2 => format!("data/maps/map_ancient02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins3 => format!("data/maps/map_ancient03_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Dragon => "data/maps/map_boss01e.dat".into(),
                MapArea::DeRolLe => "data/maps/map_boss02e.dat".into(),
                MapArea::VolOpt => "data/maps/map_boss03e.dat".into(),
                MapArea::DarkFalz => "data/maps/map_boss04e.dat".into(),

                MapArea::Pioneer2Ep2 => "data/maps/map_labo00_00e.dat".into(),
                MapArea::VrTempleAlpha => format!("data/maps/map_ruins01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::VrTempleBeta => format!("data/maps/map_ruins02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::VrSpaceshipAlpha => format!("data/maps/map_space01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::VrSpaceshipBeta => format!("data/maps/map_space02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Cca => format!("data/maps/map_jungle01_0{}e.dat", self.minor),
                MapArea::JungleAreaNorth => format!("data/maps/map_jungle02_0{}e.dat", self.minor),
                MapArea::JungleAreaEast => format!("data/maps/map_jungle03_0{}e.dat", self.minor),
                MapArea::Mountain => format!("data/maps/map_jungle04_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Seaside => format!("data/maps/map_jungle05_0{}e.dat", self.minor),
                MapArea::SeabedUpper => format!("data/maps/map_seabed01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SeabedLower => format!("data/maps/map_seabed02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::GalGryphon => "data/maps/map_boss05e.dat".into(),
                MapArea::OlgaFlow => "data/maps/map_boss06e.dat".into(),
                MapArea::BarbaRay => "data/maps/map_boss07e.dat".into(),
                MapArea::GolDragon => "data/maps/map_boss08e.dat".into(),

                MapArea::Pioneer2Ep4 => "data/maps/map_city02_00_00e.dat".into(),
                MapArea::CraterEast => format!("data/maps/map_wilds01_00_0{}e.dat", self.minor),
                MapArea::CraterWest => format!("data/maps/map_wilds01_01_0{}e.dat", self.minor),
                MapArea::CraterSouth => format!("data/maps/map_wilds01_02_0{}e.dat", self.minor),
                MapArea::CraterNorth => format!("data/maps/map_wilds01_03_0{}e.dat", self.minor),
                MapArea::CraterInterior => format!("data/maps/map_crater01_00_0{}e.dat", self.minor),
                MapArea::SubDesert1 => format!("data/maps/map_desert01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SubDesert2 => format!("data/maps/map_desert02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SubDesert3 => format!("data/maps/map_desert03_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SaintMillion => "data/maps/map_boss09_00_00e.dat".into(),
            }
        } else { // Offline
            match self.map {
                MapArea::Pioneer2Ep1 => "data/maps/map_city00_00e_s.dat".into(),
                MapArea::Forest1 => format!("data/maps/map_forest01_0{}_offe.dat", self.minor*2),
                MapArea::Forest2 => {
                    match self.minor {
                        0 => format!("data/maps/map_forest02_00_offe.dat"),
                        1 => format!("data/maps/map_forest02_03_offe.dat"),
                        2 => format!("data/maps/map_forest02_04_offe.dat"),
                        _ => unreachable!()
                }},
                MapArea::Caves1 => format!("data/maps/map_cave01_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::Caves2 => format!("data/maps/map_cave02_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::Caves3 => format!("data/maps/map_cave03_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::Mines1 => format!("data/maps/map_machine01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Mines2 => format!("data/maps/map_machine02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins1 => format!("data/maps/map_ancient01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins2 => format!("data/maps/map_ancient02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Ruins3 => format!("data/maps/map_ancient03_0{}_0{}e.dat", self.major, self.minor),
                MapArea::Dragon => "data/maps/map_boss01e.dat".into(),
                MapArea::DeRolLe => "data/maps/map_boss02e.dat".into(),
                MapArea::VolOpt => "data/maps/map_boss03e.dat".into(),
                MapArea::DarkFalz => "data/maps/map_boss04e.dat".into(),

                MapArea::Pioneer2Ep2 => "data/maps/map_labo00_00e_s.dat".into(),
                MapArea::VrTempleAlpha => format!("data/maps/map_ruins01_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::VrTempleBeta => format!("data/maps/map_ruins02_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::VrSpaceshipAlpha => format!("data/maps/map_space01_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::VrSpaceshipBeta => format!("data/maps/map_space02_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::Cca => format!("data/maps/map_jungle01_0{}_offe.dat", self.minor),
                MapArea::JungleAreaNorth => format!("data/maps/map_jungle02_0{}_offe.dat", self.minor),
                MapArea::JungleAreaEast => format!("data/maps/map_jungle03_0{}_offe.dat", self.minor),
                MapArea::Mountain => format!("data/maps/map_jungle04_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::Seaside => format!("data/maps/map_jungle05_0{}_offe.dat", self.minor),
                MapArea::SeabedUpper => format!("data/maps/map_seabed01_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::SeabedLower => format!("data/maps/map_seabed02_0{}_0{}_offe.dat", self.major, self.minor),
                MapArea::GalGryphon => "data/maps/map_boss05e.dat".into(),
                MapArea::OlgaFlow => "data/maps/map_boss06e.dat".into(),
                MapArea::BarbaRay => "data/maps/map_boss07e.dat".into(),
                MapArea::GolDragon => "data/maps/map_boss08e.dat".into(),

                MapArea::Pioneer2Ep4 => "data/maps/map_city02_00_00e_s.dat".into(),
                MapArea::CraterEast => format!("data/maps/map_wilds01_00_0{}e.dat", self.minor),
                MapArea::CraterWest => format!("data/maps/map_wilds01_01_0{}e.dat", self.minor),
                MapArea::CraterSouth => format!("data/maps/map_wilds01_02_0{}e.dat", self.minor),
                MapArea::CraterNorth => format!("data/maps/map_wilds01_03_0{}e.dat", self.minor),
                MapArea::CraterInterior => format!("data/maps/map_crater01_00_0{}e.dat", self.minor),
                MapArea::SubDesert1 => format!("data/maps/map_desert01_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SubDesert2 => format!("data/maps/map_desert02_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SubDesert3 => format!("data/maps/map_desert03_0{}_0{}e.dat", self.major, self.minor),
                MapArea::SaintMillion => "data/maps/map_boss09_00_00e.dat".into(),
            }
        }
    }

    fn obj_file(&self) -> String {
        if self.mode == MapVariantMode::Online {
            match self.map {
                MapArea::Pioneer2Ep1 => "data/maps/map_city00_00o.dat".into(),
                MapArea::Forest1 => format!("data/maps/map_forest01_0{}o.dat", self.minor),
                MapArea::Forest2 => format!("data/maps/map_forest02_0{}o.dat", self.minor),
                MapArea::Caves1 => format!("data/maps/map_cave01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Caves2 => format!("data/maps/map_cave02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Caves3 => format!("data/maps/map_cave03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Mines1 => format!("data/maps/map_machine01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Mines2 => format!("data/maps/map_machine02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins1 => format!("data/maps/map_ancient01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins2 => format!("data/maps/map_ancient02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins3 => format!("data/maps/map_ancient03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Dragon => "data/maps/map_boss01o.dat".into(),
                MapArea::DeRolLe => "data/maps/map_boss02o.dat".into(),
                MapArea::VolOpt => "data/maps/map_boss03o.dat".into(),
                MapArea::DarkFalz => "data/maps/map_boss04o.dat".into(),

                MapArea::Pioneer2Ep2 => "data/maps/map_labo00_00o.dat".into(),
                MapArea::VrTempleAlpha => format!("data/maps/map_ruins01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::VrTempleBeta => format!("data/maps/map_ruins02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::VrSpaceshipAlpha => format!("data/maps/map_space01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::VrSpaceshipBeta => format!("data/maps/map_space02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Cca => format!("data/maps/map_jungle01_0{}o.dat", self.minor),
                MapArea::JungleAreaNorth => format!("data/maps/map_jungle02_0{}o.dat", self.minor),
                MapArea::JungleAreaEast => format!("data/maps/map_jungle03_0{}o.dat", self.minor),
                MapArea::Mountain => format!("data/maps/map_jungle04_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Seaside => format!("data/maps/map_jungle05_0{}o.dat", self.minor),
                MapArea::SeabedUpper => format!("data/maps/map_seabed01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SeabedLower => format!("data/maps/map_seabed02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::GalGryphon => "data/maps/map_boss05o.dat".into(),
                MapArea::OlgaFlow => "data/maps/map_boss06o.dat".into(),
                MapArea::BarbaRay => "data/maps/map_boss07o.dat".into(),
                MapArea::GolDragon => "data/maps/map_boss08o.dat".into(),

                MapArea::Pioneer2Ep4 => "data/maps/map_city02_00_00o.dat".into(),
                MapArea::CraterEast => format!("data/maps/map_wilds01_00_0{}o.dat", self.minor),
                MapArea::CraterWest => format!("data/maps/map_wilds01_01_0{}o.dat", self.minor),
                MapArea::CraterSouth => format!("data/maps/map_wilds01_02_0{}o.dat", self.minor),
                MapArea::CraterNorth => format!("data/maps/map_wilds01_03_0{}o.dat", self.minor),
                MapArea::CraterInterior => format!("data/maps/map_crater01_00_0{}o.dat", self.minor),
                MapArea::SubDesert1 => format!("data/maps/map_desert01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SubDesert2 => format!("data/maps/map_desert02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SubDesert3 => format!("data/maps/map_desert03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SaintMillion => "data/maps/map_boss09_00_00o.dat".into(),
            }
        } else {
            match self.map {
                MapArea::Pioneer2Ep1 => "data/maps/map_city00_00o_s.dat".into(),
                MapArea::Forest1 => format!("data/maps/map_forest01_0{}o.dat", self.minor*2),
                MapArea::Forest2 => {
                    match self.minor {
                        0 => format!("data/maps/map_forest02_00o.dat"),
                        1 => format!("data/maps/map_forest02_03o.dat"),
                        2 => format!("data/maps/map_forest02_04o.dat"),
                        _ => unreachable!()
                }},
                MapArea::Caves1 => format!("data/maps/map_cave01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Caves2 => format!("data/maps/map_cave02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Caves3 => format!("data/maps/map_cave03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Mines1 => format!("data/maps/map_machine01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Mines2 => format!("data/maps/map_machine02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins1 => format!("data/maps/map_ancient01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins2 => format!("data/maps/map_ancient02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Ruins3 => format!("data/maps/map_ancient03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::Dragon => "data/maps/map_boss01o.dat".into(),
                MapArea::DeRolLe => "data/maps/map_boss02o.dat".into(),
                MapArea::VolOpt => "data/maps/map_boss03o.dat".into(),
                MapArea::DarkFalz => "data/maps/map_boss04_offo.dat".into(),

                MapArea::Pioneer2Ep2 => "data/maps/map_labo00_00o_s.dat".into(),
                MapArea::VrTempleAlpha => format!("data/maps/map_ruins01_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::VrTempleBeta => format!("data/maps/map_ruins02_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::VrSpaceshipAlpha => format!("data/maps/map_space01_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::VrSpaceshipBeta => format!("data/maps/map_space02_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::Cca => format!("data/maps/map_jungle01_0{}_offo.dat", self.minor),
                MapArea::JungleAreaNorth => format!("data/maps/map_jungle02_0{}_offo.dat", self.minor),
                MapArea::JungleAreaEast => format!("data/maps/map_jungle03_0{}_offo.dat", self.minor),
                MapArea::Mountain => format!("data/maps/map_jungle04_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::Seaside => format!("data/maps/map_jungle05_0{}_offo.dat", self.minor),
                MapArea::SeabedUpper => format!("data/maps/map_seabed01_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::SeabedLower => format!("data/maps/map_seabed02_0{}_0{}_offo.dat", self.major, self.minor),
                MapArea::GalGryphon => "data/maps/map_boss05_offo.dat".into(),
                MapArea::OlgaFlow => "data/maps/map_boss06_offo.dat".into(),
                MapArea::BarbaRay => "data/maps/map_boss07_offo.dat".into(),
                MapArea::GolDragon => "data/maps/map_boss08_offo.dat".into(),

                MapArea::Pioneer2Ep4 => "data/maps/map_city02_00_00o_s.dat".into(),
                MapArea::CraterEast => format!("data/maps/map_wilds01_00_0{}o.dat", self.minor),
                MapArea::CraterWest => format!("data/maps/map_wilds01_01_0{}o.dat", self.minor),
                MapArea::CraterSouth => format!("data/maps/map_wilds01_02_0{}o.dat", self.minor),
                MapArea::CraterNorth => format!("data/maps/map_wilds01_03_0{}o.dat", self.minor),
                MapArea::CraterInterior => format!("data/maps/map_crater01_00_0{}o.dat", self.minor),
                MapArea::SubDesert1 => format!("data/maps/map_desert01_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SubDesert2 => format!("data/maps/map_desert02_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SubDesert3 => format!("data/maps/map_desert03_0{}_0{}o.dat", self.major, self.minor),
                MapArea::SaintMillion => "data/maps/map_boss09_00_00o.dat".into(),
            }
        }
    }

    fn pkt_header(&self) -> [u8; 2] {
        [self.major, self.minor]
    }
}


pub fn objects_from_stream(cursor: &mut impl Read, episode: &Episode, map_area: &MapArea) -> Vec<Option<MapObject>> {
    let mut object_data = Vec::new();

    while let Ok(raw_object) = RawMapObject::from_byte_stream(cursor) {
        let object = MapObject::from_raw(raw_object.clone(), *episode, map_area);
        object_data.push(object.ok());
    }
    object_data
}

fn objects_from_map_data(path: PathBuf, episode: &Episode, map_area: &MapArea) -> Vec<Option<MapObject>> {
    let mut cursor = File::open(path.clone()).unwrap();
    objects_from_stream(&mut cursor, episode, map_area)
}

fn parse_enemy(episode: &Episode, map_area: &MapArea, raw_enemy: RawMapEnemy) -> Vec<Option<MapEnemy>> {
    let enemy = MapEnemy::from_raw(raw_enemy, episode, map_area);
    enemy
        .map_or(vec![None], |monster| {
            let mut monsters = Vec::new();
            monsters.push(Some(monster));

            match monster.monster {
                MonsterType::Monest => {
                    for _ in 0..30 {
                        monsters.push(Some(MapEnemy::new(MonsterType::Mothmant, monster.map_area)));
                    }
                },
                MonsterType::PofuillySlime => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::PofuillySlime, monster.map_area)));
                    }
                },
                MonsterType::PanArms => {
                    monsters.push(Some(MapEnemy::new(MonsterType::Hidoom, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::Migium, monster.map_area)));
                },
                MonsterType::SinowBeat => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::SinowBeat, monster.map_area)));
                    }
                },
                MonsterType::SinowGold => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::SinowGold, monster.map_area)));
                    }
                },
                MonsterType::Canane => {
                    for _ in 0..8 {
                        monsters.push(Some(MapEnemy::new(MonsterType::RingCanadine, monster.map_area)));
                    }
                },
                MonsterType::ChaosSorcerer => {
                    monsters.push(Some(MapEnemy::new(MonsterType::BeeR, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::BeeL, monster.map_area)));
                },
                MonsterType::Bulclaw => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::Claw, monster.map_area)));
                    }
                },
                MonsterType::DeRolLe => {
                    for _ in 0..10 {
                        monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeBody, monster.map_area)));
                    }
                    for _ in 0..9 {
                        monsters.push(Some(MapEnemy::new(MonsterType::DeRolLeMine, monster.map_area)));
                    }
                },
                MonsterType::VolOptPartA => {
                    for _ in 0..6 {
                        monsters.push(Some(MapEnemy::new(MonsterType::VolOptPillar, monster.map_area)));
                    }
                    for _ in 0..24 {
                        monsters.push(Some(MapEnemy::new(MonsterType::VolOptMonitor, monster.map_area)));
                    }
                    for _ in 0..2 {
                        monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area)));
                    }
                    monsters.push(Some(MapEnemy::new(MonsterType::VolOptAmp, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::VolOptCore, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::VolOptUnused, monster.map_area)));
                },
                // TOOD: this cares about difficulty (theres an ult-specific darvant?)
                MonsterType::DarkFalz => {
                    for _ in 0..509 {
                        monsters.push(Some(MapEnemy::new(MonsterType::Darvant, monster.map_area)));
                    }
                    monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz3, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz2, monster.map_area)));
                    monsters.push(Some(MapEnemy::new(MonsterType::DarkFalz1, monster.map_area)));
                },
                MonsterType::OlgaFlow => {
                    for _ in 0..512 {
                        monsters.push(Some(MapEnemy::new(MonsterType::OlgaFlow, monster.map_area)));
                    }
                },
                MonsterType::BarbaRay => {
                    for _ in 0..47 {
                        monsters.push(Some(MapEnemy::new(MonsterType::PigRay, monster.map_area)));
                    }
                },
                MonsterType::GolDragon => {
                    for _ in 0..5 {
                        monsters.push(Some(MapEnemy::new(MonsterType::GolDragon, monster.map_area)));
                    }
                },
                MonsterType::SinowBerill => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::SinowBerill, monster.map_area))); // unused clones
                    }
                },
                MonsterType::SinowSpigell => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::SinowSpigell, monster.map_area))); // unused clones
                    }
                },
                MonsterType::Recobox => { // + recons
                    for _ in 0..raw_enemy.children {
                        monsters.push(Some(MapEnemy::new(MonsterType::Recon, monster.map_area)));
                    }
                },
                MonsterType::Epsilon => {
                    for _ in 0..4 {
                        monsters.push(Some(MapEnemy::new(MonsterType::Epsiguard, monster.map_area)));
                    }
                },
                _ => {
                    for _ in 0..raw_enemy.children {
                        monsters.push(Some(MapEnemy::new(monster.monster, monster.map_area)));
                    }
                }
            }
            monsters
        })
}


pub fn enemy_data_from_stream(cursor: &mut impl Read, map_area: &MapArea, episode: &Episode) -> Vec<Option<MapEnemy>> {
    let mut enemy_data = Vec::new();
    while let Ok(enemy) = RawMapEnemy::from_byte_stream(cursor) {
        enemy_data.append(&mut parse_enemy(episode, map_area, enemy));
    }
    enemy_data
}

fn enemy_data_from_map_data(map_variant: &MapVariant, episode: &Episode) -> Vec<Option<MapEnemy>> {
    let path = map_variant.dat_file();
    let mut cursor = File::open(path).unwrap();
    enemy_data_from_stream(&mut cursor, &map_variant.map, episode)
}


#[derive(Error, Debug)]
#[error("")]
pub enum MapsError {
    InvalidMonsterId(usize),
    InvalidObjectId(usize),
}

#[derive(Debug)]
pub struct Maps {
    map_variants: Vec<MapVariant>,
    enemy_data: Vec<Option<MapEnemy>>,
    object_data: Vec<Option<MapObject>>,
}

impl Maps {
    pub fn new(room_mode: RoomMode) -> Maps {
        let map_variants = match (room_mode.episode(), room_mode.single_player()) {
            (Episode::One, 0) => {
                vec![MapVariant::new(MapArea::Pioneer2Ep1, MapVariantMode::Online),
                 MapVariant::new(MapArea::Forest1, MapVariantMode::Online),
                 MapVariant::new(MapArea::Forest2, MapVariantMode::Online),
                 MapVariant::new(MapArea::Caves1, MapVariantMode::Online),
                 MapVariant::new(MapArea::Caves2, MapVariantMode::Online),
                 MapVariant::new(MapArea::Caves3, MapVariantMode::Online),
                 MapVariant::new(MapArea::Mines1, MapVariantMode::Online),
                 MapVariant::new(MapArea::Mines2, MapVariantMode::Online),
                 MapVariant::new(MapArea::Ruins1, MapVariantMode::Online),
                 MapVariant::new(MapArea::Ruins2, MapVariantMode::Online),
                 MapVariant::new(MapArea::Ruins3, MapVariantMode::Online),
                 MapVariant::new(MapArea::Dragon, MapVariantMode::Online),
                 MapVariant::new(MapArea::DeRolLe, MapVariantMode::Online),
                 MapVariant::new(MapArea::VolOpt, MapVariantMode::Online),
                 MapVariant::new(MapArea::DarkFalz, MapVariantMode::Online),
                ]
            },
            (Episode::One, 1) => {
                vec![MapVariant::new(MapArea::Pioneer2Ep1, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Forest1, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Forest2, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Caves1, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Caves2, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Caves3, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Mines1, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Mines2, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Ruins1, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Ruins2, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Ruins3, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Dragon, MapVariantMode::Offline),
                 MapVariant::new(MapArea::DeRolLe, MapVariantMode::Offline),
                 MapVariant::new(MapArea::VolOpt, MapVariantMode::Offline),
                 MapVariant::new(MapArea::DarkFalz, MapVariantMode::Offline),
                ]
            },
            (Episode::Two, 0) => {
                vec![MapVariant::new(MapArea::Pioneer2Ep2, MapVariantMode::Online),
                 MapVariant::new(MapArea::VrTempleAlpha, MapVariantMode::Online),
                 MapVariant::new(MapArea::VrTempleBeta, MapVariantMode::Online),
                 MapVariant::new(MapArea::VrSpaceshipAlpha, MapVariantMode::Online),
                 MapVariant::new(MapArea::VrSpaceshipBeta, MapVariantMode::Online),
                 MapVariant::new(MapArea::Cca, MapVariantMode::Online),
                 MapVariant::new(MapArea::JungleAreaNorth, MapVariantMode::Online),
                 MapVariant::new(MapArea::JungleAreaEast, MapVariantMode::Online),
                 MapVariant::new(MapArea::Mountain, MapVariantMode::Online),
                 MapVariant::new(MapArea::Seaside, MapVariantMode::Online),
                 MapVariant::new(MapArea::SeabedUpper, MapVariantMode::Online),
                 MapVariant::new(MapArea::SeabedLower, MapVariantMode::Online),
                 MapVariant::new(MapArea::GalGryphon, MapVariantMode::Online),
                 MapVariant::new(MapArea::OlgaFlow, MapVariantMode::Online),
                 MapVariant::new(MapArea::BarbaRay, MapVariantMode::Online),
                 MapVariant::new(MapArea::GolDragon, MapVariantMode::Online),
                ]
            },
            (Episode::Two, 1) => {
                vec![MapVariant::new(MapArea::Pioneer2Ep2, MapVariantMode::Offline),
                 MapVariant::new(MapArea::VrTempleAlpha, MapVariantMode::Offline),
                 MapVariant::new(MapArea::VrTempleBeta, MapVariantMode::Offline),
                 MapVariant::new(MapArea::VrSpaceshipAlpha, MapVariantMode::Offline),
                 MapVariant::new(MapArea::VrSpaceshipBeta, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Cca, MapVariantMode::Offline),
                 MapVariant::new(MapArea::JungleAreaNorth, MapVariantMode::Offline),
                 MapVariant::new(MapArea::JungleAreaEast, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Mountain, MapVariantMode::Offline),
                 MapVariant::new(MapArea::Seaside, MapVariantMode::Offline),
                 MapVariant::new(MapArea::SeabedUpper, MapVariantMode::Offline),
                 MapVariant::new(MapArea::SeabedLower, MapVariantMode::Offline),
                 MapVariant::new(MapArea::GalGryphon, MapVariantMode::Offline),
                 MapVariant::new(MapArea::OlgaFlow, MapVariantMode::Offline),
                 MapVariant::new(MapArea::BarbaRay, MapVariantMode::Offline),
                 MapVariant::new(MapArea::GolDragon, MapVariantMode::Offline),
                ]
            },
            (Episode::Four, _) => {
                vec![MapVariant::new(MapArea::Pioneer2Ep4, MapVariantMode::Online),
                 MapVariant::new(MapArea::CraterEast, MapVariantMode::Online),
                 MapVariant::new(MapArea::CraterWest, MapVariantMode::Online),
                 MapVariant::new(MapArea::CraterSouth, MapVariantMode::Online),
                 MapVariant::new(MapArea::CraterNorth, MapVariantMode::Online),
                 MapVariant::new(MapArea::CraterInterior, MapVariantMode::Online),
                 MapVariant::new(MapArea::SubDesert1, MapVariantMode::Online),
                 MapVariant::new(MapArea::SubDesert2, MapVariantMode::Online),
                 MapVariant::new(MapArea::SubDesert3, MapVariantMode::Online),
                 MapVariant::new(MapArea::SaintMillion, MapVariantMode::Online),
                ]
            },
            _ => unreachable!()
        };

        let maps = Maps {
            enemy_data: map_variants.iter().fold(Vec::new(), |mut enemy_data, map_variant| {
                enemy_data.append(&mut enemy_data_from_map_data(&map_variant, &room_mode.episode()));
                enemy_data
            }),
            object_data: map_variants.iter().map(|map_variant| {
                objects_from_map_data(map_variant.obj_file().into(), &room_mode.episode(), &map_variant.map)
            }).flatten().collect(),
            map_variants: map_variants,
        };
        maps
    }

    pub fn enemy_by_id(&self, id: usize) -> Result<MapEnemy, MapsError> {
        self.enemy_data[id].ok_or(MapsError::InvalidMonsterId(id))
    }

    pub fn object_by_id(&self, id: usize) -> Result<MapObject, MapsError> {
        self.object_data[id].ok_or(MapsError::InvalidObjectId(id))
    }

    pub fn map_headers(&self) -> [u32; 0x20] {
        self.map_variants.iter()
            .enumerate()
            .fold([0; 0x20], |mut header, (i, map_variant)| {
                let [major, minor] = map_variant.pkt_header();

                header[i*2] = major as u32;
                header[i*2 + 1] = minor as u32;
                header
            })
    }

    pub fn set_quest_data(&mut self, enemies: Vec<Option<MapEnemy>>, objects: Vec<Option<MapObject>>) {
        self.enemy_data = enemies;
        self.object_data = objects;
    }
}