﻿private import win32.windows;
private import std.string;
private import std.file;
private import std.c.stdio;
private import std.string;
private import etc.c.zlib;
private import libbz2.bzlib;
private import util;

//----------------------------------------------------------------
// Bga書庫のファイルヘッダ形式
//----------------------------------------------------------------

align(1) struct BgaHeader
{
	 int   checksum;        // type～fname のsigned charでの和
	char   method[4];       // "GZIP" または "BZ2\0"
	uint   compressed_size; // 圧縮後のデータサイズ( ヘッダは含まない )
	uint   original_size;   // 元のファイルサイズ
	ushort date;            // ファイルの更新日付( DOS形式 )
	ushort time;            // ファイルの更新時刻( DOS形式 )
	ubyte  attrib;          // ファイルの属性
	                        // ( 1:RO 2:Hid 4:Sys 8:Vol 16:Dir 32:Arc )
	ubyte  header_type;     // ヘッダの種類( 現在は常に 0 )
	ushort arc_type;        // アーカイブタイプ
	                        // 0:(ext==↓?非圧縮:圧縮) 1:圧縮 2:非圧縮
	                        //	.ARC, .ARJ, .BZ2, .BZA, .CAB, .GZ, .GZA, .LZH,
	                        //	.LZS, .PAK, .RAR, .TAZ, .TBZ, .TGZ, .Z, .ZIP, .ZOO
	ushort dir_name_len;    // ディレクトリ名の長さ
	ushort file_name_len;   // ファイル名の長さ
	string fname;           // dir_name_len + file_name_len ( no '\0' )
}

//----------------------------------------------------------------
// エラー発生時に投げる例外
//----------------------------------------------------------------

class BgaMelterError : Error
{
	int errcode;
	this( int e ) { super("BgaMelterError"); errcode=e; }
}

enum
{
	ERROR_FILE_OPEN=0x800D,//         ファイルを開けませんでした。
	ERROR_MAKEDIRECTORY=0x8012,//     ディレクトリが作成できません。
	ERROR_CANNOT_WRITE=0x8013,//      書き込みエラーが生じました。
	ERROR_HEADER_CRC=0x8016,//        書庫のヘッダのチェックサムが合っていません。
	ERROR_ARC_FILE_OPEN=0x8018,//     書庫を開く事が出来ません。
	ERROR_NOT_ARC_FILE=0x8019,//      書庫のファイル名が指定されていません。
	ERROR_CANNOT_READ=0x801A,//       ファイルの読み込み時にエラーが生じました。
	ERROR_FILE_STYLE=0x801B,//        指定されたファイルは有効な書庫ではありません。
	ERROR_COMMAND_NAME=0x801C,//      コマンド指定が間違っています。
	ERROR_MORE_HEAP_MEMORY=0x801D,//  作業用のためのヒープメモリが不足しています。
	ERROR_ALREADY_RUNNING=0x801F,//   既に BGA32.DLL が動作中です。
	ERROR_USER_CANCEL=0x8020,//       ユーザーによって処理を中断されました。
	ERROR_TMP_OPEN=0x8025,//          作業ファイルが作成できません。
	ERROR_ARC_READ_ONLY=0x8027,//     書き込み専用属性の書庫に対する操作はできません。
	ERROR_NOT_FIND_ARC_FILE=0x8029,// 指定されたディレクトリには書庫がありませんでした。
	ERROR_RESPONSE_READ=0x802A,//     レスポンスファイルの読み込み時にエラーが生じました。
	ERROR_TMP_COPY=0x802C,//          作業ファイルの書庫への書き戻しができませんでした。
	ERROR_NOT_FIND_FILE=0x8031,//     ファイルが見つかりません。
	ERROR_GET_ATTRIBUTES=0x8034,//    ファイル属性が取得できません。
	ERROR_GET_INFORMATION=0x8036,//   ファイル情報が取得できません。
}

//----------------------------------------------------------------
// コールバック関数の返答用型
//----------------------------------------------------------------

enum BgaAnswer
{
	MeltIt, SkipIt, Abort
}

//----------------------------------------------------------------
// Filep
//   D言語らしからぬのですがzlibやlibbz2と簡単に連携する
//   都合上std.c.stdio.FILE*でファイルを読み書きします。
//----------------------------------------------------------------

extern(C)
{
	// stdio.h
    int fileno( FILE *fp ) { return fp._file; }
	// io.h
	int lseek( int fd, int offset, int mode );
	int dup( int fd ); 
	int close( int fd );
}

class Filep
{
	private FILE* fp;
	private this( FILE* p ) { fp = p; }

	static Filep open( string filename, bool read )
	{
		FILE* fp = fopen( toStringz(filename), read?"rb":"wb" );
		return (fp ? new Filep(fp) : null);
	}

	int dup_han()
	{
		int fd = dup( fileno(fp) );
		lseek( fd, cur(), 0 );
		return fd;
	}

	void[] read( int siz )
	{
		char[] buf; buf.length = siz;
		int rsiz = fread( buf.ptr, 1, siz, fp );
		if( rsiz < 0 )
			throw new BgaMelterError(ERROR_FILE_OPEN);
		buf.length = rsiz;
		return buf;
	}

