use crate::{
    apu::Apu,
    cpu::Cpu,
    data::{BootRom, CGB_BOOT, DMG_BOOT, DMG_BOOTIX, MGB_BOOTIX, SGB_BOOT},
    devices::{printer::PrinterDevice, stdout::StdoutDevice},
    gen::{COMPILATION_DATE, COMPILATION_TIME, COMPILER, COMPILER_VERSION},
    mmu::Mmu,
    pad::{Pad, PadKey},
    ppu::{Ppu, PpuMode, Tile, FRAME_BUFFER_SIZE},
    rom::Cartridge,
    serial::Serial,
    timer::Timer,
    util::read_file,
};

use std::collections::VecDeque;

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

#[cfg(feature = "wasm")]
use crate::{
    gen::dependencies_map,
    ppu::{Palette, Pixel},
};

#[cfg(feature = "wasm")]
use std::{
    convert::TryInto,
    panic::{set_hook, take_hook, PanicInfo},
};

/// Top level structure that abstracts the usage of the
/// Game Boy system under the Boytacean emulator.
/// Should serve as the main entry-point API.
#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct GameBoy {
    cpu: Cpu,

    /// If the PPU is enabled, it will be clocked.
    ppu_enabled: bool,

    /// If the APU is enabled, it will be clocked.
    apu_enabled: bool,

    /// If the timer is enabled, it will be clocked.
    timer_enabled: bool,

    /// If the serial is enabled, it will be clocked.
    serial_enabled: bool,

    /// The current frequency at which the Game Boy
    /// emulator is being handled. This is a "hint" that
    /// may help components to adjust their internal
    /// logic to match the current frequency. For example
    /// the APU will adjust its internal clock to match
    /// this hint.
    clock_freq: u32,
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub struct Registers {
    pub pc: u16,
    pub sp: u16,
    pub a: u8,
    pub b: u8,
    pub c: u8,
    pub d: u8,
    pub e: u8,
    pub h: u8,
    pub l: u8,
    pub scy: u8,
    pub scx: u8,
    pub wy: u8,
    pub wx: u8,
    pub ly: u8,
    pub lyc: u8,
}

