use std::mem;

use bit_struct::{BitCount, FieldStorage, i4, u4, u10};

use crate::assembly_error::AssemblyError;
use crate::components::address::{AddrRepr, Address};
use crate::components::condition::{CondRepr, Condition};
use crate::components::immediate::{ImmedRepr, Immediate};
use crate::components::location::Location;
use crate::components::memory_offset::{MemoryOffset, UnsignedMemOffsetRepr};
use crate::components::register::{RegRepr, Register};
use crate::opcode::{Opcode, OpcodeRepr};
use crate::Labels;

fn u8_as_i4(num: u8) -> i4 {
    let is_neg = num >> 3 & 1 == 1;

    let result = (num & 0b111) as i16;
    let result = if is_neg {
        (-result - 1) as i8
    } else {
        result as i8
    };

    unsafe { i4::new_unchecked(result) }
}

fn i4_as_u8(num: i4) -> u8 {
    let num = num.value();
    let is_neg = num < 0;

    let mut result = if is_neg {
        (-(num as i16) - 1) as u8
    } else {
        num as u8
    } & 0b111;

    if is_neg {
        result |= 1 << 3;
    }

    result
}

/// The integer used for binary data from instructions.
pub type BinInstr = u16;

/// An enum that contains the opcode with data for that opcode.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Instruction {
    NoOperation,
    Halt,

    /// RegC = RegA + RegB
    Addition(Register, Register, Register),
    /// RegC = RegA - RegB
    Subtraction(Register, Register, Register),
    /// RegC = !(RegA | RegB)
    BitwiseNOR(Register, Register, Register),
    /// RegC = RegA & RegB
    BitwiseAND(Register, Register, Register),
    /// RegC = RegA ^ RegB
    BitwiseXOR(Register, Register, Register),
    /// RegC = RegA >> 1
    RightShift(Register, Register),

    /// RegA = Immediate
    LoadImmediate(Register, Immediate),
    /// RegA = RegA + Immediate
    AddImmediate(Register, Immediate),

    /// PC = Location
    Jump(Location),
    /// PC = Condition ? Location : PC + 1
    Branch(Condition, Location),
    /// PC = Location (and push PC + 1 to stack)
    Call(Location),
    /// PC = Top of stack (and pop stack)
    Return,

    /// RegB = Memory[RegA + MemoryOffset]
    MemoryLoad(Register, Register, MemoryOffset),
    /// Memory[RegA + MemoryOffset] = RegB
    MemoryStore(Register, Register, MemoryOffset)
}

impl Instruction {
    /// Gets the opcode from the instruction.
    pub const fn opcode(&self) -> Opcode {
        match self {
            Instruction::NoOperation            => Opcode::NoOperation,
            Instruction::Halt                   => Opcode::Halt,

            Instruction::Addition(_, _, _)      => Opcode::Addition,
            Instruction::Subtraction(_, _, _)   => Opcode::Subtraction,
            Instruction::BitwiseNOR(_, _, _)    => Opcode::BitwiseNOR,
            Instruction::BitwiseAND(_, _, _)    => Opcode::BitwiseAND,
            Instruction::BitwiseXOR(_, _, _)    => Opcode::BitwiseXOR,
            Instruction::RightShift(_, _)       => Opcode::RightShift,

            Instruction::LoadImmediate(_, _)    => Opcode::LoadImmediate,
            Instruction::AddImmediate(_, _)     => Opcode::AddImmediate,

            Instruction::Jump(_)                => Opcode::Jump,
            Instruction::Branch(_, _)           => Opcode::Branch,
            Instruction::Call(_)                => Opcode::Call,
            Instruction::Return                 => Opcode::Return,

            Instruction::MemoryLoad(_, _, _)    => Opcode::MemoryLoad,
            Instruction::MemoryStore(_, _, _)   => Opcode::MemoryStore
        }
    }

