use std::collections::HashMap;

use batpu_assembly::{components::{address::Address, condition::Condition, immediate::Immediate, location::Location, memory_offset::MemoryOffset, register::Register}, instruction::{BinInstr, Instruction}};
use bit_struct::{i4, u4, u10};

fn test_instruction(instruction: &Instruction, binary: BinInstr, binary_to_instruction: &Instruction, addr: Address, labels: &HashMap<String, Address>) {
    let binary_result = instruction.to_binary(addr, labels).unwrap();
    assert!(
        binary_result == binary,
        "Instruction '{:?}' to binary failed\n Expected {:08b}, found {:08b}",
        instruction,
        binary,
        binary_result
    );

    let instruction_result = Instruction::from_binary(binary);
    assert!(
        instruction_result == *binary_to_instruction,
        "Binary to instruction failed\n Expected '{:?}', found '{:?}'",
        binary_to_instruction,
        instruction_result
    );
}

#[test]
fn test_opcodes() {
    let labels = HashMap::new();

    let addr = Address(u10!(0));
    let reg = Register(u4!(0));
    let immed = Immediate(0);
    let offset = MemoryOffset(i4!(0));

    let loc = Location::Address(addr);
    let cond = Condition::default();

    let instructions = &[
        (Instruction::NoOperation,                      0b_0000_0000_0000_0000_u16),
        (Instruction::Halt,                             0b_0001_0000_0000_0000_u16),

        (Instruction::Addition(reg, reg, reg),          0b_0010_0000_0000_0000_u16),
        (Instruction::Subtraction(reg, reg, reg),       0b_0011_0000_0000_0000_u16),
        (Instruction::BitwiseNOR(reg, reg, reg),        0b_0100_0000_0000_0000_u16),
        (Instruction::BitwiseAND(reg, reg, reg),        0b_0101_0000_0000_0000_u16),
        (Instruction::BitwiseXOR(reg, reg, reg),        0b_0110_0000_0000_0000_u16),
        (Instruction::RightShift(reg, reg),             0b_0111_0000_0000_0000_u16),

        (Instruction::LoadImmediate(reg, immed),        0b_1000_0000_0000_0000_u16),
        (Instruction::AddImmediate(reg, immed),         0b_1001_0000_0000_0000_u16),

        (Instruction::Jump(loc.clone()),                0b_1010_0000_0000_0000_u16),
        (Instruction::Branch(cond, loc.clone()),        0b_1011_0000_0000_0000_u16),
        (Instruction::Call(loc.clone()),                0b_1100_0000_0000_0000_u16),
        (Instruction::Return,                           0b_1101_0000_0000_0000_u16),

        (Instruction::MemoryLoad(reg, reg, offset),     0b_1110_0000_0000_0000_u16),
        (Instruction::MemoryStore(reg, reg, offset),    0b_1111_0000_0000_0000_u16)
    ];

    for (instruction, binary) in instructions.iter() {
        test_instruction(instruction, *binary, instruction, addr, &labels);
    }
}

#[test]
fn test_instructions() {
    let labels = HashMap::new();

    let addr = Address(u10!(0));

    let instructions = &[
        (Instruction::NoOperation, 0b_0000_0000_0000_0000_u16),

        (Instruction::Addition(
            Register(u4!(3)),
            Register(u4!(15)),
            Register(u4!(7))
        ), 0b_0010_0011_1111_0111_u16),

        (Instruction::LoadImmediate(
            Register(u4!(14)),
            Immediate(174)
        ), 0b_1000_1110_1010_1110_u16),

        (Instruction::Jump(
            Location::Address(Address(u10!(984)))
        ), 0b_1010_0011_1101_1000_u16),

        (Instruction::Branch(
            Condition::Carry,
            Location::Address(Address(u10!(523)))
        ), 0b_1011_1010_0000_1011_u16),

        (Instruction::MemoryLoad(
            Register(u4!(5)),
            Register(u4!(9)),
            MemoryOffset(i4!(-7))
        ), 0b_1110_0101_1001_1110_u16)
    ];

    for (instruction, binary) in instructions.iter() {
        test_instruction(instruction, *binary, instruction, addr, &labels);
    }
}

#[test]
fn test_components() {
    let mut labels = HashMap::new();
    labels.insert("label".into(), Address(u10!(10)));

    let addr = Address(u10!(1020));

    let instructions = &[
        (Instruction::Branch(
            Condition::NotZero,
            Location::Offset(-5)
        ),
        Instruction::Branch(
            Condition::NotZero,
            Location::Address(Address(u10!(1015)))
        ), 0b_1011_0111_1111_0111_u16),

        (Instruction::Branch(
            Condition::NotCarry,
            Location::Label("label".into())
        ),
        Instruction::Branch(
            Condition::NotCarry,
            Location::Address(Address(u10!(10)))
        ), 0b_1011_1100_0000_1010_u16),

        (Instruction::Branch(
            Condition::Carry,
            Location::Offset(8)
        ),
        Instruction::Branch(
            Condition::Carry,
            Location::Address(Address(u10!(4)))
        ), 0b_1011_1000_0000_0100_u16),
    ];

    for (instruction, binary_to_instruction, binary) in instructions.iter() {
        test_instruction(instruction, *binary, binary_to_instruction, addr, &labels);
    }
}
