use std::fmt;
use std::ops::{Deref, DerefMut};

use serde::{Deserialize, Serialize};

use super::cartridge::Cartridge;
use super::gpu::VIDEO_RAM_SIZE;
use super::iodev::{IoDevices, WaitControl};
use super::{Addr, Bus};

pub mod consts {
    pub const WORK_RAM_SIZE: usize = 256 * 1024;
    pub const INTERNAL_RAM_SIZE: usize = 32 * 1024;

    pub const BIOS_ADDR: u32 = 0x0000_0000;
    pub const EWRAM_ADDR: u32 = 0x0200_0000;
    pub const IWRAM_ADDR: u32 = 0x0300_0000;
    pub const IOMEM_ADDR: u32 = 0x0400_0000;
    pub const PALRAM_ADDR: u32 = 0x0500_0000;
    pub const VRAM_ADDR: u32 = 0x0600_0000;
    pub const OAM_ADDR: u32 = 0x0700_0000;
    pub const GAMEPAK_WS0_LO: u32 = 0x0800_0000;
    pub const GAMEPAK_WS0_HI: u32 = 0x0900_0000;
    pub const GAMEPAK_WS1_LO: u32 = 0x0A00_0000;
    pub const GAMEPAK_WS1_HI: u32 = 0x0B00_0000;
    pub const GAMEPAK_WS2_LO: u32 = 0x0C00_0000;
    pub const GAMEPAK_WS2_HI: u32 = 0x0D00_0000;
    pub const SRAM_LO: u32 = 0x0E00_0000;
    pub const SRAM_HI: u32 = 0x0F00_0000;

    pub const PAGE_BIOS: usize = (BIOS_ADDR >> 24) as usize;
    pub const PAGE_EWRAM: usize = (EWRAM_ADDR >> 24) as usize;
    pub const PAGE_IWRAM: usize = (IWRAM_ADDR >> 24) as usize;
    pub const PAGE_IOMEM: usize = (IOMEM_ADDR >> 24) as usize;
    pub const PAGE_PALRAM: usize = (PALRAM_ADDR >> 24) as usize;
    pub const PAGE_VRAM: usize = (VRAM_ADDR >> 24) as usize;
    pub const PAGE_OAM: usize = (OAM_ADDR >> 24) as usize;
    pub const PAGE_GAMEPAK_WS0: usize = (GAMEPAK_WS0_LO >> 24) as usize;
    pub const PAGE_GAMEPAK_WS1: usize = (GAMEPAK_WS1_LO >> 24) as usize;
    pub const PAGE_GAMEPAK_WS2: usize = (GAMEPAK_WS2_LO >> 24) as usize;
    pub const PAGE_SRAM_LO: usize = (SRAM_LO >> 24) as usize;
    pub const PAGE_SRAM_HI: usize = (SRAM_HI >> 24) as usize;
}

use consts::*;

#[derive(Debug, Copy, Clone)]
pub enum MemoryAccessType {
    NonSeq,
    Seq,
}

impl fmt::Display for MemoryAccessType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "{}",
            match self {
                MemoryAccessType::NonSeq => "N",
                MemoryAccessType::Seq => "S",
            }
        )
    }
}

#[derive(Debug, PartialEq, Copy, Clone)]
pub enum MemoryAccessWidth {
    MemoryAccess8,
    MemoryAccess16,
    MemoryAccess32,
}

#[derive(Serialize, Deserialize, Clone, Debug)]
#[repr(transparent)]
pub struct BoxedMemory {
    pub mem: Box<[u8]>,
}

impl BoxedMemory {
    pub fn new(boxed_slice: Box<[u8]>) -> BoxedMemory {
        BoxedMemory { mem: boxed_slice }
    }
}

impl Bus for BoxedMemory {
    fn read_8(&self, addr: Addr) -> u8 {
        unsafe { *self.mem.get_unchecked(addr as usize) }
    }

    fn write_8(&mut self, addr: Addr, value: u8) {
        unsafe {
            *self.mem.get_unchecked_mut(addr as usize) = value;
        }
    }
}

#[derive(Serialize, Deserialize, Clone, Debug)]
struct DummyBus([u8; 4]);

impl Bus for DummyBus {
    fn read_8(&self, _addr: Addr) -> u8 {
        0
    }

    fn write_8(&mut self, _addr: Addr, _value: u8) {}
}

const CYCLE_LUT_SIZE: usize = 0x10;

