use std::{
    hint::{likely, unlikely, unreachable_unchecked},
    io::Read,
    mem::transmute,
    ops::{Index, IndexMut},
    process::exit,
    sync::atomic::AtomicU32,
    thread::sleep,
    time::{self, Duration, Instant},
};

pub(crate) struct SharedState {
    pub(crate) keyboard: AtomicU32,
    pub(crate) screen_buf: [AtomicU32; 480 * 640],
    pub(crate) external_interupts: AtomicU32,
    pub(crate) external_enabled_interupts: AtomicU32,
    pub(crate) mouse: [AtomicU32; 3],
}

pub(crate) static SHARED: SharedState = SharedState {
    keyboard: AtomicU32::new(0),
    screen_buf: [const { AtomicU32::new(0) }; 480 * 640],
    external_interupts: AtomicU32::new(0),
    external_enabled_interupts: AtomicU32::new(0),
    mouse: [const { AtomicU32::new(0) }; 3],
};

enum Op2 {
    Direct(u32),
    Register(u8),
}

struct Reg(u8);
enum Cond {
    Ifeq,
    Ifne,
    Iflt,
    Ifge,
    Ifgt,
    Ifle,
    Ifult,
    Ifuge,
    Ifugt,
    Ifule,
}

enum InteruptState {
    Disabled,
    Enabled,
    Serving(InteruptKind, u8),
}

#[derive(PartialEq, PartialOrd, Clone, Copy, Debug)]
#[repr(u32)]
enum InteruptKind {
    MMIO = 1,
    Swi = 2,
    DivByZero = 3,
    IllegalLoadStore = 4,
    UnsupportedOpcode = 5,
    IllegalOpcode = 6,
}

#[repr(u32)]
pub enum MMIOInterupt {
    Keyboard = 1,
    MouseClick = 2,
    MouseMove = 4,
    VSync = 8,
}

impl From<u8> for Cond {
    fn from(value: u8) -> Self {
        match value {
            0b0000 => Cond::Ifeq,
            0b0001 => Cond::Ifne,
            0b1000 => Cond::Iflt,
            0b1001 => Cond::Ifge,
            0b1010 => Cond::Ifgt,
            0b1011 => Cond::Ifle,
            0b1100 => Cond::Ifult,
            0b1101 => Cond::Ifuge,
            0b1110 => Cond::Ifugt,
            0b1111 => Cond::Ifule,
            _ => iot(),
        }
    }
}

impl Into<u32> for MMIOInterupt {
    fn into(self) -> u32 {
        unsafe { transmute(self) }
    }
}
impl Into<u32> for InteruptKind {
    fn into(self) -> u32 {
        unsafe { transmute(self) }
    }
}

impl From<u32> for InteruptKind {
    fn from(v: u32) -> Self {
        unsafe { transmute(v as u32) }
    }
}

impl Cond {
    fn eval(self, a: u32, b: u32) -> bool {
        match self {
            Cond::Ifeq => a == b,
            Cond::Ifne => a != b,
            Cond::Iflt => (a as i32) < (b as i32),
            Cond::Ifge => (a as i32) >= (b as i32),
            Cond::Ifgt => (a as i32) > (b as i32),
            Cond::Ifle => (a as i32) <= (b as i32),
            Cond::Ifult => a < b,
            Cond::Ifuge => a >= b,
            Cond::Ifugt => a > b,
            Cond::Ifule => a <= b,
        }
    }
}

enum Instruction {
    Copy(Reg, Op2),
    Add(Reg, Reg, Op2),
    Sub(Reg, Reg, Op2),
    Or(Reg, Reg, Op2),
    And(Reg, Reg, Op2),
    Xor(Reg, Reg, Op2),
    Lsl(Reg, Reg, Op2),
    Lsr(Reg, Reg, Op2),
    Asr(Reg, Reg, Op2),
    Smull(Reg, Reg, Op2),
    Smulh(Reg, Reg, Op2),
    Umull(Reg, Reg, Op2),
    Umulh(Reg, Reg, Op2),
    Div(Reg, Reg, Op2),
    Mod(Reg, Reg, Op2),
    Store(Reg, Op2, Reg),
    Load(Reg, Reg, Op2),
    Push(Op2),
    Pop(Reg),
    Skip(u8, Cond, Reg, Op2),
    Jump(u32), //address / 4
    Call(u32), //address / 4
    Ret(),
    Reti(),
    Swi(),
    Eint(),
    Dint(),
    GetStack(Reg),
    SetStack(Op2),
}

