Documentation
mod mbc1;
mod mbc2;
mod mbc3;
mod mbc5;

use ambassador::{delegatable_trait, Delegate};
use log::warn;
use serde::{Deserialize, Serialize};

use crate::{
    context,
    gameboy::Error,
    rom::{self, Rom},
    util::trait_alias,
};

trait_alias!(pub trait Context = context::Rom + context::ExternalRam);

#[allow(unused_variables)]
#[delegatable_trait]
pub trait MbcTrait {
    fn read(&mut self, ctx: &mut impl Context, addr: u16) -> u8;
    fn write(&mut self, ctx: &mut impl Context, addr: u16, data: u8) {}
    fn internal_ram(&self) -> Option<&[u8]> {
        None
    }
}

#[derive(Serialize, Deserialize)]
pub struct NullMbc {}

impl NullMbc {
    fn new(rom: &Rom) -> Self {
        assert_eq!(
            rom.rom_size,
            32 * 1024,
            "ROM only cartridge should be 32KiB"
        );
        Self {}
    }
}

impl MbcTrait for NullMbc {
    fn read(&mut self, ctx: &mut impl Context, addr: u16) -> u8 {
        match addr {
            0x0000..=0x3FFF => ctx.rom().data[addr as usize],
            0x4000..=0x7FFF => ctx.rom().data[addr as usize],
            0xA000..=0xBFFF => ctx.external_ram()[addr as usize - 0xA000],
            _ => panic!("{addr:04X}"),
        }
    }
    fn write(&mut self, ctx: &mut impl Context, addr: u16, data: u8) {
        match addr {
            0xA000..=0xBFFF => ctx.external_ram_mut()[addr as usize - 0xA000] = data,
            _ => warn!("Invalid address write: ${addr:04X} = ${data:02X}"),
        }
    }
}

macro_rules! def_mbc {
    ($($id:ident => $ty:ty,)*) => {
        #[derive(Serialize, Deserialize, Delegate)]
        #[delegate(MbcTrait)]
        pub enum Mbc {
            NullMbc(NullMbc),
            $(
                $id($ty),
            )*
        }

        pub fn create_mbc(rom: &Rom, internal_ram: Option<Vec<u8>>) -> Result<Mbc, Error> {
            let cart_type = rom.cartridge_type.clone();
            Ok(match cart_type.mbc {
                None => Mbc::NullMbc(NullMbc::new(rom)),
                $(
                    Some(rom::Mbc::$id) => Mbc::$id(<$ty>::new(rom, internal_ram)),
                )*
                Some(mbc) => Err(Error::UnsupportedMbc(mbc))?,
            })
        }
    }
}

def_mbc! {
    Mbc1 => mbc1::Mbc1,
    Mbc2 => mbc2::Mbc2,
    Mbc3 => mbc3::Mbc3,
    Mbc5 => mbc5::Mbc5,
}