module y4d_aux.cacheloader;

private import ytl.y4d_result;
private import std.stream;
private import ytl.vector;
private import ytl.fastmap;
private import y4d_aux.filesys;
private import y4d_aux.lineparser;
private import y4d_aux.cacheobject;

///	キャッシュメカニズムを実現するための基底クラス
/**
	読み込んだファイルをためておき、古くなったファイルを自動的に
	解放していく仕組み。

	派生クラス側で loadFile と releaseFile をオーバーライドして使います。

	詳しいことは、このクラスの派生クラス( SoundLoader , SurfaceLoader )
	を見ればいいかも。

	なお、オブジェクトは CacheObject の派生クラスだと仮定しています。
*/
class CacheLoader {

	///	定義ファイルを読み込む
	/**
		読み込みエラーが発生したときは非0が返る。
<PRE>
		・定義ファイル仕様

		file名 , 読み込み番号(load,getで指定するときの番号) , option1,option2
			の繰り返し。読み込み番号,option1,2は省略可能。

		# 以下はコメント

		読み込み番号が指定された場合、そのあとは連番になる。
		例)
			a.wav , 2
			b.wav	  # このファイルの番号は3になる
			c.wav	  # このファイルの番号は4になる

		ここで指定されているoptionが、どのような意味を持つかは、
		このCacheLoaderクラスを使用するクラス(派生クラス？)に依存する。

		その他、細かい解析ルールは、 LineParser クラスの内容に依存する。
</PRE>
	*/
	y4d_result	loadDefFile(char[] filename)
	{
		ubyte[] mem = cast(ubyte[])(FileSys.read(filename));
		if (!mem) return y4d_result.file_not_found; // 読み込みエラー
		return loadDefRW(mem);
	}

	/// キャッシュリストを手渡しするための関数
	/**
		ファイルから読み込むかわりに、手渡しすることが出来る

	<PRE>
	例)
		SoundLoader ca = new SoundLoader;
		ca.loadDefRW("ptn0.ogg \n ptn1.ogg \n");
		ca.play(1); // ptn1.oggが再生される
	</PRE>

	*/
	y4d_result	loadDefRW(void[] memory) {
		releaseFileList();
		// ubyte[] mem = cast(ubyte[])(FileSys.read(filename));
		// if (!mem) return 1; // 読み込みエラー
		ubyte[] mem = cast(ubyte[])memory;
		std.stream.MemoryStream m = new std.stream.MemoryStream(mem);
		LineParser lp = new LineParser;
		while (!m.eof) {
			char[] linebuf = cast(char[]) m.readLine();
			lp.setLine(linebuf);
			char[] filename = lp.getStr();
			if (!filename) continue; // ダメやん

			Info info = createInfo();
			info.filename = filename;

			long no2 = lp.getNum(long.min);
			if (no2!=long.min) { lastKey = cast(int) no2; }

			long opt1 = lp.getNum(long.min);
			if (opt1!=long.min) { info.option1 = cast(int) opt1; }

			long opt2 = lp.getNum(long.min);
			if (opt2!=long.min) { info.option2 = cast(int) opt2; }

			filelist.insert(lastKey++,info);
		}
		updateFileList();
		return y4d_result.no_error;
	}

	///	一行だけ渡して、そいつを読み込ませる機能
	/**
		loadDefRWではファイルから読み込ませますが、
		そのファイルの内容を一行だけ食わせると考えてください。<BR>
	例)
		ca.readLine("ptn0.ogg");
	*/
	y4d_result	readLine(char[] linebuf){
		LineParser lp = new LineParser;
		lp.setLine(linebuf);

		Info info = createInfo();

		char[] filename = lp.getStr();
		if (!filename) return y4d_result.file_read_error; // ダメやん

		info.filename = filename;

		long no2 = lp.getNum(long.min);
		if (no2!=long.min) { lastKey = cast(int) no2; }

		long opt1 = lp.getNum(long.min);
		if (opt1!=long.min) { info.option1 = cast(int) opt1; }

		long opt2 = lp.getNum(long.min);
		if (opt2!=long.min) { info.option2 = cast(int) opt2; }

		filelist.insert(lastKey++,info);
		updateFileList();

		return y4d_result.no_error;
	}

