module y4d_sound.cdda;

private import SDL;
private import ytl.singleton;
private import ytl.y4d_result;

///	CD-ROMの再生(CDDA)
/**
	複数ドライブの同時再生にも対応。

	スレッドセーフではないのであちこちのスレッドから
	同じドライブに対して再生したり停止したりしないように。

	使用例)
<PRE>
	CDDA cdda = new CDDA;
	cdda.open(0);
	cdda.play();	//	再生開始!

	while (!GameFrame.pollEvent()){
		int u = cdda.getCurrentPos();
		int m,s,f;
		cdda.FRAMES_TO_MSF(u,m,s,f);
		printf("%d:%d:%d\n",m,s,f);
	}
</PRE>

*/
class CDDA {

	///	接続されているCD-ROMの数を返す
	/**
		接続されていないときは0。
	*/
	static int getDriveNum() { return getControl().getDriveNum(); }

	///	CD-ROMをopenする
	/**
		システムディフォルトのCDは必ず no は 0。
		(getDriveNum で返ってくる値) - 1 までを指定することが出来る。<BR>
	*/
	y4d_result	open(int no){
		init();
		if (cdrom) return y4d_result.already_done; // already open
		if (getDriveNum() <= no) return y4d_result.invalid_parameter;
		cdrom = SDL_CDOpen(no);
		if (!cdrom) return y4d_result.SDL_error; // can't open
		return y4d_result.no_error;
	}

	///	CDを閉じる
	/**
		CD使いおわったら閉じてください。
		デストラクタから close が呼び出されるので
		閉じ忘れても一応は安全ですが。
	*/
	void close(){
		init();
		if (cdrom) {
			stop();
			//	closeする前に停止させないと再生されたままになる(SDL)
			SDL_CDClose(cdrom);
			cdrom = null;
		}
	}

	///	CDを停止させる
	void stop(){
		init();
		if (cdrom) {
			SDL_CDStop(cdrom);
		}
	}

	///	指定したトラックを再生します
	/**
		トラックナンバーは0～
	*/
	y4d_result playTrack(int track)
	{
		init();
		stop();
		if (!cdrom) return y4d_result.precondition_error; // not opened
		if ( CD_INDRIVE(SDL_CDStatus(cdrom)) ) {
			int result = SDL_CDPlayTracks(cdrom, track, 0, track+1, 0);
			if (result) {
				return y4d_result.SDL_error; // can't play
			}
		}
		return y4d_result.no_error;
	}

	///	先頭から再生する
	y4d_result play()
	{
		return playTrack(0);
	}

	///	CD再生中かを返す。
	/**
		playTrackのあと呼び出すと良い。
	*/
	bool isPlay(){
		init();
		if (!cdrom) return false;
			return cast(bool) (SDL_CDStatus(cdrom) == CD_PLAYING);
	}

	/// CDのeject
	/**
		openしているCDに対してしかejectできない。
	*/
	y4d_result	eject() {
		init();

		if (cdrom) {
			stop();
			int result = SDL_CDEject(cdrom);
			if (result) {
				return y4d_result.SDL_error; // can't play
			}
			return y4d_result.no_error;
		} else {
			//	CDないで?
			return y4d_result.precondition_error;
		}
	}

	/// CDのpause
	/**
		再生中でなければpauseできない。
	*/
	y4d_result	pause() {
		if (cdrom && isPlay()) {
			int result = SDL_CDPause(cdrom);
			if (result) {
				return y4d_result.SDL_error; // can't play
			}
			return y4d_result.no_error;
		} else {
			//	CDないで?
			return y4d_result.precondition_error;
		}
	}

	///	CDのresume
	/**
		pauseで止めた再生を再開する
	*/
	y4d_result	resume() {
		if (cdrom) {
			int result = SDL_CDResume(cdrom);
			if (result) {
				return y4d_result.SDL_error; // can't play
			}
			return y4d_result.no_error;
		} else {
			//	CDないで?
			return y4d_result.precondition_error;
		}
	}

	///	現在の再生トラック番号取得。
	/**
		非再生ならば-1
	*/
	int getPlayingTrack() {
		if (!isPlay()) { return -1; }
		if (cdrom) {
			return cdrom.numtracks;
		}
		return -1; // どうなっとるんや..
	}

	///	現在の再生ポジション取得
	/**
		非再生ならば-1。
		この戻り値をFRAMES_TO_MSFに食わせれば
		CDの先頭からの時間が取得できる。

		ここで返されるフレームについては十分な分解能があるとは限らない。
		(少なくとも秒ぐらいは正しく取得できるだろうけど)
	*/
	int	getCurrentPos(){
		if (!isPlay()) { return -1; }
		if (cdrom) {
			return cdrom.cur_frame;
		}
		return -1; // どうなっとるんや..
	}

	///	FRAMEからMSFに変換。
	/**
		getCurrentPosの戻り値を渡して使う。
		f : フレーム[in]
		M,S,F : 分,秒,フレーム(秒間75フレームと仮定)[out]
	*/
	static void FRAMES_TO_MSF(int f, out int M, out int S, out int F)
	{
		const uint CD_FPS	= 75;
		int value = f;
		F = value % CD_FPS;
		value /= CD_FPS;
		S = value % 60;
		value /= 60;
		M = value;
	}

	~this() { close(); }

	///	SDLのCD構造体を直接取得。
	/**
		このクラスのメソッドで足りなければ自分でSDL_CDを用いて直接
		なんとかしる。
	*/
	SDL_CD* getSDL_CD() { return cdrom; }

private:
	SDL_CD *cdrom;

	static CDDAControl getControl() { return singleton!(CDDAControl).get(); }

	void init() { getControl().init(); }

	static class CDDAControl {
		this() { init(); }
		~this() { terminate(); }

		int getDriveNum() {
			return driveNum;
		}

		void init() {
			if (bInit) return ;
			bInit = true;
			if (SDL_InitSubSystem(SDL_INIT_CDROM) < 0) {
				noCDRom = true;
				return ;
			}
			driveNum = SDL_CDNumDrives();
			if (driveNum == -1) {
				driveNum = 0; // -1とかになってると後々ややこしいナリよ
			}
		}

		void terminate() {
			//	終了時に後始末。
			if (bInit && !noCDRom) {
				SDL_QuitSubSystem(SDL_INIT_CDROM);
			}
		}

	private:
		bool bInit;
		bool noCDRom;
		int	driveNum;
	}
}

