/*	
 *	clipgbs.h
 *
 *	P/ECE GBS Driver
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2004 Naoyuki Sawa
 *
 *	* Thu Jan 15 20:09:00 JST 2004 Naoyuki Sawa
 *	- 作成開始。
 *	* Fri Feb 18 18:41:00 JST 2005 Naoyuki Sawa
 *	- dmg_reset()にGameBoy擬似IPL実行用の引数が追加されたことに追従し、
 *	  dmg_reset()を利用しているコードを修正しました。
 *	  GBS DriverはGameBoy擬似IPLを使わないので、動作に影響ありません。
 */
#include "clip.h"

/****************************************************************************
 *	DMGエミュレーション用外部関数
 ****************************************************************************/

void
gbs_dmg_out(DMG* dmg, unsigned char addr, unsigned char data)
{
	GBSDRIVER* gbs = (GBSDRIVER*)dmg;
	DMGSOUND* ds = &gbs->ds;

	/* 0x10-0x3f: DMG-Soundレジスタ。 */
	if(addr >= 0x10 && addr <= 0x3f) {
		dmgsound_write(ds, addr, data);
		return;
	}

	/* DMG-Soundレジスタ以外。 */
	gbs->mem[0xff00 | addr] = data;

	/* 割り込みタイミング設定。 */
	switch(addr) {
	case 0x06: /* TMA (Timer Modulo ) */
	case 0x07: /* TAC (Timer Control) */
		if(gbs->header->timer_ctl & 4) {
			/* タイマ割り込みタイミング。 */
			switch(gbs->mem[0xff07] & 3) {
			case 0: /* 基本クロック1024分周(=  4096Hz) * (256-TMA) */
				gbs->interrupt_interval = 1024 * (256 - gbs->mem[0xff06]);
				break;
			case 1: /* 基本クロック  16分周(=262144Hz) * (256-TMA) */
				gbs->interrupt_interval =   16 * (256 - gbs->mem[0xff06]);
				break;
			case 2: /* 基本クロック  64分周(= 65536Hz) * (256-TMA) */
				gbs->interrupt_interval =   64 * (256 - gbs->mem[0xff06]);
				break;
			default:/* 基本クロック 256分周(= 16384Hz) * (256-TMA) */
				gbs->interrupt_interval =  256 * (256 - gbs->mem[0xff06]);
				break;
			}
		} else {
			/* V-BLANK割り込みタイミング。(V-BLANK=59.7Hz) */
			gbs->interrupt_interval = (int)(GBS_CLOCK / 59.7);
		}
		gbs->interrupt_progress = gbs->interrupt_interval;
		return;
	}
}

/****************************************************************************
 *	GBSドライバ内部関数
 ****************************************************************************/

/* 初期化ルーチン/演奏ルーチンの呼び出し元プログラム */
static const char gbs_program[] = {
	'\xcd',		/* +0: CALL nn */
	'\x00',		/* +1: ここに呼び出し先アドレスの下位バイトをセット */
	'\x00',		/* +2: ここに呼び出し先アドレスの上位バイトをセット */
	'\x76',		/* +3: HALT */
};

/* 呼び出し元プログラムを転送するアドレス
 * * ROM Area0の予\約領域の隙間、
 *	0x0000〜0x006?: RST xxH/割り込みベクタ)
 *   と
 *	0x0100〜      : GameBoy起動ルーチン
 *   の間に置くことにします。
 */
#define GBS_PROGRAM_ADDRESS	0x0080 /* 調整可 */

/* 呼び出し先(初期化ルーチン/演奏ルーチン)アドレスをセットするアドレス
 * 呼び出し元プログラムの「CALL nn」命令のオペランドとなります。
 */
#define GBS_RUN_PC_ADDRESS	(GBS_PROGRAM_ADDRESS + 1)

void
gbs_dmg_setup(GBSDRIVER* gbs, int a, int pc)
{
	DMG* dmg = &gbs->dmg;
	unsigned char* mem = gbs->mem;

	/* 呼び出し元プログラム経由で、呼び出し先プログラムを呼び出します。 */
	mem[GBS_RUN_PC_ADDRESS + 0] = pc & 0xff;
	mem[GBS_RUN_PC_ADDRESS + 1] = pc >> 8;
	dmg->af = a << 8;
	dmg->pc = GBS_PROGRAM_ADDRESS;
	dmg->halt = 0;  /* 必要!! */
	dmg->cycle = 0; /* 必要!! */
}