	///	保持していたファイルリスト、キャッシュを解放する
	void releaseFileList() {
		filelist.clear();
		cachefiles = 0;
		nowtime = 0;
		lastKey = 0;
	}

	/// 保持していたキャッシュを解放する(ファイルリストは解放しない)
	void releaseAll() {
		foreach(Info info;filelist) {
			if(info.bLoaded) {
				if (releaseFile(info.obj)==0){
					info.bLoaded = false;
					info.bLoadFailed = false;
					--cachefiles;
				} else {
					//	解放失敗はどないするんや？(´Д`)
					info.accesstime = 0;
				}
			}
		}
		nowtime = 0;
	}

	///	保持しているファイルリストの表示(デバッグ用)
	void print() {
	/*
		printf("filelist--\n");
		foreach(int key,Info info;filelist) {
			printf("filename: %.*s : %d ,opt =  %d\n",
				info.filename,key,info.option);
		}
	*/
		foreach(int key,Info info;filelist) {
			printf("filename: %.*s : %d ,opt1,2 =  %d,%d \n",
				info.filename,key,info.option1,info.option2);
		}
	}

	///	ファイルリストを反映させる
	/**
		ファイルリストは内部的には連想配列になっているが、
		巡回するときはvectorで巡回したほうが速い。
		そこで、連想配列の内容をvectorに反映させるために呼び出す。
		(loadDefFile/LoadDefRWでファイルを読み込むときはこの関数を
		呼び出す必要はない。内部的に自動的に呼び出される)

		※　必要があれば、派生クラス側でオーバーライドすること。
	*/
	void	updateFileList() {
		filelist.update();
	}

	///	noをアクセスする。暗黙のうちにオブジェクトは構築され、loadされる。
	/**
	　このクラスのものは仮コード。この派生クラスで実装しる
	*/
	Object get(int no) {
		return /*cast(Sound)*/ getHelpper(no);
	}

	///	noを取得する。暗黙のうちにオブジェクトは構築されるが、loadはされない。
	/**
	　このクラスのものは仮コード。この派生クラスで実装しる
	*/
	Object getObj(int no) {
		return /*cast(Sound)*/ getHelpperObj(no);
	}

	///	オブジェクトが生成されているかを調べる
	/**
		例えば、Soundのloaderを考えてみよう。
		Sound オブジェクトの生成と、Sound を読み込んでいるかとは
		本来は別である。<BR>

		cacheがあふれた場合、Sound.release()で、サウンドは解放するが、
		Sound オブジェクト自体は解放しない。(これを解放すると、つぎに
		要求されたときにnewが必要となるので、なるべくゲーム中に不要なnewを
		発生させないためにこうなっている)<BR>

		このクラスの派生クラス、例えば SoundLoader のgetメンバを呼び出すと、
		Sound オブジェクトが生成されていなければ、生成し、かつ、サウンドを
		読み込む。すなわち Sound.load が自動的に呼び出される。<BR>

		このとき、サウンドオブジェクトが構築されているかどうかを
		判定したりアクセスしたいということがある。基底クラスでは、
		判定するほうのメンバを提供する。派生クラス側では、Sound.loadを
		呼び出さない、単なるアクセス手段を提供する。
	*/
	bool isin(int no){
		return filelist.isin(no);
	}

	///	一番昔の時刻にアクセスされたファイルを解放するメソッド
	/**
		通常、呼び出す必要はないが、派生クラス側で必要ならば呼び出してちょ
		※　必要ならば派生クラス側でオーバーライドすること。
	*/
	void	releaseOldObject() {
		ulong oldtime = ulong.max;
		Info theLast;
		int nRetryTimes = 3; // 3回
	retry:;
		foreach(Info info;filelist) {
			if (info.bLoaded && info.accesstime < oldtime){
				theLast = info; oldtime = info.accesstime;
			}
		}
		if (theLast) {
			// 解放妥当性を調べる
			if (!checkRelease(theLast)) {
				theLast.accesstime = ++nowtime;
				if (--nRetryTimes == 0) return ; // 解放できず
				theLast = null; goto retry;
			}
			if (releaseFile(theLast.obj)==0){
				theLast.bLoaded = false;
				theLast.bLoadFailed = false;
				--cachefiles;
			} else {
				//	解放失敗はどないするんや？(´Д`)
			}
		}
	}

	///	cacheしているファイル数を返す
	int getCacheFiles() { return cachefiles; }

