//! MMU (Memory Management Unit) functions and structures.

use boytacean_common::util::SharedThread;
use std::sync::Mutex;

use crate::{
    apu::Apu,
    assert_pedantic_gb,
    dma::Dma,
    gb::{Components, GameBoyConfig, GameBoyMode, GameBoySpeed},
    pad::Pad,
    panic_gb,
    ppu::Ppu,
    rom::Cartridge,
    serial::Serial,
    timer::Timer,
    warnln,
};

pub const BOOT_SIZE_DMG: usize = 256;
pub const BOOT_SIZE_CGB: usize = 2304;

pub const RAM_SIZE_DMG: usize = 8192;
pub const RAM_SIZE_CGB: usize = 32768;

pub trait BusComponent {
    fn read(&self, addr: u16) -> u8;
    fn write(&mut self, addr: u16, value: u8);
    fn read_many(&self, addr: u16, count: usize) -> Vec<u8> {
        (0..count)
            .map(|offset| self.read(addr + offset as u16))
            .collect()
    }
    fn write_many(&mut self, addr: u16, values: &[u8]) {
        for (offset, &value) in values.iter().enumerate() {
            self.write(addr + offset as u16, value);
        }
    }
}

pub struct Mmu {
    /// Register that controls the interrupts that are considered
    /// to be enabled and should be triggered.
    pub ie: u8,

    /// Register that controls the compatibility mode in use, this
    /// value comes directly from 0x0143 (CGB flag). The possible (and
    /// valid) values are: 0x80 for games that support CGB enhancements
    /// and 0xC0 for games that are compatible only with a CGB device
    /// (CGB only).
    pub key0: u8,

    /// Flags that controls if the system is currently in the process
    /// of switching between the double and single speed modes.
    pub switching: bool,

    /// The speed (frequency) at which the system is currently running,
    /// it may be either normal (4.194304 MHz) or double (8.388608 MHz).
    speed: GameBoySpeed,

    /// Callback to be called when the speed of the system changes, it
    /// should provide visibility over the current speed of the system.
    speed_callback: fn(speed: GameBoySpeed),

    /// Reference to the PPU (Pixel Processing Unit) that is going
    /// to be used both for VRAM reading/writing and to forward
    /// some of the access operations.
    ppu: Ppu,

    /// Reference to the APU (Audio Processing Unit) that is going
    /// to be used both for register reading/writing and to forward
    /// some of the access operations.
    apu: Apu,

    /// Reference to the DMA (Direct Memory Access) controller that is going
    /// to be used for quick and CPU offloaded memory transfers.
    /// There are multiple execution modes for the DMA.
    dma: Dma,

    /// Reference to the Gamepad structure that is going to control
    /// the I/O access to this device.
    pad: Pad,

    /// The timer controller to be used as part of the I/O access
    /// that is memory mapped.
    timer: Timer,

    /// The serial data transfer controller to be used to control the
    /// link cable connection, this component is memory mapped.
    serial: Serial,

    /// The cartridge ROM that is currently loaded into the system,
    /// going to be used to access ROM and external RAM banks.
    rom: Cartridge,

    /// Flag that control the access to the boot section in the
    /// 0x0000-0x00FE memory area, this flag should be unset after
    /// the boot sequence has been finished.
    boot_active: bool,

    /// Buffer to be used to store the boot ROM, this is the code
    /// that is going to be executed at the beginning of the Game
    /// Boy execution. The buffer effectively used is of 256 bytes
    /// for the "normal" Game Boy (MGB) and 2308 bytes for the
    /// Game Boy Color (CGB). Note that in the case of the CGB
    /// the BIOS which is 2308 bytes long is in fact only 2048 bytes
    /// as the 256 bytes in range 0x0100-0x01FF are meant to be
    /// overwritten byte the cartridge header.
    boot: Vec<u8>,

    /// Buffer that is used to store the RAM of the system, this
    /// value varies between DMG and CGB emulation, being 8KB for
    /// the DMG and 32KB for the CGB. Mapped in range 0xC000-0xDFFF.
    ram: Vec<u8>,

    /// The RAM bank to be used in the read and write operation of
    /// the 0xD000-0xDFFF memory range (CGB only).
    ram_bank: u8,

    /// The offset to be used in the read and write operation of
    /// the RAM, this value should be consistent with the RAM bank
    /// that is currently selected (CGB only).
    ram_offset: u16,

