/*
 *	mbc.c
 *
 *	GameBoy/EMU
 *
 *	* Thu Mar 03 20:00:00 JST 2005 Naoyuki Sawa
 *	- 作成開始。
 */
#ifdef PIECE
#include "app.h"
#endif /*PIECE*/

/****************************************************************************
 *	
 ****************************************************************************/

typedef struct _MBC {
	int rom_bank_no;
	int ram_batt_size;			/* バッテリバックアップRAMのバイト数 */
	/* MBC1 */
	int mbc1_memory_model;			/* 0 = 16Mbit ROM/8KByte RAM, 1 = 4Mbit ROM/32KByte RAM */
	/* MBC3 */
	unsigned char mbc3_rtc_latch[5];	/* [0] = Sec, [1] = Min, [2] = DayLo, [3] = DayHi */
#ifdef PIECE
	char save_fname[MAXFILENAME];
#else /*PIECE*/
	char save_fname[_MAX_PATH];
#endif /*PIECE*/
} MBC;
MBC mbc;

#define PAGE_NUM	5			/* SRAM上にキャッシュするバンクの数 (調整可) */
typedef struct _ROMTAG {
	int bank;				/* バンク番号 */
	unsigned char* page/*[0x4000]*/;	/* ページの先頭アドレス */
} ROMTAG;
ROMTAG rom_tag[PAGE_NUM];
unsigned char rom_page[PAGE_NUM - 1][0x4000];	/* 最後の1ページはdmgmem[0x4000..0x7fff]を利用 */

/****************************************************************************
 *	
 ****************************************************************************/

void
mbc_set_rom_bank(int no)
{
	int i;
	ROMTAG tmp;

	/* 要求されたバンクが既に選択されていたら、何もしません。 */
	if(rom_tag[0].bank == no) {
		return;
	}

	/* 要求されたバンクが既にキャッシュページに読み込まれているか? */
	for(i = 0; i < PAGE_NUM; i++) {
		if(rom_tag[i].bank == no) { /* キャッシュヒット!! */
			if(i) { /* ヒットしたエントリをMRU先頭へ持ってきます */
				tmp = rom_tag[i];
				do {
					rom_tag[i] = rom_tag[i - 1];
				} while(--i);
				rom_tag[0] = tmp;
			}
			goto L_HIT; /* キャッシュミス時の処理を飛ばします */
		}
	}
	/* 普通にループを抜けたら、キャッシュミスです。 */

	/* タグの先頭に空きエントリを作り、LRUエントリを破棄します。 */
	tmp = rom_tag[PAGE_NUM - 1];
	memmove(&rom_tag[1], &rom_tag[0], sizeof(ROMTAG) * (PAGE_NUM - 1));
	rom_tag[0] = tmp;

	/* 破棄したエントリが使っていたページに、ROMバンクを読み込みます。 */
	rom_tag[0].bank = no;
	fseek(rom_fp, 0x4000 * no, SEEK_SET);
	fread(rom_tag[0].page, 1, 0x4000, rom_fp);

L_HIT:
	ROM_BANK = rom_tag[0].page - 0x4000;	/* $4000-$7FFF: ROM bank #1 */
}

void
mbc_set_ram_bank(int no)
{
	RAM_BANK = &ram[0x2000 * no - 0xa000];	/* $A000-$BFFF: RAM bank #0 */
}

