module yamalib.auxil.fileex;

private import std.string;
private import std.path;
private import std.stream;
private import std.thread;

private import y4d_aux.filesys;
private import y4d_aux.direnumerator;
private import ytl.y4d_result;

private import yamalib.log.log;
private import yamalib.log.performancelog;
private import yamalib.util.clzss;
//private import yamalib.auxil.tempfilemanager;


/**
	<yanepack用構造体について>
		ファイルヘッダー "yanepack" 8バイト + 格納ファイル数 uint
		その後、以下のFileInfoが格納ファイル数だけ来て、あとはデータ
*/
struct FileInfo {
	char[256]	filename;
	uint		startpos;	//	seek pos
	uint		filesize;	//	file size
};

struct FileInfoEx {
	/// 文字列表現を返却する
	char[] toString() {
		char[] str;
		str ~= "FileInfoEx=[";
		str ~= "filename=" ~ filename ~ ",";
		str ~= "startpos=" ~ std.string.toString(startpos) ~ ",";
		str ~= "filesize=" ~ std.string.toString(filesize) ~ ",";
		str ~= "arcsize=" ~ std.string.toString(arcsize);
		str ~= "]";
		
		return str;
	}
	
	char[256]	filename;
	uint		startpos;	//	seek pos
	uint		filesize;	//	file size(圧縮サイズ)
	uint		arcsize;	//	arc size (展開後のファイルサイズ)
	
};

/**
	レガシーyanePackファイルの読み込みクラス
	このクラスはスレッドセーフであることが保証されなくてはならない
*/
class FileEx : FileArchiverBase {
	
	static bool DEBUG = false;
	
	/// コンストラクタ
	this() {
		this.headerBuffer = new ubyte[FileInfoEx.sizeof];
		this.lszz = new CLZSS();
	}
	
	/// 読み込みバッファと書き出しバッファを初期化
	/**
		アーカイブから読み出すときのテンポラリバッファを用意しておくことで
		より高速に読み込みが完了します。
		アーカイブからの読み込みを使用しない場合は、指定しなくてよいでしょう。
	*/
	this(uint readBuffSize, uint outBuffSize) {
		this.data_buffer = new ubyte[readBuffSize];
		this.dstBuffer = new ubyte[outBuffSize];
		
		Log.print("FileEx read buffer size:%s", readBuffSize);
		Log.print("FileEx out buffer size:%s", outBuffSize);
		this();
	}
	
	/**
		指定したDATファイル名から、focusしたディレクトリを
		テンポラリフォルダに展開する
		Params :
		 datFilename = アーカイブデータ（dat拡張子でなければなりません）
		 focus = 絞り込むディレクトリ, null であればすべて展開する
	*/
	void expandDat(char[] datFilename, char[] focus) 
	in
	{
		assert( !(datFilename is null) );
		assert( std.string.tolower(std.path.getExt(datFilename)) == "dat" );
	}
	body
	{
		FileInfoEx* fileinfo;
		int loop = 0;
//		bool bak = isExistThenWrite();
//		setExistThenWrite(true);
		char[] topDir = cast(char[]) std.path.getBaseName(datFilename)[0..length-4];
		topDir ~= std.path.sep;
		
		while ( !((fileinfo = getInfo(datFilename,loop)) is null) ) {
			++loop;
			if ( !(focus is null) ) {
				if ( std.string.find(fileinfo.filename, focus) == -1 ) {
					continue;
				}
			}
			Log.print("FileEx#expandDat %s", fileinfo.filename);
			void[] data = read(topDir ~ fileinfo.filename);
		}
		
//		setExistThenWrite(bak);
	}
	
	/// ファイル名から情報を取得する
	static FileInfoEx* getInfo(char[] filename) {
		return new FileInfoEx();
	}
	
	/// 有効なファイル形式ならばtrue
	static bool isValid(char[] filename) {
		if ( !FileSys.isExist(filename) ) {
			return false;
		}
		
		// 拡張子は"dat"固定
		if ( "dat" != std.string.tolower(std.path.getExt(filename)) ) {
			return false;
		}
		
		scope File file = new File(filename);
		try {
			FileInfoEx* header;

			// アーカイブファイルが８バイト以下ってことはない。。。
			if (file.size() < 8)
				return false;
	
			char[] signature = new char[8];
			file.readExact(cast(void*) signature, 8);
	
			// シグネチャチェック
			if ( signature == "yanepack" ) {
				return false;
			} else if ( signature == "yanepkDx" ) {
				return true;
			} 

			return false;
		} catch (Exception e) {
		} finally {
			file.close();
		}
		
		return false;
	}
	
