//! Softpal script (.src)
mod disasm;

use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::*;
use anyhow::Result;
use disasm::*;
use std::collections::HashMap;
use std::io::{Read, Write};

#[derive(Debug)]
/// Softpal script builder
pub struct SoftpalScriptBuilder {}

impl SoftpalScriptBuilder {
    /// Create a new Softpal script builder
    pub fn new() -> Self {
        Self {}
    }
}

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

    fn build_script(
        &self,
        buf: 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(SoftpalScript::new(
            buf, filename, encoding, config, archive,
        )?))
    }

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

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

    fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
        if buf_len >= 4 && buf.starts_with(b"Sv20") {
            return Some(10);
        }
        None
    }
}

#[derive(Debug)]
/// Softpal SRC Script
pub struct SoftpalScript {
    data: MemReader,
    strs: Vec<PalString>,
    texts: MemReader,
    encoding: Encoding,
    label_offsets: Vec<u32>,
    add_message_index: bool,
}

impl SoftpalScript {
    /// Create a new Softpal script
    pub fn new(
        buf: Vec<u8>,
        filename: &str,
        encoding: Encoding,
        config: &ExtraConfig,
        archive: Option<&Box<dyn Script>>,
    ) -> Result<Self> {
        let texts = Self::load_texts_data(Self::load_file(filename, archive, "TEXT.DAT")?)?;
        let points_data = MemReader::new(Self::load_file(filename, archive, "POINT.DAT")?);
        let label_offsets = Self::load_point_data(points_data)?;
        let strs = Disasm::new(&buf, &label_offsets)?.disassemble::<MemWriter>(None)?;
        Ok(Self {
            data: MemReader::new(buf),
            strs,
            encoding,
            texts,
            label_offsets,
            add_message_index: config.softpal_add_message_index,
        })
    }

    fn load_file(filename: &str, archive: Option<&Box<dyn Script>>, name: &str) -> Result<Vec<u8>> {
        if let Some(archive) = archive {
            Ok(archive
                .open_file_by_name(name, true)
                .map_err(|e| anyhow::anyhow!("Failed to open file {} in archive: {}", name, e))?
                .data()?)
        } else {
            let mut path = std::path::PathBuf::from(filename);
            path.set_file_name(name);
            path = crate::utils::files::get_ignorecase_path(&path)?;
            std::fs::read(path).map_err(|e| anyhow::anyhow!("Failed to read file {}: {}", name, e))
        }
    }

    fn load_texts_data(data: Vec<u8>) -> Result<MemReader> {
        let mut writer = MemWriter::from_vec(data);
        if writer.data.len() >= 0x14 {
            let ind = writer.cpeek_u32_at(0x10)?;
            writer.pos = 0x10;
            if ind != 0 {
                let mut shift = 4;
                for _ in 0..(writer.data.len() / 4 - 4) {
                    let mut data = writer.cpeek_u32()?;
                    let mut add = data.to_le_bytes();
                    add[0] = add[0].rotate_left(shift);
                    shift = (shift + 1) % 8;
                    data = u32::from_le_bytes(add);
                    data ^= 0x084DF873 ^ 0xFF987DEE;
                    writer.write_u32(data)?;
                }
            }
        }
        Ok(MemReader::new(writer.into_inner()))
    }

    fn load_point_data(mut data: MemReader) -> Result<Vec<u32>> {
        let mut magic = [0u8; 16];
        data.read_exact(&mut magic)?;
        if magic != *b"$POINT_LIST_****" && magic != *b"_POINT_LIST_****" {
            return Err(anyhow::anyhow!("Invalid point list magic: {:?}", magic));
        }
        let is_crypted = magic[0] == b'$' && {
            let first_offset = data.cpeek_u32()?;
            first_offset & 0xFF000000 != 0
        };
        let mut label_offsets = Vec::new();
        let mut shift = 4;
        while !data.is_eof() {
            let mut val = data.read_u32()?;
            if is_crypted {
                let mut add = val.to_le_bytes();
                add[0] = add[0].rotate_left(shift);
                shift = (shift + 1) % 8;
                val = u32::from_le_bytes(add);
                val ^= 0x084DF873 ^ 0xFF987DEE;
            }
            label_offsets.push(val + CODE_OFFSET);
        }
        label_offsets.reverse();
        Ok(label_offsets)
    }
}

