/* SPDX-FileCopyrightText: © 2024-2025 Decompollaborate */
/* SPDX-License-Identifier: MIT */

#![allow(clippy::uninlined_format_args)]

use rabbitizer::display_flags::InstructionDisplayFlags;
use rabbitizer::instr::{Instruction, InstructionFlags};
#[cfg(any(feature = "RSP", feature = "R4000ALLEGREX"))]
use rabbitizer::isa::IsaExtension;
use rabbitizer::opcodes::Opcode;
use rabbitizer::operands::OPERAND_COUNT_MAX;
use rabbitizer::vram::Vram;

#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct TestEntry {
    pub instr: Instruction,
    pub imm_override: Option<&'static str>,
    pub display_flags: InstructionDisplayFlags,

    pub valid: bool,

    pub expected: &'static str,
    pub expected_opcode: Opcode,
    pub opcode_str: &'static str,
    pub operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],

    pub test_encoder: bool,
}

impl TestEntry {
    #[allow(dead_code)]
    const fn new_impl(
        word: u32,
        vram: Vram,
        flags: InstructionFlags,
        expected: &'static str,
        expected_opcode: Opcode,
        opcode_str: &'static str,
        operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],
    ) -> Self {
        Self {
            instr: Instruction::new(word, vram, flags),
            imm_override: None,
            display_flags: InstructionDisplayFlags::new(),
            valid: true,
            expected,
            expected_opcode,
            opcode_str,
            operands_str,
            test_encoder: true,
        }
    }

    #[allow(dead_code)]
    pub const fn new(
        word: u32,
        flags: InstructionFlags,
        expected: &'static str,
        expected_opcode: Opcode,
        opcode_str: &'static str,
        operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],
    ) -> Self {
        Self::new_impl(
            word,
            Vram::new(0x80000000),
            flags,
            expected,
            expected_opcode,
            opcode_str,
            operands_str,
        )
    }

    #[cfg(feature = "RSP")]
    #[allow(dead_code)]
    pub const fn new_rsp(
        word: u32,
        flags: InstructionFlags,
        expected: &'static str,
        expected_opcode: Opcode,
        opcode_str: &'static str,
        operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],
    ) -> Self {
        Self::new_impl(
            word,
            Vram::new(0xA4000000),
            flags,
            expected,
            expected_opcode,
            opcode_str,
            operands_str,
        )
    }

    #[cfg(feature = "RSP")]
    #[allow(dead_code)]
    pub const fn new_rsp_invalid(
        word: u32,
        flags: InstructionFlags,
        expected: &'static str,
    ) -> Self {
        Self {
            instr: Instruction::new(
                word,
                Vram::new(0xA4000000),
                flags.with_isa_extension(Some(IsaExtension::RSP)),
            ),
            imm_override: None,
            display_flags: InstructionDisplayFlags::default(),
            valid: false,
            expected,
            expected_opcode: Opcode::ALL_INVALID,
            opcode_str: "INVALID",
            operands_str: [None, None, None, None, None],
            test_encoder: false,
        }
    }

    #[cfg(feature = "R4000ALLEGREX")]
    #[allow(dead_code)]
    pub const fn new_r4000allegrex(
        word: u32,
        expected: &'static str,
        expected_opcode: Opcode,
        opcode_str: &'static str,
        operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],
    ) -> Self {
        use rabbitizer::abi::Abi;

        Self::new(
            word,
            InstructionFlags::new_extension(IsaExtension::R4000ALLEGREX).with_abi(Abi::EABI64),
            expected,
            expected_opcode,
            opcode_str,
            operands_str,
        )
    }

    #[allow(dead_code)]
    pub const fn new_full_invalid(
        word: u32,
        flags: InstructionFlags,
        expected: &'static str,
    ) -> Self {
        Self {
            instr: Instruction::new(word, Vram::new(0x80000000), flags),
            imm_override: None,
            display_flags: InstructionDisplayFlags::default(),
            valid: false,
            expected,
            expected_opcode: Opcode::ALL_INVALID,
            opcode_str: "INVALID",
            operands_str: [None, None, None, None, None],
            test_encoder: false,
        }
    }

    #[allow(dead_code)]
    pub const fn new_semi_invalid(
        word: u32,
        flags: InstructionFlags,
        expected: &'static str,
        expected_opcode: Opcode,
        opcode_str: &'static str,
        operands_str: [Option<&'static str>; OPERAND_COUNT_MAX],
    ) -> Self {
        Self {
            instr: Instruction::new(word, Vram::new(0x80000000), flags),
            imm_override: None,
            display_flags: InstructionDisplayFlags::default(),
            valid: false,
            expected,
            expected_opcode,
            opcode_str,
            operands_str,
            test_encoder: false,
        }
    }

    #[allow(dead_code)]
    pub const fn with_display_flags(self, display_flags: InstructionDisplayFlags) -> Self {
        Self {
            display_flags,
            ..self
        }
    }

    pub fn compare_source_info(&self, other: &Self) -> bool {
        self.instr.word() == other.instr.word()
            && self.instr.flags() == other.instr.flags()
            && self.imm_override == other.imm_override
            && self.display_flags == other.display_flags
            && self.valid == other.valid
    }

    pub fn check_validity(&self) -> u32 {
        let mut errors = 0;

        if self.instr.is_valid() != self.valid {
            println!(
                "'{}' ({:08X}) has incorrect validity. Expected '{}', got '{}'",
                self.opcode_str,
                self.instr.word(),
                self.valid,
                self.instr.is_valid()
            );
            errors += 1;
        }
        if self.instr.opcode() != self.expected_opcode {
            println!(
                "'{}' ({:08X}) has incorrect decoded opcode. Expected '{:?}', got '{:?}'",
                self.opcode_str,
                self.instr.word(),
                self.expected_opcode,
                self.instr.opcode()
            );
            errors += 1;
        }
        let generated_mnemonic = self.instr.mnemonic_display(&self.display_flags).to_string();
        if generated_mnemonic != self.opcode_str {
            println!(
                "'{}' ({:08X}) has incorrect mnemonic. Expected '{}', got '{}'",
                self.opcode_str,
                self.instr.word(),
                self.opcode_str,
                generated_mnemonic,
            );
            errors += 1;
        }

        if self.instr.opcode().is_branch() {
            if self.instr.get_branch_offset_generic().is_none() {
                println!(
                    "'{}' ({:08X}) is a branch opcode but `get_branch_offset_generic` returned `None`.",
                    self.opcode_str,
                    self.instr.word()
                );
                errors += 1;
            }
            if self.instr.get_branch_vram_generic().is_none() {
                println!(
                    "'{}' ({:08X}) is a branch but `get_branch_vram_generic` returned `None`.",
                    self.opcode_str,
                    self.instr.word()
                );
                errors += 1;
            }
        }

        if self.instr.opcode() == Opcode::core_j {
            if self.instr.flags().j_as_branch() {
                if self.instr.get_branch_offset_generic().is_none() {
                    println!(
                        "'{}' ({:08X}) is the `j` opcode but `get_branch_offset_generic` returned `None`.",
                        self.opcode_str,
                        self.instr.word()
                    );
                    errors += 1;
                }
                if self.instr.get_branch_vram_generic().is_none() {
                    println!(
                        "'{}' ({:08X}) is the `j` but `get_branch_vram_generic` returned `None`.",
                        self.opcode_str,
                        self.instr.word()
                    );
                    errors += 1;
                }
            } else {
                if self.instr.get_branch_offset_generic().is_some() {
                    println!(
                        "'{}' ({:08X}) is the `j` opcode but `get_branch_offset_generic` returned `Some` when `j_as_branch` is turned off.",
                        self.opcode_str,
                        self.instr.word()
                    );
                    errors += 1;
                }
                if self.instr.get_branch_vram_generic().is_some() {
                    println!(
                        "'{}' ({:08X}) is the `j` opcode but `get_branch_vram_generic` returned `Some` when `j_as_branch` is turned off.",
                        self.opcode_str,
                        self.instr.word()
                    );
                    errors += 1;
                }
            }
        }

        errors
    }

    pub fn check_disassembly(&self) -> u32 {
        let mut errors = 0;
        let display_flags = self.display_flags.with_debug_word_comment_info(true);

        let disasm = self
            .instr
            .display(&display_flags, self.imm_override, 0)
            .to_string();
        // println!("    {}", disasm);
        if disasm != self.expected {
            println!(
                "'{}' ({:08X}) did not match the expected string.",
                self.opcode_str,
                self.instr.word(),
            );
            println!("    Expected: '{}'", self.expected,);
            println!("    Got:      '{}'", disasm,);
            errors += 1;
        }

        // Check disassembling specific operands
        {
            let mut j = 0;
            for (i, operand) in self.instr.operands_iter().enumerate() {
                let operand_str = operand
                    .display(&self.instr, &display_flags, self.imm_override)
                    .to_string();
                let maybe_expected_str = self.operands_str[i];

                if let Some(expected_str) = maybe_expected_str {
                    if operand_str != expected_str {
                        println!(
                            "'{}' ({:08X}) has incorrect disassembled operand. Expected '{}', got '{}'",
                            self.opcode_str,
                            self.instr.word(),
                            expected_str,
                            operand_str
                        );
                        errors += 1;
                    }
                } else {
                    println!(
                        "'{}' ({:08X}) has an unexpected operand at index {}. Got: '{}'",
                        self.opcode_str,
                        self.instr.word(),
                        i,
                        operand_str,
                    );
                    errors += 1;
                }
                j = i;
            }

            if !self.operands_str[j + 1..].iter().all(|x| x.is_none()) {
                println!(
                    "'{}' ({:08X}) has unhandled expected operands. Values: '{:?}'",
                    self.opcode_str,
                    self.instr.word(),
                    &self.operands_str[j..]
                );
                errors += 1;
            }
        }

        errors
    }

    #[cfg(feature = "encoder")]
    pub fn check_encoding(&self) -> u32 {
        use rabbitizer::encoder::{EncoderFlags, EncoderIterator};

        if !self.valid {
            return 0;
        }
        if self.expected.starts_with(".word") {
            return 0;
        }
        if self.imm_override.is_some() {
            // TODO: We don't support `imm_override`s yet while encoding.
            return 0;
        }

        let mut encoder_flags = EncoderFlags::new(*self.instr.flags());
        if !self.display_flags.use_dollar() {
            *encoder_flags.allow_dollarless_mut() = true;
        }
        #[cfg(feature = "R5900EE")]
        {
            *encoder_flags.r5900ee_prodg_sn_as_inverted_regs_mut() =
                self.display_flags.r5900ee_prodg_sn_as_inverted_regs();
        }

        let mut errors = 0;
        let mut encoder = EncoderIterator::new(self.expected, self.instr.vram(), encoder_flags);
        let display_flags = self.display_flags.with_debug_word_comment_info(true);

        match encoder.next() {
            None => {
                println!("Unable to encode? '{}'", self.expected);
                errors += 1;
            }
            Some(Err(e)) => {
                // TODO: add error
                println!(
                    "Unable to encode '{}' due to error:\n    {}",
                    self.expected, e
                );
                errors += 1;
            }
            Some(Ok((instr, raw))) => {
                let disasm = instr
                    .display(&display_flags, self.imm_override, 0)
                    .to_string();
                if !instr.is_valid() {
                    println!(
                        "Encoded instruction '{}' (encoded '{}') is not valid?",
                        self.expected, disasm
                    );
                    errors += 1;
                }
                if instr.word() != self.instr.word() {
                    println!("Encoded instruction does not match the expected word.");
                    println!(
                        "    Expected: '0x{:08X}' '{}'",
                        self.instr.word(),
                        self.expected
                    );
                    println!("    Got:      '0x{:08X}' '{}'", instr.word(), disasm);
                    errors += 1;
                }
                if raw.trim() != self.expected.trim() {
                    println!("The returned raw range of the instruction '{}' does not match the expected text '{}'", raw, self.expected);
                    errors += 1;
                }
            }
        }

        match encoder.next() {
            None => {}
            Some(_) => {
                println!(
                    "What? A second instruction was tried to be encoded while encoding '{}'?",
                    self.expected
                );
                errors += 1;
            }
        }

        errors
    }
}

pub fn entries_sanity_check(entries: &[TestEntry]) {
    for (i, x) in entries.iter().enumerate() {
        for y in entries[i + 1..].iter() {
            assert!(
                !x.compare_source_info(y),
                "Duplicated entry. Word: '0x{:08X}'. imm_override: '{:?}'",
                x.instr.word(),
                x.imm_override
            );
        }
    }
}

pub fn check_test_entries(entries: &[TestEntry]) -> (u32, u32) {
    let mut instructions_with_errors = 0;
    let mut individual_errors = 0;

    entries_sanity_check(entries);

    for entry in entries {
        let mut errors = entry.check_validity();
        errors += entry.check_disassembly();
        #[cfg(feature = "encoder")]
        if entry.test_encoder {
            errors += entry.check_encoding();
        }

        individual_errors += errors;
        if errors != 0 {
            instructions_with_errors += 1;
            println!();
        }
    }

    (instructions_with_errors, individual_errors)
}