	/// 指定したアーカイブからファイル番号から情報を取得する
	static void[] readStatic(char[] arcName, int no) 
		in {
			assert( !(arcName is null) );
			assert( no >= 0 );
		}
		body
		{
			int fileType;
			uint fileNum;
			// データ展開先エリア
			ubyte[] lpDst;
			int filesize;
			scope ubyte[] lpData;
			scope File file = new File(arcName);
//			FileInfoEx* header;
			
			try {
				// アーカイブファイルが８バイト以下ってことはない。。。
				if (file.size() < 8)
					return null;
		
				char[] signature = new char[8];
				file.readExact(cast(void*) signature, 8);
		
				// シグネチャチェック
				if ( signature == "yanepack" ) {
					fileType = 1;
					return null;
				} else if ( signature == "yanepkDx" ) {
					fileType = 2;
				} else {
					// 未知の形式っぽ...
					Log.printError("This dat file is unkowun format");
					return null;
				}
			
				// 格納ファイル数取得
				file.read(fileNum);
				if (  no >= fileNum || no < 0) {
					return null;
				}
				
	//			Log.print("fileType:%s, filenum:%s\n", fileType, fileNum);
				
				// パスワードチェック
				char[8] passwd;
				file.readExact(cast(void*) passwd, 8);
				
				if ( !checkPasswd(passwd) ) {
					Log.printError("Arcaive Passwd is not match!");
					return null;
				}
				
				CLZSS lszz = new CLZSS();
	
				// yanePackDx形式だけでいいでそ？
				if (fileType == 2) {
					// ファイル情報データの先頭ポインタ取得
					FileInfoEx* header;
					int pos = -1;
					lpData = new ubyte[FileInfoEx.sizeof];
					
					// 引数で指定されたファイルを検索する
					for (int i; i < fileNum; ++i ) {
						file.read(lpData);
						header = cast(FileInfoEx*) lpData;
						
						if ( i != no ) {
							continue;
						}
						
					}
					
					// 圧縮、伸張サイズが同じなら非圧縮
					if (header.filesize == header.arcsize) {
						lpDst = new ubyte[filesize];
						file.seekSet(pos);
						file.read(lpDst);
					} else {
						
						// ファイル実体データの先頭ポインタ取得
						scope ubyte[] lpSrc = new ubyte[header.arcsize];
						
						file.seekSet(pos);
						file.read(lpSrc);
										
						// 展開先メモリサイズ取得
						lpDst = new ubyte[filesize];
						
						// デコード！
						lszz.decode(cast(ubyte*) lpSrc, cast(ubyte*) lpDst, filesize, false);
						
						Log.print("decoded size:%s \n",lpDst.length);
					}
				}
					
			} catch (Exception e) {
				Log.printFatal("RuntimeException! : readStatic(char[],int) %s", e.toString());
			} finally {
				if ( !file ) {
					file.close();
				}
			}
			
			return lpData;
		}
	
	/// アーカイブファイル名をしていし、その中のファイルを番号で指定しする
	static FileInfoEx* getInfo(char[] arcName, int no) 
		in
		{
			assert( !(arcName is null) );
			assert( no >= 0 );
		}
		body
		{
			int fileType;
			uint fileNum;
			// データ展開先エリア
			ubyte[] lpDst;
			int filesize;
			scope ubyte[] lpData;
			scope File file = new File(arcName);
			FileInfoEx* header;
			
			try {
				// アーカイブファイルが８バイト以下ってことはない。。。
				if (file.size() < 8)
					return null;
		
				char[] signature = new char[8];
				file.readExact(cast(void*) signature, 8);
		
				// シグネチャチェック
				if ( signature == "yanepack" ) {
					fileType = 1;
					return null;
				} else if ( signature == "yanepkDx" ) {
					fileType = 2;
				} else {
					// 未知の形式っぽ...
					Log.printError("This dat file is unkowun format");
					return null;
				}
			
				// 格納ファイル数取得
				file.read(fileNum);
				if (  no >= fileNum || no < 0) {
					return null;
				}
				
	//			Log.print("fileType:%s, filenum:%s\n", fileType, fileNum);
				
				// パスワードチェック
				char[8] passwd;

				file.readExact(cast(void*) passwd, 8);
				
				if ( !checkPasswd(passwd) ) {
					Log.printError("Arcaive Passwd is not match!");
					return null;
				}
	
				// yanePackDx形式だけでいいでそ？
				if (fileType == 2) {
					// ファイル情報データの先頭ポインタ取得
					int pos = -1;
					lpData = new ubyte[FileInfoEx.sizeof];
					
					// 引数で指定されたファイルを検索する
					for (int i; i < fileNum; ++i ) {
						file.read(lpData);
						header = cast(FileInfoEx*) lpData;
	
						if ( no == i ) {
							break;
						}
					}
				}
					
			} catch {
				Log.printError("RuntimeException! : getInfo(char[],int)");
			} finally {
				if ( !file ) {
					file.close();
				}
			}
			
			return header;
		}
	