impl TryFrom<u32> for Instruction {
    #[inline(always)]
    fn try_from(value: u32) -> Result<Self, Self::Error> {
        Ok(match value >> 30 {
            0b00 => {
                let t = value & (1 << 29); // 3rd bit set
                let value = value - t;
                let t = t != 0;
                if t {
                    Self::Call(value)
                } else {
                    Self::Jump(value)
                }
            }
            fmt => {
                let imediate = (value & (1 << 28)) != 0;
                let s = (value & 1 << 29) != 0;
                const MASK: u32 = 0b1111;
                let opcode = (value >> 24) & MASK;
                let rd = Reg(((value >> 20) & MASK) as u8);
                let rx = Reg(((value >> 16) & MASK) as u8);
                let op2 = {
                    if imediate {
                        let value = if s {
                            value | 0xFFFF0000
                        } else {
                            value & 0x0000FFFF
                        };
                        Op2::Direct(value)
                    } else {
                        Op2::Register((value >> 12 & MASK) as u8)
                    }
                };
                match (fmt, opcode) {
                    (1, 0b0000) => Self::Copy(rd, op2),
                    (1, 0b0001) => Self::Add(rd, rx, op2),
                    (1, 0b0010) => Self::Sub(rd, rx, op2),
                    (1, 0b0011) => Self::Or(rd, rx, op2),
                    (1, 0b0100) => Self::And(rd, rx, op2),
                    (1, 0b0101) => Self::Xor(rd, rx, op2),
                    (1, 0b0110) => Self::Lsl(rd, rx, op2),
                    (1, 0b0111) => Self::Lsr(rd, rx, op2),
                    (1, 0b1000) => Self::Asr(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1001) => Self::Smull(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1010) => Self::Smulh(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1011) => Self::Umull(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1100) => Self::Umulh(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1101) => Self::Div(rd, rx, op2),
                    #[cfg(feature = "div_mul")]
                    (1, 0b1110) => Self::Mod(rd, rx, op2),
                    #[cfg(not(feature = "div_mul"))]
                    (1, 0b1001)
                    | (1, 0b1010)
                    | (1, 0b1011)
                    | (1, 0b1100)
                    | (1, 0b1101)
                    | (1, 0b1110) => return Err((InteruptKind::UnsupportedOpcode, rd, rx, op2)),
                    (2, 0b0000) => Self::Store(rx, op2, rd),
                    (2, 0b0001) => Self::Load(rd, rx, op2),
                    (2, 0b0010) => Self::Push(op2),
                    (2, 0b0011) => Self::Pop(rd),
                    (2, 0b1000) => {
                        if s {
                            Self::Reti()
                        } else {
                            Self::Ret()
                        }
                    }
                    (2, 0b1001) => Self::Swi(),
                    (2, 0b1011) => Self::Dint(),
                    (2, 0b1100) => Self::Eint(),
                    (2, 0b1101) => Self::GetStack(rd),
                    (2, 0b1110) => Self::SetStack(op2),
                    (3, skip) => Self::Skip(rd.0, (skip as u8).into(), rx, op2),
                    _ => return Err((InteruptKind::IllegalOpcode, rd, rx, op2)),
                }
            }
        })
    }

    type Error = (InteruptKind, Reg, Reg, Op2);
}

pub struct Computer {
    creation: Instant,
    ram: Box<[u32; 0x01000000 / 4]>,
    regs: [u32; 16],
    pc: usize,
    sp: usize,
    interupts: InteruptState,
}

impl Index<Reg> for Computer {
    type Output = u32;

    fn index(&self, index: Reg) -> &Self::Output {
        &self.regs[index.0 as usize]
    }
}

impl IndexMut<Reg> for Computer {
    fn index_mut(&mut self, index: Reg) -> &mut Self::Output {
        &mut self.regs[index.0 as usize]
    }
}

fn iot() -> ! {
    exit(1);
}

