//! Qlie Pack Archive (.pack)
mod delphi;
mod encryption;
mod twister;
mod types;
mod v31;

use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::struct_pack::*;
use anyhow::Result;
use encryption::Encryption;
use std::io::{Read, Seek, SeekFrom};
use std::sync::{Arc, Mutex};
use types::*;

#[derive(Debug)]
pub struct QliePackArchiveBuilder {}

impl QliePackArchiveBuilder {
    pub fn new() -> Self {
        Self {}
    }
}

impl ScriptBuilder for QliePackArchiveBuilder {
    fn default_encoding(&self) -> Encoding {
        Encoding::Cp932
    }

    fn default_archive_encoding(&self) -> Option<Encoding> {
        Some(Encoding::Cp932)
    }

    fn build_script(
        &self,
        data: Vec<u8>,
        filename: &str,
        _encoding: Encoding,
        archive_encoding: Encoding,
        config: &ExtraConfig,
        _archive: Option<&Box<dyn Script>>,
    ) -> Result<Box<dyn Script + Send + Sync>> {
        Ok(Box::new(QliePackArchive::new(
            MemReader::new(data),
            archive_encoding,
            config,
            filename,
        )?))
    }

    fn build_script_from_file(
        &self,
        filename: &str,
        _encoding: Encoding,
        archive_encoding: Encoding,
        config: &ExtraConfig,
        _archive: Option<&Box<dyn Script>>,
    ) -> Result<Box<dyn Script + Send + Sync>> {
        if filename == "-" {
            let data = crate::utils::files::read_file(filename)?;
            Ok(Box::new(QliePackArchive::new(
                MemReader::new(data),
                archive_encoding,
                config,
                filename,
            )?))
        } else {
            let f = std::fs::File::open(filename)?;
            let reader = std::io::BufReader::new(f);
            Ok(Box::new(QliePackArchive::new(
                reader,
                archive_encoding,
                config,
                filename,
            )?))
        }
    }

    fn build_script_from_reader<'a>(
        &self,
        reader: Box<dyn ReadSeek + Send + Sync + 'a>,
        filename: &str,
        _encoding: Encoding,
        archive_encoding: Encoding,
        config: &ExtraConfig,
        _archive: Option<&Box<dyn Script>>,
    ) -> Result<Box<dyn Script + Send + Sync + 'a>> {
        Ok(Box::new(QliePackArchive::new(
            reader,
            archive_encoding,
            config,
            filename,
        )?))
    }

    fn extensions(&self) -> &'static [&'static str] {
        &["pack"]
    }

    fn script_type(&self) -> &'static ScriptType {
        &ScriptType::QliePack
    }

    fn is_this_format(&self, filename: &str, _buf: &[u8], _buf_len: usize) -> Option<u8> {
        // Workround: Check only if filename exists in filesystem.
        // This means that we cannot detect .pack files in another archive.
        // Pack file header is at the end of file, so we cannot check signature here with header buffer.
        match is_this_format(filename) {
            Ok(true) => Some(30),
            _ => None,
        }
    }

    fn is_archive(&self) -> bool {
        true
    }

    fn create_archive(
        &self,
        filename: &str,
        files: &[&str],
        _encoding: Encoding,
        config: &ExtraConfig,
    ) -> Result<Box<dyn Archive>> {
        let f = std::fs::File::create(filename)?;
        let buf = std::io::BufWriter::new(f);
        Ok(Box::new(v31::QliePackArchiveWriterV31::new(
            buf, files, config,
        )?))
    }
}

/// Check if the given file is Qlie Pack Archive format
pub fn is_this_format<P: AsRef<std::path::Path> + ?Sized>(path: &P) -> Result<bool> {
    let path = path.as_ref();
    if !path.exists() || !path.is_file() {
        return Ok(false);
    }
    let mut file = std::fs::File::open(path)?;
    file.seek(SeekFrom::End(-0x1C))?;
    let header = QlieHeader::unpack(&mut file, false, Encoding::Utf8, &None)?;
    Ok(header.is_valid())
}

#[derive(Debug)]
pub struct QliePackArchive<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> {
    header: QlieHeader,
    encryption: Box<dyn Encryption + Send + Sync>,
    reader: Arc<Mutex<T>>,
    qkey: Option<QlieKey>,
    entries: Vec<QlieEntry>,
    common_key: Option<Vec<u8>>,
    _mark: std::marker::PhantomData<&'b ()>,
}

impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> QliePackArchive<'b, T> {
    pub fn new(
        mut reader: T,
        archive_encoding: Encoding,
        _config: &ExtraConfig,
        filename: &str,
    ) -> Result<Self> {
        reader.seek(SeekFrom::End(-0x1C))?;
        let header = QlieHeader::unpack(&mut reader, false, archive_encoding, &None)?;
        if !header.is_valid() {
            return Err(anyhow::anyhow!("Invalid Qlie Pack Archive header"));
        }
        let file_size = reader.stream_position()?;
        if header.index_offset > file_size - 0x1C {
            return Err(anyhow::anyhow!(
                "Invalid index offset in Qlie Pack Archive header"
            ));
        }
        let major = header.major_version();
        let minor = header.minor_version();
        let mut game_key = None;
        if major == 3 && minor == 0 {
            game_key = encryption::find_key_data(filename)?;
        }
        let mut encryption = encryption::create_encryption(major, minor, game_key)?;
        // Read key
        let mut key = 0;
        let mut qkey = None;
        if major >= 2 {
            reader.seek(SeekFrom::End(-0x440))?;
            let mut qk = QlieKey::unpack(&mut reader, false, archive_encoding, &None)?;
            if qk.hash_size as u64 > file_size || qk.hash_size < 0x44 {
                return Err(anyhow::anyhow!("Invalid Qlie Pack Archive key"));
            }
            if major >= 3 {
                key = encryption.compute_hash(&qk.key[..0x100])? & 0xFFFFFFF;
            }
            encryption::decrypt(&mut qk.signature, key)?;
            if &qk.signature != QLIE_KEY_SIGNATURE {
                eprintln!(
                    "WARNING: Invalid Qlie Pack Archive key signature, decryption key may be incorrect"
                );
                crate::COUNTER.inc_warning();
            }
            qkey = Some(qk);
        }
        // Read entries
        let mut entries = if major >= 2 {
            Self::read_entries(&mut reader, key, &header, archive_encoding, &encryption)?
        } else {
            let possible_encs: [Box<dyn Encryption + Send + Sync>; 3] = [
                Box::new(encryption::Encryption10::new()),
                Box::new(encryption::Encryption20::new_no_hash()),
                Box::new(encryption::Encryption20::new()),
            ];
            let mut t = None;
            for enc in possible_encs {
                match Self::read_entries(&mut reader, key, &header, archive_encoding, &enc) {
                    Ok(entries) => {
                        encryption = enc;
                        t = Some(entries);
                        break;
                    }
                    Err(_) => continue,
                }
            }
            t.ok_or_else(|| {
                anyhow::anyhow!(
                    "Failed to read Qlie Pack Archive entries with any encryption method"
                )
            })?
        };
        let mut common_key = None;
        if major >= 3 {
            common_key = encryption::find_game_key(filename)?;
            if let Some(common_key_entry) = entries.iter_mut().find(|e| e.name == QLIE_KEY_FILE) {
                reader.seek(SeekFrom::Start(common_key_entry.offset))?;
                common_key_entry.common_key = common_key.clone();
                let stream = StreamRegion::with_size(&mut reader, common_key_entry.size as u64)?;
                let mut decrypted = encryption.decrypt_entry(Box::new(stream), common_key_entry)?;
                if common_key_entry.is_packed != 0 {
                    decrypted = encryption::decompress(decrypted)?;
                }
                let mut key_data = Vec::new();
                decrypted.read_to_end(&mut key_data)?;
                if minor == 1 {
                    common_key = Some(encryption::get_common_key(&key_data)?);
                } else {
                    common_key = Some(key_data);
                }
            }
        }
        Ok(Self {
            header,
            encryption,
            reader: Arc::new(Mutex::new(reader)),
            qkey,
            entries,
            common_key,
            _mark: std::marker::PhantomData,
        })
    }

    fn read_entries(
        reader: &mut T,
        key: u32,
        header: &QlieHeader,
        archive_encoding: Encoding,
        encryption: &Box<dyn Encryption + Send + Sync>,
    ) -> Result<Vec<QlieEntry>> {
        let mut entries = Vec::new();
        reader.seek(SeekFrom::Start(header.index_offset))?;
        let has_hash = encryption.index_has_hash();
        for _ in 0..header.file_count {
            let name_length = reader.read_u16()?;
            let raw_name_length = if encryption.is_unicode() {
                name_length as usize * 2
            } else {
                name_length as usize
            };
            let mut raw_name = reader.read_exact_vec(raw_name_length)?;
            let name = encryption.decrypt_name(&mut raw_name, key as i32, archive_encoding)?;
            let offset = reader.read_u64()?;
            let size = reader.read_u32()?;
            let unpacked_size = reader.read_u32()?;
            let is_packed = reader.read_u32()?;
            let is_encrypted = reader.read_u32()?;
            let hash = if has_hash { reader.read_u32()? } else { 0 };
            let entry = QlieEntry {
                raw_name,
                name,
                offset,
                size,
                unpacked_size,
                is_packed,
                is_encrypted,
                hash,
                key,
                common_key: None,
            };
            entries.push(entry);
        }
        Ok(entries)
    }
}

impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> Script for QliePackArchive<'b, T> {
    fn default_output_script_type(&self) -> OutputScriptType {
        OutputScriptType::Json
    }

    fn default_format_type(&self) -> FormatOptions {
        FormatOptions::None
    }

    fn is_archive(&self) -> bool {
        true
    }

    fn iter_archive_filename<'a>(
        &'a self,
    ) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
        Ok(Box::new(self.entries.iter().map(|e| Ok(e.name.clone()))))
    }

    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
        let mut entry = self
            .entries
            .get(index)
            .ok_or_else(|| anyhow::anyhow!("Invalid file index {} for Qlie Pack Archive", index))?
            .clone();
        if self.common_key.is_some() && entry.common_key.is_none() {
            entry.common_key = self.common_key.clone();
        }
        let stream = StreamRegion::with_size(
            MutexWrapper::new(self.reader.clone(), entry.offset),
            entry.size as u64,
        )?;
        let stream_clone = StreamRegion::with_size(
            MutexWrapper::new(self.reader.clone(), entry.offset),
            entry.size as u64,
        )?;
        let mut stream = self.encryption.decrypt_entry(Box::new(stream), &entry)?;
        let mut stream_clone = self
            .encryption
            .decrypt_entry(Box::new(stream_clone), &entry)?;
        if entry.is_packed != 0 {
            stream = encryption::decompress(stream)?;
            stream_clone = encryption::decompress(stream_clone)?;
        }
        let mut entry = QliePackArchiveContent::new(stream, entry);
        let mut header_buffer = [0u8; 1024];
        let readed = stream_clone.read_most(&mut header_buffer)?;
        entry.typ = detect_script_type(&entry.entry.name, &header_buffer, readed);
        Ok(Box::new(entry))
    }
}

fn detect_script_type(_name: &str, buf: &[u8], buf_len: usize) -> Option<ScriptType> {
    if super::super::script::is_this_format(buf, buf_len) {
        return Some(ScriptType::Qlie);
    }
    #[cfg(feature = "qlie-img")]
    {
        if buf_len >= 4 && buf.starts_with(b"DPNG") {
            return Some(ScriptType::QlieDpng);
        }
        if buf_len >= 6 && buf.starts_with(b"abmp1") {
            let ver = buf[5];
            if ver >= b'0' && ver <= b'2' {
                return Some(ScriptType::QlieAbmp10);
            }
        }
    }
    None
}

#[derive(Debug)]
struct QliePackArchiveContent<T: Read + std::fmt::Debug> {
    reader: T,
    entry: QlieEntry,
    typ: Option<ScriptType>,
}

impl<T: Read + std::fmt::Debug> QliePackArchiveContent<T> {
    pub fn new(reader: T, entry: QlieEntry) -> Self {
        Self {
            reader,
            entry,
            typ: None,
        }
    }
}

impl<T: Read + std::fmt::Debug> Read for QliePackArchiveContent<T> {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.reader.read(buf)
    }
}

impl<T: Read + std::fmt::Debug> ArchiveContent for QliePackArchiveContent<T> {
    fn name(&self) -> &str {
        &self.entry.name
    }

    fn script_type(&self) -> Option<&ScriptType> {
        self.typ.as_ref()
    }
}