int
mbc_init(const char* rom_fname)
{
	int i;
	FILE* fp;
	char* p;

	/* メインメモリ、RAMバンク、MBC設定クリアします。 */
	memset(dmgmem, 0, 0x10000);
	memset(ram, 0, sizeof ram);
	memset(&mbc, 0, sizeof mbc);

	/* ROMバンクキャッシュを初期化します。 */
	for(i = 0; i < PAGE_NUM - 1; i++) {
		rom_tag[i].bank = -1; /* 無効 */
		rom_tag[i].page = rom_page[i];
	}
	rom_tag[i].bank = -1;
	rom_tag[i].page = &dmgmem[0x4000]; /* 最後の1ページはdmgmem[0x4000..0x7fff]を利用 */

	/* ROMファイルを開きます。 */
	rom_fp = fopen(rom_fname, "rb");
	if(!rom_fp) {
		goto L_ERR;
	}

	/* 先頭のバンクを読み込みます。 */
	if(fread(dmgmem, 1, 0x4000, rom_fp) != 0x4000) { /* $0000-$3FFF: ROM bank #0 */
		goto L_ERR;
	}

	/* MBCの種類を確認します。 */
	switch(dmgmem[0x147]) {
	case 0x00: /* ROM ONLY */
	case 0x01: /* ROM+MBC1 */
	case 0x02: /* ROM+MBC1+RAM */
	case 0x03: /* ROM+MBC1+RAM+BATT */
	case 0x05: /* ROM+MBC2 */
	case 0x06: /* ROM+MBC2+BATTERY */
	case 0x08: /* ROM+RAM */
	case 0x09: /* ROM+RAM+BATTERY */
	case 0x0b: /* ROM+MMM01 */		// MBC1と同じ?
	case 0x0c: /* ROM+MMM01+SRAM */		// MBC1と同じ?
	case 0x0d: /* ROM+MMM01+SRAM+BATT */	// MBC1と同じ?
	case 0x0f: /* ROM+MBC3+TIMER+BATT */
	case 0x10: /* ROM+MBC3+TIMER+RAM+BATT */
	case 0x11: /* ROM+MBC3 */
	case 0x12: /* ROM+MBC3+RAM */
	case 0x13: /* ROM+MBC3+RAM+BATT */
	case 0x19: /* ROM+MBC5 */
	case 0x1a: /* ROM+MBC5+RAM */
	case 0x1b: /* ROM+MBC5+RAM+BATT */
	case 0x1c: /* ROM+MBC5+RUMBLE */
	case 0x1d: /* ROM+MBC5+RUMBLE+SRAM */
	case 0x1e: /* ROM+MBC5+RUMBLE+SRAM+BATT */
	//case 0x1f: /* Pocket Camera */	// 未対応
	//case 0xfd: /* Bandai TAMA5 */		// 未対応
	case 0xfe: /* Hudson HuC-3 */		// MBC3と同じ?
	case 0xff: /* Hudson HuC-1 */		// MBC1と同じ?
		break;
	default:
		goto L_ERR; /* 未対応 */
	}

	/* ROM容量は検査しません。
	 * P/ECEのフラッシュメモリに置いたまま利用するので、論理上限無しです。
	 */

	/* RAM容量を取得します。
	 * - RAM容量不明時は、RAM容量=0で強行することにします。
	 *   本プログラムは、バッテリバックアップのためだけにRAM容量を参照します。
	 *   RAM容量不明でも、実行中のRAM使用はできるので、上手く行けば動作するかも…
	 */
	switch(dmgmem[0x0149]) {
	//case 0: /* None */
	case 1: /*  16kBit =   2kB =  1 bank  */
		mbc.ram_batt_size = 1024 * 2;
		break;
	case 2: /*  64kBit =   8kB =  1 bank  */
		mbc.ram_batt_size = 1024 * 8;
		break;
	case 3: /* 256kBit =  32kB =  4 banks */
		mbc.ram_batt_size = 1024 * 32;
		break;
	case 4: /*   1MBit = 128kB = 16 banks */
		mbc.ram_batt_size = 1024 * 128;
		break;
	}
	switch(dmgmem[0x147]) { /* MBC2特例 */
	case 0x05: /* ROM+MBC2 */
	case 0x06: /* ROM+MBC2+BATTERY */
		/* 注意!!
		 * 「8bit×256バイト」ではありません!!「4bit×512バイト」です!!
		 * 既存のGameBoyエミュレータでも間違っているものがあり、(VisualBoyAdvanceなど)
		 * それらのエミュレータでは、魔界塔士Sa・Gaの継続プレイができません。
		 */
		mbc.ram_batt_size = 512;
		break;
	}
	if(mbc.ram_batt_size > sizeof ram) {
		goto L_ERR;
	}

	/* バッテリバックアップを持たないMBCならば、RAM容量をキャンセルします。 */
	switch(dmgmem[0x147]) {
	//case 0x00: /* ROM ONLY */
	//case 0x01: /* ROM+MBC1 */
	//case 0x02: /* ROM+MBC1+RAM */
	case 0x03: /* ROM+MBC1+RAM+BATT */
	//case 0x05: /* ROM+MBC2 */
	case 0x06: /* ROM+MBC2+BATTERY */
	//case 0x08: /* ROM+RAM */
	case 0x09: /* ROM+RAM+BATTERY */
	//case 0x0b: /* ROM+MMM01 */
	//case 0x0c: /* ROM+MMM01+SRAM */
	case 0x0d: /* ROM+MMM01+SRAM+BATT */
	case 0x0f: /* ROM+MBC3+TIMER+BATT */
	case 0x10: /* ROM+MBC3+TIMER+RAM+BATT */
	//case 0x11: /* ROM+MBC3 */
	//case 0x12: /* ROM+MBC3+RAM */
	case 0x13: /* ROM+MBC3+RAM+BATT */
	//case 0x19: /* ROM+MBC5 */
	//case 0x1a: /* ROM+MBC5+RAM */
	case 0x1b: /* ROM+MBC5+RAM+BATT */
	//case 0x1c: /* ROM+MBC5+RUMBLE */
	//case 0x1d: /* ROM+MBC5+RUMBLE+SRAM */
	case 0x1e: /* ROM+MBC5+RUMBLE+SRAM+BATT */
	//case 0x1f: /* Pocket Camera */
	//case 0xfd: /* Bandai TAMA5 */
	//case 0xfe: /* Hudson HuC-3 */
	//case 0xff: /* Hudson HuC-1 */
		break;
	default:
		mbc.ram_batt_size = 0;
		break;
	}

	/* いったんROMファイルを閉じます。(MMCは1ファイルしか開けないため) */
	fclose(rom_fp);
	rom_fp = NULL;

	/* バッテリバックアップRAMを復元します。 */
	if(mbc.ram_batt_size) {
#ifdef PIECE
		strcpy(mbc.save_fname, rom_fname);
#else /*PIECE*/
		_splitpath(rom_fname, NULL, NULL, mbc.save_fname, NULL); /* 現在のフォルダにセーブ */
#endif /*PIECE*/
		p = strrchr(mbc.save_fname, '.');
		if(p) {
			*p = '\0';
		}
#ifdef PIECE
		/* MMCファイルには、P/ECEファイル名として無効な文字があるので、置き換えます。 */
		p = mbc.save_fname;
		while(*p) {
			i = *p;
			if(!((i >= '0' && i <= '9') ||
			     (i >= 'a' && i <= 'z') ||
			     (i == '_'))) {
				*p = '_';
			}
			p++;
		}
#endif /*PIECE*/
		strcat(mbc.save_fname, ".sav");
		fp = fopen(mbc.save_fname, "rb");
		if(fp) {
			fread(ram, 1, mbc.ram_batt_size, fp);
			fclose(fp);
		}
	}

	/* もういちどROMファイルを開きます。 */
	rom_fp = fopen(rom_fname, "rb");
	if(!rom_fp) {
		goto L_ERR;
	}

	/* ROMバンク、RAMバンクを初期化します。(共通) */
	mbc_set_rom_bank(1); /* $4000-$7FFF: ROM bank #1 */
	mbc_set_ram_bank(0); /* $A000-$BFFF: RAM bank #0 */

	return 0;
L_ERR:
	/* ROMファイルが開かれていたら、閉じます。 */
	if(rom_fp) {
		fclose(rom_fp);
		rom_fp = NULL;
	}
	return -1;
}

