use super::disasm::*;
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::*;
use anyhow::Result;
use std::io::Write;
use std::sync::Mutex;
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug)]
/// Sliky mes script builder
pub struct MesBuilder {}

impl MesBuilder {
    /// Create a new Sliky mes script builder
    pub fn new() -> Self {
        Self {}
    }
}

impl ScriptBuilder for MesBuilder {
    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(Mes::new(buf, encoding, config)?))
    }

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

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

struct TextParser<'a> {
    data: Vec<&'a str>,
    typ: SlikyStringType,
    opcodes: &'static Opcodes,
    encoding: Encoding,
    pos: usize,
}

impl<'a> TextParser<'a> {
    fn new(
        s: &'a str,
        typ: SlikyStringType,
        opcodes: &'static Opcodes,
        encoding: Encoding,
    ) -> Self {
        let data = s.graphemes(true).collect();
        Self {
            data,
            typ,
            opcodes,
            encoding,
            pos: 0,
        }
    }

    fn parse(mut self) -> Result<Vec<u8>> {
        match self.typ {
            SlikyStringType::Internal => Err(anyhow::anyhow!(
                "Internal strings cannot be parsed from text."
            )),
            SlikyStringType::Name => {
                let mut m = MemWriter::new();
                m.write_u8(self.opcodes.push_string)?;
                let s = encode_string(self.encoding, &self.data.join(""), false)?;
                m.write_all(&s)?;
                m.write_u8(0)?;
                Ok(m.into_inner())
            }
            SlikyStringType::Message => {
                let mut m = MemWriter::new();
                let mut in_ruby = false;
                let mut in_normal_text = false;
                while let Some(c) = self.next() {
                    match c {
                        "[" => {
                            if in_ruby {
                                return Err(anyhow::anyhow!("Nested ruby tags are not allowed."));
                            }
                            if in_normal_text {
                                m.write_u8(0)?;
                                in_normal_text = false;
                            }
                            in_ruby = true;
                            m.write_u8(self.opcodes.escape_sequence)?;
                            m.write_u8(1)?; // ruby start
                            m.write_u8(self.opcodes.message2)?;
                        }
                        "]" => {
                            if !in_ruby {
                                return Err(anyhow::anyhow!("Unmatched closing ruby tag."));
                            }
                            in_ruby = false;
                            m.write_u8(0)?;
                            m.write_u8(self.opcodes.r#yield)?;
                        }
                        "\n" => {
                            if in_ruby {
                                return Err(anyhow::anyhow!("Newline inside ruby is not allowed."));
                            }
                            if in_normal_text {
                                m.write_u8(0)?;
                                in_normal_text = false;
                            }
                            m.write_u8(self.opcodes.escape_sequence)?;
                            m.write_u8(0)?; // new line
                        }
                        _ => {
                            if !in_ruby && !in_normal_text {
                                in_normal_text = true;
                                m.write_u8(self.opcodes.message2)?;
                            }
                            let s = encode_string(self.encoding, c, false)?;
                            m.write_all(&s)?;
                        }
                    }
                }
                if in_ruby {
                    m.write_u8(0)?;
                    m.write_u8(self.opcodes.r#yield)?;
                }
                if in_normal_text {
                    m.write_u8(0)?;
                }
                Ok(m.into_inner())
            }
        }
    }

    fn next(&mut self) -> Option<&'a str> {
        if self.pos < self.data.len() {
            let c = self.data[self.pos];
            self.pos += 1;
            Some(c)
        } else {
            None
        }
    }
}

#[derive(Debug)]
pub struct Mes {
    disasm: Mutex<Box<dyn Disasm + Send + Sync>>,
    encoding: Encoding,
    texts: Vec<SlikyString>,
}

impl Mes {
    pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
        let reader = MemReader::new(buf);
        let num_message = reader.cpeek_u32()?;
        let code_offset = 4 + num_message as u64 * 4;
        let first_line_offset = reader.cpeek_u32_at(4)? as u64 + code_offset;
        let mut disasm: Box<dyn Disasm + Send + Sync> = if reader.cpeek_u8_at(first_line_offset)?
            == 0x19
            && reader.cpeek_u32_at(first_line_offset + 1)? == 0
        {
            Box::new(Ai6WinDisasm::new(reader)?)
        } else {
            Box::new(PlusDisasm::new(reader)?)
        };
        disasm.read_header()?;
        let texts = disasm.read_code()?;
        Ok(Self {
            disasm: Mutex::new(disasm),
            encoding,
            texts,
        })
    }

