/*	
 *	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;
}