void
mbc_exit()
{
	FILE* fp;
	int len;
#ifdef PIECE
	FILEACC fa;
	int size;
	int sct;
	int turbo_save;
#endif /*PIECE*/

	/* ROMファイルが開かれていたら、閉じます。 */
	if(rom_fp) {
		fclose(rom_fp);
		rom_fp = NULL;
	}

	/* バッテリバックアップRAMを保存します。 */
	if(mbc.ram_batt_size) {
		/* 内容が変っているかどうか調べます。 */
		len = 0;
		fp = fopen(mbc.save_fname, "rb");
		if(fp) {
			while(len < mbc.ram_batt_size) {
				if(fgetc(fp) != ram[len]) {
					break;
				}
				len++;
			}
			fclose(fp);
		}
		/* 内容が変わっている場合のみ、更新します。 */
		if(len < mbc.ram_batt_size) {
#ifdef PIECE
			turbo_save = turbo(0); /* 必要!! */
			if(pceFileCreate(mbc.save_fname, mbc.ram_batt_size) == 0) {
				if(pceFileOpen(&fa, mbc.save_fname, FOMD_WR) == 0) {
					render_string_framed(&render, (DISP_X - 5 * 15) / 2, (DISP_Y - 10) / 2,
					//	 012345678901234
						"セーブ中です...",
						0x80, 0x330);
					pceLCDTrans();
					//
					size = mbc.ram_batt_size;
					sct = 0;
					while(size) {
						len = size;
						if(len > BLOCKSIZE) {
							len = BLOCKSIZE;
						}
						pceFileWriteSct(&fa, &ram[BLOCKSIZE * sct], sct, len);
						size -= len;
						sct++;
					}
					pceFileClose(&fa);
				}
			}
			turbo(turbo_save); /* 必要!! */
#else /*PIECE*/
			fp = fopen(mbc.save_fname, "wb");
			if(fp) {
				fwrite(ram, 1, mbc.ram_batt_size, fp);
				fclose(fp);
			}
#endif /*PIECE*/
		}
	}
}

