module yamalib.draw.movie;


private import SDL_video;
private import SDL_audio;
private import SDL_image;
private import SDL_smpeg;
private import SDL_mutex;
private import SDL_mixer;

private import opengl;

private import y4d_draw.drawbase;
private import y4d_draw.screen;
private import y4d_draw.texture;
private import y4d_draw.surface;
private import y4d_aux.filesys;
private import y4d_sound.sound;

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

private import yamalib.log.log;

public class Movie {
		
	/// <summary>
	/// オーディオの再生周波数等は、Yanesdk.Sound.Sound.SoundConfigで設定された値で初期化される。
	/// </summary>
	public this()
	{
		info = new SDL_smpeg.SMPEG_Info();
		lockObject = new Object();
		callback =  cast(SMPEG_DisplayCallback) &smpeg_DisplayCallback;
//		callback  = new SMPEG_DisplayCallback(&smpeg_DisplayCallback);
//		callback = cast(SMPEG_DisplayCallback) function void(SDL_Surface* dst, int x, int y, uint w, uint h) { updated = true; };
		assert(callback !is null);
		
		//mix_hook = new SDL.mix_func_t(SDL_smpegSMPEG_playAudioSDL);
		mix_hook = cast(SDL_mixer.mix_func_t) &mix_HookMusic;

		// 排他処理オブジェクトの作成
		mutex = SDL_mutex.SDL_CreateMutex();

/*
		// Audioの初期化が必要
		y4d_result result = Sound.Sound.SoundConfig.Update();
		Debug.Assert(result == y4d_result.NoError);

		// MasterVolumeが変更されたときにそれに追随して再生Volumeが変更されるように。
		Yanesdk.Sound.Sound.ChunkManager.OnMasterVolumeChanged
			+= OnMasterVolumeChanged;
*/
	}
	
	
	/// <summary>
	///  Disposeを呼び出したあとは再生できない。
	/// 単に解放したいだけならばReleaseで行なうべし。
	/// </summary>
	public ~this()
	{
		release();

/+
		// MasterVolumeが変更されたときにそれに追随して再生Volumeが変更されるようにしてあったのを戻す。
		Yanesdk.Sound.Sound.ChunkManager.OnMasterVolumeChanged
			-= OnMasterVolumeChanged;

		initAudio.Dispose();
		initVideo.Dispose();
+/

		SDL_mutex.SDL_DestroyMutex(mutex);
	}

	/// <summary>
	/// SDL VideoとAudioの初期化用クラス。
	/// </summary>
//	private SDL_Video_Initializer initVideo = new SDL_Video_Initializer;
//	private SDL_AUDIO_Initializer initAudio = new SDL_AUDIO_Initializer;
//	private singleton!(Screen.SDLVideoInitializer) initializer;

	/// <summary>
	/// マスターvolumeが変更になったときのハンドラ
	/// </summary>
	private void onMasterVolumeChanged()
	{
		// ボリュームの再設定を行なう。
		// setterではmaster volumeに対して掛け算されていることを利用する。
		volume = m_volume;
	}

//	#region ILoaderの実装
	/// <summary>
	/// mpegファイルを読み込む
	/// </summary>
	/// <param name="filename">ファイル名</param>
	public y4d_result load(char[] filename) {
		synchronized (lockObject)
		{
			release();
			tmpFile = FileSys.getTmpFile(filename);
			if (tmpFile is null || tmpFile.getFileName is null)
			{
				return y4d_result.file_not_found;
			}

			mpeg = SDL_smpeg.SMPEG_new(cast(char*) tmpFile.getFileName, info, 1);
			if (SDL_smpeg.SMPEG_error(mpeg) !is null)
			{
				return y4d_result.SDL_error; // …SDL？
			}

			// 初期設定
		//	loop = false;
			// ループ設定はここでは変更しないほうが良い。

			m_paused = false;
			
			SDL_smpeg.SMPEG_enablevideo(mpeg, 1);
			SDL_smpeg.SMPEG_enableaudio(mpeg, 0);
			if (info.has_audio != 0)
			{
				SDL_audio.SDL_AudioSpec* audiofmt = new SDL_audio.SDL_AudioSpec();
				audiofmt.format = cast(ushort)Sound.getConfig.audio_format;
				audiofmt.freq = cast(ushort)Sound.getConfig.audio_rate;
				audiofmt.channels = cast(byte)Sound.getConfig.audio_channels;
				audiofmt.samples = cast(ushort)Sound.getConfig.audio_buffers;
				SDL_smpeg.SMPEG_actualSpec(mpeg, audiofmt);
//				SDL_mixer.Mix_HookMusic(mix_hook, mpeg);
				SDL_smpeg.SMPEG_enableaudio(mpeg, 1);
			}

			volume = m_volume;

			// ついでに描画先サーフェスの作成とsmpegへの登録。
			// 拡大・縮小はSMPEG側にやらせると重いっぽいので、原寸大で。
			m_surface = new Surface();
			y4d_result result = m_surface.createDIB(width, height, false);
			if (result != y4d_result.no_error)
			{
				return result;
			}

			SDL_smpeg.SMPEG_setdisplay(mpeg, m_surface.getSurface, mutex, callback);
			SDL_smpeg.SMPEG_scaleXY(mpeg, m_surface.getWidth, m_surface.getHeight);

			this.m_fileName = filename;

			// おしまい。
			return y4d_result.no_error;
		}
	}
	