impl Script for SoftpalScript {
    fn default_output_script_type(&self) -> OutputScriptType {
        OutputScriptType::Json
    }

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

    fn is_output_supported(&self, _: OutputScriptType) -> bool {
        true
    }

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

    fn extract_messages(&self) -> Result<Vec<Message>> {
        let mut messages = Vec::new();
        let mut name = None;
        let max_len = self.texts.data.len() as u32;
        for str in &self.strs {
            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
            if addr - 4 > max_len {
                continue;
            }
            let idx = self.texts.cpeek_u32_at(addr as u64)?;
            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
            let text =
                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
            let text = if self.add_message_index {
                format!("[{}]{}", idx, text)
            } else {
                text
            };
            match str.typ {
                StringType::Name => {
                    if text.is_empty() {
                        continue; // Skip empty names
                    }
                    name = Some(text);
                }
                StringType::Message => messages.push(Message {
                    name: name.take(),
                    message: text,
                }),
                StringType::Hover => messages.push(Message::new(text, None)),
                StringType::Label => {} // Ignore labels
            }
        }
        Ok(messages)
    }

    fn extract_multiple_messages(&self) -> Result<HashMap<String, Vec<Message>>> {
        let mut hovers = Vec::new();
        let mut messages = Vec::new();
        let mut label = None;
        let mut name = None;
        let mut result = HashMap::new();
        let max_len = self.texts.data.len() as u32;
        for str in &self.strs {
            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
            if addr - 4 > max_len {
                continue;
            }
            let idx = self.texts.cpeek_u32_at(addr as u64)?;
            let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
            let ptext =
                decode_to_string(self.encoding, text.as_bytes(), false)?.replace("<br>", "\n");
            let text = if self.add_message_index {
                format!("[{}]{}", idx, ptext)
            } else {
                ptext.clone()
            };
            match str.typ {
                StringType::Name => {
                    if text.is_empty() {
                        continue; // Skip empty names
                    }
                    name = Some(text);
                }
                StringType::Message => messages.push(Message::new(text, name.take())),
                StringType::Hover => hovers.push(Message::new(text, None)),
                StringType::Label => {
                    if !messages.is_empty() {
                        let key = label.take().unwrap_or_else(|| "default".to_string());
                        if result.contains_key(&key) {
                            eprintln!(
                                "Warning: Duplicate label '{}', overwriting previous messages.",
                                key
                            );
                            crate::COUNTER.inc_warning();
                        }
                        result.insert(key, messages);
                        messages = Vec::new();
                    }
                    label = Some(ptext);
                }
            }
        }
        if !messages.is_empty() {
            let key = label.take().unwrap_or_else(|| "default".to_string());
            result.insert(key, messages);
        }
        if !hovers.is_empty() {
            result.insert("hover".to_string(), hovers);
        }
        Ok(result)
    }