#[derive(Serialize, Deserialize, Clone)]
struct CycleLookupTables {
    n_cycles32: [usize; CYCLE_LUT_SIZE],
    s_cycles32: [usize; CYCLE_LUT_SIZE],
    n_cycles16: [usize; CYCLE_LUT_SIZE],
    s_cycles16: [usize; CYCLE_LUT_SIZE],
}

impl Default for CycleLookupTables {
    fn default() -> CycleLookupTables {
        CycleLookupTables {
            n_cycles32: [1; CYCLE_LUT_SIZE],
            s_cycles32: [1; CYCLE_LUT_SIZE],
            n_cycles16: [1; CYCLE_LUT_SIZE],
            s_cycles16: [1; CYCLE_LUT_SIZE],
        }
    }
}

impl CycleLookupTables {
    pub fn init(&mut self) {
        self.n_cycles32[PAGE_EWRAM] = 6;
        self.s_cycles32[PAGE_EWRAM] = 6;
        self.n_cycles16[PAGE_EWRAM] = 3;
        self.s_cycles16[PAGE_EWRAM] = 3;

        self.n_cycles32[PAGE_OAM] = 2;
        self.s_cycles32[PAGE_OAM] = 2;
        self.n_cycles16[PAGE_OAM] = 1;
        self.s_cycles16[PAGE_OAM] = 1;

        self.n_cycles32[PAGE_VRAM] = 2;
        self.s_cycles32[PAGE_VRAM] = 2;
        self.n_cycles16[PAGE_VRAM] = 1;
        self.s_cycles16[PAGE_VRAM] = 1;

        self.n_cycles32[PAGE_PALRAM] = 2;
        self.s_cycles32[PAGE_PALRAM] = 2;
        self.n_cycles16[PAGE_PALRAM] = 1;
        self.s_cycles16[PAGE_PALRAM] = 1;
    }

