use log::warn;
use std::collections::{HashMap, BTreeMap, BTreeSet};
use std::fs::File;
use std::io::{Read, Write, Cursor, Seek, SeekFrom};
use std::path::PathBuf;
use std::convert::TryInto;
use thiserror::Error;
use serde::{Serialize, Deserialize};
use ages_prs::{LegacyPrsDecoder, LegacyPrsEncoder};
use byteorder::{LittleEndian, ReadBytesExt};
use libpso::util::array_to_utf16;
use crate::ship::map::{MapArea, MapAreaError, MapObject, MapEnemy, enemy_data_from_stream, objects_from_stream};
use crate::ship::room::Episode;
use crate::ship::map::area::{MapAreaLookup, MapAreaLookupBuilder};


#[derive(Debug, Serialize, Deserialize, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct QuestCategory {
    index: usize,
    pub name: String,
    pub description: String,
}


#[derive(Debug, Serialize, Deserialize, Hash)]
struct QuestListEntry {
    bin: String,
    dat: String,
}

#[derive(Debug, Serialize, Deserialize, Hash)]
struct QuestListCategory {
    list_order: usize,
    description: String,
    quests: Vec<QuestListEntry>,
}

#[derive(Debug, Serialize, Deserialize)]
struct QuestListConfig {
    questlist: HashMap<String, Vec<QuestListEntry>>,
}