	/// <summary>
	/// テクスチャに描画。
	/// 更新されていない場合処理しないので、専用のテクスチャを用意しておいて、
	/// 毎回それを指定すること。
	/// </summary>
	public void draw(Texture tx)
	{
		assert(isReadyToDraw);
		// ↑これ呼び出し元でチェックしといて、
		// falseなうちは描画しない方がいい。(´ω`)

		synchronized (lockObject)
		{
			// lock
			int p = SDL_mutex.SDL_mutexP(mutex); 
			assert(p == 0);

			// フラグ操作
			//*
			if (!updated)
			{
				// unlock
				int vv = SDL_mutex.SDL_mutexV(mutex); 
				assert(vv == 0);
				return;
			}
			//Rectangle rc = updateRect.Value; // 高速化に使えそうな予感もするものの…。
			updated = false;
			/*/
			if (!updated) {
				// unlock
				int vv = SDL.SDL_mutexV(mutex); Debug.Assert(vv == 0);
				return;
			}
			updated = false;
			//*/

//			tx.subSurfaceFast(m_surface);

			// テクスチャに描画
			tx.bind();
			opengl.glTexSubImage2D(tx.getTextureType(), 0,
				0, 0, m_surface.getWidth(), m_surface.getHeight(),
				m_surface.isAlpha ? opengl.GL_RGBA : opengl.GL_RGB,
				opengl.GL_UNSIGNED_BYTE,m_surface.getPixels);
			tx.unbind();

			// unlock
			int v = SDL_mutex.SDL_mutexV(mutex); 
			assert(v == 0);

		}
	}


	/// <summary>
	/// 再生開始
	/// </summary>
	public void play()
	{
		synchronized (lockObject)
		{
			if (isPlaying)
			{
				if (!m_paused)
				{
					innerStop();
				}
				m_paused = false;
				SDL_smpeg.SMPEG_rewind(mpeg); // 巻き戻す
			} 
			else
			{
				if (!loaded && fileName !is null)
				{
					load(fileName);
				}
			}
/+
				Stop();
+/
			readyToDraw = false; // 描画しなおされるまでは描画準備状態とする
			innerPlay();
		}
	}


	/// <summary>
	///	play中のムービーを停止させる
	/// </summary>
	public void stop()
	{
		synchronized (lockObject)
		{
			if (fileName !is null) {
				// Release()で一端filenameはnullになるので、終わってから再設定する。(´ω`)
				char[] tmp = fileName;
				release();
				m_fileName = tmp;
			}
/+			
			Debug.Assert(Loaded);
			if (IsPlaying)
			{
				if (!paused)
				{
					InnerStop();
				}
				paused = false;
				SMPEG.SMPEG_rewind(mpeg); // 巻き戻す
			}
+/
		}
	}	
	
	/// <summary>
	/// 一時停止
	/// </summary>
	public void pause()
	{
		synchronized (lockObject)
		{
			if (isPlaying && !m_paused)
			{
				// SMPEG_pause()はstop時にバグりがちなので使うのやめとく。
				m_paused = true;
			}
		}
	}
	/// <summary>
	/// pause中かを表すメソッド
	/// </summary>
	private bool m_paused = false;
	
	
	/// <summary>
	/// 再生開始とループ設定の反映
	/// </summary>
	private void innerPlay()
	{
		SDL_smpeg.SMPEG_play(mpeg);
		readyToDraw = true;
		// ループ設定を反映させる
		if (loop) {
			SDL_smpeg.SMPEG_loop(mpeg, loop ? 1 : 0);
		}
	}

