use crate::prelude::*;

/// Not to be used directly, just call [`Instruction::encode`].
pub trait Encode {
    fn encode(self, opcode: u8) -> u32;
}

/// Encodes a zero argument instruction.
fn encode_no_args(opcode: u8) -> u32 {
    let opcode = u32::from(opcode);
    let sr1 = Register::Null as u32;
    let sr2 = Register::Null as u32;
    let dr = Register::Null as u32;
    let shamt = 0;

    (opcode << 26) | (sr1 << 21) | (sr2 << 16) | (dr << 11) | (shamt << 6)
}

/// Expands to a match statement that calls encode on instructions that implement
/// [`Encode`]:
///
/// # Usage
///
/// ```rs
/// encode_instruction!(self, with_args: [...], no_args: [...], special: [...] )
/// ```
macro_rules! encode_instruction {
    ($self:expr, with_args: [$($variant:ident),+ $(,)?], no_args: [$($no_arg_variant:ident),* $(,)?] $(, special: [$($special:pat => $body:expr),* $(,)?])?) => {
        match $self {
            $(
                Instruction::$variant(args) => args.encode($self.opcode()),
            )+
            $(
                Instruction::$no_arg_variant => encode_no_args($self.opcode()),
            )*
            $($(
                $special => $body,
            )*)?
        }
    };
}

impl Encode for Instruction {
    fn encode(self, _: u8) -> u32 {
        encode_instruction!(
            self,
            with_args: [
                Mov, MovSigned, LoadByte, LoadByteSigned, LoadHalfword,
                LoadHalfwordSigned, LoadWord, StoreByte, StoreHalfword,
                StoreWord, LoadLowerImmediate, LoadUpperImmediate, Jump,
                JumpEq, JumpNeq, JumpGt, JumpGe, JumpLt, JumpLe, Compare,
                Add, Sub, Increment, Decrement, ShiftLeft, ShiftRight,
                And, Or, Not, Xor, Nand, Nor, Xnor, AddImmediate, SubImmediate
            ],
            no_args: [Nop, IntReturn, Halt],
            special: [
                Self::Data(data) => data,
                Self::Interrupt(interrupt) => {
                    let opcode = u32::from(self.opcode());
                    (opcode << 26) | u32::from(interrupt.as_u8())
                },
                Self::Segment(segment) => {
                    let opcode = u32::from(self.opcode());
                    (opcode << 26) | u32::from(segment as u8)
                }
            ]
        )
    }
}

#[cfg(test)]
mod tests;