pub trait AudioProvider {
    fn audio_output(&self) -> u8;
    fn audio_buffer(&self) -> &VecDeque<u8>;
    fn clear_audio_buffer(&mut self);
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy {
    #[cfg_attr(feature = "wasm", wasm_bindgen(constructor))]
    pub fn new() -> Self {
        let ppu = Ppu::default();
        let apu = Apu::default();
        let pad = Pad::default();
        let timer = Timer::default();
        let serial = Serial::default();
        let mmu = Mmu::new(ppu, apu, pad, timer, serial);
        let cpu = Cpu::new(mmu);
        Self {
            cpu,
            ppu_enabled: true,
            apu_enabled: true,
            timer_enabled: true,
            serial_enabled: true,
            clock_freq: GameBoy::CPU_FREQ,
        }
    }

    pub fn reset(&mut self) {
        self.ppu().reset();
        self.apu().reset();
        self.timer().reset();
        self.serial().reset();
        self.mmu().reset();
        self.cpu.reset();
    }

    pub fn clock(&mut self) -> u8 {
        let cycles = self.cpu_clock();
        if self.ppu_enabled {
            self.ppu_clock(cycles);
        }
        if self.apu_enabled {
            self.apu_clock(cycles);
        }
        if self.timer_enabled {
            self.timer_clock(cycles);
        }
        if self.serial_enabled {
            self.serial_clock(cycles);
        }
        cycles
    }

    pub fn key_press(&mut self, key: PadKey) {
        self.pad().key_press(key);
    }

    pub fn key_lift(&mut self, key: PadKey) {
        self.pad().key_lift(key);
    }

    pub fn cpu_clock(&mut self) -> u8 {
        self.cpu.clock()
    }

    pub fn ppu_clock(&mut self, cycles: u8) {
        self.ppu().clock(cycles)
    }

    pub fn apu_clock(&mut self, cycles: u8) {
        self.apu().clock(cycles)
    }

    pub fn timer_clock(&mut self, cycles: u8) {
        self.timer().clock(cycles)
    }

    pub fn serial_clock(&mut self, cycles: u8) {
        self.serial().clock(cycles)
    }

    pub fn ppu_ly(&mut self) -> u8 {
        self.ppu().ly()
    }

    pub fn ppu_mode(&mut self) -> PpuMode {
        self.ppu().mode()
    }

    pub fn ppu_frame(&mut self) -> u16 {
        self.ppu().frame_index()
    }

    pub fn boot(&mut self) {
        self.cpu.boot();
    }

    pub fn load_boot(&mut self, data: &[u8]) {
        self.cpu.mmu().write_boot(0x0000, data);
    }

    pub fn load_boot_static(&mut self, boot_rom: BootRom) {
        match boot_rom {
            BootRom::Dmg => self.load_boot(&DMG_BOOT),
            BootRom::Sgb => self.load_boot(&SGB_BOOT),
            BootRom::DmgBootix => self.load_boot(&DMG_BOOTIX),
            BootRom::MgbBootix => self.load_boot(&MGB_BOOTIX),
            BootRom::Cgb => self.load_boot(&CGB_BOOT),
        }
    }

    pub fn load_boot_default(&mut self) {
        self.load_boot_dmg();
    }

    pub fn load_boot_dmg(&mut self) {
        self.load_boot_static(BootRom::DmgBootix);
    }

    pub fn load_boot_cgb(&mut self) {
        self.load_boot_static(BootRom::Cgb);
    }

    pub fn vram_eager(&mut self) -> Vec<u8> {
        self.ppu().vram().to_vec()
    }

    pub fn hram_eager(&mut self) -> Vec<u8> {
        self.ppu().vram().to_vec()
    }

    pub fn frame_buffer_eager(&mut self) -> Vec<u8> {
        self.frame_buffer().to_vec()
    }

    pub fn audio_buffer_eager(&mut self, clear: bool) -> Vec<u8> {
        let buffer = Vec::from(self.audio_buffer().clone());
        if clear {
            self.clear_audio_buffer();
        }
        buffer
    }

    pub fn audio_output(&self) -> u8 {
        self.apu_i().output()
    }

    pub fn audio_all_output(&self) -> Vec<u8> {
        vec![
            self.audio_output(),
            self.audio_ch1_output(),
            self.audio_ch2_output(),
            self.audio_ch3_output(),
            self.audio_ch4_output(),
        ]
    }

    pub fn audio_ch1_output(&self) -> u8 {
        self.apu_i().ch1_output()
    }

    pub fn audio_ch2_output(&self) -> u8 {
        self.apu_i().ch2_output()
    }

    pub fn audio_ch3_output(&self) -> u8 {
        self.apu_i().ch3_output()
    }

    pub fn audio_ch4_output(&self) -> u8 {
        self.apu_i().ch4_output()
    }

    pub fn cartridge_eager(&mut self) -> Cartridge {
        self.mmu().rom().clone()
    }

    pub fn ram_data_eager(&mut self) -> Vec<u8> {
        self.mmu().rom().ram_data_eager()
    }

    pub fn set_ram_data(&mut self, ram_data: Vec<u8>) {
        self.mmu().rom().set_ram_data(ram_data)
    }

    pub fn registers(&mut self) -> Registers {
        let ppu_registers = self.ppu().registers();
        Registers {
            pc: self.cpu.pc,
            sp: self.cpu.sp,
            a: self.cpu.a,
            b: self.cpu.b,
            c: self.cpu.c,
            d: self.cpu.d,
            e: self.cpu.e,
            h: self.cpu.h,
            l: self.cpu.l,
            scy: ppu_registers.scy,
            scx: ppu_registers.scx,
            wy: ppu_registers.wy,
            wx: ppu_registers.wx,
            ly: ppu_registers.ly,
            lyc: ppu_registers.lyc,
        }
    }

    /// Obtains the tile structure for the tile at the
    /// given index, no conversion in the pixel buffer
    /// is done so that the color reference is the GB one.
    pub fn get_tile(&mut self, index: usize) -> Tile {
        self.ppu().tiles()[index]
    }

    /// Obtains the pixel buffer for the tile at the
    /// provided index, converting the color buffer
    /// using the currently loaded palette.
    pub fn get_tile_buffer(&mut self, index: usize) -> Vec<u8> {
        let tile = self.get_tile(index);
        tile.palette_buffer(self.ppu().palette())
    }

    /// Obtains the name of the compiler that has been
    /// used in the compilation of the base Boytacean
    /// library. Can be used for diagnostics.
    pub fn compiler(&self) -> String {
        String::from(COMPILER)
    }

    pub fn compiler_version(&self) -> String {
        String::from(COMPILER_VERSION)
    }

    pub fn compilation_date(&self) -> String {
        String::from(COMPILATION_DATE)
    }

    pub fn compilation_time(&self) -> String {
        String::from(COMPILATION_TIME)
    }

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

    pub fn set_ppu_enabled(&mut self, value: bool) {
        self.ppu_enabled = value;
    }

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

    pub fn set_apu_enabled(&mut self, value: bool) {
        self.apu_enabled = value;
    }

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

    pub fn set_timer_enabled(&mut self, value: bool) {
        self.timer_enabled = value;
    }

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

    pub fn set_serial_enabled(&mut self, value: bool) {
        self.serial_enabled = value;
    }

    pub fn clock_freq(&self) -> u32 {
        self.clock_freq
    }

    pub fn set_clock_freq(&mut self, value: u32) {
        self.clock_freq = value;
        self.apu().set_clock_freq(value);
    }

    pub fn attach_stdout_serial(&mut self) {
        self.serial().set_device(Box::<StdoutDevice>::default());
    }

    pub fn attach_printer_serial(&mut self) {
        self.serial().set_device(Box::<PrinterDevice>::default());
    }
}

/// Gameboy implementations that are meant with performance
/// in mind and that do not support WASM interface of copy.
impl GameBoy {
    /// The logic frequency of the Game Boy
    /// CPU in hz.
    pub const CPU_FREQ: u32 = 4194304;