#[derive(Error, Debug)]
#[error("")]
pub enum ParseDatError {
    IoError(#[from] std::io::Error),
    MapError(#[from] MapAreaError),
    UnknownDatHeader(u32),
    CouldNotDetermineEpisode,
    InvalidMapAreaId(u16),
}

const DAT_OBJECT_HEADER_ID: u32 = 1;
const DAT_ENEMY_HEADER_ID: u32 = 2;
const DAT_WAVE_HEADER_ID: u32 = 3;

enum DatBlock {
    Object(Vec<Option<MapObject>>),
    Enemy(Vec<Option<MapEnemy>>),
    Wave,
}


fn read_dat_section_header<T: Read + Seek>(cursor: &mut T, episode: &Episode, map_areas: &MapAreaLookup) -> Result<DatBlock, ParseDatError> {
    let header = cursor.read_u32::<LittleEndian>()?;
    let _offset = cursor.read_u32::<LittleEndian>()?;
    let area = cursor.read_u16::<LittleEndian>()?;
    let _unknown1 = cursor.read_u16::<LittleEndian>()?;
    let length = cursor.read_u32::<LittleEndian>()?;

    let map_area = map_areas.get_area_map(area).map_err(|_| ParseDatError::InvalidMapAreaId(area))?;

    match header {
        DAT_OBJECT_HEADER_ID => {
            let mut obj_data = vec![0u8; length as usize];
            cursor.read_exact(&mut obj_data)?;
            let mut obj_cursor = Cursor::new(obj_data);

            let objects = objects_from_stream(&mut obj_cursor, episode, map_area);
            Ok(DatBlock::Object(objects))
        },
        DAT_ENEMY_HEADER_ID => {
            let mut enemy_data = vec![0u8; length as usize];
            cursor.read_exact(&mut enemy_data)?;
            let mut enemy_cursor = Cursor::new(enemy_data);

            let enemies = enemy_data_from_stream(&mut enemy_cursor, map_area, episode);

            Ok(DatBlock::Enemy(enemies))
        },
        DAT_WAVE_HEADER_ID => {
            cursor.seek(SeekFrom::Current(length as i64))?;
            Ok(DatBlock::Wave)
        },
        _ => Err(ParseDatError::UnknownDatHeader(header))
    }
}

fn quest_episode(bin: &[u8]) -> Option<Episode> {
    for bytes in bin.windows(3) {
        // set_episode
        if bytes[0] == 0xF8 && bytes[1] == 0xBC {
            return Episode::from_quest(bytes[2]).ok()
        }
    }
    None
}

fn map_area_mappings(bin: &[u8]) -> MapAreaLookup {
    let mut map_areas = MapAreaLookupBuilder::default();
    for bytes in bin.windows(4) {
        // BB_Map_Designate
        if bytes[0] == 0xF9 && bytes[1] == 0x51 {
            //return Some(Episode::from_quest(bytes[2]).ok()?)
            let floor_value = bytes[2] as u16;
            if let Some(map_area) = MapArea::from_bb_map_designate(bytes[3]) {
                map_areas = map_areas.add(floor_value, map_area);
            }
        }
    }
    map_areas.build()
}

#[allow(clippy::type_complexity)]
fn parse_dat(dat: &[u8], episode: &Episode, map_areas: &MapAreaLookup) -> Result<(Vec<Option<MapEnemy>>, Vec<Option<MapObject>>), ParseDatError> {
    let mut cursor = Cursor::new(dat);

    let header_iter = std::iter::from_fn(move || {
        match read_dat_section_header(&mut cursor, episode, map_areas) {
            Ok(dat_block) => Some(dat_block),
            Err(err) => {
                warn!("unknown header in dat: {:?}", err);
                None
            }
        }
    });

    Ok(header_iter.fold((Vec::new(), Vec::new()), |(mut enemies, mut objects), dat_block| {
        match dat_block {
            DatBlock::Object(mut objs) => {
                objects.append(&mut objs)
            },
            DatBlock::Enemy(mut enemy) => {
                enemies.append(&mut enemy)
            },
            _ => {}
        }

        (enemies, objects)
    }))
}

#[derive(Error, Debug)]
#[error("")]
pub enum QuestLoadError {
    IoError(#[from] std::io::Error),
    ParseDatError(#[from] ParseDatError),
    CouldNotReadMetadata,
    CouldNotLoadConfigFile,
}

#[derive(Debug)]
pub struct Quest {
    pub name: String,
    pub description: String,
    pub full_description: String,
    pub language: u16,
    pub id: u16,
    pub bin_blob: Vec<u8>,
    pub dat_blob: Vec<u8>,
    pub enemies: Vec<Option<MapEnemy>>,
    pub objects: Vec<Option<MapObject>>,
    pub map_areas: MapAreaLookup,
}

impl Quest {
    fn from_bin_dat(bin: Vec<u8>, dat: Vec<u8>) -> Result<Quest, QuestLoadError> {
        let id = u16::from_le_bytes(bin[16..18].try_into().map_err(|_| QuestLoadError::CouldNotReadMetadata)?);
        let language = u16::from_le_bytes(bin[18..20].try_into().map_err(|_| QuestLoadError::CouldNotReadMetadata)?);
        let name = array_to_utf16(&bin[24..88]);
        let description = array_to_utf16(&bin[88..334]);
        let full_description = array_to_utf16(&bin[334..920]);

        let episode = quest_episode(&bin).ok_or(ParseDatError::CouldNotDetermineEpisode)?;
        let map_areas = map_area_mappings(&bin);
        let (enemies, objects) = parse_dat(&dat, &episode, &map_areas)?;

        let mut prs_bin = LegacyPrsEncoder::new(Vec::new());
        prs_bin.write_all(&bin)?;
        let mut prs_dat = LegacyPrsEncoder::new(Vec::new());
        prs_dat.write_all(&dat)?;

        Ok(Quest {
            name,
            description,
            full_description,
            id,
            language,
            bin_blob: prs_bin.into_inner().map_err(|_| QuestLoadError::CouldNotReadMetadata)?,
            dat_blob: prs_dat.into_inner().map_err(|_| QuestLoadError::CouldNotReadMetadata)?,
            enemies,
            objects,
            map_areas,
        })
    }
}

// QuestCollection
pub type QuestList = BTreeMap<QuestCategory, Vec<Quest>>;

pub fn load_quest(bin_path: PathBuf, dat_path: PathBuf, quest_path: PathBuf) -> Option<Quest> {
    let dat_file = File::open(quest_path.join(dat_path.clone()))
        .map_err(|err| {
            warn!("could not load quest file {:?}: {:?}", dat_path, err)
        }).ok()?;
    let bin_file = File::open(quest_path.join(bin_path.clone()))
        .map_err(|err| {
            warn!("could not load quest file {:?}: {:?}", bin_path, err)
        }).ok()?;
    let mut dat_prs = LegacyPrsDecoder::new(dat_file);
    let mut bin_prs = LegacyPrsDecoder::new(bin_file);

    let mut dat = Vec::new();
    let mut bin = Vec::new();
    dat_prs.read_to_end(&mut dat).ok()?;
    bin_prs.read_to_end(&mut bin).ok()?;

    let quest = Quest::from_bin_dat(bin, dat).map_err(|err| {
        warn!("could not parse quest file {:?}/{:?}: {:?}", bin_path, dat_path, err)
    }).ok()?;
    Some(quest)
}


pub fn load_quests(quest_path: &mut PathBuf) -> Result<QuestList, QuestLoadError> {
    let mut f = File::open(quest_path.clone()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?;
    let mut s = String::new();
    f.read_to_string(&mut s)?;
    quest_path.pop(); // remove quests.toml from the path
    let mut used_quest_ids = BTreeSet::new();
    let ql: BTreeMap<String, QuestListCategory> = toml::from_str(s.as_str()).map_err(|_| QuestLoadError::CouldNotLoadConfigFile)?;

    Ok(ql.into_iter().map(|(category, category_details)| {
        let quests = category_details.quests
            .into_iter()
            .filter_map(|quest| {
                load_quest(quest.bin.into(), quest.dat.into(), quest_path.to_path_buf())
                    .and_then(|quest | {
                        if used_quest_ids.contains(&quest.id) {
                            warn!("quest id already exists: {}", quest.id);
                            return None;
                        }
                        used_quest_ids.insert(quest.id);
                        Some(quest)
                    })
            });
        (QuestCategory{
            index: category_details.list_order,
            name: category,
            description: category_details.description,
        }, quests.collect())
    }).collect())
}








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

    // the quest phantasmal world 4 uses the tower map twice, to do this it had to remap
    // one of the other maps to be a second tower
    #[test]
    fn test_quest_with_remapped_floors() {
        let pw4 = load_quest("q236-ext-bb.bin".into(), "q236-ext-bb.dat".into(), "data/quests/bb/ep2/multi".into()).unwrap();
        let enemies_not_in_tower = pw4.enemies.iter()
            .filter(|enemy| {
                enemy.is_some()
            })
            .filter(|enemy| {
                enemy.unwrap().map_area != MapArea::Tower
            });
        assert!(enemies_not_in_tower.count() == 0);

    }
}