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, TryFrom};
use thiserror::Error;
use serde::{Serialize, Deserialize};
use ages_prs::{LegacyPrsDecoder, LegacyPrsEncoder};
use byteorder::{LittleEndian, ReadBytesExt};
use libpso::packet::ship::QuestChunk;
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;


#[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("")]
enum ParseDatError {
    IoError(#[from] std::io::Error),
    MapError(#[from] MapAreaError),
    UnknownDatHeader(u32),
    CouldNotDetermineEpisode,
}

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) -> Result<DatBlock, ParseDatError> {
    let header = cursor.read_u32::<LittleEndian>()?;
    let offset = cursor.read_u32::<LittleEndian>()?;
    let area = cursor.read_u32::<LittleEndian>()?;
    let length = cursor.read_u32::<LittleEndian>()?;

    let map_area = MapArea::from_value(episode, area)?;

    match header {
        DAT_OBJECT_HEADER_ID => {
            let mut obj_data = vec![0u8; length as usize];
            cursor.read(&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(&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) {
        if bytes[0] == 0xF8 && bytes[1] == 0xBC {
            warn!("ep? {:?}", bytes[2]);
            return Some(Episode::from_quest(bytes[2]).ok()?)
        }
    }
    None
}

fn parse_dat(dat: &[u8], episode: &Episode) -> 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) {
            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("")]
enum QuestLoadError {
    ParseDatError(#[from] ParseDatError),
}

#[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>>,
}

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().unwrap());
        let language = u16::from_le_bytes(bin[18..20].try_into().unwrap());
        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 (enemies, objects) = parse_dat(&dat, &episode)?;

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

        Ok(Quest {
            name: name,
            description: description,
            full_description: full_description,
            id: id,
            language: language,
            bin_blob: prs_bin.into_inner().unwrap(),
            dat_blob: prs_dat.into_inner().unwrap(),
            enemies: enemies,
            objects: objects,
        })
    }
}

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

pub fn load_quests(quest_path: PathBuf) -> QuestList {
    let mut f = File::open(quest_path).unwrap();
    let mut s = String::new();
    f.read_to_string(&mut s);

    let mut used_quest_ids = BTreeSet::new();
    let ql: BTreeMap<String, QuestListCategory> = toml::from_str(s.as_str()).unwrap();

    ql.into_iter().map(|(category, category_details)| {
        let quests = category_details.quests
            .into_iter()
            .filter_map(|quest| {
                warn!("{:?}", quest.bin);
                let dat_file = File::open(format!("data/quests/{}", quest.dat)).unwrap();
                let bin_file = File::open(format!("data/quests/{}", quest.bin)).unwrap();
                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).unwrap();
                bin_prs.read_to_end(&mut bin).unwrap();
                
                let quest = Quest::from_bin_dat(bin, dat).unwrap();
                if used_quest_ids.contains(&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()
    
}