	/// <summary>
	/// 再生停止
	/// </summary>
	private void innerStop()
	{
		SDL_smpeg.SMPEG_loop(mpeg, 0); // ループしてると止まらないらしい
		SDL_smpeg.SMPEG_stop(mpeg);
	}
	

	/// <summary>
	/// ファイルを読み込んでいる場合、読み込んでいるファイル名を返す
	/// </summary>
	public char[] fileName()
	{
		return m_fileName;
	}
	private char[] m_fileName = null;

	/// <summary>
	/// SDL.Mix_HookMusic()に登録する関数
	/// </summary>
	private void mix_HookMusic(void * udata, ubyte* stream, int len)
	{
		// lock (lockObject) // これはデッドロックするかも。。

		if (mpeg !is null && SDL_smpeg.SMPEG_status(mpeg) == SDL_smpeg.SMPEG_PLAYING)
		{
			SDL_smpeg.SMPEG_playAudio(mpeg, stream, len);
		}
	}

	/// <summary>
	/// 読み込んでいた動画の解放
	/// </summary>
	public void release()
	{
		synchronized (lockObject)
		{
			m_fileName = null;

			if (loaded)
			{
				SDL_mixer.Mix_HookMusic(null, null);

				stop();
				SDL_smpeg.SMPEG_delete(mpeg);
				mpeg = null;
			}
			if (tmpFile !is null)
			{
//				tmpFile.Dispose();
				tmpFile = null;
			}
			if (m_surface !is null)
			{
				m_surface.release();
				m_surface = null;
			}
			
		}
	}

	/// <summary>
	/// ファイルの読み込みが完了しているかを返す
	/// </summary>
	public bool loaded()
	{
		return mpeg !is null;
	}
	
	static class MovieState {
		private bool m_updated = false;
		bool updated () {
			return m_updated;
		}
		bool updated(bool value) {
			return m_updated = value;
		}
	}
	
	private static bool[uint] map;
	
	/// <summary>
	/// 描画時に呼び出されるコールバック関数。
	/// 別スレッドらしいので、GL周りの処理とかは止めとくのが無難っぽい。
	/// </summary>
	extern (C) private void smpeg_DisplayCallback(SDL_Surface* dst, int x, int y, uint w, uint h)
	{

		//lock (lockObject) // これはデッドロックするかも。。

		//lock (callback) // ← SMPEG側でmutexでlockされてるのでこれは要らない
		//*
		Rect rc;
		rc.setRect(x, y, cast(int)w, cast(int)h);
//		updateRect = updateRect.HasValue ? Rectangle.Union(updateRect.Value, rc) : rc;
		updateRect = rc;
		updated = true;
		
		// readyToDraw = true; // 一度でも描画されたら描画準備完了と見なす。
		if (!readyToDraw)
		{
			// 全面が更新されたら描画準備完了
			Rect* r = &updateRect;
			if (//r.Left <= 0 && r.Right <= 0 &&
				width <= r.getWidth && height <= r.getHeight)
			{
				readyToDraw = true;
			}
		}
	}
	
	/// <summary>
	/// ループするのかどうか、取得・設定。
	/// 
	/// 一度変更すると、Loadしても変更はされない。
	/// 再度設定されるまで有効。初期状態ではfalse。
	/// </summary>
	public bool isLoop() 
	{
		return loop;
	}
	public bool isLoop(bool value) 
	{
		synchronized (lockObject)
		{
			loop = value;
			if (isPlaying) // 再生時じゃないとうまく反映されないらしい
			{
				// 現在読み込み中の動画があればループモードを設定
				SDL_smpeg.SMPEG_loop(mpeg, loop ? 1 : 0);
			}
			return loop;
		}
	}

	/// <summary>
	/// ループモードなのかどうか。
	/// </summary>
	private bool loop = false;

	/// <summary>
	/// 読み込んだリソースのサイズを取得するメソッド
	/// </summary>
	public long resourceSize()
	{
		if (!loaded) {
			 return 0;
		}
		return info.total_size;
	}