/* RAMバンクのプロテクションには対応していません。
 * RAMバンクのプロテクションに依存した動作を行うゲームは、たぶん無いと思います。
 */
void
mbc_write(int addr, int data)
{
	time_t t;
	struct tm* tm;

	switch(dmgmem[0x147]) {
	case 0x01: /* ROM+MBC1 */
	case 0x02: /* ROM+MBC1+RAM */
	case 0x03: /* ROM+MBC1+RAM+BATT */
	case 0x0b: /* ROM+MMM01 */		// MBC1と同じ?
	case 0x0c: /* ROM+MMM01+SRAM */		// MBC1と同じ?
	case 0x0d: /* ROM+MMM01+SRAM+BATT */	// MBC1と同じ?
	case 0xff: /* Hudson HuC-1 */		// MBC1と同じ?
		{
			/* $0000-$1FFF */
			if(addr < 0x2000) {
				/** no job **/

			/* $2000-$3FFF */
			} else if(addr < 0x4000) {
				data &= 0x1f; /* xxxBBBBB */
				if(!mbc.mbc1_memory_model) { /* 0 = 16Mbit ROM/8KByte RAM */
					mbc.rom_bank_no &= ~0x1f;
					mbc.rom_bank_no |=  data;
				} else { /* 1 = 4Mbit ROM/32KByte RAM */
					mbc.rom_bank_no = data;
				}
				mbc_set_rom_bank(mbc.rom_bank_no ? mbc.rom_bank_no : 1);

			/* $4000-$5FFF */
			} else if(addr < 0x6000) {
				data &= 0x03; /* xxxxxxBB */
				if(!mbc.mbc1_memory_model) { /* 0 = 16Mbit ROM/8KByte RAM */
					mbc.rom_bank_no &= ~(0x03 << 5);
					mbc.rom_bank_no |=  (data << 5);
					mbc_set_rom_bank(mbc.rom_bank_no ? mbc.rom_bank_no : 1);
				} else { /* 1 = 4Mbit ROM/32KByte RAM */
					mbc_set_ram_bank(data);
				}

			/* $6000-$7FFF */
			} else { /* addr < 0x8000 */
				mbc.mbc1_memory_model = data & 1; /* xxxxxxxS */
			}
			return;
		}
	case 0x05: /* ROM+MBC2 */
	case 0x06: /* ROM+MBC2+BATTERY */
		{
			/* $0000-$1FFF */
			if(addr < 0x2000) {
				/** no job **/

			/* $2000-$3FFF */
			} else if(addr < 0x4000) {
				data &= 0x0f; /* xxxxBBBB */
				mbc.rom_bank_no = data;
				mbc_set_rom_bank(mbc.rom_bank_no ? mbc.rom_bank_no : 1);
			}
			return;
		}
	case 0x0f: /* ROM+MBC3+TIMER+BATT */
	case 0x10: /* ROM+MBC3+TIMER+RAM+BATT */
	case 0x11: /* ROM+MBC3 */
	case 0x12: /* ROM+MBC3+RAM */
	case 0x13: /* ROM+MBC3+RAM+BATT */
	case 0xfe: /* Hudson HuC-3 */		// MBC3と同じ?
		{
			/* $0000-$1FFF */
			if(addr < 0x2000) {
				/** no job **/

			/* $2000-$3FFF */
			} else if(addr < 0x4000) {
				data &= 0x7f; /* xBBBBBBB */
				mbc.rom_bank_no = data;
				mbc_set_rom_bank(mbc.rom_bank_no ? mbc.rom_bank_no : 1);

			/* $4000-$5FFF */
			} else if(addr < 0x6000) {
				if(data < 8) {
					mbc_set_ram_bank(data);
				} else if(data < 13) {
					/* メインメモリの$A000-$BFFFは使わないので、RTCバンクとして流用します。 */
					//memset(&dmgmem[0xa000], mbc.mbc3_rtc_latch[data - 8], 0x2000);
					//↓本当は上のようにすべきですが、高速化のために先頭1バイトだけ設定します。
					//↓わざわざ$A000以外の場所からRTC値を読み出すゲームは無い、と推測します。
					dmgmem[0xa000] = mbc.mbc3_rtc_latch[data - 8];
					RAM_BANK = &dmgmem[0xa000 - 0xa000];	/* $A000-$BFFF: RTC bank #8-12 */
				}

			/* $6000-$7FFF */
			} else { /* addr < 0x8000 */
				/* 本当は0->1エッジでラッチするのですけれど、この挙動に依存するゲームは無い、と推測します。
				 * $6000-$7FFFに何か書き込んだら、常にラッチすることにします。
				 */
				time(&t);
				tm = localtime(&t);
				mbc.mbc3_rtc_latch[0] = tm->tm_sec;			/* RAM Bank# 8 */
				mbc.mbc3_rtc_latch[1] = tm->tm_min;			/* RAM Bank# 9 */
				mbc.mbc3_rtc_latch[2] = tm->tm_hour;			/* RAM Bank#10 */
				mbc.mbc3_rtc_latch[3] = (tm->tm_yday >> 0) & 0xff;	/* RAM Bank#11 */
				mbc.mbc3_rtc_latch[4] = (tm->tm_yday >> 8) & 0xff;	/* RAM Bank#12 */
			}
			return;
		}
	case 0x19: /* ROM+MBC5 */
	case 0x1a: /* ROM+MBC5+RAM */
	case 0x1b: /* ROM+MBC5+RAM+BATT */
	case 0x1c: /* ROM+MBC5+RUMBLE */
	case 0x1d: /* ROM+MBC5+RUMBLE+SRAM */
	case 0x1e: /* ROM+MBC5+RUMBLE+SRAM+BATT */
		{
			if(addr < 0x2000) {
				/** no job **/
			} else if(addr < 0x3000) {
				//data &= 0xff; /* BBBBBBBB */
				mbc.rom_bank_no &= ~0xff;
				mbc.rom_bank_no |=  data;
				mbc_set_rom_bank(mbc.rom_bank_no); /* Bank#0 可 */
			} else if(addr < 0x4000) {
				data &= 0x01; /* xxxxxxxB */
				mbc.rom_bank_no &= ~(0x01 << 8);
				mbc.rom_bank_no |=  (data << 8);
				mbc_set_rom_bank(mbc.rom_bank_no); /* Bank#0 可 */
			} else if(addr < 0x6000) {
				if(dmgmem[0x147] < 0x1c) {
				//case 0x19: /* ROM+MBC5 */
				//case 0x1a: /* ROM+MBC5+RAM */
				//case 0x1b: /* ROM+MBC5+RAM+BATT */
					data &= 0x0f; /* xxxxBBBB */
				} else {
				//case 0x1c: /* ROM+MBC5+RUMBLE */
				//case 0x1d: /* ROM+MBC5+RUMBLE+SRAM */
				//case 0x1e: /* ROM+MBC5+RUMBLE+SRAM+BATT */
					data &= 0x07; /* xxxxMBBB */
				}
				mbc_set_ram_bank(data);
			}
			return;
		}
	}
}
