//! Escu:de Archive File (.bin)
use super::crypto::*;
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::{decode_to_string, encode_string};
use anyhow::Result;
use std::collections::HashMap;
use std::ffi::CString;
use std::io::{Read, Seek, SeekFrom, Write};
use std::sync::{Arc, Mutex};

#[derive(Debug)]
/// Escu:de Archive Builder
pub struct EscudeBinArchiveBuilder {}

impl EscudeBinArchiveBuilder {
    /// Creates a new instance of `EscudeBinArchiveBuilder`
    pub const fn new() -> Self {
        EscudeBinArchiveBuilder {}
    }
}

impl ScriptBuilder for EscudeBinArchiveBuilder {
    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(EscudeBinArchive::new(
            MemReader::new(data),
            archive_encoding,
            config,
        )?))
    }

    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(EscudeBinArchive::new(
                MemReader::new(data),
                archive_encoding,
                config,
            )?))
        } else {
            let f = std::fs::File::open(filename)?;
            let reader = std::io::BufReader::new(f);
            Ok(Box::new(EscudeBinArchive::new(
                reader,
                archive_encoding,
                config,
            )?))
        }
    }

    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(EscudeBinArchive::new(
            reader,
            archive_encoding,
            config,
        )?))
    }

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

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

    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
        if buf_len > 8 && buf.starts_with(b"ESC-ARC2") {
            return Some(255);
        }
        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 writer = std::io::BufWriter::new(f);
        let archive = EscudeBinArchiveWriter::new(writer, files, encoding, config)?;
        Ok(Box::new(archive))
    }
}

#[derive(Debug)]
struct BinEntry {
    name_offset: u32,
    data_offset: u32,
    length: u32,
}

struct Entry {
    name: String,
    data: MemReader,
}

impl Read for Entry {
    fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
        self.data.read(buf)
    }
}

impl ArchiveContent for Entry {
    fn name(&self) -> &str {
        &self.name
    }

    fn script_type(&self) -> Option<&ScriptType> {
        if self.data.data.starts_with(b"ESCR1_00") {
            Some(&ScriptType::Escude)
        } else if self.data.data.starts_with(b"LIST") {
            Some(&ScriptType::EscudeList)
        } else {
            None
        }
    }

    fn data(&mut self) -> Result<Vec<u8>> {
        Ok(self.data.data.clone())
    }

    fn to_data<'a>(&'a mut self) -> Result<Box<dyn ReadSeek + Send + Sync + 'a>> {
        Ok(Box::new(&mut self.data))
    }
}

#[derive(Debug)]
/// Escu:de Binary Archive
pub struct EscudeBinArchive<'b, T: Read + Seek + std::fmt::Debug + 'b> {
    reader: Arc<Mutex<T>>,
    file_count: u32,
    entries: Vec<BinEntry>,
    archive_encoding: Encoding,
    _mark: std::marker::PhantomData<&'b ()>,
}

impl<'b, T: Read + Seek + std::fmt::Debug + 'b> EscudeBinArchive<'b, T> {
    /// Creates a new `EscudeBinArchive` from a reader
    ///
    /// * `reader` - The reader to read the archive from
    /// * `archive_encoding` - The encoding used for the archive filenames
    /// * `config` - Extra configuration options
    pub fn new(mut reader: T, archive_encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
        let mut header = [0u8; 8];
        reader.read_exact(&mut header)?;
        if &header != b"ESC-ARC2" {
            return Err(anyhow::anyhow!("Invalid Escude binary script header"));
        }
        reader.seek(SeekFrom::Start(0xC))?;
        let mut crypto_reader = CryptoReader::new(&mut reader)?;
        let file_count = crypto_reader.read_u32()?;
        let _name_tbl_len = crypto_reader.read_u32()?;
        let mut entries = Vec::with_capacity(file_count as usize);
        for _ in 0..file_count {
            let name_offset = crypto_reader.read_u32()?;
            let data_offset = crypto_reader.read_u32()?;
            let length = crypto_reader.read_u32()?;
            entries.push(BinEntry {
                name_offset,
                data_offset,
                length,
            });
        }
        Ok(EscudeBinArchive {
            reader: Arc::new(Mutex::new(reader)),
            file_count,
            entries,
            archive_encoding,
            _mark: std::marker::PhantomData,
        })
    }
}

