/**
 * @file mmlInterpreter.c
 * @author AIG-Soft/Shinichiro Nakamura
 * @copyright
 * ===============================================================
 * A tiny MML_INTERPRETER parser (Version 0.5.0)
 * ===============================================================
 * Copyright (c) 2014-2015 Shinichiro Nakamura
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
// 2015/09/25 for X-BASIC' by AIG-Soft

#include "mml.h"
#include <stdio.h>

//#define DEBUG2	//!!test!!debug!!

//--------------------------------------------------------------------------------------

// 1つタイマー設定
// tim : 設定するウエイト時間
void setTimer1(MML_INTERPRETER *handle,int tim)
{
	printFunc();
	nslog(@"wait=%d",tim);
	handle->timerStart=*handle->tickUp;
	handle->usingTimers=0;
	handle->timerEnd[handle->usingTimers++]=handle->timerStart+tim;
}

// 2つタイマー設定
// tim1 : 設定するウエイト時間1(length分)
// tim2 : 設定するウエイト時間2(ratio分)
// tim1>=tim2であること
void setTimer2(MML_INTERPRETER *handle,int tim1,int tim2)
{
	printFunc();
	nslog(@"wait=%d,rate=%d",tim1,tim2);
	handle->timerStart=*handle->tickUp;
	handle->usingTimers=0;
	handle->timerEnd[handle->usingTimers++]=handle->timerStart+tim1;
	handle->timerEnd[handle->usingTimers++]=handle->timerStart+tim2;
}

// 時間が経過したかどうか
// @return 2=tim2が経過 , 1=tim1も経過 , 0=どちらも経過せず , -1=タイマーの設定はされていない
int checkTimer(MML_INTERPRETER *handle)
{
	printFunc();
	if (handle->usingTimers==0) {
		return -1;
	}
	// 長い方から判定
	for (int i=0;i<handle->usingTimers;i++) {
		if (*handle->tickUp>=handle->timerEnd[i]) {
			handle->usingTimers=i; // [0]で一致したら0,[1]なら1とし、そのタイマーのチェックを止める
			return i+1;
		}
	}
	// どのタイマーも経過してない
	return 0;
}

// waitを強制的に止める
void clearWaitEnd(MML_INTERPRETER *handle)
{
	handle->usingTimers=0;
	// 念のため
	for (int i=0;i<MAX_TIMER;i++) {
		handle->timerEnd[i]=0;
	}
	handle->timerStart=0;
}

// usingTimers=2ならtim2だけ経過していればOK
// usingTimers=1ならtim1が　経過していればOK
BOOL isWaitEnd(MML_INTERPRETER *handle)
{
	if (handle->usingTimers==0) return YES; // タイマー設定がないときは即時経過している
	return (*handle->tickUp>=handle->timerEnd[handle->usingTimers-1]);
}

//--------------------------------------------------------------------------------------

void callback(MML_INTERPRETER *handle,id obj,SEL selector,MML_INFO *info)
{
	if (handle->callback!=NULL) {
		handle->callback(info, handle->extobj);
	}
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"	// 警告回避のため
//	[obj performSelector:selector withObject:[NSValue valueWithBytes:info objCType:@encode(MML_INFO)]];
	// infoを通じて値の遣り取りをするためアドレス渡しにする
	[obj performSelector:selector withObject:[NSValue valueWithBytes:&info objCType:@encode(MML_INFO *)]];
#pragma clang diagnostic pop
}

//------------------------------------------
// MML_INTERPRETER解釈

// 解釈開始処理
MML_RESULT mml_fetch_start(MML_INTERPRETER *handle)
{
	printFunc();
	
	if (handle->state != MML_STATE_FETCH_READY) {
		nslog(@"MML_RESULT_NOT_READY_FOR_FETCH");
		return MML_RESULT_NOT_READY_FOR_FETCH;
	}
	
	char *p=handle->mmlObj;
	int adrs=0;
	
	// 識別子チェック
	if (memcmp(p,MML_HEADER,SZ_MML_HEADER)!=0) {
		nslog(@"識別子がない=データがおかしい");
		return MML_RESULT_ILLEGAL_DATA;
	}
	p   +=SZ_MML_HEADER;
	adrs+=SZ_MML_HEADER;
	
	// コメント領域へのオフセットを得ておく
	int ofst;
	UWord *wp=(UWord *)p;
	ofst =*wp++;
	ofst|=((*wp++)<<16);
	adrs+=SZ_WORD*2;

	handle->commentTop=handle->mmlObj+ofst;
	handle->p			= (char *)wp;
	handle->adrs		= adrs;

	handle->fjump		= NO;
	handle->fwaon		= NO;
	handle->ftie		= NO;
	handle->fsync		= FSYNC_NONE; // 標準では同期しない(1CH用)
	handle->fwait		= FWAIT_NONE;
	handle->waonCnt 	= 0;
	handle->ratio		= 0;
	handle->count		= 0;
	clearWaitEnd(handle); // ウエイト系クリア
	
	return MML_RESULT_OK;
}

// iOSエントリ
// 1回コールで1つ進める
// id型は構造体に入れられないのでやむを得ず毎回与えている
// @return		MML_RESULT_OK	1コマンド処理正常終了
//				MML_RESULT_EOT	MML終了
//				他				エラー発生
MML_RESULT mml_fetch_once(MML_INTERPRETER *handle,id obj,SEL selector)
{
	printFunc();
	
	// NOTE/NOTEWは分割処理で保存が必要なので
	static MML_INFO info;
	static MML_INFO ninfo[MML_MAX_WAON];

	if (handle->fsync==FSYNC_NOW) {
		nslog(@"初回発声同期中");
		// これの解除は外部で行うこと
		return MML_RESULT_OK;
	}
	
	// オリジナルはNOTEにticksも渡していたが、
	// こちらはNOTEは発声開始のみで、消音は別途コマンドを発行する
	// 音源側も消音を考慮しなくていいので簡単になる
	
	int chk=checkTimer(handle);
	switch (chk) {
		case 1: // NOTE/RESTウエイト
			nslog2(@"NOTE/RESTウエイト終了:ftie=%d,fwait=%d",handle->ftie,handle->fwait);
			switch (handle->fwait) {
				case FWAIT_NOTE: // ratioはNOTEしかありえないけど念のため
					info.type = MML_TYPE_NOTE_FINISH;
					info.subValue=handle->ftie;
					callback(handle,obj,selector,&info);
				default: // RESTもこちら
					handle->fwait=FWAIT_NONE;	// NOTEウエイトでの消音は不要
					break;
			}
			// ウエイトが終わったので、以下普通処理可能
		case -1:// タイマーは設定されていない=素通り
			break;
		//
		case 2: // ratioタイマー
			nslog(@"RATEウエイト終了");
			if (handle->fwait==FWAIT_NOTE) { // ratioはNOTEしかありえないけど念のため
				info.type = MML_TYPE_NOTE_FINISH;
				info.subValue=NO; // 発声割合があるときは当然タイはなし
				callback(handle,obj,selector,&info);
				handle->fwait=FWAIT_NOTER; // 消音したけどウエイトは継続
			}
		default: // ウエイト中(どのタイマーも終了していない)
			switch (handle->fwait) {
				case FWAIT_NOTER:// 消音後の実発声後
				case FWAIT_NOTE: // 実発声後
				case FWAIT_REST: // 給付後
					// NOTE/RESTは先のウエイトが終了しないと次は実行できない
#ifdef DEBUG
					fprintf(stdout,"-> まだウエイト中なので遅延\n");
#endif
					// なので次のNOTE/W/RESTは実行できない
					return MML_RESULT_OK;
				default:
					break;
			}
			break;
	}
	//  NOTE/W/REST以外はどんどん実行していって構わない
	
	int wk1,wk2;
	int com,val;
	UWord *wp;
	
	switch (handle->fwait) { // NOTEの2段処理
		case FWAIT_NOTE_GO: // 準備から実発声へ(単音)
			com=MML_COM_NOTE_GO;
			break;
		case FWAIT_NOTEW_GO: // 準備から実発声へ(和音)
			com=MML_COM_WAON_GO;
			break;
		case FWAIT_REST_GO: // 準備から実給付へ
			com=MML_COM_REST_GO;
			break;

		default:
			com=*handle->p++;
			val=*handle->p++;
			wp=(UWord *)handle->p;
#ifdef DEBUG2
			fprintf(stdout,"I%04x ",handle->adrs);
#endif
			handle->adrs+=2;
			break;
	}
	
	int w;
	BOOL ftie=NO;
	
	switch (com) {
		case MML_COM_NOP:
#ifdef DEBUG2
		{
			char  *jumps[]={"","|:","SEGNO","CODA","DO"};
			fprintf(stdout,"[%c:%02x]No Operation=%s\n",com,val,jumps[val]);
		}
#endif
			break;
		
		case MML_COM_RATIO:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]発声割合=%d\n",com,val,val);
#endif
			if (val==MML_MAX_RATIO) val=0; // 全長の時は設定なしとみなす
			handle->ratio=val;
			break;
		
		case MML_COM_SOUND:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]音色=%d\n",com,val,val);
#endif
			info.args.sound.value = val;
			info.type = MML_TYPE_SOUND;
			callback(handle,obj,selector,&info);
			break;
		
		case MML_COM_PANPOT:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]パンポット=%d\n",com,val,val);
#endif
			info.args.sound.value = val;
			info.type = MML_TYPE_PANPOT;
			callback(handle,obj,selector,&info);
			break;
		
		case MML_COM_VOLUME:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]音量=%d\n",com,val,val);
#endif
			info.args.volume.value = val;
			info.type = MML_TYPE_VOLUME;
			callback(handle,obj,selector,&info);
			break;
		
		case MML_COM_OCTAVE:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]オクターブ=%d\n",com,val,val);
#endif
			info.args.octave.value = val;
			info.type = MML_TYPE_OCTAVE;
			callback(handle,obj,selector,&info);
			break;
		
		case MML_COM_TEMPO:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]テンポ=%d\n",com,val,val);
#endif
			info.args.tempo.value = val;
			info.type = MML_TYPE_TEMPO;
			callback(handle,obj,selector,&info);
			break;
			
		case MML_COM_FINE:
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]FINE\n",com,val);
#endif
			if (!handle->fjump) {
				nslog(@"ジャンプ系は未実行ないので何もしない");
				break;
			}
			// ジャンプ系を実行したあとなので終了する
		case MML_COM_END:
			nslog(@"MML_COM_END");
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]終了\n",com,val);
#endif
			return MML_RESULT_EOT;
		
		//--------------------
			
		case MML_COM_REST_GO:
			nslog(@"実休符");
			info.type = MML_TYPE_REST_GO;
			setTimer1(handle, handle->ticks);
			callback(handle,obj,selector,&info);
			handle->fwait=FWAIT_REST;
			handle->count++; // 実発声/休符数(	和音は全体で1音扱い)
			break;
			
		case MML_COM_REST:
			nslog(@"休符準備");
			w=*wp++;
			handle->p	+=2;
			handle->adrs+=2;
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%5d]休符=%d,ticks=%d\n",com,val,w,val,w);
#endif
			info.type = MML_TYPE_REST; // 準備
			handle->ticks=w;
			callback(handle,obj,selector,&info);
			handle->fwait=FWAIT_REST_GO;
			if (handle->fsync==FSYNC_WAIT) handle->fsync=FSYNC_NOW; // ここで同期待ち開始
			break;
	
		case MML_COM_NOTET:
			ftie=YES;
		case MML_COM_NOTE:
			nslog(@"単音準備:%@",ftie? @"(タイ)":@"");
			w=*wp++;
			handle->p	+=2;
			handle->adrs+=2;
			int scale;
//#ifdef DEBUG2
			int octave=
//#endif
			numberToOctaveAndScale(val,&scale);
//#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%5d]音%s=オクターブ%d,%s,ticks=%d\n",com,val,w,(com==MML_COM_NOTET ? "(タイ付き)":""),octave,scale_string[scale],w);
//#endif
			info.type = MML_TYPE_NOTE;  // 準備
			info.args.note.number=val;
			handle->ftie =ftie; // 和音の時は最後のが有効
			if (handle->fwaon) {
				nslog(@"和音モードの時:溜め込みのみ->[%d]",handle->waonCnt);
				if (handle->waonCnt==0) handle->ticks=w; // 和音の時は[0]が有効
				ninfo[handle->waonCnt++]=info;
			} else {
				// 単音モードの時
				handle->ticks=w; 	// 和音の時は[0]が有効
				callback(handle,obj,selector,&info);
				handle->fwait=FWAIT_NOTE_GO;
				if (handle->fsync==FSYNC_WAIT) handle->fsync=FSYNC_NOW; // ここで同期待ち開始
			}
			break;
			
		case MML_COM_NOTE_GO:
		{
			nslog(@"単音実発声");
			info.type = MML_TYPE_NOTE_GO; // 実発声
			// 他のパラメーターは準備側で作成済み
			int waitTicks=handle->ticks;
			if (handle->ratio>0) {
				// 発声割合あり
				setTimer2(handle, waitTicks, waitTicks*handle->ratio/MML_MAX_RATIO);
			} else {
				// 発声割合なし
				setTimer1(handle, waitTicks);
			}
			handle->fwait=FWAIT_NOTE;
			callback(handle,obj,selector,&info);
			handle->count++; // 実発声/休符数(	和音は全体で1音扱い)
		}
			break;
			
		case MML_COM_WAON_GO:
		{
			nslog(@"和音実発声");
			ninfo[0].type = MML_TYPE_NOTEW_GO; // 実発声
			int waitTicks=handle->ticks;
			if (handle->ratio>0) {
				// 発声割合あり
				setTimer2(handle, waitTicks, waitTicks*handle->ratio/MML_MAX_RATIO);
			} else {
				// 発声割合なし
				setTimer1(handle, waitTicks);
			}
			callback(handle,obj,selector,ninfo);
			handle->fwait=FWAIT_NOTE;
			handle->count++;  // 実発声/休符数(和音は全体で1音扱い)
		}
			break;

		case MML_COM_WAON:
			nslog(@"和音準備");
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x]和音:%d\n",com,val,val);
#endif
			if (!handle->fwaon) {
				nslog(@"和音モードOFF->ON");
				handle->fwaon=YES;
				handle->waonCnt=0;
				// 以下溜め込み開始
			} else {
				nslog(@"和音モードON->OFF");
				// ここで一気に出力する
				ninfo[0].type=MML_TYPE_NOTEW; // 和音データに変更
				ninfo[0].subValue=handle->waonCnt; // 和音数
				nslog(@"和音数=%d",handle->waonCnt);
				callback(handle,obj,selector,ninfo);
				handle->fwait=FWAIT_NOTEW_GO;
				handle->fwaon=NO; // 和音終了
				if (handle->fsync==FSYNC_WAIT) handle->fsync=FSYNC_NOW; // ここで同期待ち開始
			}
			break;

		//--------------------
			
		case MML_COM_COMMENT:
			w=*wp++;
			handle->p	+=2;
			handle->adrs+=2;
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%04x]コメント=%s\n",com,val,w,&handle->commentTop[w]);
#endif
			info.args.comment.name=&handle->commentTop[w];
			info.type = MML_TYPE_COMMENT;
			callback(handle,obj,selector,&info);
			break;

		case MML_COM_LOOP:
			w=*wp++;
			handle->p+=2;
			wk1=*handle->p++;
			wk2=*handle->p++;
			handle->adrs+=4;
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%04x:%02x:%02x]ループ:回数=%d,adrs=$%04x,現在回数=%d,wk2=%d\n",com,val,wk1,wk2,w ,val,w,wk1,wk2);
#endif
			if (++wk1<val) {
#ifdef DEBUG2
				fprintf(stdout,"まだループする:残り%d\n",wk1);
#endif
				// ループ数を記録
				*(handle->p-2)=wk1;
				handle->p=handle->mmlObjTop+w;
			} else {
				// ループ終了
				// ループ回数を初期化する戻す
				*(handle->p-2)=0;
			}
			// ループ終了時はすり抜ける
			break;
			
		case MML_COM_JUMP:
			w=*wp++;
			handle->adrs+=2;
			// pはwの位置
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%04x]ジャンプ:adrs=$%04x\n",com,val,w ,w);
#endif
			// 実はどのジャンプも動作は同じ
			handle->fjump=YES;
			if (val<0) {
				// 無限ジャンプ
				handle->p=handle->mmlObjTop+w;
			} else {
				if (val==0) {
					// 初回
					*(handle->p-1)=1; // 実行した印(valの位置)
					handle->p=handle->mmlObjTop+w;
				} else {
					// 2回めは素通り
					handle->p+=2; // w分を飛ばす
				}
			}
			break;
			
		case MML_COM_NEJUMP:
		{
			w=*wp++; // カウンターadrs
			int w2=*wp++; // ジャンプadrs
			handle->adrs+=4;
			// pはwの位置
#ifdef DEBUG2
			fprintf(stdout,"[%c:%02x:%04x:%04x]!=ジャンプ:カウンターadrs=$%04x,ジャンプadrs=$%04x\n",com,val,w,w2 ,w,w2);
#endif
			char *cntP=handle->mmlObjTop+w; // カウンターを読む
			if (*cntP!=val) {
				// 今回の分ではない→飛ばす
				handle->p=handle->mmlObjTop+w2;
			} else {
				// 今回の分である->素通り
				handle->p+=4; // w,w2分を飛ばす
			}
		}
			break;
		
		default:// 通常ありえないけど念のため
			nslog(@"不正なコマンド:%c(%02x)",com,com);
			return MML_RESULT_ILLEGAL_COMMAND;
	}
	nslog(@"fetch_once 終了");
	return MML_RESULT_OK;
}

//----------------------------------------------------------
// 初期化
// extobj : callback呼び出し時に第2引数として渡る

MML_RESULT mml_init(MML_INTERPRETER *handle, MML_CALLBACK callback, void *extobj)
{
	printFunc();
	
	handle->callback = callback;
	handle->extobj   = extobj;
	handle->mmlObjTop= handle->mmlObj=NULL;
	handle->fjump	 = NO;
	
	handle->state 	 = MML_STATE_WAIT_SETUP;
	
	return MML_RESULT_OK;
}

//----------------------------------------------------------
// 設定

MML_RESULT mml_setup(MML_INTERPRETER *handle, MML_OPTION *option, char *mmlObject)
{
	printFunc();
	
	if ((handle->state != MML_STATE_WAIT_SETUP) && (handle->state != MML_STATE_FETCH_READY)) {
		nslog(@"MML_RESULT_NOT_READY_FOR_SETUP");
		return MML_RESULT_NOT_READY_FOR_SETUP;
	}

	handle->mmlObjTop=handle->mmlObj=mmlObject;
	
	handle->state 	= MML_STATE_FETCH_READY;
	
	return MML_RESULT_OK;
}

//----------------------------------------------------------

// MML内のジャンプ用ワークを初期化する
MML_RESULT mml_init_work(MML_INTERPRETER *handle)
{
	printFunc();
	
	char *p=handle->mmlObj;
	
	// 識別子チェック
	if (memcmp(p,MML_HEADER,SZ_MML_HEADER)!=0) {
		nslog(@"識別子がない=データがおかしい");
		return MML_RESULT_ILLEGAL_DATA;
	}
	p+=SZ_MML_HEADER;
	
	char com,val;

	// コメント領域へのオフセットを得ておく
	p+=(SZ_WORD*2);
	
	MML_RESULT mr=MML_RESULT_EOT;
	do {
		com=*p++;
		val=*p++;
		switch (com) {
			case MML_COM_NOP:
			case MML_COM_RATIO:
			case MML_COM_SOUND:
			case MML_COM_PANPOT:
			case MML_COM_VOLUME:
			case MML_COM_OCTAVE:
			case MML_COM_TEMPO:
			case MML_COM_WAON:
			case MML_COM_FINE:
				break;
			
			case MML_COM_END:
				return MML_RESULT_EOT;
			//
			case MML_COM_NOTE:
			case MML_COM_REST:
			case MML_COM_COMMENT:
				p+=2; // w分
				break;

			case MML_COM_LOOP:
				p+=2; // w分
				nslog(@"[%c:%02x:0000:00:00]ループ:回数",com,val);
				*p=0;
				p+=2; // wk1/2分
				break;
				
			case MML_COM_JUMP:
				// pはwの位置
				nslog(@"[%c:%02x:0000]ジャンプ",com,val);
				*(p-1)=0; // 実行してない印(valの位置)
				p+=2; // w分
				break;
				
			default:// 通常ありえないけど念のため
				nslog(@"不正なコマンド:%c(%02x)",com,com);
				mr=MML_RESULT_ILLEGAL_COMMAND;
				break;
		}
	} while (com!=MML_COM_END);
	nslog(@"終了");
	return mr;
}

//----------------------------------------------------------