	void write( void[] buf )
	{
		while( buf.length > 0 )
		{
			int rsiz = fwrite( buf.ptr, 1, buf.length, fp );
			if( rsiz < 0 ) return;
			buf = buf[rsiz .. $];
		}
	}

	int cur()
	{
		return ftell(fp);
	}

	void seek_to( int i )
	{
		fseek( fp, i, std.c.stdio.SEEK_SET );
	}

	void close()
	{
		fclose(fp);
	}

	FILE* get_fp()
	{
		return fp;
	}
}


//----------------------------------------------------------------
// メインクラス
//----------------------------------------------------------------

class BgaMelter
{
	alias BgaAnswer delegate(ref BgaHeader) FileHandler;
	alias BgaAnswer delegate(int, int)        ProgressHandler;

	private Filep fp = null;

	this( string arc_name )
	{
		fp = Filep.open( arc_name, true );
		if( fp is null )
			throw new BgaMelterError(ERROR_FILE_OPEN);
	}

	void close()
	{
		if( fp ) { fp.close(); fp = null; }
	}

	void start( FileHandler fh, ProgressHandler ph )
	{
		try
		{
			// ヘッダを探す
			int hpos = find_header();
			if( hpos == -1 )
				throw new BgaMelterError(ERROR_NOT_ARC_FILE);
			fp.seek_to(hpos);

			// ループ：
			// ヘッダ読みとり
			BgaHeader hdr;
			while( read_header(hdr) )
			{
				// 次のヘッダ位置を計算しておく
				uint nextpos = fp.cur() + hdr.compressed_size;
				try
				{
					// callback
					BgaAnswer a = fh(hdr);
					if( a == BgaAnswer.Abort )  return;
					if( a == BgaAnswer.SkipIt ) continue;

					// 出力先ファイルを開く
					if( lastChar(hdr.fname)=='\\' )
					  { pathMake(hdr.fname); continue; }

					Filep outf = Filep.open( pathMake(hdr.fname), false );
					if( outf is null )
						throw new BgaMelterError(ERROR_FILE_OPEN);

					// 解凍処理
					bool bContinue = true;
					if( !is_compressed(hdr) )
						bContinue = NcDec( hdr.original_size, outf, ph );
					else if( hdr.method == "GZIP" )
						bContinue = GzDec( hdr.compressed_size, hdr.original_size, outf, ph );
					else if( hdr.method == "BZ2\0" )
						bContinue = BzDec( hdr.original_size, outf, ph );

					// 閉じて属性設定
					outf.close();
					dosSetFTime( hdr.fname, hdr.date, hdr.time );
					SetFileAttributesA( toStringz(hdr.fname), hdr.attrib );
					if( !bContinue )
						return;
				}
				finally { fp.seek_to(nextpos); }
			}
		}
		finally { close(); }
	}

	static int signed_char( char c )
	{
		int cn = c;
		return (cn>=0x80 ? cn|0xffffff00 : cn);
	}

	private int find_header()
	{
		string dat = cast(string) fp.read(0x10000);

		for( int i=0; i<dat.length-28; ++i )
		{
			if( dat[i+4]!='G' && dat[i+4]!='B' ) continue;
			if( dat[i+5]!='Z'                  ) continue;
			if( dat[i+6]!='I' && dat[i+6]!='2' ) continue;
			if( dat[i+7]!='P' && dat[i+7]!='\0') continue;

			 int checksum = dat[i+0]+(dat[i+1]<<8)+(dat[i+2]<<16)+(dat[i+3]<<24);
			uint fnlen    = dat[i+24]+(dat[i+25]<<8)+dat[i+26]+(dat[i+27]<<8);
			if( i+28+fnlen > dat.length )        continue;

			int sum = 0;
			for( int j=i+4; j!=i+28+fnlen; ++j )
				sum += signed_char(dat[j]);
			if( checksum == sum )
				return i;
		}

		return -1;
	}

	private bool read_header( out BgaHeader hdr )
	{
		// リトルエンディアンを仮定。ヘッダ読み込み
		string buf = cast(string) fp.read(28);
		if( buf.length < 28 ) return false;
		buf.length = BgaHeader.sizeof;
		hdr = (cast(BgaHeader[]) buf)[0];
		hdr.fname = "";

		// ファイル名
		hdr.fname = cast(string) fp.read(hdr.dir_name_len + hdr.file_name_len);
		if( hdr.fname.length < hdr.dir_name_len + hdr.file_name_len ) return false;

		// チェックサム
		int sum = 0;
		for( int i=4; i!=28; ++i )    sum += signed_char(buf[i]);
		foreach( char c ; hdr.fname ) sum += signed_char(c);
		return (sum == hdr.checksum);
	}

