use std::io::{Cursor, Read};

use crate::{
    chip8::Chip8,
    chip8::{Quirk, DISPLAY_HEIGHT, DISPLAY_WIDTH, FONT_SET},
    util::random,
};

#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;

const RAM_SIZE: usize = 4096;
const STACK_SIZE: usize = 16;
const REGISTERS_SIZE: usize = 16;
const KEYS_SIZE: usize = 16;

/// The starting address for the ROM loading, should be
/// the initial PC position for execution.
const ROM_START: usize = 0x200;

#[derive(PartialEq)]
enum WaitVblank {
    NotWaiting,
    Waiting,
    Vblank,
}

pub struct QuirkFlags {
    vf_reset: bool,
    memory: bool,
    display_blank: bool,
    clipping: bool,
    shifting: bool,
    jumping: bool,
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct Chip8Neo {
    ram: [u8; RAM_SIZE],
    vram: [u8; DISPLAY_WIDTH * DISPLAY_HEIGHT],
    stack: [u16; STACK_SIZE],
    regs: [u8; REGISTERS_SIZE],
    pc: u16,
    i: u16,
    sp: u8,
    dt: u8,
    st: u8,
    keys: [bool; KEYS_SIZE],
    last_key: u8,
    paused: bool,
    wait_vblank: WaitVblank,
    quirks: QuirkFlags,
}

impl Chip8 for Chip8Neo {
    fn name(&self) -> &str {
        "neo"
    }

    fn reset(&mut self) {
        self.vram = [0u8; DISPLAY_WIDTH * DISPLAY_HEIGHT];
        self.stack = [0u16; STACK_SIZE];
        self.regs = [0u8; REGISTERS_SIZE];
        self.pc = ROM_START as u16;
        self.i = 0x0;
        self.sp = 0x0;
        self.dt = 0x0;
        self.st = 0x0;
        self.keys = [false; KEYS_SIZE];
        self.last_key = 0x0;
        self.paused = false;
        self.wait_vblank = WaitVblank::Waiting;
        self.load_default_font();
    }

    fn reset_hard(&mut self) {
        self.ram = [0u8; RAM_SIZE];
        self.reset();
    }

    fn pause(&mut self) {
        self.paused = true;
    }

    fn paused(&self) -> bool {
        return self.paused;
    }

    fn beep(&self) -> bool {
        self.st > 0
    }

    fn pc(&self) -> u16 {
        self.pc
    }

    fn sp(&self) -> u8 {
        self.sp
    }

    fn ram(&self) -> Vec<u8> {
        self.ram.to_vec()
    }

    fn vram(&self) -> Vec<u8> {
        self.vram.to_vec()
    }

    fn set_quirk(&mut self, quirk: Quirk, value: bool) {
        match quirk {
            Quirk::VfReset => self.quirks.vf_reset = value,
            Quirk::Memory => self.quirks.memory = value,
            Quirk::DisplayBlank => self.quirks.display_blank = value,
            Quirk::Clipping => self.quirks.clipping = value,
            Quirk::Shifting => self.quirks.shifting = value,
            Quirk::Jumping => self.quirks.jumping = value,
        }
    }

    fn get_state(&self) -> Vec<u8> {
        let mut buffer: Vec<u8> = Vec::new();
        buffer.extend(self.ram.iter());
        buffer.extend(self.vram.iter());
        buffer.extend(self.stack.map(|v| v.to_le_bytes()).iter().flatten());
        buffer.extend(self.regs.iter());
        buffer.extend(self.pc.to_le_bytes().iter());
        buffer.extend(self.i.to_le_bytes().iter());
        buffer.extend(self.sp.to_le_bytes().iter());
        buffer.extend(self.dt.to_le_bytes().iter());
        buffer.extend(self.st.to_le_bytes().iter());
        buffer.extend(self.keys.map(|v| v as u8).iter());
        buffer.extend(self.last_key.to_le_bytes().iter());
        buffer
    }

