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

use crate::{debugln, gb::GameBoyMode, genie::GameGenie, util::read_file, warnln};

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

pub const ROM_BANK_SIZE: usize = 16384;
pub const RAM_BANK_SIZE: usize = 8192;

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub enum RomType {
    RomOnly = 0x00,
    Mbc1 = 0x01,
    Mbc1Ram = 0x02,
    Mbc1RamBattery = 0x03,
    Mbc2 = 0x05,
    Mbc2Battery = 0x06,
    RomRam = 0x08,
    RomRamBattery = 0x09,
    Mmm01 = 0x0b,
    Mmm01Ram = 0x0c,
    Mmm01RamBattery = 0x0d,
    Mbc3TimerBattery = 0x0f,
    Mbc3TimerRamBattery = 0x10,
    Mbc3 = 0x11,
    Mbc3Ram = 0x12,
    Mbc3RamBattery = 0x13,
    Mbc5 = 0x19,
    Mbc5Ram = 0x1a,
    Mbc5RamBattery = 0x1b,
    Mbc5Rumble = 0x1c,
    Mbc5RumbleRam = 0x1d,
    Mbc5RumbleRamBattery = 0x1e,
    Mbc6 = 0x20,
    Mbc7SensorRumbleRamBattery = 0x22,
    PocketCamera = 0xfc,
    BandaiTama5 = 0xfd,
    HuC3 = 0xfe,
    HuC1RamBattery = 0xff,
    Unknown = 0xef,
}

impl RomType {
    pub fn description(&self) -> &'static str {
        match self {
            RomType::RomOnly => "ROM Only",
            RomType::Mbc1 => "MBC1",
            RomType::Mbc1Ram => "MBC1 + RAM",
            RomType::Mbc1RamBattery => "MBC1 + RAM + Battery",
            RomType::Mbc2 => "MBC2",
            RomType::Mbc2Battery => "MBC2 + RAM",
            RomType::RomRam => "ROM + RAM",
            RomType::RomRamBattery => "ROM + RAM + BATTERY",
            RomType::Mmm01 => "MMM01",
            RomType::Mmm01Ram => "MMM01 + RAM",
            RomType::Mmm01RamBattery => "MMM01 + RAM + BATTERY",
            RomType::Mbc3TimerBattery => "MBC3 + TIMER + BATTERY",
            RomType::Mbc3TimerRamBattery => "MBC3 + TIMER + RAM + BATTERY",
            RomType::Mbc3 => "MBC3",
            RomType::Mbc3Ram => "MBC3 + RAM",
            RomType::Mbc3RamBattery => "MBC3 + RAM + BATTERY",
            RomType::Mbc5 => "MBC5",
            RomType::Mbc5Ram => "MBC5 + RAM",
            RomType::Mbc5RamBattery => "MBC5 + RAM + BATTERY",
            RomType::Mbc5Rumble => "MBC5 + RUMBLE",
            RomType::Mbc5RumbleRam => "MBC5 + RUMBLE + RAM",
            RomType::Mbc5RumbleRamBattery => "MBC5 + RUMBLE + RAM + BATTERY",
            RomType::Mbc6 => "MBC6",
            RomType::Mbc7SensorRumbleRamBattery => "MBC6 + SENSOR + RUMBLE + RAM + BATTERY",
            RomType::PocketCamera => "POCKET CAMERA",
            RomType::BandaiTama5 => "BANDAI TAMA5",
            RomType::HuC3 => "HuC3",
            RomType::HuC1RamBattery => "HuC1 + RAM + BATTERY",
            RomType::Unknown => "Unknown",
        }
    }
}

impl Display for RomType {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description())
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub enum RomSize {
    Size32K,
    Size64K,
    Size128K,
    Size256K,
    Size512K,
    Size1M,
    Size2M,
    Size4M,
    Size8M,
    SizeUnknown,
}

impl RomSize {
    pub fn description(&self) -> &'static 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::Size4M => "4 MB",
            RomSize::Size8M => "8 MB",
            RomSize::SizeUnknown => "Unknown",
        }
    }

    pub fn rom_banks(&self) -> u16 {
        match self {
            RomSize::Size32K => 2,
            RomSize::Size64K => 4,
            RomSize::Size128K => 8,
            RomSize::Size256K => 16,
            RomSize::Size512K => 32,
            RomSize::Size1M => 64,
            RomSize::Size2M => 128,
            RomSize::Size4M => 256,
            RomSize::Size8M => 512,
            RomSize::SizeUnknown => 0,
        }
    }
}

