/*	
 *	cliphes.h
 *
 *	P/ECE HES Driver
 *
 *	CLiP - Common Library for P/ECE
 *	Copyright (C) 2001-2004 Naoyuki Sawa
 *
 *	* Mon Feb 23 04:28:00 JST 2004 Naoyuki Sawa
 *	- 作成開始。
 */
#include "clip.h"

//このシンボルを定義すると、HuC6280 READ外部関数をインストールして、
//READアクセスを調べます。テスト用です。通常は定義しないでください。
//#define HES_DEBUG

/* MPR0=$FF(I/O area)と仮定、I/O areaの末尾16バイトが空きと仮定して、
 * I/O areaの末尾16バイトを特殊用途に利用します。
 */
#define PLAYER_ADDR	0x1ff0	/* プレイヤールーチン作成アドレス */
#define HALT_ADDR	0x1fff	/* ここに任意の値を書き込むとHALT */

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

#ifdef HES_DEBUG
unsigned char
hes_huc6280_read(HUC6280* huc6280, unsigned short addr) /* テスト用 */
{
	HESDRIVER* hes = (HESDRIVER*)huc6280;
	unsigned char* mem = hes->mem;

	if(addr < PLAYER_ADDR) {
		TRACE("READ %04x\n", addr);
	}

	return mem[addr];
}
#endif /*HES_DEBUG*/

void
hes_huc6280_out(HUC6280* huc6280, unsigned short addr, unsigned char data)
{
	HESDRIVER* hes = (HESDRIVER*)huc6280;
	unsigned char* mem = hes->mem;
	//
	int addr_hi = addr >> 8;
	int addr_lo = addr & 0xff;

	/* まず格納。 */
	mem[addr] = data;

	switch(addr_hi) {
	/***** VDC *****/
	case 0x00:
		switch(addr_lo) {
		case 0: /* VDC書き込みレジスタ選択 */
			hes->vdc_regsel = data;
			return;
		case 2: /* VDCレジスタ書き込み(Lo) */
			if(hes->vdc_regsel == 5) {
				hes->vblank_enable = (data >> 3) & 1;
			}
			return;
		//case 3: /* VDCレジスタ書き込み(Hi) */
		//	/* HES演奏には関係ありません。 */
		//	return;
		}
		return;

	/***** PSG *****/
	case 0x08:
		huc6280psg_write(&hes->psg, addr_lo, data);
		return;

	/***** TIMER *****/
	case 0x0c:
		switch(addr_lo) {
		case 0:	/* ?ppppppp (p:タイマリロード値) */
			/* * タイマ入力クロックはCPUクロックの1024分周です。
			 * * 実際には、タイマアンダーフロー(0→-1)で割り込みが発生します。
			 *   1→0を判定する方が楽なので、あらかじめリロード値を+1しておきます。
			 */
			data &= 0x7f;
			hes->timer_period = (data + 1) << 10;
			return;
		case 1:	/* ???????e (e:タイマStart/Stop) */
			data &= 1;
			if(hes->timer_start == data) return;	/* 変化なし */
			hes->timer_start = data;	/* 変化後の値を格納 */
			if(hes->timer_start) {	/* Stop => Start */
				hes->timer_progress = hes->timer_period;
			}
			/* タイマ割り込みリミッター処理を行います。
			 * - PC-Engineのタイマ割り込みは、最速で約7KHzです。
			 *   7KHz割り込みでPSGのDirect D/Aを駆動し、PCMサウンドを鳴らすソフトがあります。
			 *   (スーパースターソルジャー、出たな!!ツインビー等)
			 * - P/ECEの処理速度では、7KHzの割り込みには付いていけず、ハングアップします。
			 *   そこで、速すぎる割り込み要求は無視することにしました。
			 *   数KHzものタイマ割り込みは、おそらくDirect D/A専用の処理しか行っていないので、
			 *   タイマ割り込みを止めてしまっても、VBLANK割り込みだけで一応演奏は進行します。
			 * - しかしながら、数十〜百数十KHz程度の低速タイマ割り込みで演奏処理を行っているソフトもあります。
			 *   (カトちゃんケンちゃん、邪聖剣ネクロマンサー等)
			 *   これらのタイマ割り込みを止めてしまうと、通常の演奏処理まで止まってしまう恐れがあります。
			 *   つまり、無条件にタイマ割り込みを無視することはできません。
			 * - そこで、約3.5KHz以上の高速割り込みだけを無視することにしました。
			 *   タイマ割り込み設定値とtimer_progressの関係は、次のとおりです。(すぐ上の場所で計算しています)
			 *	timer_period = (設定値 + 1) << 10 [CPUサイクル]
			 *   設定値が0のとき最速で、
			 *	7.16MHz / ((0 + 1) << 10) = 7.16MHz / 0x400 = 6.99KHz
			 *   となります。設定値が1のとき、
			 *	7.16MHz / ((1 + 1) << 10) = 7.16MHz / 0x800 = 3.50KHz
			 *   となります。今回は、この二つの設定の場合だけ、タイマ割り込みを無視することにしました。
			 *   実際には、Direct D/Aを使うソフトはほとんど最速の6.99KHz割り込みを要求しているようですが、
			 *   念のため、一つ遅い設定の3.50KHz割り込み要求も無視することにしました。
			 * - HESプレイヤーアプリケーションから、タイマ割り込みが無視されたかどうか調べられるように、
			 *   タイマ割り込みを無視したらtimer_limitフィールドに1をマークすることにしました。
			 *   hes_timer_limit()アプリケーション関数を使って、このフィールドの値を取得できます。
			 * - タイマ割り込みが無視されてtimer_limitが1になった後、明示的にタイマが停止されるとtimer_limitは0に戻ります。
			 *   また、(まずありえませんが)遅いタイマ割り込みが再要求されて受け付けられた場合も、timer_limitは0に戻ります。
			 *   つまり、タイマ割り込みが実際に無視されている期間だけ、timer_limitが1になります。
			 */
			if(hes->timer_start) {
				if(hes->timer_period <= (1 + 1) << 10) {
					hes->timer_start = 0;	/* タイマ割り込み無視 */
					hes->timer_limit = 1;	/* リミッターが働いたことをマーク */
				} else {
					hes->timer_limit = 0;	/* リミッターマークをクリア */
				}
			} else {
				hes->timer_limit = 0;		/* リミッターマークをクリア */
			}
			return;
		}
		return;

	/***** INTERRUPT *****/
	//case 0x14:
	//	switch(addr_lo) {
	//	case 2:	/* ?????tab (t:タイマ割り込み0:ON/1:OFF, a:IRQ1 0:ON/1:OFF, b:IRQ2 0:ON/1:OFF) */
	//		/* * 割り込み生成ルーチンから、直接メモリを参照します。
	//		 *   よって、このアドレスへの書き込みを特別に処理する必要はありません。
	//		 */
	//		return;
	//	case 3:	/* ???????? (書き込み値に関係なく、タイマをリスタートします。※要確認) */
	//		/* * PC-Engineのタイマは周期タイマではありません。
	//		 *   アンダーフロー割り込み発生後、明示的にリスタートする必要があります。
	//		 *   今回はエミュレーションの都合で、自動リスタートする周期タイマとして扱うことにしました。
	//		 *   よって、このアドレスへの書き込み(リスタート指示)は無視します。
	//		 * * 実際のプログラムでは、TIMER isrの先頭でタイマをリスタートさせているようです。
	//		 *   即ち、おおよそ周期タイマと見なしても構わないと思います。
	//		 */
	//		return;
	//	}
	//	return;

	/***** HALT (P/ECE HES Driver 独自仕様) *****/
	case (HALT_ADDR >> 8):
		if(addr_lo == (HALT_ADDR & 0xff)) {
			ASSERT(data == 0); /* STZなので0書き込みのはず */
			huc6280->halt = 1;
		}
		return;
	}
}