	/// ファイルの存在チェック
	override bool isExist(char[] filename) 
		in
		{
//			assert( !(filename is null) );
		}
		body
		{
			if ( filename is null ) {
				return false;
			}
			
			// yanePackの仕様は、アーカイブのファイル名をトップディレクトリとみなすから
			// セパレータがひとつもないfailenameも存在しない。
			if (  std.string.find( std.string.replace(filename, "/", "\\" ), "\\" ) == -1 ) {
				return false;
			}
			
//			if (exitThenWrite) {
//				// パスの先頭がテンポラリとおなじでなくてはならない
//				if ( std.string.ifind(filename, TmpFileManager.getTmpPath()) == -1 ) {
//					return false;
//				} else {
//					filename = filename[TmpFileManager.getTmpPath().length .. length];
//				}
//				
//				if (DEBUG) {
//					Log.print("FileEx.isExist() : %s\n", filename);
//				}
//				
//				void[] data = read( filename );
//				
//				if (data is null) {
//					Log.printLook("yanapack read. File not find! %s", filename);
//					return false;
//				}
//	
//			} 
	
			// 存在チェックのためにいちいち、アーカイブロードするのは無茶なんで・・・
			return true;
		}
	
	/// アーカイブファイルへの書き出し
	override y4d_result write(char[] filename,void[] rdata){
		// 実装しとらへんがな
		return y4d_result.not_implemented;
	}
		
	///	enumeratorも実装しなくてok
	override DirEnumerator getEnumerator(char[] filename) { 
		return null; 
	}				
	
	/// ファイル読み込み
	override void[] read(char[] filename) 
		in
		{
			assert( !(filename is null) );
		}
		body 
		{
			// datファイルを探しにいく
			return innerRead(filename);
		}
	
	/// 内部読み込み
	private void[] innerRead(char[] filename) 
		in
		{
			assert( !(filename is null) );
			assert( filename.length != 0 );
		}
		body
		{
			try {
				// パフォーマンス測定
//				PerformanceLog.abort();
//				PerformanceLog.logFileReadArc(filename, null);
				
				// セパレータを"\\"にする
				filename = cast(char[]) replace(filename, "/", "\\");
				char[][] dirs = cast(char[][]) std.string.split(filename, "\\");
				char[] wrkStr;
				char[] datNm;

				for (int i = dirs.length-1; i >= 0; --i) {

					// ファイルパスを作成する
					wrkStr.length = 0;
					for (int j = 0; j <= i; ++j) {
						wrkStr ~= dirs[j] ~ "\\";
					}
					wrkStr.length = wrkStr.length-1;
					datNm = wrkStr ~ ".dat";
					
					// アーカイブファイルがあるか
					if ( !(FileSys.isRealExist( datNm ) is null) ) {
						if (DEBUG) {
							Log.print("FIND DAT %s", datNm);
						}
						char[] subFileNm;
						for (int j = i + 1; j < dirs.length; ++j) {
							subFileNm ~= dirs[j] ~ "\\";
						}
						// 最後に余計なのがあるのでとる
						subFileNm.length = subFileNm.length - 1;
						
						// ファイルあった。対象ファイルを抜き出してみよう
						void[] data = readPack( datNm, subFileNm);
						if (data is null) {
							Log.printError("yanePack file read Error : %d", subFileNm);
							// なんや、エラー..
							return null;
						}
//						if (exitThenWrite || std.path.getExt(filename) == "ogg") {
//							datNm = datNm[0..length-4] ~ "\\";
//							// ファイルシステムとして存在しないと困るんだったら、書き出す
//							TmpFileManager.write(getBaseName(subFileNm), data, datNm ~ getDirName(subFileNm) );
//						}
						return data;
					} else {
						wrkStr ~= "\\";
					}
				}
				
			Log.printWarn("FileEx#innerRead DatFileNotFound! : %s", std.path.getBaseName(filename) );
//			Log.printWarn("FileEx#innerRead DatFileNotFound! : %s", "****");

			} catch(ThreadError e) {
				Log.printFatal("FileEx#innerRead: ThreadError! [%s] [%s]", e.toString(), e.msg );
	
			} catch(Exception e) {
				Log.printFatal("FileEx#innerRead: Exception! [%s] [%s]", e.toString(), e.msg );
	
			} catch {
				// なんらかのエラー
				Log.printFatal("FileEx#innerRead: RuntimeException! unknown error!");
			} finally {
//				PerformanceLog.endLog();
			}
			
			return null;
		}
	