impl Display for RomSize {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description())
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub enum RamSize {
    NoRam,
    Unused,
    Size8K,
    Size16K,
    Size32K,
    Size64K,
    Size128K,
    SizeUnknown,
}

impl RamSize {
    pub fn description(&self) -> &'static str {
        match self {
            RamSize::NoRam => "No RAM",
            RamSize::Unused => "Unused",
            RamSize::Size8K => "8 KB",
            RamSize::Size16K => "16 KB",
            RamSize::Size32K => "32 KB",
            RamSize::Size128K => "128 KB",
            RamSize::Size64K => "64 KB",
            RamSize::SizeUnknown => "Unknown",
        }
    }

    pub fn ram_banks(&self) -> u16 {
        match self {
            RamSize::NoRam => 0,
            RamSize::Unused => 0,
            RamSize::Size8K => 1,
            RamSize::Size16K => 2,
            RamSize::Size32K => 4,
            RamSize::Size64K => 8,
            RamSize::Size128K => 16,
            RamSize::SizeUnknown => 0,
        }
    }
}

impl Display for RamSize {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description())
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
pub enum CgbMode {
    NoCgb = 0x00,
    CgbCompatible = 0x80,
    CgbOnly = 0xc0,
}

impl CgbMode {
    pub fn description(&self) -> &'static str {
        match self {
            CgbMode::NoCgb => "No CGB support",
            CgbMode::CgbCompatible => "CGB backwards compatible",
            CgbMode::CgbOnly => "CGB only",
        }
    }
}

impl Display for CgbMode {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description())
    }
}

/// Structure that defines the ROM and ROM contents
/// of a Game Boy cartridge. Should correctly address
/// the specifics of all the major MBCs (Memory Bank
/// Controllers).
#[cfg_attr(feature = "wasm", wasm_bindgen)]
#[derive(Clone)]
pub struct Cartridge {
    /// The complete data of the ROM cartridge, should
    /// include the complete set o ROM banks.
    rom_data: Vec<u8>,

    /// The base RAM that is going to be used to store
    /// temporary data for basic cartridges.
    ram_data: Vec<u8>,

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

    /// The current memory handler in charge of handling the
    /// memory access for the current cartridge.
    /// Typically this is the same as the MBC, but to allow
    /// memory patching (ex: Game Genie) we may need another
    /// level of indirection.
    handler: &'static Mbc,

    /// The number of ROM banks (of 8KB) that are available
    /// to the current cartridge, this is a computed value
    /// to allow improved performance.
    rom_bank_count: u16,

    /// The number of RAM banks (of 8KB) that are available
    /// to the current cartridge, this is a computed value
    /// to allow improved performance.
    ram_bank_count: u16,

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

    /// The offset address to the ERAM bank that is
    /// currently in use by the ROM cartridge.
    ram_offset: usize,

    /// If the RAM access ia enabled, this flag allows
    /// control of memory access to avoid corruption.
    ram_enabled: bool,

    /// The final offset of the last character of the title
    /// that is considered to be non zero (0x0) so that a
    /// proper safe conversion to UTF-8 string can be done.
    title_offset: usize,

    /// The current rumble state of the cartridge, this
    /// boolean value controls if vibration is currently active.
    rumble_active: bool,

    /// Callback function to be called whenever there's a new
    /// rumble vibration triggered or when it's disabled.
    rumble_cb: fn(active: bool),

    /// Optional reference to the Game Genie instance that
    /// would be used for the "cheating" by patching the
    /// current ROM's cartridge data.
    game_genie: Option<GameGenie>,
}

impl Cartridge {
    pub fn new() -> Self {
        Self {
            rom_data: vec![],
            ram_data: vec![],
            mbc: &NO_MBC,
            handler: &NO_MBC,
            rom_bank_count: 0,
            ram_bank_count: 0,
            rom_offset: 0x4000,
            ram_offset: 0x0000,
            ram_enabled: false,
            title_offset: 0x0143,
            rumble_active: false,
            rumble_cb: |_| {},
            game_genie: None,
        }
    }

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

