Documentation
use std::fmt::Debug;

use derive_more::UpperHex;
use strum_macros::{Display, EnumDiscriminants, EnumIter};
use thiserror::Error;
use ux::{u12, u4};

/// One of the 16 CHIP-8 variable registers `V0`–`VF`.
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub struct Register(pub u4);

impl From<Register> for usize {
    fn from(register: Register) -> Self {
        usize::try_from(register.0).unwrap()
    }
}

/// The possible byte operands in a CHIP-8 opcode: A nibble representing a [`Register`] holding a byte, or an immediate byte value.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Byte {
    Register(Register),
    Immediate(u8),
}

#[derive(UpperHex)]
#[upper_hex(fmt = "UpperHex")]
enum Address {
    Address(u12),
    LongAddress(u16),
}

// TODO
impl Default for Byte {
    fn default() -> Self {
        //Self::Register(Register::default())
        Self::Immediate(u8::default())
    }
}

impl From<Byte> for u8 {
    fn from(byte: Byte) -> Self {
        match byte {
            Byte::Register(Register(x)) => u8::from(x),
            Byte::Immediate(x) => x,
        }
    }
}

/// CHIP-8 instructions.
///
/// This instruction set is mostly based on what Octo supports, which comprises the specifications for CHIP-8, SUPER-CHIP and XO-CHIP.
///
/// However, there is also support for some more esoteric instructions, especially where there are no collisions in the opcode space.
#[non_exhaustive]
#[derive(EnumIter, EnumDiscriminants, Copy, Clone, Debug, Display, PartialEq, Eq)]
#[strum_discriminants(derive(Display))]
pub enum Instruction {
    /// Halt the CHIP-8 interpreter.
    ///
    /// Based on Octo's behavior when encountering `0000`.
    Halt,
    /// Exit the CHIP-8 interpreter with an optional exit code.
    ///
    /// Based on the following opcodes:
    /// * `00FD`: Exit interpreter (from SUPER-CHIP 1.1)
    /// * `001N`: Exit with exit code `N` (from [`chip8run`](http://chip8.sourceforge.net/))
    Exit(Option<u8>),
    /// Scroll the display up.
    ///
    /// Based on the following opcodes:
    /// * `00BN`: Scroll up by N pixels (from [Massung's SUPER-CHIP interpreter](https://chip-8.github.io/extensions/#super-chip-with-scroll-up) and Mega-Chip)
    /// * `00DN`: Scroll up by N pixels (from [XO-CHIP](http://johnearnest.github.io/Octo/docs/XO-ChipSpecification.html))
    ScrollUp(u4),
    /// Scroll the display down.
    ///
    /// Based on the following opcode:
    /// * `00CN`: Scroll down by N pixels
    ScrollDown(u4),
    /// Scroll the display right.
    ScrollRight,
    /// Scroll the display left.
    ScrollLeft,
    /// Clear the display.
    Clear,
    /// Return from subroutine.
    Return,
    /// Toggle the behavior of `Instruction::Load` and `Instruction::Store`
    ToggleLoadStoreQuirk,
    /// Change display to low resolution ("lores") mode, 64x32 pixels
    LoRes,
    /// Change display to high resolution ("hires") mode, 128x64 pixels
    HiRes,
    /// Call machine code routine.
    CallMachineCode(u12),
    /// Jump to memory address
    Jump(u12),
    /// Call subroutine
    Call(u12),
    /// Skip the next instruction if
    SkipIfEqual(Register, Byte),
    SkipIfNotEqual(Register, Byte),
    Add(Register, Byte),
    Set(Register, Byte),
    Or(Register, Register),
    And(Register, Register),
    Xor(Register, Register),
    Sub(Register, Register),
    ShiftRight(Register, Register),
    ShiftLeft(Register, Register),
    SubReverse(Register, Register),
    SetIndex(u12),
    SetIndexLong(u16),
    JumpRelative(u12),
    Random(Register, u8),
    /// Draw a sprite on the display.
    Draw(Register, Register, u4),
    SkipKey(Register),
    SkipNotKey(Register),
    LoadAudio,
    LoadDelay(Register),
    BlockKey(Register),
    SelectPlane(u4),
    SetPitch(Register),
    SetDelay(Register),
    SetSound(Register),
    AddRegisterToIndex(Register),
    FontCharacter(Register),
    BigFontCharacter(Register),
    Bcd(Register),
    Store(Register),
    Load(Register),
    StoreRange(Register, Register),
    LoadRange(Register, Register),
    StoreFlags(Register),
    LoadFlags(Register),
}

#[non_exhaustive]
#[derive(EnumIter, Copy, Clone, Debug, Display, UpperHex, PartialEq, Eq)]
#[upper_hex(fmt = "UpperHex")]
pub enum Opcode {
    Opcode(u16),
    LongOpcode(u32),
}