	/// <summary>
	/// 横幅の取得
	/// </summary>
	public int width()
	{
		return info.width;
	}

	/// <summary>
	/// 縦幅の取得
	/// </summary>
	public int height()
	{
		return info.height;
	}

	/// <summary>
	/// 現在の動画サーフェスの取得
	/// </summary>
	public Surface surface()
	{
		return m_surface;
	}
	private Surface m_surface = null;

	/// <summary>
	/// 再生中なのかどうか。
	/// Pause中もtrueを返す。
	/// ループしない場合、これがfalseになったら終わったと思えばOK。
	/// </summary>
	/// <seealso cref="IsReadyToDraw"/>
	public bool isPlaying()
	{
		synchronized (lockObject)
		{
			return loaded && (m_paused || SDL_smpeg.SMPEG_status(mpeg) == SDL_smpeg.SMPEG_PLAYING);
		}
	}

	/// <summary>
	/// 描画可能な状態ならtrueを返す。
	/// 通常、これがtrueだった場合のみテクスチャにDraw()して、それをScreenにBltすればよい。
	/// </summary>
	/// <remarks>
	/// 基本的には IsPlaying と同じなのだが、
	/// SMPEG側に、Stop()してからPlay()すると、一瞬Stop()したときの画像が
	/// 表示されてしまうバグがあるので、
	/// Play()が呼ばれてからsurfaceにちゃんと描画されるまではfalseを返すようになっている。
	/// </remarks>
	public bool isReadyToDraw()
	{
		return isPlaying && readyToDraw;
	}

	private static bool readyToDraw = true;

/+
	/// <summary>
	/// マスターvolumeの設定
	/// すべてのSoundクラスに影響する。
	///		出力volume = (master volumeの値)×(volumeの値)
	/// である。
	/// </summary>
	/// <param name="volume"></param>
	public static float masterVolume() 
	{
		return Yanesdk.Sound.Sound.ChunkManager.MasterVolume;
	}
	public static float masterVolume(float value) 
	{
		return Yanesdk.Sound.Sound.ChunkManager.MasterVolume = value;
	}
+/

	/// <summary>
	/// volumeの設定。0.0～1.0までの間で。
	/// 1.0なら100%の意味。
	///
	/// master volumeのほうも変更できる。
	///		出力volume = (master volumeの値)×(ここで設定されたvolumeの値)
	/// である。
	///
	/// ここで設定された値はLoad/Play等によっては変化しない。
	/// 再設定されるまで有効である。
	/// </summary>
	/// <param name="volume"></param>
	/// <returns></returns>
	public float volume()
	{
		return volume;
	}
	public float volume(float value) {
		synchronized (lockObject)
		{
			m_volume = value;
	
			if (loaded)
			{
				SDL_smpeg.SMPEG_setvolume(mpeg, cast(int)(100 * /*masterVolume * */ m_volume));
			}
			return m_volume;
		}
	}
	
	private float m_volume = 1.0f;
	private Rect updateRect;
	private static bool updated = false;
		
	/// <summary>
	/// movieのためのテンポラリファイル
	/// </summary>
	private FileSys.TmpFile tmpFile = null;

	/// <summary>
	/// movieファイルへのhandle(smpeg内部で使われている)
	/// </summary>
	private SMPEG* mpeg = null;

	/// <summary>
	/// 読み込まれたmovie情報
	/// </summary>
	private SDL_smpeg.SMPEG_Info* info;

	/// <summary>
	/// surfaceに書き込みを行うmpegデコードスレッドと、
	/// Draw()との排他処理を行うSDL_mutex。
	/// </summary>
	private SDL_mutex* mutex;

	/// <summary>
	/// SMPEGから描画時に呼び出されるコールバック。
	/// GCされないようにメンバに持ってしまう。
	/// </summary>
	private SDL_smpeg.SMPEG_DisplayCallback callback;

	/// <summary>
	/// SDL.Mix_HookMusic()に登録するdelegate(中身はSDL_smpegSMPEG_playAudioSDL)
	/// callbackと同様、GCされないようにメンバに。
	/// </summary>
	private SDL_mixer.mix_func_t mix_hook;

	/// <summary>
	/// lock用ダミーオブジェクト。
	/// SMPEGは複数のスレッドから使うようには作られていないと思われるので、
	/// 呼び出し側で片っ端からlockしておく。
	/// </summary>
	private Object lockObject;
}