/* I/Oレジスタ初期値 */
static const unsigned char gbs_iop_init[][2] = {
	{ 0x05, 0x00 }, /* TIMA */
	{ 0x06, 0x00 }, /* TMA */
	{ 0x07, 0x00 }, /* TAC */
	{ 0x10, 0x80 }, /* NR10 */
	{ 0x11, 0xbf }, /* NR11 */
	{ 0x12, 0xf3 }, /* NR12 */
	//{ 0x14, 0xbf }, /* NR14 */ →初期化直後の発声を防ぐために削除
	{ 0x16, 0x3f }, /* NR21 */
	{ 0x17, 0x00 }, /* NR22 */
	//{ 0x19, 0xbf }, /* NR24 */ →初期化直後の発声を防ぐために削除
	{ 0x1a, 0x7f }, /* NR30 */
	{ 0x1b, 0xff }, /* NR31 */
	{ 0x1c, 0x9f }, /* NR32 */
	{ 0x1e, 0xbf }, /* NR33 */
	{ 0x20, 0xff }, /* NR41 */
	{ 0x21, 0x00 }, /* NR42 */
	{ 0x22, 0x00 }, /* NR43 */
	{ 0x23, 0xbf }, /* NR30 */
	{ 0x24, 0x77 }, /* NR50 */
	{ 0x25, 0xf3 }, /* NR51 */
	{ 0x26, 0xf1 }, /* NR52 */
	{ 0x40, 0x91 }, /* LCDC */
	{ 0x42, 0x00 }, /* SCY */
	{ 0x43, 0x00 }, /* SCX */
	{ 0x45, 0x00 }, /* LYC */
	{ 0x47, 0xfc }, /* BGP */
	{ 0x48, 0xff }, /* OBP0 */
	{ 0x49, 0xff }, /* OBP1 */
	{ 0x4a, 0x00 }, /* WY */
	{ 0x4b, 0x00 }, /* WX */
	{ 0xff, 0x00 }, /* IE */
};

int
gbs_init(GBSDRIVER* gbs, const void* data, int len, int i_song)
{
	DMG* dmg = &gbs->dmg;
	unsigned char* mem = gbs->mem;
	DMGSOUND* ds = &gbs->ds;
	//
	GBSHEADER* header = (GBSHEADER*)data;
	const unsigned char* code = (const unsigned char*)(header + 1);
	int bank0_size;
	int i;

	ASSERT(sizeof(GBSHEADER) == 0x70);

	/* まずクリアします。 */
	memset(gbs, 0, sizeof(GBSDRIVER));

	/* シグネチャ検査。 */
	if(memcmp(header->signature, "GBS", 3) != 0) return -1;

	/* GBSHEADERアドレス格納。 */
	gbs->header = header;

	/* RST xxH をロードアドレス相対位置にリダイレクト。 */
	for(i = 0x00; i <= 0x38; i += 0x08) {
		mem[i + 0] = 0xc3; /* JP nn */
		mem[i + 1] = (header->load_addr + i) & 0xff;
		mem[i + 2] = (header->load_addr + i) >> 8;
	}

	/* 初期化ルーチン/演奏ルーチン呼び出し元プログラムを転送します。 */
	memcpy(&mem[GBS_PROGRAM_ADDRESS], gbs_program, sizeof gbs_program);

	/* ロードアドレスが呼び出し元プログラム転送エリアより後ろであることを確認。 */
	if(header->load_addr < GBS_PROGRAM_ADDRESS + sizeof gbs_program) return -1;

	/* ROM Bank0部分のサイズ算出。 */
	bank0_size = 0x4000 - header->load_addr; /* 負値…Bank0部分なし */

	/* ROM Area0部分があれば、読み込みます。 */
	if(bank0_size > 0) memcpy(mem + header->load_addr, code, bank0_size);

	/* ROM Bank1先頭アドレス算出、格納。 */
	gbs->bank1 = code + bank0_size;

	/* bank1以降の有効バイト数算出、格納。 */
	gbs->length = len - sizeof(GBSHEADER) - bank0_size;

	/* 初期状態で、ROM Area1にBank1を選択しておきます。
	 * (最初のメモリクリアでgbs->rom1_bankは0に初期化されているので、
	 *  0=>1の変化となり、gbs_rom1_bank()はきちんと処理を行うはずです)
	 */
	gbs_rom1_bank(gbs, 1);

	/* CPUリセット。 */
	dmg_reset(dmg, NULL/*gbs_dmg_read*/, gbs_dmg_write, 0/*GameBoy擬似IPLは使いません*/);
	dmg->sp = header->stack_ptr; /* スタック初期化 */

	/* Soundリセット。 */
	dmgsound_reset(ds, &mem[0xff00]/*0xff00〜0xffffを共有*/, GBS_CLOCK);

	/* I/Oレジスタ初期化。 */
	for(i = 0; i < sizeof gbs_iop_init / sizeof *gbs_iop_init; i++) {
		gbs_dmg_out(dmg, gbs_iop_init[i][0], gbs_iop_init[i][1]);
	}

	/* 割り込みタイミング初期化。 */
	gbs_dmg_out(dmg, 0x06, header->timer_mod);
	gbs_dmg_out(dmg, 0x07, header->timer_ctl);

	/* 演奏する曲番号を決定。
	 * 注意: GBSHEADER.first_songだけが、1ベースです。
	 *	 それ以外は、init_addrへ渡すAレジスタ値(曲番号)も、
	 *	 このgbs_init()関数に渡すi_song引数も、0ベースです。
	 */
	if(i_song < 0) i_song = header->first_song/*1-based!*/ - 1;
	if(i_song < 0 || i_song > header->num_songs - 1) return -1;

	/* 初期化ルーチンを呼び出します。 */
	gbs_dmg_setup(gbs, i_song, header->init_addr);
	dmg_run(dmg, GBS_CLOCK);

	return 0;
}