    pub fn from_file(path: &str) -> Self {
        let data = read_file(path);
        Self::from_data(&data)
    }

    pub fn read(&self, addr: u16) -> u8 {
        match addr & 0xf000 {
            0x0000 | 0x1000 | 0x2000 | 0x3000 | 0x4000 | 0x5000 | 0x6000 | 0x7000 => {
                (self.handler.read_rom)(self, addr)
            }
            0xa000 | 0xb000 => (self.handler.read_ram)(self, addr),
            _ => {
                debugln!("Reading from unknown Cartridge control 0x{:04x}", addr);
                0x00
            }
        }
    }

    pub fn write(&mut self, addr: u16, value: u8) {
        match addr & 0xf000 {
            0x0000 | 0x1000 | 0x2000 | 0x3000 | 0x4000 | 0x5000 | 0x6000 | 0x7000 => {
                (self.handler.write_rom)(self, addr, value)
            }
            0xa000 | 0xb000 => (self.handler.write_ram)(self, addr, value),
            _ => debugln!("Writing to unknown Cartridge address 0x{:04x}", addr),
        }
    }

    pub fn reset(&mut self) {
        self.rom_data = vec![];
        self.ram_data = vec![];
        self.mbc = &NO_MBC;
        self.rom_bank_count = 0;
        self.ram_bank_count = 0;
        self.rom_offset = 0x4000;
        self.ram_offset = 0x0000;
        self.ram_enabled = false;
        self.title_offset = 0x0143;
        self.rumble_active = false;
        self.rumble_cb = |_| {};
    }

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

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

    pub fn get_mbc(&self) -> &'static Mbc {
        match self.rom_type() {
            RomType::RomOnly => &NO_MBC,
            RomType::Mbc1 => &MBC1,
            RomType::Mbc1Ram => &MBC1,
            RomType::Mbc1RamBattery => &MBC1,
            RomType::Mbc3TimerBattery => &MBC3,
            RomType::Mbc3TimerRamBattery => &MBC3,
            RomType::Mbc3 => &MBC3,
            RomType::Mbc3Ram => &MBC3,
            RomType::Mbc3RamBattery => &MBC3,
            RomType::Mbc5 => &MBC5,
            RomType::Mbc5Ram => &MBC5,
            RomType::Mbc5RamBattery => &MBC5,
            RomType::Mbc5Rumble => &MBC5,
            RomType::Mbc5RumbleRam => &MBC5,
            RomType::Mbc5RumbleRamBattery => &MBC5,
            rom_type => panic!("No MBC controller available for {}", rom_type),
        }
    }

    pub fn has_rumble(&mut self) -> bool {
        matches!(
            self.rom_type(),
            RomType::Mbc5Rumble | RomType::Mbc5RumbleRam | RomType::Mbc5RumbleRamBattery
        )
    }

    pub fn set_rom_bank(&mut self, rom_bank: u8) {
        self.rom_offset = rom_bank as usize * ROM_BANK_SIZE;
    }

    pub fn set_ram_bank(&mut self, ram_bank: u8) {
        self.ram_offset = ram_bank as usize * RAM_BANK_SIZE;
    }

    pub fn set_rumble_cb(&mut self, rumble_cb: fn(active: bool)) {
        self.rumble_cb = rumble_cb;
    }

    pub fn trigger_rumble(&self) {
        (self.rumble_cb)(self.rumble_active);
    }

    fn set_data(&mut self, data: &[u8]) {
        self.rom_data = data.to_vec();
        self.rom_offset = 0x4000;
        self.ram_offset = 0x0000;
        self.set_mbc();
        self.set_computed();
        self.set_title_offset();
        self.allocate_ram();
        self.set_rom_bank(1);
        self.set_ram_bank(0);
    }

    fn set_mbc(&mut self) {
        self.mbc = self.get_mbc();
        self.handler = self.mbc;
    }

    fn set_computed(&mut self) {
        self.rom_bank_count = self.rom_size().rom_banks();
        self.ram_bank_count = self.ram_size().ram_banks();
    }

    pub fn set_title_offset(&mut self) {
        let mut offset: usize = 0;
        for byte in &self.rom_data[0x0134..=0x0143] {
            if *byte == 0u8 {
                break;
            }

            // in we're at the final byte of the title and the value
            // is one that is reserved for CGB compatibility testing
            // then we must ignore it for title processing purposes
            if offset > 14
                && (*byte == CgbMode::CgbCompatible as u8 || *byte == CgbMode::CgbOnly as u8)
            {
                break;
            }

            offset += 1;
        }
        self.title_offset = 0x0134 + offset;
    }

    pub fn game_genie(&self) -> &Option<GameGenie> {
        &self.game_genie
    }

    pub fn game_genie_mut(&mut self) -> &mut Option<GameGenie> {
        &mut self.game_genie
    }

    pub fn set_game_genie(&mut self, game_genie: Option<GameGenie>) {
        self.game_genie = game_genie;
    }

    fn allocate_ram(&mut self) {
        let ram_banks = max(self.ram_size().ram_banks(), 1);
        self.ram_data = vec![0u8; ram_banks as usize * RAM_BANK_SIZE];
    }
}