    fn import_messages<'a>(
        &'a self,
        messages: Vec<Message>,
        mut file: Box<dyn WriteSeek + 'a>,
        filename: &str,
        encoding: Encoding,
        replacement: Option<&'a ReplacementTable>,
    ) -> Result<()> {
        let mut texts_filename = std::path::PathBuf::from(filename);
        texts_filename.set_file_name("TEXT.DAT");
        let mut texts = Vec::new();
        let mut reader = self.texts.to_ref();
        reader.pos = 0x10;
        while !reader.is_eof() {
            reader.pos += 4; // Skip index
            texts.push(reader.read_cstring()?)
        }
        let mut texts_file = std::fs::File::create(&texts_filename)
            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
        file.write_all(&self.data.data)?;
        let mut mes = messages.iter();
        let mut mess = mes.next();
        let texts_data_len = self.texts.data.len() as u32;
        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
        for str in &self.strs {
            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
            if addr + 4 > texts_data_len {
                continue;
            }
            if str.typ.is_label() {
                continue; // Ignore labels
            }
            let m = match mess {
                Some(m) => m,
                None => return Err(anyhow::anyhow!("Not enough messages.")),
            };
            let mut text = match str.typ {
                StringType::Name => match &m.name {
                    Some(name) => name.clone(),
                    None => return Err(anyhow::anyhow!("Missing name for message.")),
                },
                StringType::Message => {
                    let m = m.message.clone();
                    mess = mes.next();
                    m
                }
                StringType::Hover => {
                    let m = m.message.clone();
                    mess = mes.next();
                    m
                }
                StringType::Label => continue, // Ignore labels
            };
            if let Some(repl) = replacement {
                for (from, to) in repl.map.iter() {
                    text = text.replace(from, to);
                }
            }
            text = text.replace("\n", "<br>");
            let encoded = encode_string(encoding, &text, false)?;
            let s = std::ffi::CString::new(encoded)?;
            let num = texts.len() as u32;
            num_offset_map.insert(num, str.offset);
            texts.push(s);
        }
        if mess.is_some() || mes.next().is_some() {
            return Err(anyhow::anyhow!("Some messages were not processed."));
        }
        texts_file.write_all(b"$TEXT_LIST__")?;
        texts_file.write_u32(texts.len() as u32)?;
        let mut nf = MemWriter::new();
        for (num, text) in texts.into_iter().enumerate() {
            let num = num as u32;
            let newaddr = nf.pos as u32 + 0x10;
            if let Some(offset) = num_offset_map.get(&num) {
                file.write_u32_at(*offset as u64, newaddr)?;
            }
            nf.write_u32(num)?;
            nf.write_cstring(&text)?;
        }
        nf.pos = 0;
        let mut shift = 4;
        for _ in 0..(nf.data.len() / 4) {
            let mut data = nf.cpeek_u32()?;
            data ^= 0x084DF873 ^ 0xFF987DEE;
            let mut add = data.to_le_bytes();
            add[0] = add[0].rotate_right(shift);
            shift = (shift + 1) % 8;
            data = u32::from_le_bytes(add);
            nf.write_u32(data)?;
        }
        texts_file.write_all(&nf.data)?;
        Ok(())
    }

    fn import_multiple_messages<'a>(
        &'a self,
        messages: HashMap<String, Vec<Message>>,
        mut file: Box<dyn WriteSeek + 'a>,
        filename: &str,
        encoding: Encoding,
        replacement: Option<&'a ReplacementTable>,
    ) -> Result<()> {
        let mut texts_filename = std::path::PathBuf::from(filename);
        texts_filename.set_file_name("TEXT.DAT");
        let mut texts = Vec::new();
        let mut reader = self.texts.to_ref();
        reader.pos = 0x10;
        while !reader.is_eof() {
            reader.pos += 4; // Skip index
            texts.push(reader.read_cstring()?)
        }
        let mut texts_file = std::fs::File::create(&texts_filename)
            .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?;
        file.write_all(&self.data.data)?;
        let hover_messages = messages.get("hover").cloned().unwrap_or_default();
        let mut hover_iter = hover_messages.iter();
        let mut hover_mes = hover_iter.next();
        let mut cur_label: Option<String> = None;
        let mut cur_messages = messages
            .get(cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default"))
            .cloned()
            .unwrap_or_default();
        let mut cur_iter = cur_messages.iter();
        let mut cur_mes = cur_iter.next();
        let texts_data_len = self.texts.data.len() as u32;
        let mut num_offset_map: HashMap<u32, u32> = HashMap::new();
        for str in &self.strs {
            let addr = self.data.cpeek_u32_at(str.offset as u64)?;
            if addr + 4 > texts_data_len {
                continue;
            }
            let mut text = match str.typ {
                StringType::Label => {
                    if cur_mes.is_some() || cur_iter.next().is_some() {
                        return Err(anyhow::anyhow!(
                            "Not all messages were used for label {}.",
                            cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
                        ));
                    }
                    let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?;
                    let text = decode_to_string(self.encoding, text.as_bytes(), false)?
                        .replace("<br>", "\n");
                    cur_messages = messages.get(text.as_str()).cloned().unwrap_or_default();
                    cur_iter = cur_messages.iter();
                    cur_mes = cur_iter.next();
                    cur_label = Some(text);
                    // We don't need update labels
                    continue;
                }
                StringType::Hover => {
                    let m = match hover_mes {
                        Some(m) => m,
                        None => return Err(anyhow::anyhow!("Not enough hover messages.")),
                    };
                    let m = m.message.clone();
                    hover_mes = hover_iter.next();
                    m
                }
                StringType::Name => {
                    let m = match cur_mes {
                        Some(m) => m,
                        None => {
                            return Err(anyhow::anyhow!(
                                "Not enough messages for label {}.",
                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
                            ));
                        }
                    };
                    let name = match &m.name {
                        Some(name) => name.clone(),
                        None => return Err(anyhow::anyhow!("Missing name for message.")),
                    };
                    name
                }
                StringType::Message => {
                    let m = match cur_mes {
                        Some(m) => m,
                        None => {
                            return Err(anyhow::anyhow!(
                                "Not enough messages for label {}.",
                                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
                            ));
                        }
                    };
                    let m = m.message.clone();
                    cur_mes = cur_iter.next();
                    m
                }
            };
            if let Some(repl) = replacement {
                for (from, to) in repl.map.iter() {
                    text = text.replace(from, to);
                }
            }
            text = text.replace("\n", "<br>");
            let encoded = encode_string(encoding, &text, false)?;
            let s = std::ffi::CString::new(encoded)?;
            let num = texts.len() as u32;
            num_offset_map.insert(num, str.offset);
            texts.push(s);
        }
        if cur_mes.is_some() || cur_iter.next().is_some() {
            return Err(anyhow::anyhow!(
                "Some messages were not processed for label {}.",
                cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")
            ));
        }
        if hover_mes.is_some() || hover_iter.next().is_some() {
            return Err(anyhow::anyhow!("Some hover messages were not processed."));
        }
        texts_file.write_all(b"$TEXT_LIST__")?;
        texts_file.write_u32(texts.len() as u32)?;
        let mut nf = MemWriter::new();
        for (num, text) in texts.into_iter().enumerate() {
            let num = num as u32;
            let newaddr = nf.pos as u32 + 0x10;
            if let Some(offset) = num_offset_map.get(&num) {
                file.write_u32_at(*offset as u64, newaddr)?;
            }
            nf.write_u32(num)?;
            nf.write_cstring(&text)?;
        }
        nf.pos = 0;
        let mut shift = 4;
        for _ in 0..(nf.data.len() / 4) {
            let mut data = nf.cpeek_u32()?;
            data ^= 0x084DF873 ^ 0xFF987DEE;
            let mut add = data.to_le_bytes();
            add[0] = add[0].rotate_right(shift);
            shift = (shift + 1) % 8;
            data = u32::from_le_bytes(add);
            nf.write_u32(data)?;
        }
        texts_file.write_all(&nf.data)?;
        Ok(())
    }

    fn custom_output_extension<'a>(&'a self) -> &'a str {
        "txt"
    }

    fn custom_export(&self, filename: &std::path::Path, _encoding: Encoding) -> Result<()> {
        let file = std::fs::File::create(filename)
            .map_err(|e| anyhow::anyhow!("Failed to create file {}: {}", filename.display(), e))?;
        let mut file = std::io::BufWriter::new(file);
        Disasm::new(&self.data.data, &self.label_offsets)?.disassemble(Some(&mut file))?;
        Ok(())
    }
}