	/// このインスタンスごとのキャッシュヒット数を返却する（リストファイルごとではない）	
	int getCacheHitCount() {
		return hit_cache;
	}
	int getCacheMisshitCount() {
		return misshit_cache;
	}

	alias fastmap!(Info,int) InfoList;

	///	古いキャッシュの破棄メソッドの登録
	/**
		必要ならば設定してやってちょーだい。
		派生クラスならば、単にcheckCacheメソッドをオーバーライドしても良い。
	<PRE>
		// 読み込み数が最大数(10)なら一番古いファイルを解放する場合は、
		//	こんなコードを書けば良い
		setUpdateCacheDelegate(delegate void updateCache(CacheLoader c) {
			if (c.getCacheFiles() >= 10)
				c.releaseOldObject();
		});
	</PRE>
		注意:戻り値はdelegateなので、この派生クラスの派生クラス以降で
		この関数を使う場合は、delegate関数のなかで、この関数の戻りの
		delegateを呼び出す必要がある。
	*/
	void delegate(CacheLoader)
	  setUpdateCacheDelegate(void delegate(CacheLoader)dg) {
		void delegate(CacheLoader) old = updateCacheDelegate;
		updateCacheDelegate = dg;
		return old;
	}

	///	cacheするバッファの上限を決める
	/**
		cacheの単位はbyte。ディフォルトサイズは派生クラスで定義されている。
		-1を設定すれば無制限キャッシュ。
	*/
	void	setCacheSize(long size){
		cacheSize = size;
	}

	///	cacheするバッファの上限を取得
	long getCacheSize() { return cacheSize; }

	///	ファイルリストの取得
	/**
		最悪、こいつ使っていじってやって。
	*/
	InfoList getInfoList() { return filelist; }

	///	foreachに対応させるためのもの
	/**
		事前にすべてのを読み込んでおきたいならば、
		foreachがあったほうが便利である。<BR>

		keyを返すバージョン。
	*/
	int opApply(int delegate(inout int) dg)
	{
		int result = 0;
		foreach(inout int key;filelist.getKeys())
		{
			result = dg(key);
			if (result)	break;
		}
		return result;
	}

	this() { filelist = new InfoList; }

	~this() {
	//	releaseAll();	//	一応、いま解放できるものは解放してしまう
	/**
		↑どうもテンプレートの終了時の解放順序、GCに何かbugがあるようだ。

		ここでメモリエラーになるなら、このクラスの派生クラスを
		returnする前にdeleteを行なってください。
	*/
	}

protected:
	char[] readPath;			//　実ファイル読み込みフォルダ
	InfoList filelist;			//	ファイルリスト

	void delegate(CacheLoader) updateCacheDelegate;
	//	updateCacheで呼び出すdelegate

protected:
	///	解放妥当性のチェック
	/**
		エンドレス再生中のBGM等、いくら一番最後にアクセスされたからと
		言っても解放してはいけないデータがある。そういう場合は、派生クラス側で
		この関数がfalseを返すようにコーディングすること。
	*/
	bool checkRelease(Info info){
		return true;
	}

	/// cacheするサウンドバッファの上限(-1ならば無制限)
	long cacheSize;

	/// キャッシュ構造体のfactory
	/**
		構造体にさらに追加したい場合は、派生クラス側でこのfactoryを
		オーバーライドすること。
	*/
	Info createInfo () { return new Info; }

	///	番号に対応するオブジェクトを返す。
	/**
		ファイルリストに登録されていない番号ならnull。
		実体化されていないならば実体化する。loadは行なわない。
	*/
	Object getHelpperObj(int no){
		//	アクセスしたことにはしない
		Info info = filelist.get(no);
		if (!info) return null; // 登録されていないものはどうしようもない
		if (!info.obj) {
			info.obj = createInstance();
		}
		return info.obj;
	}

	///	cloneメソッド。
	/**
		派生クラス側でオーバーライドする
	*/
	Object createInstance() { return null; }

