use crate::{Eu4Error, Eu4Save, Eu4SaveMeta, FailedResolveStrategy, GameState, Meta, TokenLookup};
use jomini::{BinaryDeserializerBuilder, TextDeserializer, TextTape};
use serde::de::DeserializeOwned;
use std::fmt;
use std::io::{Read, Seek, SeekFrom};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Encoding {
    Text,
    TextZip,
    BinZip,
}
impl Encoding {
    pub fn as_str(&self) -> &'static str {
        match self {
            Encoding::Text => "text",
            Encoding::TextZip => "textzip",
            Encoding::BinZip => "binzip",
        }
    }
}
impl fmt::Display for Encoding {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.as_str())
    }
}
#[derive(Debug, Clone, Copy)]
pub enum Extraction {
    InMemory,
    #[cfg(feature = "mmap")]
    MmapTemporaries,
}
#[derive(Debug, Clone)]
pub struct Eu4ExtractorBuilder {
    extraction: Extraction,
    on_failed_resolve: FailedResolveStrategy,
}
impl Default for Eu4ExtractorBuilder {
    fn default() -> Self {
        Eu4ExtractorBuilder::new()
    }
}
impl Eu4ExtractorBuilder {
    pub fn new() -> Self {
        Eu4ExtractorBuilder {
            extraction: Extraction::InMemory,
            on_failed_resolve: FailedResolveStrategy::Ignore,
        }
    }
    pub fn with_extraction(mut self, extraction: Extraction) -> Self {
        self.extraction = extraction;
        self
    }
    pub fn with_on_failed_resolve(mut self, strategy: FailedResolveStrategy) -> Self {
        self.on_failed_resolve = strategy;
        self
    }
    pub fn build(self) -> Eu4Extractor {
        Eu4Extractor {
            extraction: self.extraction,
            on_failed_resolve: self.on_failed_resolve,
        }
    }
}
#[derive(Debug, Clone)]
pub struct Eu4Extractor {
    extraction: Extraction,
    on_failed_resolve: FailedResolveStrategy,
}
impl Default for Eu4Extractor {
    fn default() -> Self {
        Eu4ExtractorBuilder::new().build()
    }
}
impl Eu4Extractor {
    pub fn extract_meta<R>(&self, mut reader: R) -> Result<(Meta, Encoding), Eu4Error>
    where
        R: Read + Seek,
    {
        let mut header = [0; "EU4txt".len()];
        reader.read_exact(&mut header).map_err(Eu4Error::IoErr)?;
        let mut buffer = Vec::with_capacity(0);
        if is_text(&header).is_some() {
            reader.read_to_end(&mut buffer).map_err(Eu4Error::IoErr)?;
            let meta = TextDeserializer::from_slice(&buffer)?;
            Ok((meta, Encoding::Text))
        } else if is_zip(&header) {
            reader.seek(SeekFrom::Start(0)).map_err(Eu4Error::IoErr)?;
            let mut zip = zip::ZipArchive::new(reader).map_err(Eu4Error::ZipCentralDirectory)?;
            match self.extraction {
                Extraction::InMemory => {
                    melt_in_memory(&mut buffer, "meta", &mut zip, self.on_failed_resolve)
                }
                #[cfg(feature = "mmap")]
                Extraction::MmapTemporaries => {
                    melt_with_temporary("meta", &mut zip, self.on_failed_resolve)
                }
            }
        } else {
            Err(Eu4Error::UnknownHeader)
        }
    }
    pub fn extract_save<R>(&self, mut reader: R) -> Result<(Eu4Save, Encoding), Eu4Error>
    where
        R: Read + Seek,
    {
        let mut header = [0; "EU4txt".len()];
        reader.read_exact(&mut header).map_err(Eu4Error::IoErr)?;
        let mut buffer = Vec::with_capacity(0);
        if is_text(&header).is_some() {
            reader.read_to_end(&mut buffer).map_err(Eu4Error::IoErr)?;
            let tape = TextTape::from_slice(&buffer)?;
            let meta: Meta = TextDeserializer::from_tape(&tape)?;
            let game: GameState = TextDeserializer::from_tape(&tape)?;
            Ok((Eu4Save { meta, game }, Encoding::Text))
        } else if is_zip(&header) {
            reader.seek(SeekFrom::Start(0)).map_err(Eu4Error::IoErr)?;
            let mut zip = zip::ZipArchive::new(reader).map_err(Eu4Error::ZipCentralDirectory)?;
            let (meta, encoding) = match self.extraction {
                Extraction::InMemory => {
                    melt_in_memory(&mut buffer, "meta", &mut zip, self.on_failed_resolve)
                }
                #[cfg(feature = "mmap")]
                Extraction::MmapTemporaries => {
                    melt_with_temporary("meta", &mut zip, self.on_failed_resolve)
                }
            }?;
            let (game, _) = match self.extraction {
                Extraction::InMemory => {
                    melt_in_memory(&mut buffer, "gamestate", &mut zip, self.on_failed_resolve)
                }
                #[cfg(feature = "mmap")]
                Extraction::MmapTemporaries => {
                    melt_with_temporary("gamestate", &mut zip, self.on_failed_resolve)
                }
            }?;
            Ok((Eu4Save { meta, game }, encoding))
        } else {
            Err(Eu4Error::UnknownHeader)
        }
    }
    
    
    pub fn extract_meta_optimistic<R>(
        &self,
        mut reader: R,
    ) -> Result<(Eu4SaveMeta, Encoding), Eu4Error>
    where
        R: Read + Seek,
    {
        let mut header = [0; "EU4txt".len()];
        reader.read_exact(&mut header).map_err(Eu4Error::IoErr)?;
        let mut buffer = Vec::with_capacity(0);
        
        
        if is_text(&header).is_some() {
            reader.read_to_end(&mut buffer).map_err(Eu4Error::IoErr)?;
            let tape = TextTape::from_slice(&buffer)?;
            let meta: Meta = TextDeserializer::from_tape(&tape)?;
            let game: Option<GameState> = TextDeserializer::from_tape(&tape).map(Some)?;
            Ok((Eu4SaveMeta { meta, game }, Encoding::Text))
        } else if is_zip(&header) {
            reader.seek(SeekFrom::Start(0)).map_err(Eu4Error::IoErr)?;
            let mut zip = zip::ZipArchive::new(reader).map_err(Eu4Error::ZipCentralDirectory)?;
            let (meta, encoding) = match self.extraction {
                Extraction::InMemory => {
                    melt_in_memory(&mut buffer, "meta", &mut zip, self.on_failed_resolve)
                }
                #[cfg(feature = "mmap")]
                Extraction::MmapTemporaries => {
                    melt_with_temporary("meta", &mut zip, self.on_failed_resolve)
                }
            }?;
            Ok((Eu4SaveMeta { meta, game: None }, encoding))
        } else {
            Err(Eu4Error::UnknownHeader)
        }
    }
}
fn melt_in_memory<T, R>(
    mut buffer: &mut Vec<u8>,
    name: &'static str,
    zip: &mut zip::ZipArchive<R>,
    on_failed_resolve: FailedResolveStrategy,
) -> Result<(T, Encoding), Eu4Error>
where
    R: Read + Seek,
    T: DeserializeOwned,
{
    buffer.clear();
    let mut zip_file = zip
        .by_name(name)
        .map_err(|e| Eu4Error::ZipMissingEntry(name, e))?;
    
    if zip_file.size() > 1024 * 1024 * 200 {
        return Err(Eu4Error::ZipSize(name));
    }
    buffer.reserve(zip_file.size() as usize);
    zip_file
        .read_to_end(&mut buffer)
        .map_err(|e| Eu4Error::ZipExtraction(name, e))?;
    if let Some(data) = is_bin(&buffer) {
        let res = BinaryDeserializerBuilder::new()
            .on_failed_resolve(on_failed_resolve)
            .from_slice(data, TokenLookup)
            .map_err(|e| Eu4Error::Deserialize {
                part: Some(name.to_string()),
                err: e,
            })?;
        Ok((res, Encoding::BinZip))
    } else if let Some(data) = is_text(&buffer) {
        let res = TextDeserializer::from_slice(data)?;
        Ok((res, Encoding::TextZip))
    } else {
        Err(Eu4Error::UnknownHeader)
    }
}
#[cfg(feature = "mmap")]
fn melt_with_temporary<T, R>(
    name: &'static str,
    zip: &mut zip::ZipArchive<R>,
    on_failed_resolve: FailedResolveStrategy,
) -> Result<(T, Encoding), Eu4Error>
where
    R: Read + Seek,
    T: DeserializeOwned,
{
    use std::io::{BufWriter, Write};
    let mut zip_file = zip
        .by_name(name)
        .map_err(|e| Eu4Error::ZipMissingEntry(name, e))?;
    
    if zip_file.size() > 1024 * 1024 * 200 {
        return Err(Eu4Error::ZipSize(name));
    }
    let file = tempfile::tempfile().map_err(Eu4Error::IoErr)?;
    let mut writer = BufWriter::new(file);
    std::io::copy(&mut zip_file, &mut writer).map_err(|e| Eu4Error::ZipExtraction(name, e))?;
    writer.flush().map_err(Eu4Error::IoErr)?;
    let file = writer.into_inner().unwrap();
    let mmap = unsafe {
        memmap::MmapOptions::new()
            .map(&file)
            .map_err(Eu4Error::IoErr)?
    };
    let buffer = &mmap[..];
    if let Some(data) = is_bin(&buffer) {
        let res = BinaryDeserializerBuilder::new()
            .on_failed_resolve(on_failed_resolve)
            .from_slice(data, TokenLookup)
            .map_err(|e| Eu4Error::Deserialize {
                part: Some(name.to_string()),
                err: e,
            })?;
        Ok((res, Encoding::BinZip))
    } else if let Some(data) = is_text(&buffer) {
        let res = TextDeserializer::from_slice(data)?;
        Ok((res, Encoding::TextZip))
    } else {
        Err(Eu4Error::UnknownHeader)
    }
}
fn is_text(data: &[u8]) -> Option<&[u8]> {
    let sentry = b"EU4txt";
    if data.get(..sentry.len()).map_or(false, |x| x == sentry) {
        Some(&data[sentry.len()..])
    } else {
        None
    }
}
fn is_bin(data: &[u8]) -> Option<&[u8]> {
    let sentry = b"EU4bin";
    if data.get(..sentry.len()).map_or(false, |x| x == sentry) {
        Some(&data[sentry.len()..])
    } else {
        None
    }
}
fn is_zip(data: &[u8]) -> bool {
    let sentry = [0x50, 0x4b, 0x03, 0x04];
    data.get(..sentry.len()).map_or(false, |x| x == sentry)
}