#[cfg_attr(feature = "wasm", wasm_bindgen)]
impl Cartridge {
    pub fn title(&self) -> String {
        String::from(
            std::str::from_utf8(&self.rom_data[0x0134..self.title_offset])
                .unwrap()
                .trim(),
        )
    }

    pub fn cgb_flag(&self) -> CgbMode {
        match self.rom_data[0x0143] {
            0x80 => CgbMode::CgbCompatible,
            0xc0 => CgbMode::CgbOnly,
            _ => CgbMode::NoCgb,
        }
    }

    pub fn gb_mode(&self) -> GameBoyMode {
        match self.cgb_flag() {
            CgbMode::CgbCompatible | CgbMode::CgbOnly => GameBoyMode::Cgb,
            _ => GameBoyMode::Dmg,
        }
    }

    /// A cartridge is considered legacy if it does
    /// not have a CGB flag bit (bit 7 of 0x0143) set.
    /// These are the monochromatic only Cartridges built
    /// for the original DMG Game Boy.
    pub fn is_legacy(&self) -> bool {
        self.rom_data[0x0143] & 0x80 == 0x00
    }

    pub fn rom_type(&self) -> RomType {
        match self.rom_data[0x0147] {
            0x00 => RomType::RomOnly,
            0x01 => RomType::Mbc1,
            0x02 => RomType::Mbc1Ram,
            0x03 => RomType::Mbc1RamBattery,
            0x05 => RomType::Mbc2,
            0x06 => RomType::Mbc2Battery,
            0x08 => RomType::RomRam,
            0x09 => RomType::RomRamBattery,
            0x0b => RomType::Mmm01,
            0x0c => RomType::Mmm01Ram,
            0x0d => RomType::Mmm01RamBattery,
            0x0f => RomType::Mbc3TimerBattery,
            0x10 => RomType::Mbc3TimerRamBattery,
            0x11 => RomType::Mbc3,
            0x12 => RomType::Mbc3Ram,
            0x13 => RomType::Mbc3RamBattery,
            0x19 => RomType::Mbc5,
            0x1a => RomType::Mbc5Ram,
            0x1b => RomType::Mbc5RamBattery,
            0x1c => RomType::Mbc5Rumble,
            0x1d => RomType::Mbc5RumbleRam,
            0x1e => RomType::Mbc5RumbleRamBattery,
            0x20 => RomType::Mbc6,
            0x22 => RomType::Mbc7SensorRumbleRamBattery,
            0xfc => RomType::PocketCamera,
            0xfd => RomType::BandaiTama5,
            0xfe => RomType::HuC3,
            0xff => RomType::HuC1RamBattery,
            _ => RomType::Unknown,
        }
    }