    fn set_state(&mut self, state: &[u8]) {
        let mut u8_buffer = [0u8; 1];
        let mut u16_buffer = [0u8; 2];
        let mut regs_buffer = [0u8; REGISTERS_SIZE * 2];
        let mut keys_buffer = [0u8; KEYS_SIZE];

        let mut cursor = Cursor::new(state.to_vec());

        cursor.read_exact(&mut self.ram).unwrap();
        cursor.read_exact(&mut self.vram).unwrap();
        cursor.read_exact(&mut regs_buffer).unwrap();
        self.stack.clone_from_slice(
            regs_buffer
                .chunks(2)
                .map(|v| {
                    u16_buffer.clone_from_slice(&v[0..2]);
                    u16::from_le_bytes(u16_buffer)
                })
                .collect::<Vec<u16>>()
                .as_slice(),
        );
        cursor.read_exact(&mut self.regs).unwrap();
        cursor.read_exact(&mut u16_buffer).unwrap();
        self.pc = u16::from_le_bytes(u16_buffer);
        cursor.read_exact(&mut u16_buffer).unwrap();
        self.i = u16::from_le_bytes(u16_buffer);
        cursor.read_exact(&mut u8_buffer).unwrap();
        self.sp = u8::from_le_bytes(u8_buffer);
        cursor.read_exact(&mut u8_buffer).unwrap();
        self.dt = u8::from_le_bytes(u8_buffer);
        cursor.read_exact(&mut u8_buffer).unwrap();
        self.st = u8::from_le_bytes(u8_buffer);
        cursor.read_exact(&mut keys_buffer).unwrap();
        self.keys.clone_from_slice(
            keys_buffer
                .map(|v| if v == 1 { true } else { false })
                .iter()
                .as_slice(),
        );
        cursor.read_exact(&mut u8_buffer).unwrap();
        self.last_key = u8::from_le_bytes(u8_buffer);
    }

    fn load_rom(&mut self, rom: &[u8]) {
        self.ram[ROM_START..ROM_START + rom.len()].clone_from_slice(&rom);
    }

