#![allow(clippy::wrong_self_convention)]
use super::instruction;
use crate::{instructions::control, primitives::Spec, Host, Interpreter};
use std::boxed::Box;
pub type Instruction<H> = fn(&mut Interpreter, &mut H);
pub type InstructionTable<H> = [Instruction<H>; 256];
pub type DynInstruction<'a, H> = dyn Fn(&mut Interpreter, &mut H) + 'a;
pub type BoxedInstruction<'a, H> = Box<DynInstruction<'a, H>>;
pub type BoxedInstructionTable<'a, H> = [BoxedInstruction<'a, H>; 256];
pub enum InstructionTables<'a, H: ?Sized> {
    Plain(InstructionTable<H>),
    Boxed(BoxedInstructionTable<'a, H>),
}
impl<H: Host + ?Sized> InstructionTables<'_, H> {
    #[inline]
    pub const fn new_plain<SPEC: Spec>() -> Self {
        Self::Plain(make_instruction_table::<H, SPEC>())
    }
}
impl<'a, H: Host + ?Sized + 'a> InstructionTables<'a, H> {
    #[inline]
    pub fn insert(&mut self, opcode: u8, instruction: Instruction<H>) {
        match self {
            Self::Plain(table) => table[opcode as usize] = instruction,
            Self::Boxed(table) => table[opcode as usize] = Box::new(instruction),
        }
    }
    #[inline]
    pub fn to_boxed(&mut self) -> &mut BoxedInstructionTable<'a, H> {
        self.to_boxed_with(|i| Box::new(i))
    }
    #[inline]
    pub fn to_boxed_with<F>(&mut self, f: F) -> &mut BoxedInstructionTable<'a, H>
    where
        F: FnMut(Instruction<H>) -> BoxedInstruction<'a, H>,
    {
        match self {
            Self::Plain(_) => self.to_boxed_with_slow(f),
            Self::Boxed(boxed) => boxed,
        }
    }
    #[cold]
    fn to_boxed_with_slow<F>(&mut self, f: F) -> &mut BoxedInstructionTable<'a, H>
    where
        F: FnMut(Instruction<H>) -> BoxedInstruction<'a, H>,
    {
        let Self::Plain(table) = self else {
            unreachable!()
        };
        *self = Self::Boxed(make_boxed_instruction_table(table, f));
        let Self::Boxed(boxed) = self else {
            unreachable!()
        };
        boxed
    }
    #[inline]
    pub fn get_boxed(&mut self, opcode: u8) -> &mut BoxedInstruction<'a, H> {
        &mut self.to_boxed()[opcode as usize]
    }
    #[inline]
    pub fn insert_boxed(&mut self, opcode: u8, instruction: BoxedInstruction<'a, H>) {
        *self.get_boxed(opcode) = instruction;
    }
    #[inline]
    pub fn replace_boxed(
        &mut self,
        opcode: u8,
        instruction: BoxedInstruction<'a, H>,
    ) -> BoxedInstruction<'a, H> {
        core::mem::replace(self.get_boxed(opcode), instruction)
    }
    #[inline]
    pub fn update_boxed<F>(&mut self, opcode: u8, f: F)
    where
        F: Fn(&DynInstruction<'a, H>, &mut Interpreter, &mut H) + 'a,
    {
        update_boxed_instruction(self.get_boxed(opcode), f)
    }
    #[inline]
    pub fn update_all<F>(&mut self, f: F)
    where
        F: Fn(&DynInstruction<'a, H>, &mut Interpreter, &mut H) + Copy + 'a,
    {
        match self {
            Self::Plain(_) => {
                self.to_boxed_with(|prev| Box::new(move |i, h| f(&prev, i, h)));
            }
            Self::Boxed(boxed) => boxed
                .iter_mut()
                .for_each(|instruction| update_boxed_instruction(instruction, f)),
        }
    }
}
#[inline]
pub const fn make_instruction_table<H: Host + ?Sized, SPEC: Spec>() -> InstructionTable<H> {
    const {
        let mut tables: InstructionTable<H> = [control::unknown; 256];
        let mut i = 0;
        while i < 256 {
            tables[i] = instruction::<H, SPEC>(i as u8);
            i += 1;
        }
        tables
    }
}
#[inline]
pub fn make_boxed_instruction_table<'a, H, FN>(
    table: &InstructionTable<H>,
    mut f: FN,
) -> BoxedInstructionTable<'a, H>
where
    H: Host + ?Sized,
    FN: FnMut(Instruction<H>) -> BoxedInstruction<'a, H>,
{
    core::array::from_fn(|i| f(table[i]))
}
#[inline]
pub fn update_boxed_instruction<'a, H, F>(instruction: &mut BoxedInstruction<'a, H>, f: F)
where
    H: Host + ?Sized + 'a,
    F: Fn(&DynInstruction<'a, H>, &mut Interpreter, &mut H) + 'a,
{
    let prev = core::mem::replace(instruction, Box::new(|_, _| {}));
    *instruction = Box::new(move |i, h| f(&prev, i, h));
}