    pub fn update_gamepak_waitstates(&mut self, waitcnt: WaitControl) {
        static S_GAMEPAK_NSEQ_CYCLES: [usize; 4] = [4, 3, 2, 8];
        static S_GAMEPAK_WS0_SEQ_CYCLES: [usize; 2] = [2, 1];
        static S_GAMEPAK_WS1_SEQ_CYCLES: [usize; 2] = [4, 1];
        static S_GAMEPAK_WS2_SEQ_CYCLES: [usize; 2] = [8, 1];

        let ws0_first_access = waitcnt.ws0_first_access() as usize;
        let ws1_first_access = waitcnt.ws1_first_access() as usize;
        let ws2_first_access = waitcnt.ws2_first_access() as usize;
        let ws0_second_access = waitcnt.ws0_second_access() as usize;
        let ws1_second_access = waitcnt.ws1_second_access() as usize;
        let ws2_second_access = waitcnt.ws2_second_access() as usize;

        // update SRAM access
        let sram_wait_cycles = 1 + S_GAMEPAK_NSEQ_CYCLES[waitcnt.sram_wait_control() as usize];
        self.n_cycles32[PAGE_SRAM_LO] = sram_wait_cycles;
        self.n_cycles32[PAGE_SRAM_LO] = sram_wait_cycles;
        self.n_cycles16[PAGE_SRAM_HI] = sram_wait_cycles;
        self.n_cycles16[PAGE_SRAM_HI] = sram_wait_cycles;
        self.s_cycles32[PAGE_SRAM_LO] = sram_wait_cycles;
        self.s_cycles32[PAGE_SRAM_LO] = sram_wait_cycles;
        self.s_cycles16[PAGE_SRAM_HI] = sram_wait_cycles;
        self.s_cycles16[PAGE_SRAM_HI] = sram_wait_cycles;

        // update both pages of each waitstate
        for i in 0..2 {
            self.n_cycles16[PAGE_GAMEPAK_WS0 + i] = 1 + S_GAMEPAK_NSEQ_CYCLES[ws0_first_access];
            self.s_cycles16[PAGE_GAMEPAK_WS0 + i] = 1 + S_GAMEPAK_WS0_SEQ_CYCLES[ws0_second_access];

            self.n_cycles16[PAGE_GAMEPAK_WS1 + i] = 1 + S_GAMEPAK_NSEQ_CYCLES[ws1_first_access];
            self.s_cycles16[PAGE_GAMEPAK_WS1 + i] = 1 + S_GAMEPAK_WS1_SEQ_CYCLES[ws1_second_access];

            self.n_cycles16[PAGE_GAMEPAK_WS2 + i] = 1 + S_GAMEPAK_NSEQ_CYCLES[ws2_first_access];
            self.s_cycles16[PAGE_GAMEPAK_WS2 + i] = 1 + S_GAMEPAK_WS2_SEQ_CYCLES[ws2_second_access];

            // ROM 32bit accesses are split into two 16bit accesses 1N+1S
            self.n_cycles32[PAGE_GAMEPAK_WS0 + i] =
                self.n_cycles16[PAGE_GAMEPAK_WS0 + i] + self.s_cycles16[PAGE_GAMEPAK_WS0 + i];
            self.n_cycles32[PAGE_GAMEPAK_WS1 + i] =
                self.n_cycles16[PAGE_GAMEPAK_WS1 + i] + self.s_cycles16[PAGE_GAMEPAK_WS1 + i];
            self.n_cycles32[PAGE_GAMEPAK_WS2 + i] =
                self.n_cycles16[PAGE_GAMEPAK_WS2 + i] + self.s_cycles16[PAGE_GAMEPAK_WS2 + i];

            self.s_cycles32[PAGE_GAMEPAK_WS0 + i] = 2 * self.s_cycles16[PAGE_GAMEPAK_WS0 + i];
            self.s_cycles32[PAGE_GAMEPAK_WS1 + i] = 2 * self.s_cycles16[PAGE_GAMEPAK_WS1 + i];
            self.s_cycles32[PAGE_GAMEPAK_WS2 + i] = 2 * self.s_cycles16[PAGE_GAMEPAK_WS2 + i];
        }
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct SysBus {
    pub io: IoDevices,

    bios: BoxedMemory,
    onboard_work_ram: BoxedMemory,
    internal_work_ram: BoxedMemory,
    pub cartridge: Cartridge,
    dummy: DummyBus,

    cycle_luts: CycleLookupTables,

    pub trace_access: bool,
}

#[repr(transparent)]
#[derive(Clone)]
pub struct SysBusPtr {
    ptr: *mut SysBus,
}

impl Default for SysBusPtr {
    fn default() -> SysBusPtr {
        SysBusPtr {
            ptr: std::ptr::null_mut::<SysBus>(),
        }
    }
}

impl SysBusPtr {
    pub fn new(ptr: *mut SysBus) -> SysBusPtr {
        SysBusPtr { ptr: ptr }
    }
}

impl Deref for SysBusPtr {
    type Target = SysBus;
    fn deref(&self) -> &Self::Target {
        unsafe { &*self.ptr }
    }
}

impl DerefMut for SysBusPtr {
    fn deref_mut(&mut self) -> &mut Self::Target {
        unsafe { &mut *self.ptr }
    }
}

macro_rules! memory_map {
    (read($sb:ident, $read_fn:ident, $addr:expr)) => {
        match $addr & 0xff000000 {
            BIOS_ADDR => {
                if $addr >= 0x4000 {
                    0
                } else {
                    $sb.bios.$read_fn($addr)
                }
            }
            EWRAM_ADDR => $sb.onboard_work_ram.$read_fn($addr & 0x3_ffff),
            IWRAM_ADDR => $sb.internal_work_ram.$read_fn($addr & 0x7fff),
            IOMEM_ADDR => {
                let addr = if $addr & 0xffff == 0x8000 {
                    0x800
                } else {
                    $addr & 0x7ff
                };
                $sb.io.$read_fn(addr)
            }
            PALRAM_ADDR => $sb.io.gpu.palette_ram.$read_fn($addr & 0x3ff),
            VRAM_ADDR => {
                let mut ofs = $addr & ((VIDEO_RAM_SIZE as u32) - 1);
                if ofs > 0x18000 {
                    ofs -= 0x8000;
                }
                $sb.io.gpu.vram.$read_fn(ofs)
            }
            OAM_ADDR => $sb.io.gpu.oam.$read_fn($addr & 0x3ff),
            GAMEPAK_WS0_LO | GAMEPAK_WS0_HI | GAMEPAK_WS1_LO | GAMEPAK_WS1_HI | GAMEPAK_WS2_LO => {
                $sb.cartridge.$read_fn($addr)
            }
            GAMEPAK_WS2_HI => $sb.cartridge.$read_fn($addr),
            SRAM_LO | SRAM_HI => $sb.cartridge.$read_fn($addr),
            _ => {
                // warn!("trying to read invalid address {:#x}", $addr);
                // TODO open bus
                0
            }
        }
    };
    (write($sb:ident, $write_fn:ident, $addr:expr, $value:expr)) => {
        match $addr & 0xff000000 {
            BIOS_ADDR => {}
            EWRAM_ADDR => $sb.onboard_work_ram.$write_fn($addr & 0x3_ffff, $value),
            IWRAM_ADDR => $sb.internal_work_ram.$write_fn($addr & 0x7fff, $value),
            IOMEM_ADDR => {
                let addr = if $addr & 0xffff == 0x8000 {
                    0x800
                } else {
                    $addr & 0x7ff
                };
                $sb.io.$write_fn(addr, $value)
            }
            PALRAM_ADDR => $sb.io.gpu.palette_ram.$write_fn($addr & 0x3ff, $value),
            VRAM_ADDR => {
                let mut ofs = $addr & ((VIDEO_RAM_SIZE as u32) - 1);
                if ofs > 0x18000 {
                    ofs -= 0x8000;
                }
                $sb.io.gpu.vram.$write_fn(ofs, $value)
            }
            OAM_ADDR => $sb.io.gpu.oam.$write_fn($addr & 0x3ff, $value),
            GAMEPAK_WS0_LO | GAMEPAK_WS0_HI => {}
            GAMEPAK_WS2_HI => $sb.cartridge.$write_fn($addr, $value),
            SRAM_LO | SRAM_HI => $sb.cartridge.$write_fn($addr, $value),
            _ => {
                // warn!("trying to write invalid address {:#x}", $addr);
                // TODO open bus
            }
        }
    };
}

impl SysBus {
    pub fn new(io: IoDevices, bios_rom: Box<[u8]>, cartridge: Cartridge) -> SysBus {
        let mut luts = CycleLookupTables::default();
        luts.init();
        luts.update_gamepak_waitstates(io.waitcnt);

        SysBus {
            io: io,

            bios: BoxedMemory::new(bios_rom),
            onboard_work_ram: BoxedMemory::new(vec![0; WORK_RAM_SIZE].into_boxed_slice()),
            internal_work_ram: BoxedMemory::new(vec![0; INTERNAL_RAM_SIZE].into_boxed_slice()),
            cartridge: cartridge,
            dummy: DummyBus([0; 4]),

            cycle_luts: luts,

            trace_access: false,
        }
    }

    /// must be called whenever this object is instanciated
    pub fn created(&mut self) {
        let ptr = SysBusPtr::new(self as *mut SysBus);
        // HACK
        self.io.set_sysbus_ptr(ptr.clone());
    }

    pub fn on_waitcnt_written(&mut self, waitcnt: WaitControl) {
        self.cycle_luts.update_gamepak_waitstates(waitcnt);
    }

    #[inline(always)]
    pub fn get_cycles(
        &self,
        addr: Addr,
        access: MemoryAccessType,
        width: MemoryAccessWidth,
    ) -> usize {
        use MemoryAccessType::*;
        use MemoryAccessWidth::*;
        let page = (addr >> 24) as usize;

        // TODO optimize out by making the LUTs have 0x100 entries for each possible page ?
        if page > 0xF {
            // open bus
            return 1;
        }
        match width {
            MemoryAccess8 | MemoryAccess16 => match access {
                NonSeq => self.cycle_luts.n_cycles16[page],
                Seq => self.cycle_luts.s_cycles16[page],
            },
            MemoryAccess32 => match access {
                NonSeq => self.cycle_luts.n_cycles32[page],
                Seq => self.cycle_luts.s_cycles32[page],
            },
        }
    }
}

impl Bus for SysBus {
    fn read_32(&self, addr: Addr) -> u32 {
        memory_map!(read(self, read_32, addr & !3))
    }

    fn read_16(&self, addr: Addr) -> u16 {
        memory_map!(read(self, read_16, addr & !1))
    }

    fn read_8(&self, addr: Addr) -> u8 {
        memory_map!(read(self, read_8, addr))
    }

    fn write_32(&mut self, addr: Addr, value: u32) {
        memory_map!(write(self, write_32, addr & !3, value));
    }

    fn write_16(&mut self, addr: Addr, value: u16) {
        memory_map!(write(self, write_16, addr & !1, value));
    }

    fn write_8(&mut self, addr: Addr, value: u8) {
        memory_map!(write(self, write_8, addr, value));
    }
}