    /// The current running mode of the emulator, this
    /// may affect many aspects of the emulation.
    mode: GameBoyMode,

    /// The pointer to the parent configuration of the running
    /// Game Boy emulator, that can be used to control the behaviour
    /// of Game Boy emulation.
    gbc: SharedThread<GameBoyConfig>,
}

impl Mmu {
    pub fn new(
        components: Components,
        mode: GameBoyMode,
        gbc: SharedThread<GameBoyConfig>,
    ) -> Self {
        Self {
            ppu: components.ppu,
            apu: components.apu,
            dma: components.dma,
            pad: components.pad,
            timer: components.timer,
            serial: components.serial,
            rom: Cartridge::new(),
            boot_active: true,
            boot: vec![],
            ram: vec![],
            ram_bank: 0x1,
            ram_offset: 0x1000,
            ie: 0x0,
            key0: 0x0,
            speed: GameBoySpeed::Normal,
            switching: false,
            speed_callback: |_| {},
            mode,
            gbc,
        }
    }

    pub fn reset(&mut self) {
        self.rom = Cartridge::new();
        self.boot_active = true;
        self.boot = vec![];
        self.ram = vec![];
        self.ram_bank = 0x1;
        self.ram_offset = 0x1000;
        self.ie = 0x0;
        self.key0 = 0x0;
        self.speed = GameBoySpeed::Normal;
        self.switching = false;
    }

    pub fn allocate_default(&mut self) {
        self.allocate_dmg();
    }

    pub fn allocate_dmg(&mut self) {
        self.boot = vec![0x00; BOOT_SIZE_DMG];
        self.ram = vec![0x00; RAM_SIZE_DMG];
    }

    pub fn allocate_cgb(&mut self) {
        self.boot = vec![0x00; BOOT_SIZE_CGB];
        self.ram = vec![0x00; RAM_SIZE_CGB];
    }

    /// Notifies the system that a VBlank interrupt has been
    /// triggered, would usually be the perfect time to update
    /// some of the internal memory structures.
    pub fn vblank(&mut self) {
        let writes = self.rom.vblank();
        if let Some(writes) = writes {
            for (base_addr, addr, value) in writes {
                match base_addr {
                    0xa000 => self.rom.ram_data_mut()[addr as usize] = value,
                    0xc000 => self.ram[addr as usize] = value,
                    _ => panic_gb!("Invalid base address for write: 0x{:04x}", base_addr),
                }
            }
        }
    }

    /// Switches the current system's speed toggling between
    /// the normal and double speed modes.
    pub fn switch_speed(&mut self) {
        self.speed = self.speed.switch();
        self.switching = false;
        (self.speed_callback)(self.speed);
    }

    pub fn speed(&self) -> GameBoySpeed {
        self.speed
    }

    pub fn set_speed(&mut self, value: GameBoySpeed) {
        self.speed = value;
    }

    pub fn set_speed_callback(&mut self, callback: fn(speed: GameBoySpeed)) {
        self.speed_callback = callback;
    }

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

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

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

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

    pub fn dma(&mut self) -> &mut Dma {
        &mut self.dma
    }

    pub fn dma_i(&self) -> &Dma {
        &self.dma
    }

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

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

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

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

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

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

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

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

    pub fn clock_dma(&mut self, cycles: u16) {
        if !self.dma.active() {
            return;
        }

        if self.dma.active_dma() {
            let cycles_dma = self.dma.cycles_dma().saturating_sub(cycles);
            if cycles_dma == 0x0 {
                let data = self.read_many((self.dma.value_dma() as u16) << 8, 160);
                self.write_many(0xfe00, &data);
                self.dma.set_active_dma(false);
            }
            self.dma.set_cycles_dma(cycles_dma);
        }

        // @TODO: Implement DMA transfer in a better way, meaning that
        // the DMA transfer should respect the timings
        //
        // In both Normal Speed and Double Speed Mode it takes about 8 μs
        // to transfer a block of $10 bytes. That is, 8 M-cycles in Normal
        // Speed Mode [1], and 16 “fast” M-cycles in Double Speed Mode [2].
        // Older MBC controllers (like MBC1-3) and slower ROMs are not guaranteed
        // to support General Purpose or HBlank DMA, that’s because there are
        // always 2 bytes transferred per microsecond (even if the itself
        // program runs it Normal Speed Mode).
        //
        // we should also respect General-Purpose DMA vs HBlank DMA
        if self.dma.active_hdma() {
            // runs a series of pre-validation on the HDMA transfer in
            // pedantic mode is currently active (performance hit)
            assert_pedantic_gb!(
                (0x0000..=0x7ff0).contains(&self.dma.source())
                    || (0xa000..=0xdff0).contains(&self.dma.source()),
                "Invalid HDMA source start memory address 0x{:04x}",
                self.dma.source()
            );
            assert_pedantic_gb!(
                (0x8000..=0x9ff0).contains(&self.dma.destination()),
                "Invalid HDMA destination start memory address 0x{:04x}",
                self.dma.destination()
            );

            // only runs the DMA transfer if the system is in CGB mode
            // this avoids issues when writing to DMG unmapped registers
            // that would otherwise cause the system to crash
            if self.mode == GameBoyMode::Cgb {
                let data = self.read_many(self.dma.source(), self.dma.pending());
                self.write_many(self.dma.destination(), &data);
            }
            self.dma.set_pending(0);
            self.dma.set_active_hdma(false);
        }
    }