void
gbs_rom1_bank(GBSDRIVER* gbs, int i_bank)
{
	int offset, size;

	/* * ROM Area1にROM Bank0は選択できません。(仕様)
	 *   Bank0が指定されたら、Bank1を選択します。
	 */
	if(!i_bank) i_bank++;
	if(gbs->rom1_bank == i_bank) return; /* 変化なし */

	/* 選択されたROM Bankを、ROM Area1にコピーします。 */
	offset = 0x4000 * (i_bank - 1/*Bank1ベース*/);
	size = gbs->length - offset;
	if(size > 0x4000) size = 0x4000;
	if(size <= 0) DIE(); /* 無効バンク? */
	memcpy(&gbs->mem[0x4000], gbs->bank1 + offset, size);
}

int
gbs_mix(GBSDRIVER* gbs, short wbuff[/*GBSBUFLEN*/])
{
	int cnt1 = GBSBUFLEN;	/* 残りサンプル数 */
	int cnt2;

	ASSERT(GBS_CLOCK >= SPEAKER_FREQUENCY);
	do {
		/* 次回割り込み発生までの残りサイクル数で、何サンプルミキシングできるか? */
		cnt2 = (gbs->interrupt_progress * SPEAKER_FREQUENCY + GBS_CLOCK - 1) / GBS_CLOCK;
		if(cnt2 > cnt1) cnt2 = cnt1;

		/* ミキシング。 */
		dmgsound_mix(&gbs->ds, wbuff, cnt2);
		wbuff += cnt2;
		cnt1 -= cnt2;

		/* 次回割り込み発生までの残りサイクル数を減らして、0になったら割り込み処理。 */
		gbs->interrupt_progress -= cnt2 * GBS_CLOCK / SPEAKER_FREQUENCY;
		while(gbs->interrupt_progress <= 0) {
			gbs->interrupt_progress += gbs->interrupt_interval;
			gbs_dmg_setup(gbs, 0, gbs->header->play_addr);
			dmg_run(&gbs->dmg, GBS_CLOCK/*最悪でも1秒以内には完了すると仮定*/);
		}
	} while(cnt1);

	return 0;
}

int
gbs_stream_callback(short* wbuff, int param)
{
	return gbs_mix((GBSDRIVER*)param, wbuff);
}

/****************************************************************************
 *	アプリケーション用関数
 ****************************************************************************/

int
gbs_play(const void* data, int len, int i_song)
{
	static GBSDRIVER gbs; /* STATICです! */

	/* まず確実に停止します。 */
	gbs_stop();

	/* GBSドライバを初期化します。 */
	if(gbs_init(&gbs, data, len, i_song) != 0) return -1;

	/* ストリーム再生を開始します。 */
	stream_play(GBSBUFLEN, gbs_stream_callback, (int)&gbs, 1/*スタック切換使用*/);

	return 0;
}

void
gbs_stop()
{
	/* ストリーム再生を停止します。 */
	stream_stop();

	/* GBSドライバのクリーンアップは不要です。 */
}