    fn clock(&mut self) {
        // in case the CPU is currently in the paused state
        // the control flow is immediately returned as there's
        // nothing pending to be done
        if self.paused {
            return;
        }

        // fetches the current instruction and increments
        // the PC (program counter) accordingly
        let instruction =
            (self.ram[self.pc as usize] as u16) << 8 | self.ram[self.pc as usize + 1] as u16;
        self.pc += 2;

        let opcode = instruction & 0xf000;
        let address = instruction & 0x0fff;
        let x = ((instruction & 0x0f00) >> 8) as usize;
        let y = ((instruction & 0x00f0) >> 4) as usize;
        let nibble = (instruction & 0x000f) as u8;
        let byte = (instruction & 0x00ff) as u8;

        match opcode {
            0x0000 => match byte {
                0xe0 => self.clear_screen(),
                0xee => {
                    self.sp -= 1;
                    self.pc = self.stack[self.sp as usize];
                }
                _ => panic!(
                    "unimplemented instruction 0x0000, instruction 0x{:04x}",
                    instruction
                ),
            },
            0x1000 => self.pc = address,
            0x2000 => {
                self.stack[self.sp as usize] = self.pc;
                self.sp += 1;
                self.pc = address;
            }
            0x3000 => self.pc += if self.regs[x] == byte { 2 } else { 0 },
            0x4000 => self.pc += if self.regs[x] != byte { 2 } else { 0 },
            0x5000 => self.pc += if self.regs[x] == self.regs[y] { 2 } else { 0 },
            0x6000 => self.regs[x] = byte,
            0x7000 => self.regs[x] = self.regs[x].wrapping_add(byte),
            0x8000 => match nibble {
                0x0 => self.regs[x] = self.regs[y],
                0x1 => {
                    self.regs[x] |= self.regs[y];
                    if self.quirks.vf_reset {
                        self.regs[0xf] = 0;
                    }
                }
                0x2 => {
                    self.regs[x] &= self.regs[y];
                    if self.quirks.vf_reset {
                        self.regs[0xf] = 0;
                    }
                }
                0x3 => {
                    self.regs[x] ^= self.regs[y];
                    if self.quirks.vf_reset {
                        self.regs[0xf] = 0;
                    }
                }
                0x4 => {
                    let (result, overflow) = self.regs[x].overflowing_add(self.regs[y]);
                    self.regs[x] = result;
                    self.regs[0xf] = overflow as u8;
                }
                0x5 => {
                    self.regs[0xf] = (self.regs[x] > self.regs[y]) as u8;
                    self.regs[x] = self.regs[x].wrapping_sub(self.regs[y]);
                }
                0x6 => {
                    self.regs[0xf] = self.regs[x] & 0x01;
                    if self.quirks.shifting {
                        self.regs[x] >>= 1;
                    } else {
                        self.regs[x] = self.regs[y] >> 1;
                    }
                }
                0x7 => {
                    self.regs[0xf] = (self.regs[y] > self.regs[x]) as u8;
                    self.regs[x] = self.regs[y].wrapping_sub(self.regs[x]);
                }
                0xe => {
                    self.regs[0xf] = (self.regs[x] & 0x80) >> 7;
                    if self.quirks.shifting {
                        self.regs[x] <<= 1;
                    } else {
                        self.regs[x] = self.regs[y] << 1;
                    }
                }
                _ => panic!(
                    "unimplemented instruction 0x8000, instruction 0x{:04x}",
                    instruction
                ),
            },
            0x9000 => self.pc += if self.regs[x] != self.regs[y] { 2 } else { 0 },
            0xa000 => self.i = address,
            0xb000 => {
                if self.quirks.jumping {
                    self.pc = address + self.regs[x] as u16;
                } else {
                    self.pc = address + self.regs[0x0] as u16;
                }
            }
            0xc000 => self.regs[x] = byte & random(),
            0xd000 => {
                self.draw_sprite(
                    self.i as usize,
                    self.regs[x] as usize,
                    self.regs[y] as usize,
                    nibble as usize,
                );
            }
            0xe000 => match byte {
                0x9e => {
                    let key = self.regs[x] as usize;
                    self.pc += if self.keys[key] { 2 } else { 0 }
                }
                0xa1 => {
                    let key = self.regs[x] as usize;
                    self.pc += if !self.keys[key] { 2 } else { 0 }
                }
                _ => panic!(
                    "unimplemented instruction 0xe000, instruction 0x{:04x}",
                    instruction
                ),
            },
            0xf000 => match byte {
                0x07 => self.regs[x] = self.dt,
                0x0a => {
                    if self.keys[self.last_key as usize] {
                        self.regs[x] = self.last_key;
                    } else {
                        self.pc -= 2
                    }
                }
                0x15 => self.dt = self.regs[x],
                0x18 => self.st = self.regs[x],
                0x1e => self.i = self.i.saturating_add(self.regs[x] as u16),
                0x29 => self.i = self.regs[x] as u16 * 5,
                0x33 => {
                    self.ram[self.i as usize] = self.regs[x] / 100;
                    self.ram[self.i as usize + 1] = (self.regs[x] / 10) % 10;
                    self.ram[self.i as usize + 2] = self.regs[x] % 10;
                }
                0x55 => {
                    self.ram[self.i as usize..self.i as usize + x + 1]
                        .clone_from_slice(&self.regs[0..x + 1]);
                    if self.quirks.memory {
                        self.i = self.i.saturating_add(1);
                    }
                }
                0x65 => {
                    self.regs[0..x + 1]
                        .clone_from_slice(&self.ram[self.i as usize..self.i as usize + x + 1]);
                    if self.quirks.memory {
                        self.i = self.i.saturating_add(1);
                    }
                }
                _ => panic!(
                    "unimplemented instruction 0xf000, instruction 0x{:04x}",
                    instruction
                ),
            },
            _ => panic!(
                "unimplemented opcode 0x{:04x}, instruction 0x{:04x}",
                opcode, instruction
            ),
        }
    }

    fn clock_dt(&mut self) {
        self.dt = self.dt.saturating_sub(1)
    }

    fn clock_st(&mut self) {
        self.st = self.st.saturating_sub(1)
    }

    fn key_press(&mut self, key: u8) {
        if key >= KEYS_SIZE as u8 {
            return;
        }
        self.keys[key as usize] = true;
        self.last_key = key;
    }