	private bool is_compressed( ref BgaHeader hdr ) // ref=just for optimization
	{
		// ヘッダから、ファイルが圧縮格納されているかどうかを判定
		if( hdr.arc_type==2 )
			return false;
		if( hdr.arc_type==0 && hdr.compressed_size==hdr.original_size )
		{
			int x = hdr.fname.lastIndexOf('.');
			if( x == -1 )
				return true;
			string ext = toLower(hdr.fname[x+1 .. $]);
			if( ext=="arc" || ext=="arj" || ext=="bz2" || ext=="bza"
			 || ext=="cab" || ext=="gz"  || ext=="gza" || ext=="lzh"
			 || ext=="lzs" || ext=="pak" || ext=="rar" || ext=="taz"
			 || ext=="tbz" || ext=="tgz" || ext=="z"   || ext=="zip"
			 || ext=="zoo" )
				return false;
		}
		return true;
	}

	static string pathMake( string path )
	{
		char* ps = cast(char*)toStringz(path);
		for(char* p=ps;;)
		{
			for(; *p!=0&&*p!='\\'&&*p!='/'; p=CharNextA(p)) {}
			if( *p==0 )
				break;
			CreateDirectoryA( toStringz(ps[0..(p-ps)]), null );
			++p;
		}
		return path;
	}

	enum { BUFSIZ = 65536 }

	private bool NcDec( uint usiz, Filep outf, ProgressHandler ph )
	{
		uint init_usiz = usiz;

		// 非圧縮。コピーするだけ
		while( usiz )
		{
			string r = cast(string) fp.read( BUFSIZ<usiz?BUFSIZ:usiz );
			usiz -= r.length;
			outf.write(cast(char[])r);

			// dlg
			if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		}

		// dlg
		if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		return true;
	}

	private bool GzDec( uint csiz, uint usiz, Filep outf, ProgressHandler ph )
	{
		uint init_usiz = usiz;

		// zlibで展開
		fp.read(10); csiz -= 10; // ヘッダ,フッタスキップ

		ubyte[]  inbuf;  inbuf.length = 65536;
		ubyte[] outbuf; outbuf.length = 65536;

		// zlib準備
		z_stream zs;
		zs.zalloc   = null;
		zs.zfree    = null;

		// 出力バッファ
		zs.next_out  = outbuf.ptr;
		zs.avail_out = outbuf.length;

		// 入力バッファ
		inbuf = cast(ubyte[]) fp.read( csiz<65536 ? csiz : 65536 );
		csiz       -= inbuf.length;
		zs.next_in  = inbuf.ptr;
		zs.avail_in = inbuf.length;

		// スタート
		inflateInit2( &zs, -15 );
		try {

		// 書庫から入力し終わるまでループ
		int err = Z_OK;
		while( csiz&&usiz )
		{
			while( zs.avail_out > 0 )
			{
				err = etc.c.zlib.inflate( &zs, Z_PARTIAL_FLUSH );
				if( err!=Z_STREAM_END && err!=Z_OK )
					csiz=0;
				if( !csiz )
					break;

				if( zs.avail_in<=0 )
				{
					inbuf = cast(ubyte[]) fp.read( csiz<65536 ? csiz : 65536 );
					csiz       -= inbuf.length;
					zs.next_in  = inbuf.ptr;
					zs.avail_in = inbuf.length;

					if( inbuf.length==0 )
					{
						err  = Z_STREAM_END;
						csiz = 0;
						break;
					}
				}
			}

			int written = outbuf.length - zs.avail_out;
			if( usiz < written ) written = usiz;
			usiz -= written;
			outf.write( outbuf[0..written] );
			zs.next_out  = outbuf.ptr;
			zs.avail_out = outbuf.length;

			// dlg
			if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		}

		// 出力残しを無くす。
		while( err!=Z_STREAM_END&&usiz )
		{
			err = etc.c.zlib.inflate(&zs,Z_PARTIAL_FLUSH);
			if( err!=Z_STREAM_END && err!=Z_OK )
				break;

			int written = outbuf.length - zs.avail_out;
			if( usiz < written ) written = usiz;
			usiz -= written;
			outf.write( outbuf[0..written] );
			zs.next_out  = outbuf.ptr;
			zs.avail_out = outbuf.length;

			// dlg
			if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		}

		// 終了
		} finally { inflateEnd(&zs); }

		// dlg
		if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		return true;
	}

	private bool BzDec( uint usiz, Filep outf, ProgressHandler ph )
	{
		uint init_usiz = usiz;

		// libbz2で展開
		int err;
		BZFILE* b = BZ2_bzReadOpen( &err, fp.get_fp(), 0, 0, null, 0 );
		if( err!=BZ_OK || b is null )
			return true;

		try
		{
			char[] buf; buf.length = BUFSIZ;
			int len;
			while( 0<(len=BZ2_bzRead( &err, b, buf.ptr, BUFSIZ<usiz?BUFSIZ:usiz )) )
			{
				outf.write( buf[0..len] );
				usiz -= len;
				if( err != BZ_OK )
					break;

				// dlg
				if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
			}
		} finally { BZ2_bzReadClose( &err, b ); }

		// dlg
		if( BgaAnswer.Abort==ph(init_usiz-usiz,usiz) ) return false;
		return true;
	}
}