    pub fn read(&self, addr: u16) -> u8 {
        match addr {
            // 0x0000-0x0FFF - BOOT (256 B) + ROM0 (4 KB/16 KB)
            0x0000..=0x0fff => {
                // in case the boot mode is active and the
                // address is withing boot memory reads from it
                if self.boot_active && addr <= 0x00ff {
                    return self.boot[addr as usize];
                }
                if self.boot_active
                    && self.mode == GameBoyMode::Cgb
                    && (0x0200..=0x08ff).contains(&addr)
                {
                    return self.boot[addr as usize];
                }
                self.rom.read(addr)
            }

            // 0x1000-0x3FFF - ROM 0 (12 KB/16 KB)
            // 0x4000-0x7FFF - ROM 1 (Banked) (16 KB)
            0x1000..=0x7fff => self.rom.read(addr),

            // 0x8000-0x9FFF - Graphics: VRAM (8 KB)
            0x8000..=0x9fff => self.ppu.read(addr),

            // 0xA000-0xBFFF - External RAM (8 KB)
            0xa000..=0xbfff => self.rom.read(addr),

            // 0xC000-0xCFFF - Working RAM 0 (4 KB)
            0xc000..=0xcfff => self.ram[(addr & 0x0fff) as usize],

            // 0xD000..=0xDFFF - Working RAM 1 (Banked) (4KB)
            0xd000..=0xdfff => self.ram[(self.ram_offset + (addr & 0x0fff)) as usize],

            // 0xE000..=0xFDFF - Working RAM Shadow
            0xe000..=0xfdff => self.ram[(addr & 0x1fff) as usize],

            // 0xFE00-0xFE9F - Object attribute memory (OAM)
            0xfe00..=0xfe9f => self.ppu.read(addr),

            // 0xFEA0-0xFEFF - Not Usable
            0xfea0..=0xfeff => 0xff,

            // 0xFF00 - Joypad input
            0xff00 => self.pad.read(addr),

            // 0xFF01-0xFF02 - Serial data transfer
            0xff01..=0xff02 => self.serial.read(addr),

            // 0xFF04-0xFF07 - Timer and divider
            0xff04..=0xff07 => self.timer.read(addr),

            // 0xFF0F — IF: Interrupt flag
            0xff0f =>
            {
                #[allow(clippy::bool_to_int_with_if)]
                (if self.ppu.int_vblank() { 0x01 } else { 0x00 }
                    | if self.ppu.int_stat() { 0x02 } else { 0x00 }
                    | if self.timer.int_tima() { 0x04 } else { 0x00 }
                    | if self.serial.int_serial() { 0x08 } else { 0x00 }
                    | if self.pad.int_pad() { 0x10 } else { 0x00 }
                    | 0xe0)
            }

            // 0xFF10-0xFF26 — Audio
            // 0xFF10-0xFF26 — Wave pattern
            0xff10..=0xff26 | 0xff30..=0xff3f => self.apu.read(addr),

            // 0xFF40-0xFF45 - PPU registers
            // 0xFF47-0xFF4B - PPU registers
            0xff40..=0xff45 | 0xff47..=0xff4b => self.ppu.read(addr),

            // 0xFF46 — DMA: OAM DMA source address & start
            0xff46 => self.dma.read(addr),

            // 0xFF4C - KEY0: Compatibility flag (CGB only)
            0xff4c => self.key0,

            // 0xFF4D - KEY1: Speed switching (CGB only)
            0xff4d => (if self.switching { 0x01 } else { 0x00 }) | ((self.speed as u8) << 7) | 0x7e,

            // 0xFF4F - VRAM Bank Select (CGB only)
            0xff4f => self.ppu.read(addr),

            // 0xFF50 - BOOT: Boot active flag
            0xff50 => u8::from(!self.boot_active),

            // 0xFF51-0xFF55 - VRAM DMA (HDMA) (CGB only)
            0xff51..=0xff55 => self.dma.read(addr),

            // 0xFF56 - RP: Infrared communications port (CGB only)
            0xff56 => 0xff,

            // 0xFF68-0xFF6B - BG / OBJ Palettes (CGB only)
            0xff68..=0xff6b => self.ppu.read(addr),

            // 0xFF70 - SVBK: WRAM bank (CGB only)
            0xff70 => (self.ram_bank & 0x07) | 0xf8,

            // 0xFF80-0xFFFE - High RAM (HRAM)
            0xff80..=0xfffe => self.ppu.read(addr),

            // 0xFFFF — IE: Interrupt enable
            0xffff => self.ie,

            addr => {
                warnln!("Reading from unknown location 0x{:04x}", addr);
                #[allow(unreachable_code)]
                0xff
            }
        }
    }