    fn key_lift(&mut self, key: u8) {
        if key >= KEYS_SIZE as u8 {
            return;
        }
        self.keys[key as usize] = false;
    }

    fn vblank(&mut self) {
        match self.wait_vblank {
            WaitVblank::Waiting => {
                self.wait_vblank = WaitVblank::Vblank;
                self.paused = false;
            }
            _ => {}
        }
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Chip8Neo {
    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
    pub fn new() -> Chip8Neo {
        let mut chip8 = Chip8Neo {
            ram: [0u8; RAM_SIZE],
            vram: [0u8; DISPLAY_WIDTH * DISPLAY_HEIGHT],
            stack: [0u16; STACK_SIZE],
            regs: [0u8; REGISTERS_SIZE],
            pc: ROM_START as u16,
            i: 0x0,
            sp: 0x0,
            dt: 0x0,
            st: 0x0,
            keys: [false; KEYS_SIZE],
            last_key: 0x0,
            paused: false,
            wait_vblank: WaitVblank::NotWaiting,
            quirks: QuirkFlags {
                vf_reset: true,
                memory: true,
                display_blank: false,
                clipping: true,
                shifting: false,
                jumping: false,
            },
        };
        chip8.load_default_font();
        chip8
    }

    fn load_font(&mut self, position: usize, font_set: &[u8]) {
        self.ram[position..position + font_set.len()].clone_from_slice(&font_set);
    }

    fn load_default_font(&mut self) {
        self.load_font(0, &FONT_SET);
    }

    #[inline(always)]
    fn clear_screen(&mut self) {
        self.vram = [0u8; DISPLAY_WIDTH * DISPLAY_HEIGHT];
    }

    #[inline(always)]
    fn draw_sprite(&mut self, addr: usize, x0: usize, y0: usize, height: usize) {
        if self.quirks.display_blank && self.wait_vblank != WaitVblank::Vblank {
            self.pause_vblank();
            return;
        }
        self.wait_vblank = WaitVblank::NotWaiting;
        self.regs[0xf] = 0;
        for y in 0..height {
            let line_byte = self.ram[(addr + y)];
            for x in 0..8 {
                if line_byte & (0x80 >> x) == 0 {
                    continue;
                }
                let yf;
                if self.quirks.clipping {
                    yf = y0 + y;
                    if yf >= DISPLAY_HEIGHT {
                        continue;
                    }
                } else {
                    yf = (y0 + y) % DISPLAY_HEIGHT;
                }
                let xf = (x0 + x) % DISPLAY_WIDTH;
                let addr = yf * DISPLAY_WIDTH + xf;
                if self.vram[addr] == 1 {
                    self.regs[0xf] = 1;
                }
                self.vram[addr] ^= 1
            }
        }
    }

    fn pause_vblank(&mut self) {
        self.paused = true;
        self.wait_vblank = WaitVblank::Waiting;
        self.pc -= 2;
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Chip8Neo {
    pub fn load_rom_ws(&mut self, rom: &[u8]) {
        self.load_rom(rom)
    }

    pub fn reset_ws(&mut self) {
        self.reset()
    }

    pub fn reset_hard_ws(&mut self) {
        self.reset_hard()
    }

    pub fn pause_ws(&mut self) {
        self.pause()
    }

    pub fn paused_ws(&mut self) -> bool {
        self.paused()
    }

    pub fn beep_ws(&self) -> bool {
        self.beep()
    }

    pub fn vram_ws(&self) -> Vec<u8> {
        self.vram()
    }

    pub fn clock_ws(&mut self) {
        self.clock()
    }

    pub fn clock_dt_ws(&mut self) {
        self.clock_dt()
    }

    pub fn clock_st_ws(&mut self) {
        self.clock_st()
    }

    pub fn key_press_ws(&mut self, key: u8) {
        self.key_press(key)
    }

    pub fn key_lift_ws(&mut self, key: u8) {
        self.key_lift(key)
    }

    pub fn vblank_ws(&mut self) {
        self.vblank()
    }
}

impl Default for Chip8Neo {
    fn default() -> Chip8Neo {
        Chip8Neo::new()
    }
}