    /// Converts the instruction to binary data.
    pub fn to_binary(&self, address: Address, labels: &Labels) -> Result<BinInstr, AssemblyError> {
        let mut bin: BinInstr = 0;
        bin |= (self.opcode().inner_raw() as BinInstr) << (BinInstr::COUNT - OpcodeRepr::COUNT);

        let set_reg = |bin: &mut BinInstr, reg: &Register, index: usize| {
            *bin |= (reg.0.value() as BinInstr) << index * RegRepr::COUNT;
        };

        let set_immed = |bin: &mut BinInstr, immed: &Immediate| {
            *bin |= immed.0 as BinInstr;
        };

        let set_loc = |bin: &mut BinInstr, loc: &Location| -> Result<(), AssemblyError> {
            let address = loc.get_address(address, labels)?;
            *bin |= address.0.value() as BinInstr;
            Ok(())
        };

        let set_cond = |bin: &mut BinInstr, cond: &Condition| {
            *bin |= (cond.inner_raw() as BinInstr) << (BinInstr::COUNT - OpcodeRepr::COUNT - CondRepr::COUNT);
        };

        let set_mem_offset = |bin: &mut BinInstr, mem_offset: &MemoryOffset| {
            let mem_offset = i4_as_u8(mem_offset.0);
            *bin |= mem_offset as BinInstr;
        };

        match self {
            Instruction::Addition(a, b, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_reg(&mut bin, c, 0);
            },
            Instruction::Subtraction(a, b, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_reg(&mut bin, c, 0);
            },
            Instruction::BitwiseNOR(a, b, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_reg(&mut bin, c, 0);
            },
            Instruction::BitwiseAND(a, b, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_reg(&mut bin, c, 0);
            },
            Instruction::BitwiseXOR(a, b, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_reg(&mut bin, c, 0);
            },
            Instruction::RightShift(a, c) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, c, 0);
            },
            Instruction::LoadImmediate(a, immediate) => {
                set_reg(&mut bin, a, 2);
                set_immed(&mut bin, immediate);
            },
            Instruction::AddImmediate(a, immediate) => {
                set_reg(&mut bin, a, 2);
                set_immed(&mut bin, immediate);
            },
            Instruction::Jump(location) => {
                set_loc(&mut bin, location)?;
            },
            Instruction::Branch(condition, location) => {
                set_cond(&mut bin, condition);
                set_loc(&mut bin, location)?;
            }
            Instruction::Call(location) => {
                set_loc(&mut bin, location)?;
            }
            Instruction::MemoryLoad(a, b, mem_offset) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_mem_offset(&mut bin, mem_offset);
            },
            Instruction::MemoryStore(a, b, mem_offset) => {
                set_reg(&mut bin, a, 2);
                set_reg(&mut bin, b, 1);
                set_mem_offset(&mut bin, mem_offset);
            },
            _ => {}
        }

        Ok(bin)
    }

    /// Converts the binary data to an instruction.
    pub fn from_binary(bin: BinInstr) -> Instruction {
        let opcode = bin >> (BinInstr::COUNT - OpcodeRepr::COUNT) & BinInstr::MAX;
        let opcode = unsafe { mem::transmute(opcode as u8) };

        let get_reg = |index: usize| -> Register {
            let reg = (bin >> (index * RegRepr::COUNT) & RegRepr::MAX as BinInstr) as u8;
            Register(unsafe { u4::new_unchecked(reg) })
        };

        let get_immed = || -> Immediate {
            let immed = (bin & ImmedRepr::MAX as BinInstr) as u8;
            Immediate(immed)
        };

        let get_loc = || -> Location {
            let addr = (bin & AddrRepr::MAX as BinInstr) as u16;
            Location::Address(Address(unsafe { u10::new_unchecked(addr) }))
        };

        let get_cond = || -> Condition {
            let cond = bin >> (BinInstr::COUNT - OpcodeRepr::COUNT - CondRepr::COUNT) & (CondRepr::MAX as BinInstr);
            unsafe { mem::transmute(cond as u8) }
        };

        let get_mem_offset = || -> MemoryOffset {
            let mem_offset = (bin & UnsignedMemOffsetRepr::MAX as BinInstr) as u8;
            let mem_offset = u8_as_i4(mem_offset);
            MemoryOffset(mem_offset)
        };

        match opcode {
            Opcode::NoOperation => Instruction::NoOperation,
            Opcode::Halt => Instruction::Halt,
            Opcode::Addition => {
                Instruction::Addition(
                    get_reg(2),
                    get_reg(1),
                    get_reg(0)
                )
            },
            Opcode::Subtraction => {
                Instruction::Subtraction(
                    get_reg(2),
                    get_reg(1),
                    get_reg(0)
                )
            },
            Opcode::BitwiseNOR => {
                Instruction::BitwiseNOR(
                    get_reg(2),
                    get_reg(1),
                    get_reg(0)
                )
            },
            Opcode::BitwiseAND => {
                Instruction::BitwiseAND(
                    get_reg(2),
                    get_reg(1),
                    get_reg(0)
                )
            },
            Opcode::BitwiseXOR => {
                Instruction::BitwiseXOR(
                    get_reg(2),
                    get_reg(1),
                    get_reg(0)
                )
            },
            Opcode::RightShift => {
                Instruction::RightShift(
                    get_reg(2),
                    get_reg(0)
                )
            },
            Opcode::LoadImmediate => {
                Instruction::LoadImmediate(
                    get_reg(2),
                    get_immed()
                )
            },
            Opcode::AddImmediate => {
                Instruction::AddImmediate(
                    get_reg(2),
                    get_immed()
                )
            },
            Opcode::Jump => {
                Instruction::Jump(
                    get_loc()
                )
            },
            Opcode::Branch => {
                Instruction::Branch(
                    get_cond(),
                    get_loc()
                )
            },
            Opcode::Call => {
                Instruction::Call(
                    get_loc()
                )
            },
            Opcode::Return => Instruction::Return,
            Opcode::MemoryLoad => {
                Instruction::MemoryLoad(
                    get_reg(2),
                    get_reg(1),
                    get_mem_offset()
                )
            },
            Opcode::MemoryStore => {
                Instruction::MemoryStore(
                    get_reg(2),
                    get_reg(1),
                    get_mem_offset()
                )
            }
        }
    }
}