impl<'b, T: Read + Seek + std::fmt::Debug + 'b> Script for EscudeBinArchive<'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(EscudeBinArchiveIter {
            entries: self.entries.iter(),
            reader: self.reader.clone(),
            file_count: self.file_count,
            archive_encoding: self.archive_encoding,
        }))
    }

    fn iter_archive_offset<'a>(&'a self) -> Result<Box<dyn Iterator<Item = Result<u64>> + 'a>> {
        Ok(Box::new(
            self.entries.iter().map(|e| Ok(e.data_offset as u64)),
        ))
    }

    fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + Send + Sync + 'a>> {
        if index >= self.entries.len() {
            return Err(anyhow::anyhow!(
                "Index out of bounds: {} (max: {})",
                index,
                self.entries.len()
            ));
        }
        let entry = &self.entries[index];
        let name = self
            .reader
            .cpeek_cstring_at(entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14)?;
        let name = decode_to_string(self.archive_encoding, name.as_bytes(), true)?;
        let mut data = self
            .reader
            .cpeek_at_vec(entry.data_offset as u64, entry.length as usize)?;
        if data.starts_with(b"acp") {
            let mut decoder = match super::lzw::LZWDecoder::new(&data) {
                Ok(decoder) => decoder,
                Err(e) => return Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e)),
            };
            data = decoder.unpack()?;
        }
        Ok(Box::new(Entry {
            name,
            data: MemReader::new(data),
        }))
    }
}

struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
    entries: T,
    reader: Arc<Mutex<R>>,
    file_count: u32,
    archive_encoding: Encoding,
}

impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
    for EscudeBinArchiveIter<'a, T, R>
{
    type Item = Result<String>;

    fn next(&mut self) -> Option<Self::Item> {
        let entry = match self.entries.next() {
            Some(entry) => entry,
            None => return None,
        };
        let name_offset = entry.name_offset as u64 + self.file_count as u64 * 12 + 0x14;
        let name = match self.reader.cpeek_cstring_at(name_offset) {
            Ok(name) => name,
            Err(e) => return Some(Err(e.into())),
        };
        let name = match decode_to_string(self.archive_encoding, name.as_bytes(), true) {
            Ok(name) => name,
            Err(e) => return Some(Err(e.into())),
        };
        Some(Ok(name))
    }
}

/// Escu:de Binary Archive Writer
pub struct EscudeBinArchiveWriter<T: Write + Seek> {
    writer: T,
    headers: HashMap<String, BinEntry>,
    name_tbl_len: u32,
    fake: bool,
}

impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
    /// Creates a new `EscudeBinArchiveWriter`
    ///
    /// * `writer` - The writer to write the archive to
    /// * `files` - The list of files to include in the archive
    /// * `encoding` - The encoding used for the archive filenames
    /// * `config` - Extra configuration options
    pub fn new(
        mut writer: T,
        files: &[&str],
        encoding: Encoding,
        config: &ExtraConfig,
    ) -> Result<Self> {
        writer.write_all(b"ESC-ARC2")?;
        let header_len = 0xC + 0xC * files.len();
        let header = vec![0u8; header_len];
        writer.write_all(&header)?;
        let mut headers = HashMap::new();
        for file in files {
            let f = file.to_string();
            let encoded = encode_string(encoding, file, false)?;
            let encoded = CString::new(encoded)?;
            let name_offset = writer.stream_position()? as u32;
            writer.write_all(encoded.as_bytes_with_nul())?;
            headers.insert(
                f,
                BinEntry {
                    name_offset,
                    data_offset: 0,
                    length: 0,
                },
            );
        }
        let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
        Ok(EscudeBinArchiveWriter {
            writer,
            headers,
            name_tbl_len,
            fake: config.escude_fake_compress,
        })
    }
}

impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
    fn new_file<'a>(
        &'a mut self,
        name: &str,
        _size: Option<u64>,
    ) -> Result<Box<dyn WriteSeek + 'a>> {
        let entry = self
            .headers
            .get_mut(name)
            .ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
        if entry.data_offset != 0 {
            return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
        }
        entry.data_offset = self.writer.stream_position()? as u32;
        Ok(Box::new(EscudeBinArchiveFileWithLzw::new(
            entry,
            &mut self.writer,
            self.fake,
        )?))
    }

    fn write_header(&mut self) -> Result<()> {
        self.writer.seek(SeekFrom::Start(0x8))?;
        let mut crypto = CryptoWriter::new(&mut self.writer)?;
        let file_count = self.headers.len() as u32;
        crypto.write_u32(file_count)?;
        crypto.write_u32(self.name_tbl_len)?;
        let mut entries: Vec<_> = self.headers.values().collect();
        entries.sort_by(|a, b| a.name_offset.cmp(&b.name_offset));
        for entry in entries {
            let name_offset = entry.name_offset - file_count * 12 - 0x14;
            crypto.write_u32(name_offset)?;
            crypto.write_u32(entry.data_offset)?;
            crypto.write_u32(entry.length)?;
        }
        Ok(())
    }
}

/// Escu:de Binary Archive File with LZW Compression
pub struct EscudeBinArchiveFileWithLzw<'a, T: Write + Seek> {
    writer: EscudeBinArchiveFile<'a, T>,
    buf: MemWriter,
    fake: bool,
}

impl<'a, T: Write + Seek> EscudeBinArchiveFileWithLzw<'a, T> {
    fn new(header: &'a mut BinEntry, writer: &'a mut T, fake: bool) -> Result<Self> {
        let writer = EscudeBinArchiveFile {
            header,
            writer,
            pos: 0,
        };
        Ok(EscudeBinArchiveFileWithLzw {
            writer,
            buf: MemWriter::new(),
            fake,
        })
    }
}

impl<'a, T: Write + Seek> Write for EscudeBinArchiveFileWithLzw<'a, T> {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.buf.write(buf)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.buf.flush()
    }
}

impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFileWithLzw<'a, T> {
    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
        self.buf.seek(pos)
    }

    fn stream_position(&mut self) -> std::io::Result<u64> {
        self.buf.stream_position()
    }

    fn rewind(&mut self) -> std::io::Result<()> {
        self.buf.rewind()
    }
}

impl<'a, T: Write + Seek> Drop for EscudeBinArchiveFileWithLzw<'a, T> {
    fn drop(&mut self) {
        let buf = self.buf.as_slice();
        let encoder = super::lzw::LZWEncoder::new();
        let data = match encoder.encode(buf, self.fake) {
            Ok(data) => data,
            Err(e) => {
                eprintln!("Failed to encode LZW data: {}", e);
                crate::COUNTER.inc_error();
                return;
            }
        };
        match self.writer.write_all(&data) {
            Ok(_) => {}
            Err(e) => {
                eprintln!("Failed to write LZW data: {}", e);
                crate::COUNTER.inc_error();
            }
        }
    }
}

/// Escu:de Binary Archive File
pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
    header: &'a mut BinEntry,
    writer: &'a mut T,
    pos: usize,
}

impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.writer.seek(SeekFrom::Start(
            self.header.data_offset as u64 + self.pos as u64,
        ))?;
        let written = self.writer.write(buf)?;
        self.pos += written;
        self.header.length = self.header.length.max(self.pos as u32);
        Ok(written)
    }

    fn flush(&mut self) -> std::io::Result<()> {
        self.writer.flush()
    }
}

impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
    fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
        let new_pos = match pos {
            SeekFrom::Start(offset) => offset as usize,
            SeekFrom::End(offset) => {
                if offset < 0 {
                    if (-offset) as usize > self.header.length as usize {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidInput,
                            "Seek from end exceeds file length",
                        ));
                    }
                    self.header.length as usize - (-offset) as usize
                } else {
                    self.header.length as usize + offset as usize
                }
            }
            SeekFrom::Current(offset) => {
                if offset < 0 {
                    if (-offset) as usize > self.pos {
                        return Err(std::io::Error::new(
                            std::io::ErrorKind::InvalidInput,
                            "Seek from current exceeds current position",
                        ));
                    }
                    self.pos.saturating_sub((-offset) as usize)
                } else {
                    self.pos + offset as usize
                }
            }
        };
        self.pos = new_pos;
        Ok(self.pos as u64)
    }
}