    pub fn set_rom_type(&mut self, rom_type: RomType) {
        self.rom_data[0x0147] = match rom_type {
            RomType::RomOnly => 0x00,
            RomType::Mbc1 => 0x01,
            RomType::Mbc1Ram => 0x02,
            RomType::Mbc1RamBattery => 0x03,
            RomType::Mbc2 => 0x05,
            RomType::Mbc2Battery => 0x06,
            RomType::RomRam => 0x08,
            RomType::RomRamBattery => 0x09,
            RomType::Mmm01 => 0x0b,
            RomType::Mmm01Ram => 0x0c,
            RomType::Mmm01RamBattery => 0x0d,
            RomType::Mbc3TimerBattery => 0x0f,
            RomType::Mbc3TimerRamBattery => 0x10,
            RomType::Mbc3 => 0x11,
            RomType::Mbc3Ram => 0x12,
            RomType::Mbc3RamBattery => 0x13,
            RomType::Mbc5 => 0x19,
            RomType::Mbc5Ram => 0x1a,
            RomType::Mbc5RamBattery => 0x1b,
            RomType::Mbc5Rumble => 0x1c,
            RomType::Mbc5RumbleRam => 0x1d,
            RomType::Mbc5RumbleRamBattery => 0x1e,
            RomType::Mbc6 => 0x20,
            RomType::Mbc7SensorRumbleRamBattery => 0x22,
            RomType::PocketCamera => 0xfc,
            RomType::BandaiTama5 => 0xfd,
            RomType::HuC3 => 0xfe,
            RomType::HuC1RamBattery => 0xff,
            RomType::Unknown => panic!("Unknown ROM type"),
        };
    }

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

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

    pub fn rom_type_s(&self) -> String {
        String::from(self.rom_type().description())
    }

    pub fn rom_size_s(&self) -> String {
        String::from(self.rom_size().description())
    }

    pub fn ram_size_s(&self) -> String {
        String::from(self.ram_size().description())
    }

    pub fn has_battery(&self) -> bool {
        matches!(
            self.rom_type(),
            RomType::Mbc1RamBattery
                | RomType::Mbc2Battery
                | RomType::RomRamBattery
                | RomType::Mmm01RamBattery
                | RomType::Mbc3TimerBattery
                | RomType::Mbc3TimerRamBattery
                | RomType::Mbc3RamBattery
                | RomType::Mbc5RamBattery
                | RomType::Mbc5RumbleRamBattery
                | RomType::Mbc7SensorRumbleRamBattery
                | RomType::HuC1RamBattery
        )
    }

    pub fn rom_data_eager(&self) -> Vec<u8> {
        self.rom_data.clone()
    }

    pub fn ram_data_eager(&self) -> Vec<u8> {
        self.ram_data.clone()
    }

    pub fn set_ram_data(&mut self, data: &[u8]) {
        self.ram_data = data.to_vec();
    }

    pub fn clear_ram_data(&mut self) {
        self.ram_data = vec![0u8; self.ram_data.len()];
    }

    pub fn attach_genie(&mut self, game_genie: GameGenie) {
        self.game_genie = Some(game_genie);
        self.handler = &GAME_GENIE;
    }

    pub fn detach_genie(&mut self) {
        self.game_genie = None;
        self.handler = self.mbc;
    }

    pub fn description(&self, column_length: usize) -> String {
        let name_l = format!("{:width$}", "Name", width = column_length);
        let type_l = format!("{:width$}", "Type", width = column_length);
        let rom_size_l = format!("{:width$}", "ROM Size", width = column_length);
        let ram_size_l = format!("{:width$}", "RAM Size", width = column_length);
        let cgb_l = format!("{:width$}", "CGB Mode", width = column_length);
        format!(
            "{}  {}\n{}  {}\n{}  {}\n{}  {}\n{}  {}",
            name_l,
            self.title(),
            type_l,
            self.rom_type(),
            rom_size_l,
            self.rom_size(),
            ram_size_l,
            self.ram_size(),
            cgb_l,
            self.cgb_flag()
        )
    }
}

impl Cartridge {
    pub fn rom_data(&self) -> &Vec<u8> {
        &self.rom_data
    }

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

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

impl Display for Cartridge {
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.description(9))
    }
}

pub struct Mbc {
    pub name: &'static str,
    pub read_rom: fn(rom: &Cartridge, addr: u16) -> u8,
    pub write_rom: fn(rom: &mut Cartridge, addr: u16, value: u8),
    pub read_ram: fn(rom: &Cartridge, addr: u16) -> u8,
    pub write_ram: fn(rom: &mut Cartridge, addr: u16, value: u8),
}