void
hes_huc6280_mprset(HUC6280* huc6280, int addr/*0〜7*/, unsigned char data)
{
	HESDRIVER* hes = (HESDRIVER*)huc6280;
	unsigned char* mem = hes->mem;
	//
	const HESHEADER* header = (HESHEADER*)hes->data;
	const HESCHUNK* chunk = (HESCHUNK*)(header + 1);
	const void* eof = (unsigned char*)hes->data + hes->size;
	//
	unsigned char* dst = &mem[addr << 13];	/* 転送先エリア先頭へのポインタ */
	int phy_top =   data      << 13;	/* マッピング元物理アドレス先頭 */
	int phy_end = ((data + 1) << 13) - 1;	/* マッピング元物理アドレス終端 */
	//
	const unsigned char* src;
	int src_top;
	int src_end;
	int ofs;
	int len;

	ASSERT(addr >= 0 && addr <= 7);

	/* デバッグしやすいよう、転送先エリアをクリアしておきます。(本来は必要ありません) */
	memset(dst, 0, (1 << 13) - 1);

	/* HESデータ終端までの、全てのチャンクを走査。 */
	while((void*)chunk < (void*)eof) {
		/* DATAチャンクのみ処理します。(実際にはDATAチャンクしかないようですが…) */
		if(memcmp(chunk->sub_id, "DATA", 4) == 0) {
			/* このチャンクに含まれるデータの先頭アドレスを取得します。 */
			src = (unsigned char*)(chunk + 1);

			/* このチャンクに含まれる物理アドレス範囲を求めます。 */
			src_top = chunk->load_address;
			src_end = chunk->load_address + chunk->data_size - 1;

			/* マッピング元物理アドレス範囲にてクリッピング。 */
			if(src_top < phy_top) {
				src += phy_top - src_top;
				src_top = phy_top;
			}
			if(src_end > phy_end) {
				src_end = phy_end;
			}

			/* マッピング元物理アドレス範囲を含んでいたら、転送します。 */
			if(src_top <= src_end) {
				ofs = src_top - phy_top;
				len = src_end - src_top + 1;
				memcpy(dst + ofs, src, len);
			}
		}

		/* 次のチャンクへ。 */
		chunk = (HESCHUNK*)((unsigned char*)(chunk + 1) + chunk->data_size);
	}
}