    pub fn write(&mut self, addr: u16, value: u8) {
        match addr {
            // 0x0000-0x0FFF - BOOT (256 B) + ROM0 (4 KB/16 KB)
            // 0x1000-0x3FFF - ROM 0 (12 KB/16 KB)
            // 0x4000-0x7FFF - ROM 1 (Banked) (16 KB)
            0x0000..=0x7fff => self.rom.write(addr, value),

            // 0x8000-0x9FFF - Graphics: VRAM (8 KB)
            0x8000..=0x9fff => self.ppu.write(addr, value),

            // 0xA000-0xBFFF - External RAM (8 KB)
            0xa000..=0xbfff => self.rom.write(addr, value),

            // 0xC000-0xCFFF - Working RAM 0 (4 KB)
            0xc000..=0xcfff => self.ram[(addr & 0x0fff) as usize] = value,

            // 0xD000..=0xDFFF - Working RAM 1 (Banked) (4KB)
            0xd000..=0xdfff => self.ram[(self.ram_offset + (addr & 0x0fff)) as usize] = value,

            // 0xE000..=0xFDFF - Working RAM Shadow
            0xe000..=0xfdff => self.ram[(addr & 0x1fff) as usize] = value,

            // 0xFE00-0xFE9F - Object attribute memory (OAM)
            0xfe00..=0xfe9f => self.ppu.write(addr, value),

            // 0xFEA0-0xFEFF - Not Usable
            0xfea0..=0xfeff => {}

            // 0xFF00 - Joypad input
            0xff00 => self.pad.write(addr, value),

            // 0xFF01-0xFF02 - Serial data transfer
            0xff01..=0xff02 => self.serial.write(addr, value),

            // 0xFF04-0xFF07 - Timer and divider
            0xff04..=0xff07 => self.timer.write(addr, value),

            // 0xFF0F — IF: Interrupt flag
            0xff0f => {
                self.ppu.set_int_vblank(value & 0x01 == 0x01);
                self.ppu.set_int_stat(value & 0x02 == 0x02);
                self.timer.set_int_tima(value & 0x04 == 0x04);
                self.serial.set_int_serial(value & 0x08 == 0x08);
                self.pad.set_int_pad(value & 0x10 == 0x10);
            }

            // 0xFF10-0xFF26 — Audio
            // 0xFF10-0xFF26 — Wave pattern
            0xff10..=0xff26 | 0xff30..=0xff3f => self.apu.write(addr, value),

            // 0xFF40-0xFF45 - PPU registers
            // 0xFF47-0xFF4B - PPU registers
            0xff40..=0xff45 | 0xff47..=0xff4b => self.ppu.write(addr, value),

            // 0xFF46 — DMA: OAM DMA source address & start
            0xff46 => self.dma.write(addr, value),

            // 0xFF4C - KEY0: Compatibility flag (CGB only)
            0xff4c => {
                self.key0 = value;
                if value == 0x04 {
                    self.ppu().set_dmg_compat(true);
                }
            }

            // 0xFF4D - KEY1: Speed switching (CGB only)
            0xff4d => self.switching = value & 0x01 == 0x01,

            // 0xFF4F - VRAM Bank Select (CGB only)
            0xff4f => self.ppu.write(addr, value),

            // 0xFF50 - BOOT: Boot active flag
            0xff50 => self.boot_active = value == 0x00,

            // 0xFF51-0xFF55 - VRAM DMA (HDMA) (CGB only)
            0xff51..=0xff55 => self.dma.write(addr, value),

            // 0xFF56 - RP: Infrared communications port (CGB only)
            0xff56 => {}

            // 0xFF68-0xFF6B - BG / OBJ Palettes (CGB only)
            0xff68..=0xff6b => self.ppu.write(addr, value),

            // 0xFF70 - SVBK: WRAM bank (CGB only)
            0xff70 => {
                let mut ram_bank = value & 0x07;
                if ram_bank == 0x0 {
                    ram_bank = 0x1;
                }
                self.ram_bank = ram_bank;
                self.ram_offset = self.ram_bank as u16 * 0x1000;
            }

            // 0xFF80-0xFFFE - High RAM (HRAM)
            0xff80..=0xfffe => self.ppu.write(addr, value),

            // 0xFFFF — IE: Interrupt enable
            0xffff => self.ie = value,

            addr => warnln!("Writing to unknown location 0x{:04x}", addr),
        }
    }