    fn code_to_text(&self, str: &SlikyString) -> Result<String> {
        let mut disasm = self
            .disasm
            .lock()
            .map_err(|_| anyhow::anyhow!("Failed to acquire disassembler lock"))?;
        let mut result = String::new();
        disasm.stream_mut().pos = str.start as usize;
        let end = str.start as usize + str.len as usize;
        let opcodes = disasm.opcodes();
        while disasm.stream().pos < end {
            let (opcode, operands) = disasm.read_instruction()?;
            if opcode == opcodes.push_string
                || (opcode == opcodes.message1 && !opcodes.is_message1_obfuscated)
                || opcode == opcodes.message2
            {
                if let Some(Obj::Str(s)) = operands.get(0) {
                    let s = disasm.stream().cpeek_fstring_at(
                        s.start,
                        s.len as usize,
                        self.encoding,
                        true,
                    )?;
                    result.push_str(&s);
                }
            } else if opcode == opcodes.message1 && opcodes.is_message1_obfuscated {
                if let Some(Obj::Str(s)) = operands.get(0) {
                    let mut deobfuscated = vec![0u8; (s.len as usize - 1) * 2];
                    let mut input_idx = 0;
                    let mut output_idx = 0;
                    let tlen = s.len - 1;
                    while input_idx < tlen {
                        let b = disasm.stream().cpeek_u8_at(s.start + input_idx)?;
                        input_idx += 1;
                        if matches!(b, 0x81..0xA0 | 0xE0..0xF0) {
                            deobfuscated[output_idx] = b;
                            output_idx += 1;
                            deobfuscated[output_idx] =
                                disasm.stream().cpeek_u8_at(s.start + input_idx)?;
                            input_idx += 1;
                            output_idx += 1;
                        } else {
                            let c = b as i32 - 0x7D62;
                            deobfuscated[output_idx] = (c >> 8) as u8;
                            output_idx += 1;
                            deobfuscated[output_idx] = (c & 0xFF) as u8;
                            output_idx += 1;
                        }
                    }
                    deobfuscated.truncate(output_idx);
                    let s = decode_to_string(self.encoding, &deobfuscated, true)?;
                    result.push_str(&s);
                }
            } else if opcode == opcodes.escape_sequence {
                if let Some(Obj::Byte(e)) = operands.get(0) {
                    match e {
                        // new line
                        0 => result.push('\n'),
                        // ruby
                        1 => result.push_str("["),
                        _ => {
                            return Err(anyhow::anyhow!("Unknown escape sequence: {}", e));
                        }
                    }
                }
            } else if opcode == opcodes.r#yield {
                result.push_str("]");
            }
        }
        Ok(result)
    }
}

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

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

    fn extract_messages(&self) -> Result<Vec<Message>> {
        let mut messages = Vec::new();
        let mut name = None;
        for t in self.texts.iter() {
            match t.typ {
                SlikyStringType::Internal => {}
                SlikyStringType::Name => {
                    name = Some(self.code_to_text(t)?);
                }
                SlikyStringType::Message => {
                    let message = self.code_to_text(t)?;
                    messages.push(Message {
                        name: name.take(),
                        message,
                    });
                }
            }
        }
        Ok(messages)
    }

    fn import_messages<'a>(
        &'a self,
        messages: Vec<Message>,
        file: Box<dyn WriteSeek + 'a>,
        _filename: &str,
        encoding: Encoding,
        replacement: Option<&'a ReplacementTable>,
    ) -> Result<()> {
        let disasm = self
            .disasm
            .lock()
            .map_err(|_| anyhow::anyhow!("Failed to acquire disassembler lock"))?;
        let opcodes = disasm.opcodes();
        let mut inp = disasm.stream().clone();
        inp.pos = 0;
        let mut patcher = BinaryPatcher::new(inp.to_ref(), file, |add| Ok(add), |add| Ok(add))?;
        let mut mess = messages.iter();
        let mut mes = mess.next();
        for text in &self.texts {
            patcher.copy_up_to(text.start)?;
            match text.typ {
                // Ignore internal strings
                SlikyStringType::Internal => {}
                SlikyStringType::Name => {
                    let m = match mes {
                        Some(m) => m,
                        None => {
                            return Err(anyhow::anyhow!("Not enough messages"));
                        }
                    };
                    let mut name = match &m.name {
                        Some(n) => n.to_string(),
                        None => {
                            return Err(anyhow::anyhow!("Message name is missing"));
                        }
                    };
                    if let Some(repl) = replacement {
                        for (k, v) in &repl.map {
                            name = name.replace(k, v);
                        }
                    }
                    let data =
                        TextParser::new(&name, SlikyStringType::Name, opcodes, encoding).parse()?;
                    patcher.replace_bytes(text.len, &data)?;
                }
                SlikyStringType::Message => {
                    let m = match mes {
                        Some(m) => m,
                        None => {
                            return Err(anyhow::anyhow!("Not enough messages"));
                        }
                    };
                    let mut message = m.message.to_string();
                    if let Some(repl) = replacement {
                        for (k, v) in &repl.map {
                            message = message.replace(k, v);
                        }
                    }
                    let data =
                        TextParser::new(&message, SlikyStringType::Message, opcodes, encoding)
                            .parse()?;
                    patcher.replace_bytes(text.len, &data)?;
                    mes = mess.next();
                }
            }
        }
        if mes.is_some() || mess.next().is_some() {
            return Err(anyhow::anyhow!("Too many messages"));
        }
        patcher.copy_up_to(inp.data.len() as u64)?;
        let code_offset = disasm.code_offset();
        for &address_offset in disasm.little_endian_addresses() {
            let orig_address = inp.cpeek_u32_at(address_offset as u64)? as u64;
            let orig_offset = orig_address + code_offset as u64;
            let new_offset = patcher.map_offset(orig_offset)?;
            let new_address = new_offset - code_offset as u64;
            patcher.patch_u32(address_offset as u64, new_address as u32)?;
        }
        for &address_offset in disasm.big_endian_addresses() {
            let orig_address = inp.cpeek_u32_be_at(address_offset as u64)? as u64;
            let orig_offset = orig_address + code_offset as u64;
            let new_offset = patcher.map_offset(orig_offset)?;
            let new_address = new_offset - code_offset as u64;
            patcher.patch_u32_be(address_offset as u64, new_address as u32)?;
        }
        Ok(())
    }
}

#[test]
fn test_text_parser() {
    let opcodes = &PLUS_OPCODES;
    let parser = TextParser::new(
        "Hello, [world]s\nThis is a test.",
        SlikyStringType::Message,
        opcodes,
        Encoding::Utf8,
    );
    let data = parser.parse().unwrap();
    assert_eq!(
        data,
        vec![
            opcodes.message2,
            b'H',
            b'e',
            b'l',
            b'l',
            b'o',
            b',',
            b' ',
            0,
            opcodes.escape_sequence,
            1,
            opcodes.message2,
            b'w',
            b'o',
            b'r',
            b'l',
            b'd',
            0,
            opcodes.r#yield,
            opcodes.message2,
            b's',
            0,
            opcodes.escape_sequence,
            0,
            opcodes.message2,
            b'T',
            b'h',
            b'i',
            b's',
            b' ',
            b'i',
            b's',
            b' ',
            b'a',
            b' ',
            b't',
            b'e',
            b's',
            b't',
            b'.',
            0
        ]
    );
}