void
hes_huc6280_vdcout(HUC6280* huc6280, int addr/*0〜2*/, unsigned char data)
{
	ASSERT(addr >= 0 && addr <= 2);

	switch(addr) {
	case 0:	/* VDC Address */
		hes_huc6280_out(huc6280, 0x0000, data);
		break;
	case 1:	/* VDC Data Lo */
		hes_huc6280_out(huc6280, 0x0002, data);
		break;
	case 2:	/* VDC Data Hi */
		hes_huc6280_out(huc6280, 0x0003, data);
		break;
	}
}

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

int
hes_init(HESDRIVER* hes, const void* data, int size, int i_song)
{
	HUC6280* huc6280 = &hes->huc6280;
	unsigned char* mem = hes->mem;
	HUC6280PSG* psg = &hes->psg;
	//
	const HESHEADER* header = (HESHEADER*)data;
	//
	int i;

	ASSERT(sizeof(HESHEADER) == 0x10);
	ASSERT(sizeof(HESCHUNK ) == 0x10);

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

	/* HESデータアドレス、HESデータサイズ格納。 */
	hes->data = data;
	hes->size = size;

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

	/* 演奏トラック番号に-1が指定されていたら、既定の開始トラック番号を使用。 */
	if(i_song < 0) i_song = HESBYTE(header->start_song);

	/* CPUリセット。 */
#ifdef HES_DEBUG
	huc6280_reset(huc6280, hes_huc6280_read, hes_huc6280_write, hes_huc6280_mprset, hes_huc6280_vdcout); /* テスト用 */
#else /*HES_DEBUG*/
	huc6280_reset(huc6280, NULL, hes_huc6280_write, hes_huc6280_mprset, hes_huc6280_vdcout);
#endif /*HES_DEBUG*/

	/* MPR初期化。 */
	for(i = 0; i < 8; i++) {
		huc6280_mpr_set(huc6280, i, HESBYTE(header->initial_mpr[i]));
	}

	/* PSGリセット。 */
	huc6280psg_reset(psg, HES_PSG_CLOCK);

	/* プレイヤールーチンセットアップ。 */
	mem[PLAYER_ADDR + 0] = 0x20;	// JSR request_address
	mem[PLAYER_ADDR + 1] = HESHALF(header->request_address) & 0xff;
	mem[PLAYER_ADDR + 2] = HESHALF(header->request_address) >> 8;
	mem[PLAYER_ADDR + 3] = 0x9c;	// STZ HALT_ADDR
	mem[PLAYER_ADDR + 4] = HALT_ADDR & 0xff;
	mem[PLAYER_ADDR + 5] = HALT_ADDR >> 8;
	mem[PLAYER_ADDR + 6] = 0x4c;	// JMP PLAYER_ADDR + 3
	mem[PLAYER_ADDR + 7] = (PLAYER_ADDR + 3) & 0xff;
	mem[PLAYER_ADDR + 8] = (PLAYER_ADDR + 3) >> 8;

	/* プレイヤールーチン実行セットアップ。 */
	huc6280->a = i_song;
	huc6280->pc = PLAYER_ADDR;

	/* プレイヤルーチン実行。初期化完了してHALTするところまで。 */
	huc6280_run(huc6280, HES_CPU_CLOCK/*最悪でも1秒以内には完了すると仮定*/);
	if(!huc6280->halt) return -1; /* 初期化完了しなければ演奏不可とします */

	return 0;
}

int
hes_stream_callback(short* wbuff, int param)
{
	return hes_mix((HESDRIVER*)param, wbuff);
}

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

static HESDRIVER g_hes;

int
hes_play(const void* data, int size, int i_song)
{
	/* まず確実に停止します。 */
	hes_stop();

	/* HESドライバを初期化します。 */
	if(hes_init(&g_hes, data, size, i_song) != 0) return -1;

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

	return 0;
}

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

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

	/* ↓演奏処理に関してはクリーンアップ不要なのですが・・・ */

	/* 演奏停止中はhes_timer_limit()が0を返す仕様なので、ゼロフィルしておきます。 */
	memset(&g_hes, 0, sizeof(HESDRIVER));
}

int
hes_timer_limit()
{
	return g_hes.timer_limit;
}