    /// Reads a byte from a certain memory address, without the typical
    /// Game Boy verifications, allowing deep read of values.
    pub fn read_raw(&mut self, addr: u16) -> u8 {
        match addr {
            0xff10..=0xff3f => self.apu.read_raw(addr),
            _ => self.read(addr),
        }
    }

    /// Writes a byte to a certain memory address without the typical
    /// Game Boy verification process. This allows for faster memory
    /// access in registers and other memory areas that are typically
    /// inaccessible.
    pub fn write_raw(&mut self, addr: u16, value: u8) {
        match addr {
            0xff10..=0xff3f => self.apu.write_raw(addr, value),
            _ => self.write(addr, value),
        }
    }

    pub fn read_many(&mut self, addr: u16, count: u16) -> Vec<u8> {
        let mut data: Vec<u8> = Vec::with_capacity(count as usize);

        for index in 0..count {
            let byte = self.read(addr + index);
            data.push(byte);
        }

        data
    }

    pub fn write_many(&mut self, addr: u16, data: &[u8]) {
        for (index, byte) in data.iter().enumerate() {
            self.write(addr + index as u16, *byte)
        }
    }

    pub fn read_many_raw(&mut self, addr: u16, count: u16) -> Vec<u8> {
        let mut data: Vec<u8> = Vec::with_capacity(count as usize);

        for index in 0..count {
            let byte = self.read_raw(addr + index);
            data.push(byte);
        }

        data
    }

    pub fn write_many_raw(&mut self, addr: u16, data: &[u8]) {
        for (index, byte) in data.iter().enumerate() {
            self.write_raw(addr + index as u16, *byte)
        }
    }

    pub fn write_boot(&mut self, addr: u16, buffer: &[u8]) {
        self.boot[addr as usize..addr as usize + buffer.len()].clone_from_slice(buffer);
    }

    pub fn write_ram(&mut self, addr: u16, buffer: &[u8]) {
        self.ram[addr as usize..addr as usize + buffer.len()].clone_from_slice(buffer);
    }

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

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

    pub fn set_ram(&mut self, value: Vec<u8>) {
        self.ram = value;
    }

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

    pub fn rom_i(&self) -> &Cartridge {
        &self.rom
    }

    pub fn set_rom(&mut self, rom: Cartridge) {
        self.rom = rom;
    }

    pub fn mode(&self) -> GameBoyMode {
        self.mode
    }

    pub fn set_mode(&mut self, value: GameBoyMode) {
        self.mode = value;
    }

    pub fn set_gbc(&mut self, value: SharedThread<GameBoyConfig>) {
        self.gbc = value;
    }
}

impl Default for Mmu {
    fn default() -> Self {
        let mode = GameBoyMode::Dmg;
        let gbc = SharedThread::new(Mutex::new(GameBoyConfig::default()));
        let components = Components {
            ppu: Ppu::new(mode, gbc.clone()),
            apu: Apu::default(),
            dma: Dma::default(),
            pad: Pad::default(),
            timer: Timer::default(),
            serial: Serial::default(),
        };
        Mmu::new(components, mode, gbc)
    }
}