pub static NO_MBC: Mbc = Mbc {
    name: "No MBC",
    read_rom: |rom: &Cartridge, addr: u16| -> u8 { rom.rom_data[addr as usize] },
    write_rom: |_rom: &mut Cartridge, addr: u16, _value: u8| {
        match addr {
            // ignores this address as Tetris and some other games write
            // to this address for some reason (probably related to
            // some kind of MBC1 compatibility issue)
            0x2000 => (),
            _ => panic!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
        };
    },
    read_ram: |rom: &Cartridge, addr: u16| -> u8 { rom.ram_data[(addr - 0xa000) as usize] },
    write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
        rom.ram_data[(addr - 0xa000) as usize] = value;
    },
};

pub static MBC1: Mbc = Mbc {
    name: "MBC1",
    read_rom: |rom: &Cartridge, addr: u16| -> u8 {
        match addr & 0xf000 {
            0x0000 | 0x1000 | 0x2000 | 0x3000 => rom.rom_data[addr as usize],
            0x4000 | 0x5000 | 0x6000 | 0x7000 => *rom
                .rom_data
                .get(rom.rom_offset + (addr - 0x4000) as usize)
                .unwrap_or(&0x0),
            _ => {
                warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
                0xff
            }
        }
    },
    write_rom: |rom: &mut Cartridge, addr: u16, value: u8| {
        match addr & 0xf000 {
            // RAM enabled flag
            0x0000 | 0x1000 => {
                rom.ram_enabled = (value & 0x0f) == 0x0a;
            }
            // ROM bank selection 5 lower bits
            0x2000 | 0x3000 => {
                let mut rom_bank = value & 0x1f;
                rom_bank &= (rom.rom_bank_count * 2 - 1) as u8;
                if rom_bank == 0 {
                    rom_bank = 1;
                }
                rom.set_rom_bank(rom_bank);
            }
            // RAM bank selection and ROM bank selection upper bits
            0x4000 | 0x5000 => {
                let ram_bank = value & 0x03;
                if ram_bank as u16 >= rom.ram_bank_count {
                    return;
                }
                rom.set_ram_bank(ram_bank);
            }
            // ROM mode selection
            0x6000 | 0x7000 => {
                if value == 0x1 && rom.rom_bank_count > 32 {
                    unimplemented!("Advanced ROM banking mode for MBC1 is not implemented");
                }
            }
            _ => warnln!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
        }
    },
    read_ram: |rom: &Cartridge, addr: u16| -> u8 {
        if !rom.ram_enabled {
            return 0xff;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize]
    },
    write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
        if !rom.ram_enabled {
            warnln!("Attempt to write to ERAM while write protect is active");
            return;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
    },
};

pub static MBC3: Mbc = Mbc {
    name: "MBC3",
    read_rom: |rom: &Cartridge, addr: u16| -> u8 {
        match addr & 0xf000 {
            0x0000 | 0x1000 | 0x2000 | 0x3000 => rom.rom_data[addr as usize],
            0x4000 | 0x5000 | 0x6000 | 0x7000 => *rom
                .rom_data
                .get(rom.rom_offset + (addr - 0x4000) as usize)
                .unwrap_or(&0x0),
            _ => {
                warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
                0xff
            }
        }
    },
    write_rom: |rom: &mut Cartridge, addr: u16, value: u8| {
        match addr & 0xf000 {
            // RAM enabled flag
            0x0000 | 0x1000 => {
                rom.ram_enabled = (value & 0x0f) == 0x0a;
            }
            // ROM bank selection
            0x2000 | 0x3000 => {
                let mut rom_bank = value & 0x7f;
                rom_bank &= (rom.rom_bank_count * 2 - 1) as u8;
                if rom_bank == 0 {
                    rom_bank = 1;
                }
                rom.set_rom_bank(rom_bank);
            }
            // RAM bank selection
            0x4000 | 0x5000 => {
                let ram_bank = value & 0x03;
                if ram_bank as u16 >= rom.ram_bank_count {
                    return;
                }
                rom.set_ram_bank(ram_bank);
            }
            _ => warnln!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
        }
    },
    read_ram: |rom: &Cartridge, addr: u16| -> u8 {
        if !rom.ram_enabled {
            return 0xff;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize]
    },
    write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
        if !rom.ram_enabled {
            warnln!("Attempt to write to ERAM while write protect is active");
            return;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
    },
};