	/// yanePackDxタイプのファイルからファイルを読み出す
	private synchronized void[] readPack(char[] datFilename, char[] filename)
		in
		{
			assert( !(datFilename is null) && datFilename.length > 0 );
			assert( !(filename is null) && filename.length > 0 );
		}
		body 
		{
			int fileType;
			uint fileNum;
			// データ展開先エリア
			ubyte[] lpDst;
			int filesize;
			
			try {
				if ( this.file is null ) {
					this.file = new File();
				}
				this.file.open(datFilename, FileMode.In);
			} catch (Exception e) {
				this.file.close();
				Log.printFatal("FileEx#readPack: File Open Error! %s [%s] [%s]", datFilename, e.toString(), e.msg );
				throw e;
			} 
			
			if ( !this.file.readable ) {
				Log.printError("%s#readPack : %s is not readable.", toString(), datFilename);
				this.file.close();
				return null;
			}

			if ( !this.file.seekable ) {
				Log.printError("%s#readPack : %s is not seekable.", toString(), datFilename);
				this.file.close();
				return null;
			}
						
			try {
				
				// アーカイブファイルが８バイト以下ってことはない。。。
				if (this.file.size() < 8)
					return null;

				try {
					char[8] signature;
					this.file.readExact(cast(void*) signature, 8);
			
					// シグネチャチェック
					if ( signature == "yanepack" ) {
						fileType = 1;
						return null;
					} else if ( signature == "yanepkDx" ) {
						fileType = 2;
					} else {
						// 未知の形式っぽ...
						Log.printError("This dat file is unkowun format");
						return null;
					}
				
				} catch (Exception e) {
					Log.printFatal("FileEx#readPack: Arcaive Signature check...[%s] [%s]!", e.toString(), e.msg);
					return null;
				}
			
				// 格納ファイル数取得
				try {
					this.file.read(fileNum);
					if (DEBUG) {
						Log.print("fileType:%s, filenum:%s\n", fileType, fileNum);
					}
				} catch (Exception e) {
					Log.printFatal("FileEx#readPack: Arcaive read filenum...[%s] [%s]!", e.toString(), e.msg);
					return null;
				}

				// パスワードチェック
				try {
					char[8] passwd;
					this.file.readExact(cast(void*) passwd, 8);
					
					if ( !checkPasswd(passwd) ) {
						Log.printError("FileEx#readPack: Arcaive Passwd is not match!");
						return null;
					}
				} catch (Exception e) {
					Log.printFatal("FileEx#readPack: Arcaive Passwd check...[%s] [%s]!", e.toString(), e.msg);
					return null;
				}
				
				// yanePackDx形式だけでいいでそ？
				if (fileType == 2) {
					// ファイル情報データの先頭ポインタ取得
					FileInfoEx* header;
					int pos = -1;
					
					// 引数で指定されたファイルを検索する
					for (int i; i < fileNum; ++i ) {
						try {
							this.file.read(headerBuffer);
							header = cast(FileInfoEx*) headerBuffer;
							
							if ( header.filename == filename) {
								// こいつや！ アーカイブファイル内での自分のデータ位置の取得
								pos = header.startpos;
								filesize = header.filesize;
								break;
							}
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: Arcaive Search file...[%s] [%s]!", e.toString(), e.msg);
							return null;
						}
					}
					
					// みつからんかった？
					if (-1 == pos) {
						return null;
					}
					
					if (DEBUG) {
						Log.print("Find Header : name=%s", filename);
					}

					// 圧縮、伸張サイズが同じなら非圧縮
					if (header.filesize == header.arcsize) {
						try {
							assert( filesize > 0 );
							
							if (isReadOneTimeResource(filename)) {
								// 共有メモリ版
								if (dstBuffer.length <= filesize) {
									dstBuffer ~= new ubyte[filesize-dstBuffer.length];
									Log.printLook("File buffer expansion(pure):%s",dstBuffer.length);
								}
								lpDst = dstBuffer[0..filesize];
								
								if (DEBUG) {
									Log.print("GET SHARERD MEMORY(pure)");
								}

							} else {
								// 安定版
								lpDst = new ubyte[filesize];
							}
							
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: OutOfMemoryError(RAW)! size=[%d] [%s] [%s]", filesize, e.toString(), e.msg );
							throw e;
						}
						try {
							this.file.seekSet(pos);
							this.file.read(lpDst);
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: FileException(RAW)! [%s] [%s]", e.toString(), e.msg );
							throw e;
						}
					} else {
						// ファイル実体データの先頭ポインタ取得
						if (data_buffer.length <= header.arcsize) {
							data_buffer ~= new ubyte[header.arcsize-data_buffer.length];
							Log.printLook("File data buffer expansion:%s",dstBuffer.length);
						}
						ubyte[] lpSrc = data_buffer[0..header.arcsize];
						
						try {
							this.file.seekSet(pos);
							this.file.read(lpSrc);
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: ArcDataLoadError! [%s] [%s]", e.toString(), e.msg );
							throw e;
						}
										
						try {
							// 展開先メモリサイズ取得
							// ファイル実体データの先頭ポインタ取得
							if (isReadOneTimeResource(filename)) {
								// 共有メモリ版
								if (dstBuffer.length <= filesize) {
									dstBuffer ~= new ubyte[filesize-dstBuffer.length];
									Log.printLook("File buffer expansion(pack):%s",dstBuffer.length);
								}
								lpDst = dstBuffer[0..filesize];
								if (DEBUG) {
									Log.print("GET SHARERD MEMORY(pack)");
								}

							} else {
								// 安定版
								lpDst = new ubyte[filesize];
							}
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: OutOfMemoryError(ARC)! [%s] [%s]", e.toString(), e.msg );
							throw e;
						}

						try {
							// デコード！
							lszz.decode(cast(ubyte*) lpSrc, cast(ubyte*) lpDst, filesize, false);
							if (DEBUG) {
								Log.print("decoded size:%s \n",lpDst.length);
							}
						} catch (Exception e) {
							Log.printFatal("FileEx#readPack: DecodeError! [%s] [%s]", e.toString(), e.msg );
							throw e;
						}

					}
				}
				
			} catch(ThreadError e) {
				Log.printFatal("FileEx#readPack: ThreadError! [%s] [%s]", e.toString(), e.msg );
				lpDst = null;
	
			} catch(Exception e) {
				Log.printFatal("FileEx#readPack: Exception! [%s] [%s]", e.toString(), e.msg );
				lpDst = null;
	
			} finally {
				if ( !(this.file is null) ) {
					this.file.close();
				}
			}
			
			// 正常終了
			return lpDst;
		}
	
//	/// ファイルの存在チェックが行われた際、DAT内に対象ファイルが見つかれば
//	/// そのファイルをテンポラリに書き出すか？
//	/// デフォルト false
//	static void setExistThenWrite( bool b ) {
//		exitThenWrite = b;
//	}
	
//	/// 存在チェック時に、テンポラリに書き出すか
//	static bool isExistThenWrite() {
//		return exitThenWrite;
//	}
	
	/// パスワードの整合性チェック
	static bool checkPasswd(char[8] passwd) {
		return cast(bool) (C_PASSWD == passwd);
	}
	
private:

	/// 一回読み込んですぐ解放するリソース
	static bool isReadOneTimeResource(char[] filename) {
		char[] ext = cast(char[]) std.string.tolower(std.path.getExt(filename));
		return ("png" == ext || "jpg" == ext || "tga" == ext || "bmp" == ext || 
				 "ogg" == ext || "wav" == ext || "mp3" == ext);
	}


//	static bool exitThenWrite;	//!< ファイルの存在チェックをかけられたときにDATファイル内にファイルが見つかれば、そのファイルをテンポラリに書き出すか
	static const char[] C_PASSWD = "12345678";
	CLZSS lszz;
	File file;
	char[][] arcFileNames;
	ubyte[] data_buffer;
	ubyte[] dstBuffer;
	ubyte[] headerBuffer;
	
}