    /// The visual frequency (refresh rate)
    /// of the Game Boy, close to 60 hz.
    pub const VISUAL_FREQ: f32 = 59.7275;

    /// The cycles taken to run a complete frame
    /// loop in the Game Boy's PPU (in CPU cycles).
    pub const LCD_CYCLES: u32 = 70224;

    pub fn cpu(&mut self) -> &mut Cpu {
        &mut self.cpu
    }

    pub fn mmu(&mut self) -> &mut Mmu {
        self.cpu.mmu()
    }

    pub fn ppu(&mut self) -> &mut Ppu {
        self.cpu.ppu()
    }

    pub fn apu(&mut self) -> &mut Apu {
        self.cpu.apu()
    }

    pub fn apu_i(&self) -> &Apu {
        self.cpu.apu_i()
    }

    pub fn pad(&mut self) -> &mut Pad {
        self.cpu.pad()
    }

    pub fn timer(&mut self) -> &mut Timer {
        self.cpu.timer()
    }

    pub fn serial(&mut self) -> &mut Serial {
        self.cpu.serial()
    }

    pub fn frame_buffer(&mut self) -> &[u8; FRAME_BUFFER_SIZE] {
        &(self.ppu().frame_buffer)
    }

    pub fn audio_buffer(&mut self) -> &VecDeque<u8> {
        self.apu().audio_buffer()
    }

    pub fn load_boot_path(&mut self, path: &str) {
        let data = read_file(path);
        self.load_boot(&data);
    }

    pub fn load_boot_file(&mut self, boot_rom: BootRom) {
        match boot_rom {
            BootRom::Dmg => self.load_boot_path("./res/boot/dmg_boot.bin"),
            BootRom::Sgb => self.load_boot_path("./res/boot/sgb_boot.bin"),
            BootRom::DmgBootix => self.load_boot_path("./res/boot/dmg_bootix.bin"),
            BootRom::MgbBootix => self.load_boot_path("./res/boot/mgb_bootix.bin"),
            BootRom::Cgb => self.load_boot_path("./res/boot/cgb_boot.bin"),
        }
    }

    pub fn load_boot_default_f(&mut self) {
        self.load_boot_dmg_f();
    }

    pub fn load_boot_dmg_f(&mut self) {
        self.load_boot_file(BootRom::DmgBootix);
    }

    pub fn load_boot_cgb_f(&mut self) {
        self.load_boot_file(BootRom::Cgb);
    }

    pub fn load_rom(&mut self, data: &[u8]) -> &Cartridge {
        let rom = Cartridge::from_data(data);
        self.mmu().set_rom(rom);
        self.mmu().rom()
    }

    pub fn load_rom_file(&mut self, path: &str) -> &Cartridge {
        let data = read_file(path);
        self.load_rom(&data)
    }
}

#[cfg(feature = "wasm")]
#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl GameBoy {
    pub fn set_panic_hook_ws() {
        let prev = take_hook();
        set_hook(Box::new(move |info| {
            hook_impl(info);
            prev(info);
        }));
    }

    pub fn load_rom_ws(&mut self, data: &[u8]) -> Cartridge {
        self.load_rom(data).clone()
    }

    pub fn set_palette_colors_ws(&mut self, value: Vec<JsValue>) {
        let palette: Palette = value
            .into_iter()
            .map(|v| Self::js_to_pixel(&v))
            .collect::<Vec<Pixel>>()
            .try_into()
            .unwrap();
        self.ppu().set_palette_colors(&palette);
    }

    pub fn wasm_engine_ws(&self) -> Option<String> {
        let dependencies = dependencies_map();
        if !dependencies.contains_key("wasm-bindgen") {
            return None;
        }
        Some(String::from(format!(
            "wasm-bindgen/{}",
            *dependencies.get("wasm-bindgen").unwrap()
        )))
    }

    fn js_to_pixel(value: &JsValue) -> Pixel {
        value
            .as_string()
            .unwrap()
            .chars()
            .collect::<Vec<char>>()
            .chunks(2)
            .map(|s| s.iter().collect::<String>())
            .map(|s| u8::from_str_radix(&s, 16).unwrap())
            .collect::<Vec<u8>>()
            .try_into()
            .unwrap()
    }
}

#[cfg(feature = "wasm")]
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = window)]
    fn panic(message: &str);
}

#[cfg(feature = "wasm")]
pub fn hook_impl(info: &PanicInfo) {
    let message = info.to_string();
    panic(message.as_str());
}

impl AudioProvider for GameBoy {
    fn audio_output(&self) -> u8 {
        self.apu_i().output()
    }

    fn audio_buffer(&self) -> &VecDeque<u8> {
        self.apu_i().audio_buffer()
    }

    fn clear_audio_buffer(&mut self) {
        self.apu().clear_audio_buffer()
    }
}

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