pub static MBC5: Mbc = Mbc {
    name: "MBC5",
    read_rom: |rom: &Cartridge, addr: u16| -> u8 {
        match addr & 0xf000 {
            0x0000 | 0x1000 | 0x2000 | 0x3000 => rom.rom_data[addr as usize],
            0x4000 | 0x5000 | 0x6000 | 0x7000 => *rom
                .rom_data
                .get(rom.rom_offset + (addr - 0x4000) as usize)
                .unwrap_or(&0x0),
            _ => {
                warnln!("Reading from unknown Cartridge ROM location 0x{:04x}", addr);
                0xff
            }
        }
    },
    write_rom: |rom: &mut Cartridge, addr: u16, value: u8| {
        match addr & 0xf000 {
            // RAM enabled flag
            0x0000 | 0x1000 => {
                rom.ram_enabled = (value & 0x0f) == 0x0a;
            }
            // ROM bank selection
            0x2000 => {
                let rom_bank = value;
                rom.set_rom_bank(rom_bank);
            }
            // RAM bank selection
            0x4000 | 0x5000 => {
                let mut ram_bank = value & 0x0f;

                // handles the rumble flag for the cartridges
                // that support the rumble operation
                if rom.has_rumble() {
                    ram_bank = value & 0x07;
                    let rumble = (value & 0x08) == 0x08;
                    if rom.rumble_active != rumble {
                        rom.rumble_active = rumble;
                        rom.trigger_rumble();
                    }
                }

                if ram_bank as u16 >= rom.ram_bank_count {
                    return;
                }

                rom.set_ram_bank(ram_bank);
            }
            _ => warnln!("Writing to unknown Cartridge ROM location 0x{:04x}", addr),
        }
    },
    read_ram: |rom: &Cartridge, addr: u16| -> u8 {
        if !rom.ram_enabled {
            return 0xff;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize]
    },
    write_ram: |rom: &mut Cartridge, addr: u16, value: u8| {
        if !rom.ram_enabled {
            warnln!("Attempt to write to ERAM while write protect is active");
            return;
        }
        rom.ram_data[rom.ram_offset + (addr - 0xa000) as usize] = value;
    },
};

pub static GAME_GENIE: Mbc = Mbc {
    name: "GameGenie",
    read_rom: |rom: &Cartridge, addr: u16| -> u8 {
        let game_genie = rom.game_genie.as_ref().unwrap();
        if game_genie.contains_addr(addr) {
            // retrieves the Game Genie code that matches the current address
            // keep in mind that this assumes that no more that one code is
            // registered for the same memory address
            let genie_code = game_genie.get_addr(addr);

            // obtains the current byte that is stored at the address using
            // the MBC, this value will probably be patched
            let data = (rom.mbc.read_rom)(rom, addr);

            // checks if the current data at the address is the same as the
            // one that is expected by the Game Genie code, if that's the case
            // applies the patch, otherwise returns the original strategy is
            // going to be used
            if genie_code.is_valid(data) {
                debugln!("Applying Game Genie code: {}", genie_code);
                return genie_code.patch_data(data);
            }
        }
        (rom.mbc.read_rom)(rom, addr)
    },
    write_rom: |rom: &mut Cartridge, addr: u16, value: u8| (rom.mbc.write_rom)(rom, addr, value),
    read_ram: |rom: &Cartridge, addr: u16| -> u8 { (rom.mbc.read_ram)(rom, addr) },
    write_ram: |rom: &mut Cartridge, addr: u16, value: u8| (rom.mbc.write_ram)(rom, addr, value),
};

#[cfg(test)]
mod tests {
    use super::{Cartridge, RomType};

    #[test]
    fn test_has_rumble() {
        let mut rom = Cartridge::new();
        rom.set_data(&vec![0; 0x8000]);
        assert!(!rom.has_rumble());

        rom.set_rom_type(RomType::Mbc5Rumble);
        assert!(rom.has_rumble());

        rom.set_rom_type(RomType::Mbc5RumbleRam);
        assert!(rom.has_rumble());

        rom.set_rom_type(RomType::Mbc5RumbleRamBattery);
        assert!(rom.has_rumble());

        rom.set_rom_type(RomType::Mbc1);
        assert!(!rom.has_rumble());
    }
}