	///	番号に対応するオブジェクトを読み込み、loadメソッドを呼び出す
	/**
		ファイルリストに登録されていない番号ならnull。
		実体化されていないならば実体化する。loadも行なう。
	*/
	Object getHelpper(int no){
		++nowtime;	//	現在時刻の加算
		Info info = filelist.get(no);
		if (!info) return null; // 登録されていないものはどうしようもない
		
		// 読み込まれているならそれを返す
		if (info.bLoaded) {
			// catche にヒットした！
			hit_cache++;
			return info.obj; 
		}
		// ミスヒット回数を増やす
		misshit_cache++;
		if (info.bLoadFailed) {
		//	以前に一度読み込み失敗しているので、もう読み込みリトライしても
		//	仕方ないという意味もある。
			return info.obj;
		}

		//	読み込むことが確定したので、キャッシュのリフレッシュを行なう
		//	(これを先に行なわないと、読み込んだ直後にキャッシュ制限に
		//	ひっかかって破棄してしまう可能性がある)
		checkCache();
		if (updateCacheDelegate)
			updateCacheDelegate(this);		//	古いものを捨てる

		//	ファイルを読み込む
		if (loadFile(FileSys.concat(readPath,info.filename),
			info.obj,info.option1,info.option2)!=0) {
			info.bLoadFailed = true;
			
			return info.obj;
			// objectは構築して返さなくては..読み込み失敗
		}
		info.bLoaded = true;
		++cachefiles;				//	cacheとして読み込んでいる数を加算
		info.accesstime = nowtime; // 最後にアクセスされた時間を登録
		return info.obj;
	}

	///	キャッシュがあふれたかを判定する
	/**
		必要ならば派生クラスでこのメソッドをオーバーライドするヨロシ
	<PRE>
		例)
			void checkCache() {
				if (getCacheFiles() >= 10)
					releaseOldObject();
			}
	</PRE>

	ディフォルトでは、全体のcache量(これは、CacheObjectのgetBufferSizeで取得
	していく)が、cacheサイズに到達したら、一番最後にアクセスされたオブジェクト
	を解放するようになっている。
	*/
	void checkCache() {
		if (cacheSize==-1) return ; // 無制限キャッシュ
		long u;
		int retryCount = 5; // 5回繰り返すべし
	retry:;
		u = 0;
		foreach(Info info;filelist){
			ICacheObject s = cast(ICacheObject)info.obj;
			if (s) {
				u += s.getBufferSize();
				if (u >= cacheSize) {
					releaseOldObject();
					if (--retryCount !=0) goto retry;
					return ; // これ以上は解放しなくていいんじゃ？
				}
			}
		}
	}

	///	読み込みメソッド(派生クラス側でオーバーライドが必要)
	/**
		ファイル名のファイルを読み込み、objに入れて返す。
		読み込みに失敗した場合は非0を返すように派生クラス側でコーディングする
	*/
	y4d_result loadFile(char[] filename,inout Object obj,int option1,int option2)
	{ return y4d_result.no_error; }

	///	解放メソッド(派生クラス側でオーバーライドが必要)
	/**
		解放に失敗した場合は非0を返すように派生クラス側でコーディングする
	*/
	y4d_result releaseFile(inout Object obj) { return y4d_result.no_error; }

	///	このクラスの保持している情報構造体
	static class Info {
		char[]	filename;	//	読み込みファイル名
		Object	obj;		//	読み込んだデータ
		bool	bLoaded;	//	読み込まれているか
		bool	bLoadFailed;//	読み込み失敗の巻
		int		option1;	//	オプション
		int		option2;	//	オプション
		ulong	accesstime;	//	一番最後に参照された時刻(nowtimeを代入)
	}

	ulong	nowtime;
	/*
		初期値は0で、アクセスごとにインクリメントされる。
		long型なので、一秒間に100000回アクセスがあったとしても、
		2^64 / 100000 / 60 / 60 / 24 / 365.24 = 5845420461年
		よって実用上、十分であると言える。
	*/

	int		cachefiles;		//	実体を読み込んでいるファイルの数
	/*
		キャッシュサイズに関して：
		システム全体で使用するテクスチャーサイズの総量を
		固定したい場合などは、複数のTextureLoader間で
		このcacheSizeを共有する必要が出てくるが、
		そういう使い方は現実的ではない（気がする）ので、
		ここでは考えないことにする。
	*/

	int		lastKey;
	/*
		次のreadLineによって割り当てられるデータ(Info)に対する
		fastmapのkey。
	*/
	
	// パフォーマンス測定
	int hit_cache;
	int misshit_cache;

}

