module y4d_timer.fpstimer;

private import SDL;
private import y4d_timer.timer;

///	フレームレート（一秒間の描画回数）調整用のタイマ。
/**
	フレームレートを60FPS（Frames Par Second）に調整する時などに使う。
*/
class FpsTimer {
	/// FPSの設定（イニシャライズを兼ねる）
	void setFps(uint fps)
	/**
		ディフォルトでは60fps。
	*/
	{
		resetTimer();
		resetElapseTimeCounter();

		m_dwCallCount = 0;
		m_nUnuseSleep = 0;
		m_nDrawCount  = 0;

		m_dwFPS = fps;
		if (fps==0) {	// non-wait mode
			return ;
		}
		// １フレームごとに何ms待つ必要があるのか？×0x10000[ms]
		m_dwFPSWait = (1000*0x10000)/m_dwFPS;
		
		// こいつは、dwFPSWaitの小数以下を16ビットの精度で保持するためにある
		m_dwFPSWaitTT = 0; //　今回の時間はゼロ(cf.DrawFlip)
	}

	///	 FPSの取得（設定値）
	uint getFps()
	{
		return m_dwFPS;
	}

	///	 FPSの取得（測定値）
	uint getRealFps()
	/**
		1秒間に何回WaitFrameを呼び出すかを、
		前回32回の呼び出し時間の平均から算出する。
	*/
	{
		if (m_nDrawCount < 16) return 0; // まだ16フレーム計測していない
		if (m_nDrawCount < 32) {
		uint t = m_dwDrawTime[(m_nDrawCount-1)]	// 前回時間
			-	m_dwDrawTime[(m_nDrawCount-16)];	// 15回前の時間
		if (t==0) {
			return 0;	//	測定不能
		}
		return (1000*15+t/2)/t;
			// 平均から算出して値を返す（端数は四捨五入する）
		}
		uint t = m_dwDrawTime[(m_nDrawCount-1) & 31] // 前回時間
			-	m_dwDrawTime[m_nDrawCount & 31];	 // 31回前の時間
		if (t==0) {
			return 0;	//	測定不能
		}	
		return (1000*31+t/2)/t;
	}

	///	CPU稼動率の取得（測定値）
	uint getCpuPower()
	/**
		ＣＰＵの稼働率に合わせて、0～100の間の値が返る。
		ただしこれは、WaitFrameでSleepした時間から算出されたものであって、
		あくまで参考値である。
	*/
	{
		if (m_nDrawCount < 16) return 0; // まだ16フレーム計測していない
		uint t=0;
		for(int i=0;i<16;i++) 
			t += m_dwElapseTime[i]; // ここ16フレーム内でFPSした時間
		// return 1-t/(1000*16/m_dwFPS)[%] ; // FPSノルマから算出して値を返す
		return 100-(t*m_dwFPS/160);
	}

	///	フレームスキップの発生した回数（測定値）
	uint getSkipFrame()
	/**
		setFpsされた値までの描画に、ToBeSkipがtrueになっていた
		フレーム数を返す。すなわち、１秒あたりのフレームスキップ数。

		ただし、ここで言うフレーム数は、
		waitFrameの呼び出しごとに１フレームとして計算。
	*/
	{	return m_dwFrameSkipCounter; }

	///	待ち時間の端数のリセット（普通、使う必要はない）
	void resetTimer()
	/**
		waitFrameでは、前回のwaitFrameを呼び出した時間を、今回の待ち時間を
		算出するのに使う（setFPSで設定した値に近づけようとするため、前回、
		不足していた時間を今回の待ち時間から減らす）ので、前回の呼び出し時間を
		現在時にすることによって、今回の待ち時間への修正項を消すのがこの関数。
		通常、使うことは無い。
	*/
	{
		m_dwLastDraw = timer.get(); // 前回描画時間は、ここで設定
		m_bFrameSkip = false;
		m_dwFrameSkipCounter = 0;
		m_dwFrameSkipCounterN = 0;
	}

