use core::fmt;
use std::fmt::{Display, Formatter};

pub const BANK_SIZE: usize = 16384;

pub enum RomType {
    RomOnly = 0x00,
    Mbc1 = 0x01,
    Mbc1Ram = 0x02,
    Mbc1RamBattery = 0x03,
    Mbc2 = 0x05,
    Mbc2Battery = 0x06,
    Unknown = 0xff,
}

impl Display for RomType {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let str = match self {
            RomType::RomOnly => "ROM Only",
            RomType::Mbc1 => "MBC 1",
            RomType::Mbc1Ram => "MBC 1 + RAM",
            RomType::Mbc1RamBattery => "MBC 1 + RAM + Battery",
            RomType::Mbc2 => "MBC 2",
            RomType::Mbc2Battery => "MBC 2 + RAM",
            RomType::Unknown => "Unknown",
        };
        write!(f, "{}", str)
    }
}

pub enum RomSize {
    Size32K,
    Size64K,
    Size128K,
    Size256K,
    Size512K,
    Size1M,
    Size2M,
    SizeUnknown,
}

impl Display for RomSize {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let str = match self {
            RomSize::Size32K => "32 KB",
            RomSize::Size64K => "64 KB",
            RomSize::Size128K => "128 KB",
            RomSize::Size256K => "256 KB",
            RomSize::Size512K => "512 KB",
            RomSize::Size1M => "1 MB",
            RomSize::Size2M => "2 MB",
            RomSize::SizeUnknown => "Unknown",
        };
        write!(f, "{}", str)
    }
}

pub enum RamSize {
    NoRam,
    Unused,
    Size8K,
    Size32K,
    Size64K,
    Size128K,
    SizeUnknown,
}

impl Display for RamSize {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        let str = match self {
            RamSize::NoRam => "No RAM",
            RamSize::Unused => "Unused",
            RamSize::Size8K => "8 KB",
            RamSize::Size32K => "32 KB",
            RamSize::Size128K => "128 KB",
            RamSize::Size64K => "64 KB",
            RamSize::SizeUnknown => "Unknown",
        };
        write!(f, "{}", str)
    }
}

pub struct Cartridge {
    /// The complete data of the ROM cartridge, should
    /// include the complete set o ROM banks.
    data: Vec<u8>,

    /// The offset address to the ROM bank (#1) that is
    /// currently in use by the ROM cartridge.
    rom_offset: u16,

    /// The MBC (Memory Bank Controller) to be used for
    /// RAM and ROM access on the current cartridge.
    mbc: &'static Mbc,
}

impl Cartridge {
    pub fn new() -> Self {
        Self {
            data: vec![],
            rom_offset: 0x0000,
            mbc: &NO_MBC,
        }
    }

    pub fn from_data(data: &[u8]) -> Self {
        let mut cartridge = Cartridge::new();
        cartridge.set_data(data);
        cartridge
    }

    pub fn read(&self, addr: u16) -> u8 {
        (self.mbc.read)(self, addr)
    }

    pub fn write(&self, addr: u16, value: u8) {
        (self.mbc.write)(self, addr, value)
    }

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

    pub fn get_bank(&self, index: u8) -> &[u8] {
        let start = index as usize * BANK_SIZE;
        let end = (index + 1) as usize * BANK_SIZE;
        &self.data[start..end]
    }

    pub fn title(&self) -> &str {
        std::str::from_utf8(&self.data[0x0134..0x0143]).unwrap()
    }

    pub fn rom_type(&self) -> RomType {
        match self.data[0x0147] {
            0x00 => RomType::RomOnly,
            0x01 => RomType::Mbc1,
            0x02 => RomType::Mbc1Ram,
            0x03 => RomType::Mbc1RamBattery,
            0x05 => RomType::Mbc2,
            0x06 => RomType::Mbc2Battery,
            _ => RomType::Unknown,
        }
    }

    pub fn rom_size(&self) -> RomSize {
        match self.data[0x0148] {
            0x00 => RomSize::Size32K,
            0x01 => RomSize::Size64K,
            0x02 => RomSize::Size128K,
            0x03 => RomSize::Size256K,
            0x04 => RomSize::Size512K,
            0x05 => RomSize::Size1M,
            0x06 => RomSize::Size2M,
            _ => RomSize::SizeUnknown,
        }
    }

    pub fn ram_size(&self) -> RamSize {
        match self.data[0x0148] {
            0x00 => RamSize::NoRam,
            0x01 => RamSize::Unused,
            0x02 => RamSize::Size8K,
            0x03 => RamSize::Size32K,
            0x04 => RamSize::Size128K,
            0x05 => RamSize::Size64K,
            _ => RamSize::SizeUnknown,
        }
    }

    fn set_data(&mut self, data: &[u8]) {
        self.data = data.to_vec();
        self.rom_offset = 0x0000;
        self.set_mbc();
    }

    fn set_mbc(&self) -> &'static Mbc {
        match self.rom_type() {
            RomType::RomOnly => &NO_MBC,
            RomType::Mbc1 => &MBC1,
            RomType::Mbc1Ram => &MBC1,
            RomType::Mbc1RamBattery => &MBC1,
            _ => &NO_MBC,
        }
    }
}

impl Display for Cartridge {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Name => {}\nType => {}\nROM Size => {}\nRAM Size => {}",
            self.title(),
            self.rom_type(),
            self.rom_size(),
            self.ram_size()
        )
    }
}

pub struct Mbc {
    pub name: &'static str,
    pub read: fn(rom: &Cartridge, addr: u16) -> u8,
    pub write: fn(rom: &Cartridge, addr: u16, value: u8),
}

pub static NO_MBC: Mbc = Mbc {
    name: "No MBC",
    read: |rom: &Cartridge, addr: u16| -> u8 { rom.data[addr as usize] },
    write: |_rom: &Cartridge, addr: u16, _value: u8| {
        println!("Writing to ROM at 0x{:04x}", addr);
    },
};

pub static MBC1: Mbc = Mbc {
    name: "MBC1",
    read: |rom: &Cartridge, addr: u16| -> u8 { 0x00 },
    write: |rom: &Cartridge, addr: u16, value: u8| {},
};