impl Computer {
    pub fn new(filename: String) -> Self {
        let mut new = Self {
            creation: Instant::now(),
            ram: unsafe { Box::new_zeroed().assume_init() },
            regs: [0; 16],
            pc: 0,
            sp: 0,
            interupts: InteruptState::Disabled,
        };
        let mut buf = String::new();
        std::fs::File::open(filename)
            .unwrap()
            .read_to_string(&mut buf)
            .unwrap();
        for (i, line) in buf.lines().enumerate() {
            match u32::from_str_radix(line, 16) {
                Ok(val) => new.ram[i] = val,
                Err(_) => break,
            }
        }
        new
    }
    #[inline(always)]
    pub fn step(&mut self) {
        match self.interupts {
            InteruptState::Disabled => {}
            InteruptState::Enabled => {
                let mmio_interupts =
                    (&SHARED.external_interupts).load(std::sync::atomic::Ordering::Acquire);
                if unlikely(mmio_interupts != 0) {
                    let enabled = (&SHARED.external_enabled_interupts)
                        .load(std::sync::atomic::Ordering::Relaxed);
                    if unlikely(mmio_interupts & !(enabled) != 0) {
                        (&SHARED.external_interupts).store(0, std::sync::atomic::Ordering::Relaxed);
                    } else {
                        let kind = mmio_interupts.highest_one().unwrap();
                        self.serve_interupt(InteruptKind::MMIO, [kind]);
                    }
                }
            }
            InteruptState::Serving(..) => {}
        }

        //potentially just changed by interupt.
        let next_opcode = self.ram[self.pc];

        let instruction = Instruction::try_from(next_opcode);

        match instruction {
            Ok(instruction) => {
                match instruction {
                    Instruction::Copy(reg, op2) => {
                        self[reg] = self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Add(reg, reg1, op2) => {
                        self[reg] = self[reg1] + self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Sub(reg, reg1, op2) => {
                        self[reg] = self[reg1] - self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Or(reg, reg1, op2) => {
                        self[reg] = self[reg1] | self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::And(reg, reg1, op2) => {
                        self[reg] = self[reg1] & self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Xor(reg, reg1, op2) => {
                        self[reg] = self[reg1] ^ self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Lsl(reg, reg1, op2) => {
                        self[reg] = self[reg1] << self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Lsr(reg, reg1, op2) => {
                        self[reg] = self[reg1] >> self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Asr(reg, reg1, op2) => {
                        self[reg] = (self[reg1] as i32 >> self.resolve(op2)) as u32;
                        self.pc += 1;
                    }
                    Instruction::Umull(reg, reg1, op2) => {
                        self[reg] = self[reg1].wrapping_mul(self.resolve(op2));
                        self.pc += 1;
                    }
                    Instruction::Smull(reg, reg1, op2) => {
                        self[reg] =
                            (self[reg1] as i32).wrapping_mul(self.resolve(op2) as i32) as u32;
                        self.pc += 1;
                    }
                    Instruction::Umulh(reg, reg1, op2) => {
                        self[reg] = self[reg1].widening_mul(self.resolve(op2)).1;
                        self.pc += 1;
                    }
                    Instruction::Smulh(reg, reg1, op2) => {
                        self[reg] =
                            (self[reg1] as i32).widening_mul(self.resolve(op2) as i32).1 as u32;
                        self.pc += 1;
                    }
                    Instruction::Div(reg, reg1, op2) => {
                        self.pc += 1;
                        let d = self.resolve(op2);
                        if unlikely(d == 0) {
                            self.serve_interupt(
                                InteruptKind::DivByZero,
                                [reg.0.into(), self[reg1]],
                            );
                            return;
                        }
                        self[reg] = self[reg1] / d;
                    }
                    Instruction::Mod(reg, reg1, op2) => {
                        self[reg] = self[reg1] % self.resolve(op2);
                        self.pc += 1
                    }
                    Instruction::Store(reg, op2, reg1) => {
                        self.pc += 1;
                        let addr = (self[reg].wrapping_add(self.resolve(op2))) as usize;
                        if !addr.is_multiple_of(4) {
                            self.serve_interupt(
                                InteruptKind::IllegalLoadStore,
                                [0, addr as u32, self[reg1]],
                            );
                            return;
                        }
                        if addr <= 0x00ffffff {
                            self.ram[addr / 4] = self[reg1];
                        } else if addr <= 0x00ff_ffff + 480 * 640 * 4 {
                            let buf_addr = (addr - 0x0100_0000) / 4;
                            let dat = self[reg1] & 0x00FF_FFFF;
                            (&SHARED.screen_buf[buf_addr])
                                .store(dat, std::sync::atomic::Ordering::Relaxed);
                        } else if addr == 0x0120_1000 {
                            (&SHARED.external_enabled_interupts)
                                .store(self[reg1], std::sync::atomic::Ordering::Relaxed);
                        } else {
                            self.serve_interupt(
                                InteruptKind::IllegalLoadStore,
                                [0, addr as u32, self[reg1]],
                            );
                        }
                    }
                    Instruction::Load(reg, reg1, op2) => {
                        self.pc += 1;
                        let addr = (self[reg1].wrapping_add(self.resolve(op2))) as usize;
                        if !addr.is_multiple_of(4) {
                            self.serve_interupt(
                                InteruptKind::IllegalLoadStore,
                                [0, addr as u32, reg.0 as u32],
                            );
                            return;
                        }
                        self[reg] = if addr <= 0x00ffffff {
                            self.ram[addr / 4]
                        } else if addr <= 0x00ffffff + 480 * 640 * 4 {
                            let buf_addr = (addr - 0x0100_0000) / 4;
                            (&SHARED.screen_buf[buf_addr])
                                .load(std::sync::atomic::Ordering::Relaxed)
                        } else if 0x0120_0000 <= addr && addr <= 0x0120_0010 {
                            match addr - 0x0120_0000 {
                                0 => SHARED.keyboard.load(std::sync::atomic::Ordering::Relaxed),
                                4 => time::Instant::now()
                                    .duration_since(self.creation)
                                    .as_millis() as u32,
                                8 => SHARED.mouse[0].load(std::sync::atomic::Ordering::Relaxed),
                                12 => SHARED.mouse[1].load(std::sync::atomic::Ordering::Relaxed),
                                16 => SHARED.mouse[2].load(std::sync::atomic::Ordering::Relaxed),
                                //guaranted by the inequality and is multiple of 4
                                _ => unsafe { unreachable_unchecked() },
                            }
                        } else {
                            self.serve_interupt(
                                InteruptKind::IllegalLoadStore,
                                [1, addr as u32, reg.0 as u32],
                            );
                            return;
                        };
                    }
                    Instruction::Push(op2) => {
                        self.sp -= 1;
                        self.ram[self.sp] = self.resolve(op2);
                        self.pc += 1;
                    }
                    Instruction::Pop(reg) => {
                        self[reg] = self.ram[self.sp];
                        self.sp += 1;
                        self.pc += 1;
                    }
                    Instruction::Skip(d, cond, reg, op2) => {
                        self.pc += 1;
                        if cond.eval(self[reg], self.resolve(op2)) {
                            self.pc += d as usize
                        }
                    }
                    Instruction::Jump(mut addr) => {
                        if addr & (1 << 28) != 0 {
                            addr += 7 << 29;
                        } else if addr == 0 {
                            println!("awaiting interupt...");
                            atomic_wait::wait(&SHARED.external_interupts, 0);
                            println!("waking!")
                        }
                        self.pc = (addr + self.pc as u32) as usize;
                    }
                    Instruction::Call(mut addr) => {
                        self.sp -= 1;
                        self.ram[self.sp] = ((self.pc << 2) + 4) as u32;

                        if addr & (1 << 28) != 0 {
                            addr += 7 << 29;
                        } else if unlikely(addr == 0) {
                            exit(0);
                        }
                        self.pc = (addr + self.pc as u32) as usize;
                    }
                    Instruction::Ret() => {
                        self.pc = (self.ram[self.sp] >> 2) as usize;
                        self.sp += 1;
                    }
                    Instruction::Reti() => {
                        let mut ret_index = None;
                        let mut ret_value = 0;

                        match self.interupts {
                            InteruptState::Serving(kind, prev) => {
                                match prev.highest_one() {
                                    None => self.interupts = InteruptState::Enabled,
                                    Some(i) => {
                                        self.interupts =
                                            InteruptState::Serving(i.into(), prev ^ (1 << i))
                                    }
                                }
                                match kind {
                                    InteruptKind::MMIO => {
                                        (&SHARED.external_interupts)
                                            .store(0, std::sync::atomic::Ordering::Relaxed);
                                        atomic_wait::wake_all(&SHARED.external_interupts);
                                        //no need to check prev because MMIO is the lowest priority
                                        self.interupts = InteruptState::Enabled
                                    }
                                    InteruptKind::Swi => {}
                                    InteruptKind::DivByZero | InteruptKind::UnsupportedOpcode => {
                                        ret_index = Some(self.regs[0]);
                                        ret_value = self.regs[1];
                                    }
                                    InteruptKind::IllegalLoadStore => {}
                                    InteruptKind::IllegalOpcode => {}
                                }
                            }
                            _ => { /* This is a troubling case but ... well it's ok */ }
                        }
                        println!("returning from interupt");
                        let ret = self.ram[self.sp];
                        self.pc = (ret & 0x0FFF_FFFF) as usize;
                        self.sp += 1 as usize;
                        for i in (0..(ret >> 28) as usize).rev() {
                            self.regs[i] = self.ram[self.sp];
                            self.sp += 1
                        }
                        if let Some(idx) = ret_index {
                            self.regs[idx as usize] = ret_value;
                        }
                    }
                    Instruction::Eint() => {
                        match self.interupts {
                            InteruptState::Disabled => self.interupts = InteruptState::Enabled,
                            _ => {}
                        }
                        self.pc += 1;
                    }
                    Instruction::Dint() => {
                        self.interupts = InteruptState::Disabled;
                        self.pc += 1;

                        (&SHARED.external_enabled_interupts)
                            .store(0, std::sync::atomic::Ordering::Relaxed);
                        (&SHARED.external_interupts).store(0, std::sync::atomic::Ordering::Relaxed);
                        atomic_wait::wake_all(&SHARED.external_interupts);
                    }
                    Instruction::Swi() => {
                        self.pc += 1;
                        self.serve_interupt(InteruptKind::Swi, []);
                    }
                    Instruction::GetStack(reg) => {
                        self[reg] = (self.sp << 2) as u32;
                        self.pc += 1;
                    }
                    Instruction::SetStack(op2) => {
                        let v = self.resolve(op2);
                        if likely(v.is_multiple_of(4)) {
                            self.sp = (v >> 2) as usize;
                        } else {
                            self.sp = usize::MAX //Yes, that means that clever program using sp to store information wont work on my emulator. Deal with it
                        }
                        self.pc += 1;
                    }
                };
            }
            Err((kind, rx, ry, op2)) => {
                self.pc += 1;
                match kind {
                    InteruptKind::UnsupportedOpcode => {
                        self.serve_interupt(kind, [rx.0.into(), self[ry], self.resolve(op2)])
                    }
                    InteruptKind::IllegalOpcode => self.serve_interupt(kind, [next_opcode]),
                    _ => unsafe { unreachable_unchecked() },
                }
            }
        }
    }
    fn resolve(&self, op2: Op2) -> u32 {
        match op2 {
            Op2::Direct(v) => v,
            Op2::Register(r) => self.regs[r as usize],
        }
    }
    fn serve_interupt<const N: usize>(&mut self, kind: InteruptKind, arg: [u32; N]) {
        if N >= 16 {
            panic!();
        }
        println!("serving interupt {kind:?} {arg:?}");
        match self.interupts {
            InteruptState::Disabled => {
                println!("Illegal Instruction whith interupt disabled {kind:?} {arg:?}");
                exit(1);
            }
            InteruptState::Enabled => self.interupts = InteruptState::Serving(kind, 0),
            InteruptState::Serving(interupt_kind, previous) => {
                if kind > interupt_kind {
                    self.interupts = InteruptState::Serving(
                        kind,
                        previous | (1_u8 << Into::<u32>::into(interupt_kind)),
                    );
                } else {
                    return;
                }
            }
        }

        for (i, a) in arg.iter().enumerate() {
            self.sp -= 1;
            self.ram[self.sp] = self.regs[i];
            self.regs[i] = *a
        }
        self.sp -= 1;
        self.ram[self.sp] = (self.pc | N << 28) as u32;
        self.pc = Into::<u32>::into(kind) as usize
    }
}