	///	１フレーム分の時間が来るまで待つ
	void waitFrame()
	/**
	　	メインループのなかでは、描画処理を行なったあと、
		このwaitFrameを呼び出せば、setFPSで設定した
		フレームレートに自動的に調整される。
	*/
	{
		uint t = timer.get(); // 現在時刻

		//	スキップレートカウンタ
		if (m_dwFPS!=0 && ((m_dwCallCount % m_dwFPS) == 0)) {
			m_dwFrameSkipCounter = m_dwFrameSkipCounterN;
			m_dwFrameSkipCounterN = 0;
		}

		m_dwDrawTime[m_nDrawCount & 31] = t;  // Drawした時間を記録することでFPSを算出する手助けにする
		if (++m_nDrawCount == 64) m_nDrawCount = 32;
		// 8に戻すことによって、0～15なら、まだ16フレームの描画が終わっていないため、
		// FPSの算出が出来ないことを知ることが出来る。

		m_dwCallCount++; // こいつをFPS測定に使うことが出来る。

		// かなり厳粛かつ正確かつ効率良く時間待ちをするはず。
		if (m_dwFPS == 0) {
			m_dwElapseTime[m_nDrawCount & 31] = 0;
			return ; // Non-wait mode
		}

		m_dwFPSWaitTT = (m_dwFPSWaitTT & 0xffff) + m_dwFPSWait; // 今回の待ち時間を計算
		// m_dwFPSWaitは、待ち時間の小数以下を１６ビットの精度で持っていると考えよ
		// これにより、double型を持ち出す必要がなくなる。

		uint dwWait = m_dwFPSWaitTT >> 16; // 結局のところ、今回は何ms待つねん？

		// １フレーム時間を経過しちょる。ただちに描画しなちゃい！
		uint dwElp = cast(uint)(t - m_dwLastDraw); // 前回描画からいくら経過しとんねん？
		if (dwElp>=dwWait) { // 過ぎてるやん！過ぎてる分、250msまでやったら次回に持ち越すで！
		uint dwDelay = dwElp-dwWait;

		//	250以上遅れていたら、フレームスキップしない（初期化のため）
		//	そして、遅れ時間は0として扱う
		if (dwDelay >= 250) {
			dwDelay = 0;
		}

		//	２フレームの描画時間以上ならば次フレームをスキップする
		m_bFrameSkip =	cast(bool) (dwDelay >= dwWait*3);
		if (m_bFrameSkip) m_dwFrameSkipCounterN++;

		if (dwDelay < 250) { t -= dwDelay; } else { t -= 250; }
		// 今回の描画時刻を偽ることで、次回の描画開始時刻を早める

			m_dwLastDraw = t;
			m_dwElapseTime[m_nDrawCount & 31] = 0;
			return ;
		}

		// ほな、時間を潰すとすっか！

		m_dwElapsedTime += dwElp; // 時間待ちした分として計上
		m_dwElapseTime[m_nDrawCount & 31] = dwElp;

		m_bFrameSkip = false;	//	次はフレームスキップしない

		SDL_Delay(dwWait-dwElp);

		// これで、時間つぶし完了！

		m_dwLastDraw += dwWait; // ぴったりで描画が完了した仮定する。（端数を持ち込まないため）

	}

	///	次のフレームでは(描画を)スキップすべきか？の判定関数
	bool toBeSkip()
	/**	
		もし、これがtrueを返してくるならば、そのフレームの描画をまるごと
		スキップしてしまえば（ただし、キャラの移動等の計算だけは行なう）、
		秒間のFPSは遅いマシンでも一定と仮定してプログラムすることが出来る。
		ただし、そのときもwaitFrameは呼び出すこと。
		詳しい解説は、やねうらおのホームページ
			http://www.sun-inet.or.jp/~yaneurao/
		の短期集中プログラミング講座の天才ゲームプログラマ養成ギプス
		第２１章を参照のこと。

		また、フレームをスキップするかどうかは、waitFrameの段階で確定する。
		そのため、次のwaitFrameを呼び出すまで、toBeSkipは同じ真偽値を
		返し続ける。
	*/
	{ 	return m_bFrameSkip; }

	/**
		waitFrameで消費した時間を累計でカウントしている。
		そのカウンタのりセットと取得するのがこの関数である。
	*/
	///		消費時間のリセット
	void resetElapseTimeCounter()
	{ m_dwElapsedTime = 0; }

	/**
		waitFrameで消費した時間を累計でカウントしている。
		そのカウンタのりセットと取得するのがこの関数である。
	*/
	///		消費時間の取得
	uint getElapseTimeCounter()
	{ return m_dwElapsedTime; }

	/**
		waitFrameを呼び出すごとに内部的な呼び出しカウンタが
		インクリメントされる。そのカウンタをリセットするのと取得するのが
		この関数である。
	*/
	///		呼出しカウンタのリセット
	void resetCallCounter()
	{ 	m_dwCallCount = 0; }

	/**
		waitFrameを呼び出すごとに内部的な呼び出しカウンタが
		インクリメントされる。そのカウンタをリセットするのと取得するのが
		この関数である。
	*/
	uint getCallCounter()
	///		呼出しカウンタの取得
	{	return m_dwCallCount; }


	this() { timer = new Timer; setFps(60); }

private:
	uint	m_dwFPS;			//	FPS(ディフォルトで60)
	uint	m_dwFPSWait;		// =1000*0x10000/FPS; // 60FPSに基づくウェイト
	uint	m_dwLastDraw;		// 前回の描画時刻
	uint	m_dwFPSWaitTT;		// 前回発生した端数時間の保持のため
	uint	m_dwElapsedTime;	// 前回リセットされてからElapseされた時間
	int		m_nUnuseSleep;		// Wait関数内でSleepを利用するか
	uint	m_dwDrawTime[32];	// FPS測定用の描画時間計算用
	uint	m_dwElapseTime[32]; // CPU Power測定用
	uint	m_nDrawCount;	// （ここ32回の呼出し時間の平均からFPSを求める）
	uint	m_dwCallCount;	// WaitFrameを呼び出された回数

	bool	m_bFrameSkip;	//	次のフレームはスキップするのか？
	uint	m_dwFrameSkipCounter;	// 　フレームスキップカウンタ
	uint	m_dwFrameSkipCounterN;	// 計測中のフレームスキップカウンタ

	Timer	timer;
}