impl From<Instruction> for Opcode {
    fn from(instruction: Instruction) -> Opcode {
        match instruction {
            Instruction::Halt => Opcode::Opcode(0x0000),
            Instruction::Exit(None) => Opcode::Opcode(0x00FD),
            Instruction::Exit(Some(n)) => Opcode::Opcode(0x0010 | u16::from(n)),
            Instruction::ScrollDown(n) => Opcode::Opcode(0x00C0 | u16::from(n)),
            Instruction::ScrollUp(n) => Opcode::Opcode(0x00D0 | u16::from(n)),
            Instruction::ToggleLoadStoreQuirk => Opcode::Opcode(0x00FA),
            Instruction::ScrollRight => Opcode::Opcode(0x00FB),
            Instruction::ScrollLeft => Opcode::Opcode(0x00FC),
            Instruction::LoRes => Opcode::Opcode(0x00FE),
            Instruction::HiRes => Opcode::Opcode(0x00FF),
            Instruction::Clear => Opcode::Opcode(0x00E0),
            Instruction::Return => Opcode::Opcode(0x00EE),
            Instruction::CallMachineCode(nnn) => Opcode::Opcode(u16::from(nnn)),
            Instruction::Jump(nnn) => Opcode::Opcode(0x1000 | u16::from(nnn)),
            Instruction::Call(nnn) => Opcode::Opcode(0x2000 | u16::from(nnn)),
            Instruction::SkipIfEqual(Register(x), Byte::Immediate(kk)) => {
                Opcode::Opcode(0x3000 | (u16::from(x) << 8) | u16::from(kk))
            }
            Instruction::SkipIfEqual(Register(x), Byte::Register(Register(y))) => {
                Opcode::Opcode(0x5000 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::SkipIfNotEqual(Register(x), Byte::Immediate(kk)) => {
                Opcode::Opcode(0x4000 | (u16::from(x) << 8) | u16::from(kk))
            }
            Instruction::SkipIfNotEqual(Register(x), Byte::Register(Register(y))) => {
                Opcode::Opcode(0x9000 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::Set(Register(x), Byte::Immediate(kk)) => {
                Opcode::Opcode(0x6000 | (u16::from(x) << 8) | u16::from(kk))
            }
            Instruction::Add(Register(x), Byte::Immediate(kk)) => {
                Opcode::Opcode(0x7000 | (u16::from(x) << 8) | u16::from(kk))
            }
            Instruction::Set(Register(x), Byte::Register(Register(y))) => {
                Opcode::Opcode(0x8000 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::Add(Register(x), Byte::Register(Register(y))) => {
                Opcode::Opcode(0x8004 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::Or(Register(x), Register(y)) => {
                Opcode::Opcode(0x8001 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::And(Register(x), Register(y)) => {
                Opcode::Opcode(0x8002 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::Xor(Register(x), Register(y)) => {
                Opcode::Opcode(0x8003 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::Sub(Register(x), Register(y)) => {
                Opcode::Opcode(0x8005 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::ShiftRight(Register(x), Register(y)) => {
                Opcode::Opcode(0x8006 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::ShiftLeft(Register(x), Register(y)) => {
                Opcode::Opcode(0x800E | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::SubReverse(Register(x), Register(y)) => {
                Opcode::Opcode(0x8007 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::SetIndex(nnn) => Opcode::Opcode(0xA000 | u16::from(nnn)),
            Instruction::SetIndexLong(nnnn) => Opcode::LongOpcode(0xF000_0000 | u32::from(nnnn)),
            Instruction::JumpRelative(nnn) => Opcode::Opcode(0xB000 | u16::from(nnn)),
            Instruction::Random(Register(x), kk) => {
                Opcode::Opcode(0xC000 | (u16::from(x) << 8) | u16::from(kk))
            }
            Instruction::Draw(Register(x), Register(y), n) => {
                Opcode::Opcode(0xD000 | (u16::from(x) << 8) | (u16::from(y) << 4) | u16::from(n))
            }
            Instruction::SkipKey(Register(x)) => Opcode::Opcode(0xE09E | (u16::from(x) << 8)),
            Instruction::SkipNotKey(Register(x)) => Opcode::Opcode(0xE0A1 | (u16::from(x) << 8)),
            Instruction::LoadAudio => Opcode::Opcode(0xF002),
            Instruction::LoadDelay(Register(x)) => Opcode::Opcode(0xF007 | (u16::from(x) << 8)),
            Instruction::BlockKey(Register(x)) => Opcode::Opcode(0xF00A | (u16::from(x) << 8)),
            Instruction::SelectPlane(n) => Opcode::Opcode(0xF001 | (u16::from(n) << 8)),
            Instruction::SetPitch(Register(x)) => Opcode::Opcode(0xF03A | (u16::from(x) << 8)),
            Instruction::SetDelay(Register(x)) => Opcode::Opcode(0xF015 | (u16::from(x) << 8)),
            Instruction::SetSound(Register(x)) => Opcode::Opcode(0xF018 | (u16::from(x) << 8)),
            Instruction::AddRegisterToIndex(Register(x)) => {
                Opcode::Opcode(0xF01E | (u16::from(x) << 8))
            }
            Instruction::FontCharacter(Register(x)) => Opcode::Opcode(0xF029 | (u16::from(x) << 8)),
            Instruction::BigFontCharacter(Register(x)) => {
                Opcode::Opcode(0xF030 | (u16::from(x) << 8))
            }
            Instruction::Bcd(Register(x)) => Opcode::Opcode(0xF033 | (u16::from(x) << 8)),
            Instruction::Store(Register(x)) => Opcode::Opcode(0xF055 | (u16::from(x) << 8)),
            Instruction::Load(Register(x)) => Opcode::Opcode(0xF065 | (u16::from(x) << 8)),
            Instruction::StoreFlags(Register(x)) => Opcode::Opcode(0xF075 | (u16::from(x) << 8)),
            Instruction::LoadFlags(Register(x)) => Opcode::Opcode(0xF085 | (u16::from(x) << 8)),
            Instruction::StoreRange(Register(x), Register(y)) => {
                Opcode::Opcode(0x5002 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
            Instruction::LoadRange(Register(x), Register(y)) => {
                Opcode::Opcode(0x5003 | (u16::from(x) << 8) | (u16::from(y) << 4))
            }
        }
    }
}

#[derive(Error)]
pub enum DecodeError {
    #[error("unknown opcode: {0}")]
    UnknownOpcodeError(Opcode),
    #[error("incomplete opcode: {0}")]
    IncompleteLongOpcodeError(InstructionDiscriminants, Box<dyn Fn(u16) -> Instruction>),
}

impl Debug for DecodeError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::UnknownOpcodeError(arg0) => {
                f.debug_tuple("UnknownOpcodeError").field(arg0).finish()
            }
            Self::IncompleteLongOpcodeError(arg0, arg1) => f
                .debug_tuple("IncompleteLongOpcodeError")
                .field(arg0)
                .finish(),
        }
    }
}

impl TryFrom<Opcode> for Instruction {
    type Error = DecodeError;

    fn try_from(op: Opcode) -> Result<Self, Self::Error> {
        match op {
            Opcode::LongOpcode(opcode) => {
                let prefix = u16::try_from(opcode >> 16).unwrap();
                let suffix = u16::try_from(opcode & 0x0000_FFFF).unwrap();
                if prefix == 0xF000 {
                    Ok(Instruction::SetIndexLong(suffix))
                } else {
                    Err(DecodeError::UnknownOpcodeError(op))
                    //Err(format!("Unknown long opcode {:#010x}", opcode))
                }
            }
            Opcode::Opcode(opcode) => {
                let x = u4::try_from((opcode & 0x0F00) >> 8).unwrap();
                let y = u4::try_from((opcode & 0x00F0) >> 4).unwrap();
                let nnn = u12::try_from(opcode & 0x0FFF).unwrap();
                let kk = (opcode & 0x00FF) as u8;
                let n = u4::try_from(opcode & 0x000F).unwrap();

                let op1 = (opcode & 0xF000) >> 12;
                let op2 = (opcode & 0x0F00) >> 8;
                let op3 = (opcode & 0x00F0) >> 4;
                let op4 = opcode & 0x000F;

                Ok(
                    match (op1, op2, op3, op4) {
                        #![allow(clippy::match_same_arms)]
                        (0x0, 0x0, 0x0, 0x0) => Instruction::Halt,
                        (0x0, 0x0, 0x1, _n) => Instruction::Exit(Some(op4 as u8)),
                        (0x0, 0x0, 0xB, _n) => Instruction::ScrollUp(n),
                        (0x0, 0x0, 0xC, _n) => Instruction::ScrollDown(n),
                        (0x0, 0x0, 0xD, _n) => Instruction::ScrollUp(n),
                        (0x0, 0x0, 0xE, 0x0) => Instruction::Clear,
                        (0x0, 0x0, 0xE, 0xE) => Instruction::Return,
                        (0x0, 0x0, 0xF, 0xA) => Instruction::ToggleLoadStoreQuirk,
                        (0x0, 0x0, 0xF, 0xB) => Instruction::ScrollRight,
                        (0x0, 0x0, 0xF, 0xC) => Instruction::ScrollLeft,
                        (0x0, 0x0, 0xF, 0xD) => Instruction::Exit(None),
                        (0x0, 0x0, 0xF, 0xE) => Instruction::LoRes,
                        (0x0, 0x0, 0xF, 0xF) => Instruction::HiRes,
                        (0x0, _, _, _) => Instruction::CallMachineCode(nnn),
                        (0x1, _, _, _) => Instruction::Jump(nnn),
                        (0x2, _, _, _) => Instruction::Call(nnn),
                        (0x3, _x, _, _) => {
                            Instruction::SkipIfEqual(Register(x), Byte::Immediate(kk))
                        }
                        (0x4, _x, _, _) => {
                            Instruction::SkipIfNotEqual(Register(x), Byte::Immediate(kk))
                        }
                        (0x5, _x, _y, 0x0) => {
                            Instruction::SkipIfEqual(Register(x), Byte::Register(Register(y)))
                        }
                        (0x5, _x, _y, 0x2) => Instruction::StoreRange(Register(x), Register(y)),
                        (0x5, _x, _y, 0x3) => Instruction::LoadRange(Register(x), Register(y)),
                        (0x6, _x, _, _) => Instruction::Set(Register(x), Byte::Immediate(kk)),
                        (0x7, _x, _, _) => Instruction::Add(Register(x), Byte::Immediate(kk)),
                        (0x8, _x, _y, 0) => {
                            Instruction::Set(Register(x), Byte::Register(Register(y)))
                        }
                        (0x8, _x, _y, 1) => Instruction::Or(Register(x), Register(y)),
                        (0x8, _x, _y, 2) => Instruction::And(Register(x), Register(y)),
                        (0x8, _x, _y, 3) => Instruction::Xor(Register(x), Register(y)),
                        (0x8, _x, _y, 4) => {
                            Instruction::Add(Register(x), Byte::Register(Register(y)))
                        }
                        (0x8, _x, _y, 5) => Instruction::Sub(Register(x), Register(y)),
                        (0x8, _x, _y, 0x6) => Instruction::ShiftRight(Register(x), Register(y)),
                        (0x8, _x, _y, 0x7) => Instruction::SubReverse(Register(x), Register(y)),
                        (0x8, _x, _y, 0xE) => Instruction::ShiftLeft(Register(x), Register(y)),
                        (0x9, _x, _y, 0x0) => {
                            Instruction::SkipIfNotEqual(Register(x), Byte::Register(Register(y)))
                        }
                        (0xA, _, _, _) => Instruction::SetIndex(nnn),
                        (0xB, _, _, _) => Instruction::JumpRelative(nnn),
                        (0xC, _x, _, _) => Instruction::Random(Register(x), kk),
                        (0xD, _x, _y, _n) => Instruction::Draw(Register(x), Register(y), n),
                        (0xE, _x, 0x9, 0xE) => Instruction::SkipKey(Register(x)),
                        (0xE, _x, 0xA, 0x1) => Instruction::SkipNotKey(Register(x)),
                        (0xF, 0x0, 0x0, 0x0) => {
                            return Err(DecodeError::IncompleteLongOpcodeError(
                                InstructionDiscriminants::SetIndexLong,
                                Box::new(Instruction::SetIndexLong),
                            ));
                            //return Err(format!("Incomplete long opcode {:#06x}", opcode))
                        }
                        (0xF, 0x0, 0x0, 0x2) => Instruction::LoadAudio,
                        (0xF, _x, 0x0, 0x7) => Instruction::LoadDelay(Register(x)),
                        (0xF, _x, 0x0, 0xA) => Instruction::BlockKey(Register(x)),
                        (0xF, _n, 0x0, 0x1) => Instruction::SelectPlane(x),
                        (0xF, _x, 0x1, 0x5) => Instruction::SetDelay(Register(x)),
                        (0xF, _x, 0x1, 0x8) => Instruction::SetSound(Register(x)),
                        (0xF, _x, 0x1, 0xE) => Instruction::AddRegisterToIndex(Register(x)),
                        (0xF, _x, 0x2, 0x9) => Instruction::FontCharacter(Register(x)),
                        (0xF, _x, 0x3, 0x0) => Instruction::BigFontCharacter(Register(x)),
                        (0xF, _x, 0x3, 0x3) => Instruction::Bcd(Register(x)),
                        (0xF, _x, 0x3, 0xA) => Instruction::SetPitch(Register(x)),
                        (0xF, _x, 0x5, 0x5) => Instruction::Store(Register(x)),
                        (0xF, _x, 0x6, 0x5) => Instruction::Load(Register(x)),
                        (0xF, _x, 0x7, 0x5) => Instruction::StoreFlags(Register(x)),
                        (0xF, _x, 0x8, 0x5) => Instruction::LoadFlags(Register(x)),
                        _ => return Err(DecodeError::UnknownOpcodeError(op)), // Err(format!("Unknown opcode {:#06x}", opcode)),
                    },
                )
            }
        }
    }
}