import core.sys.windows.dll;
import win32.windows;
import win32.winuser;
import std.string;
import std.file;
import util;
import windll;
import bga_melter;
import qbga_gui;

//----------------------------------------------------------------
// おきまりのDLL初期化ルーチン
//----------------------------------------------------------------

__gshared HINSTANCE g_hinst;

extern (Windows)
BOOL DllMain( HINSTANCE inst, ULONG reason, void* reserved )
{
	switch( reason )
	{
	case DLL_PROCESS_ATTACH:
		g_hinst = inst;
		dll_process_attach( inst, true );
		if( g_orig_dll is null )
			return false;
		break;

	case DLL_PROCESS_DETACH:
		dll_process_detach( inst, true );
		break;

	case DLL_THREAD_ATTACH:
		dll_thread_attach( true, true );
		break;

	case DLL_THREAD_DETACH:
		dll_thread_detach( true, true );
		break;

	default:
	}
	return true;
}

//----------------------------------------------------------------
// API転送処理
//----------------------------------------------------------------

__gshared WinDLL g_orig_dll = null;
__gshared UINT   WM_ARCEXTRACT;

static this()
{
	g_orig_dll = WinDLL.load( "_Bga32.DLL" );
	WM_ARCEXTRACT = RegisterWindowMessage("wm_arcextract");
}

static ~this()
{
	g_orig_dll.close();
}

template api(FnT)
{
	FnT api( string name )
	{
		return g_orig_dll.get_api!(FnT)( name );
	}
}

//----------------------------------------------------------------
// 統合アーカイバAPI：転送
//----------------------------------------------------------------

extern(Windows)
{
	int Bga( HWND a, immutable char* b, char* c, DWORD d )
	{
		int r = Bga_impl( a, b.fromStringz() );
		if( r < 0 ) // このダミーDLLでは処理できないコマンドだった時
			return api!(typeof(&Bga))("Bga")(a,b,c,d);
		return r;
	}

	WORD QBgaGetVersion()
	{
		return 5;
	}

	WORD BgaGetVersion()
	{
		return api!(typeof(&BgaGetVersion))("BgaGetVersion")();
	}

	BOOL BgaGetRunning()
	{
		return api!(typeof(&BgaGetRunning))("BgaGetRunning")();
	}

	BOOL BgaCheckArchive( char* a, int b )
	{
		return api!(typeof(&BgaCheckArchive))("BgaCheckArchive")(a,b);
	}

	BOOL BgaConfigDialog( HWND a, char* b, int c )
	{
		return api!(typeof(&BgaConfigDialog))("BgaConfigDialog")(a,b,c);
	}

	int BgaGetFileCount( char* a )
	{
		return api!(typeof(&BgaGetFileCount))("BgaGetFileCount")(a);
	}

	BOOL BgaQueryFunctionList( int a )
	{
		return api!(typeof(&BgaQueryFunctionList))("BgaQueryFunctionList")(a);
	}

	alias void* HARC;
	HARC BgaOpenArchive( HWND a, char* b, DWORD c )
	{
		return api!(typeof(&BgaOpenArchive))("BgaOpenArchive")(a,b,c);
	}

	int BgaCloseArchive( HARC a )
	{
		return api!(typeof(&BgaCloseArchive))("BgaCloseArchive")(a);
	}

	alias void* LPINDIVIDUALINFO;
	int BgaFindFirst( HARC a, char* b, LPINDIVIDUALINFO c )
	{
		return api!(typeof(&BgaFindFirst))("BgaFindFirst")(a,b,c);
	}

	int BgaFindNext( HARC a, LPINDIVIDUALINFO b )
	{
		return api!(typeof(&BgaFindNext))("BgaFindNext")(a,b);
	}

	DWORD BgaGetArcOriginalSize( HARC a )
	{
		return api!(typeof(&BgaGetArcOriginalSize))("BgaGetArcOriginalSize")(a);
	}

	DWORD BgaGetArcCompressedSize( HARC a )
	{
		return api!(typeof(&BgaGetArcCompressedSize))("BgaGetArcCompressedSize")(a);
	}

	WORD BgaGetArcRatio( HARC a )
	{
		return api!(typeof(&BgaGetArcRatio))("BgaGetArcRatio")(a);
	}

	BOOL BgaSetOwnerWindow( HWND a )
	{
		BOOL r = api!(typeof(&BgaSetOwnerWindow))("BgaSetOwnerWindow")(a);
		if( r ) BgaSetOwnerWindow_impl(a);
		return r;
	}

	BOOL BgaClearOwnerWindow()
	{
		BOOL r = api!(typeof(&BgaClearOwnerWindow))("BgaClearOwnerWindow")();
		BgaClearOwnerWindow_impl();
		return r;
	}

	alias BOOL function(HWND,UINT,UINT,EXTRACTINGINFOEX*) ARCHIVERPROC;
	BOOL BgaSetOwnerWindowEx( HWND a, ARCHIVERPROC* b )
	{
		BOOL r = api!(typeof(&BgaSetOwnerWindowEx))("BgaSetOwnerWindowEx")(a,b);
		if( r ) BgaSetOwnerWindowEx_impl(a,b);
		return r;
	}

	BOOL BgaKillOwnerWindowEx( HWND a )
	{
		BOOL r = api!(typeof(&BgaKillOwnerWindowEx))("BgaKillOwnerWindowEx")(a);
		BgaClearOwnerWindow_impl();
		return r;
	}

	alias void* UNLHA_WND_ENUMMEMBPROC;
	BOOL BgaSetEnumMembersProc( UNLHA_WND_ENUMMEMBPROC a )
	{
		return api!(typeof(&BgaSetEnumMembersProc))("BgaSetEnumMembersProc")(a);
	}

	BOOL BgaClearEnumMembersProc()
	{
		return api!(typeof(&BgaClearEnumMembersProc))("BgaClearEnumMembersProc")();
	}
}

//----------------------------------------------------------------
// 統合アーカイバAPI：実装( Bga )
//----------------------------------------------------------------

int Bga_impl( HWND wnd, string cmd_str )
{
	enum { UNSUPPORTED = -1 }

	//
	// コマンドライン解析
	//
	string[] cmd = cmd_parse(cmd_str);

	// x以外のコマンドは扱わないで本物DLLに回します。注意点として：
	//  > command はコマンドラインの最初の引数としてください。なお、command を省略
	//  > した場合は 'x' command が指定されたものとみなします。
	if( cmd.length == 0 )
		return UNSUPPORTED;
	if( cmd[0].length == 1 )
	{
		if( 0 <= "adjlmnstvADJLMNSTV".indexOf(cmd[0][0]) )
			return UNSUPPORTED;
		if( cmd[0][0]=='x' || cmd[0][0]=='X' )
			cmd = cmd[1 .. $];
	}

	// ※ この時点で、cmdにはcommandを除いた残りの引数が入っているはず

	//
	// スイッチ解析、引数解析
	//
	bool all_attrs       = false; // -a
	bool silent          = false; // -i
	bool ignore_dir      = false; // -j
	bool newfile_only    = false; // -n
	bool force_overwrite = false; // -o
	bool recursive       = false; // -r
	bool sanitize_path   = true;

	string arc_name = null;
	string base_dir = null;
	string[] paths;

	foreach( string param ; cmd )
		if( param[0] == '-' )
			switch( param[1] )
			{
			case 'a','A': all_attrs       = true; break;
			case 'i','I': silent          = true; break;
			case 'j','J': ignore_dir      = true; break;
			case 'n','N': newfile_only    = true; break;
			case 'o','O': force_overwrite = true; break;
			case 'r','R': recursive       = true; break;
			default:  break;
			}
		else if( arc_name is null )
		{
			arc_name = param;
		}
		else if( base_dir is null )
		{
			if( lastChar(param) == '\\' )
				base_dir = param;
			else {
				char[] buf;
				buf.length = GetCurrentDirectoryA(0,null)+1;
				GetCurrentDirectoryA(buf.length, buf.ptr);
				base_dir = buf.ptr.fromStringz().idup;
				if( lastChar(base_dir) != '\\' )
					base_dir ~= '\\';
			}
		}
		else
			paths ~= param;

	//
	// 展開処理にGo!
	//
	ProgressDlg dlg = null;
	if( !do_ownerwnd_proc( OP_ARC_BEGIN, null, 0, arc_name ) )
		return 0x8020;

	try
	{
		if( !silent && g_handler is null ) // -i / OwnerWndProc
		{
			dlg = new ProgressDlg(
				cast(DLGTEMPLATE*) g_orig_dll.load_dialog("#2025"), wnd );
			dlg.set_arcname(arc_name);
		}

		string    src_fname; // OwnerWndProc関係
		BgaHeader cur_hdr;   // OwnerWndProc関係

		BgaAnswer handler( ref BgaHeader hdr )
		{
			src_fname = hdr.fname;
			process_messages();

			// paths
			if( paths.length > 0 )
			{
				string fname = // -r
					(recursive ? hdr.fname[hdr.dir_name_len..$] : hdr.fname);
				foreach( string w ; paths )
					if( wild_match( w, fname ) )
						goto ok;
				return BgaAnswer.SkipIt;
			ok:;
			}
			// -a
			if( !all_attrs && (hdr.attrib&6) )
				return BgaAnswer.SkipIt;
			// dialog
			if( dlg )
				if( dlg.closed )
					return BgaAnswer.Abort;
				else
					dlg.set_filename( hdr.fname[hdr.dir_name_len..$] );
			// -j
			if( ignore_dir )
				hdr.fname = hdr.fname[hdr.dir_name_len .. $];
			// sanitize
			if( sanitize_path )
				hdr.fname = check_path(hdr.fname);
			// base_dir
			hdr.fname = (base_dir ~ hdr.fname).dup;
			// -o
			if( !force_overwrite )
				try {
				if( std.file.exists(hdr.fname) && std.file.isFile(hdr.fname) )
					// -n
					if( newfile_only )
					{
						if( newer_than(hdr.date,hdr.time,hdr.fname) )
							return BgaAnswer.SkipIt;
					}
					else
					{
						int r = MessageBoxA( dlg?dlg.hwnd:wnd,
							toStringz("Overwrite "~hdr.fname~" ?"),
							"QBga32.dll", MB_YESNOCANCEL );
						if( r == IDNO )     return BgaAnswer.SkipIt;
						if( r == IDCANCEL ) return BgaAnswer.Abort;
					}
				} catch {}

			cur_hdr = hdr;
			if( !do_ownerwnd_proc( OP_FILE_BEGIN, &cur_hdr, 0, src_fname ) )
				return BgaAnswer.Abort;
			return BgaAnswer.MeltIt;
		}

		BgaAnswer progress_handler( int cur, int max )
		{
			process_messages();
			if( dlg )
				if( dlg.closed )
					return BgaAnswer.Abort;
				else
					dlg.set_pos( cast(real)(cur)/max );
			if( !do_ownerwnd_proc( OP_FILE_MIDDLE, &cur_hdr, cur, src_fname ) )
				return BgaAnswer.Abort;
			return BgaAnswer.MeltIt;
		}

		(new BgaMelter(arc_name)).start(&handler,&progress_handler);
	}
	catch( BgaMelterError e )
	{
		return e.errcode;
	}
	finally
	{
		do_ownerwnd_proc( OP_ARC_END, null, 0, arc_name );
		if( dlg )
			dlg.close();
	}
	return 0;
}

//----------------------------------------------------------------
// 統合アーカイバAPI：実装( SetOwnerWindow )
//----------------------------------------------------------------

align(1) struct EXTRACTINGINFO
{
	DWORD   dwFileSize;
	DWORD   dwWriteSize;
	char    szSourceFileName[512 + 1];
	char    dummy1[3];
	char    szDestFileName[512 + 1];
	char    dummy[3];
}
align(1) struct EXTRACTINGINFOEX
{
	EXTRACTINGINFO exinfo;
	DWORD dwCompressedSize;
	DWORD dwCRC;
	UINT  uOSType;
	WORD  wRatio;
	WORD  wDate;
	WORD  wTime;
	char  szAttribute[8];
	char  szMode[8];
}

HWND g_owner_window;
extern(Windows) BOOL function(HWND,UINT,UINT,EXTRACTINGINFOEX*) g_handler;
extern(Windows) BOOL noex_handler( HWND w,UINT m,UINT s, EXTRACTINGINFOEX* e )
{
	return !SendMessageA( w, m, s, cast(LPARAM) &e.exinfo );
}

void BgaSetOwnerWindow_impl( HWND wnd )
{
	g_owner_window = wnd;
	g_handler      = &noex_handler;
}

void BgaClearOwnerWindow_impl()
{
	g_owner_window = null;
	g_handler      = null;
}

void BgaSetOwnerWindowEx_impl( HWND wnd, ARCHIVERPROC* proc )
{
	g_owner_window = wnd;
	g_handler      = *proc;
}

enum { OP_FILE_BEGIN, OP_FILE_MIDDLE, OP_ARC_END, OP_ARC_BEGIN }
bool do_ownerwnd_proc( UINT uState, BgaHeader* hdr, int cur, string src_fname )
{
	if( g_handler is null )
		return true;
	EXTRACTINGINFOEX ex;
	if( uState == OP_ARC_BEGIN || uState == OP_ARC_END )
	{
		lstrcpynA( ex.exinfo.szSourceFileName.ptr, toStringz(src_fname), 512 );
	}
	else
	{
		ex.exinfo.dwFileSize  = hdr.original_size;
		ex.exinfo.dwWriteSize = cur;
		lstrcpynA( ex.exinfo.szSourceFileName.ptr, toStringz(src_fname), 512 );
		lstrcpynA( ex.exinfo.szDestFileName.ptr, toStringz(hdr.fname), 512 );
		ex.dwCompressedSize = hdr.compressed_size;
		ex.wRatio = cast(ushort)( (cast(real)hdr.compressed_size)/hdr.original_size*1000 );
		ex.wDate  = hdr.date;
		ex.wTime  = hdr.time;
		ex.szAttribute[0] = (hdr.attrib&32 ? 'A': '-');
		ex.szAttribute[1] = (hdr.attrib&1  ? 'R': '-');
		ex.szAttribute[2] = (hdr.attrib&2  ? 'H': '-');
		ex.szAttribute[3] = (hdr.attrib&4  ? 'S': '-');
		ex.szAttribute[4] = (hdr.attrib&16 ? 'D': '-');
		ex.szAttribute[5] = '\0';
		if( hdr.method[0]=='G' )
			lstrcpyA(ex.szMode.ptr,"-gzip-");
		else
			lstrcpyA(ex.szMode.ptr,"-bzip2-");
	}

	return false != g_handler( g_owner_window, WM_ARCEXTRACT, uState, &ex );
}

//----------------------------------------------------------------
// パス検査系
//----------------------------------------------------------------

alias core.sys.windows.windows.IsDBCSLeadByte isDL;
string replace_yen( string s )
{
	string ans;
	int j=0;
	for(int i=0; i!=s.length; i=i+(isDL(s[i])?2:1))
		if( s[i] == '\\' )
			ans~=s[j .. i], ans~='/', j=i+1;
	ans ~= s[j .. $];
	return ans;
}

bool wild_match( string wild, string name )
{
	bool wild_match_nopath( string w, string s )
	{
		string advance( string s )
		{
			return s[(IsDBCSLeadByte(s[0])?2:1) .. $];
		}

		while( w.length>0 )
			switch( w[0] )
			{
			case '?':
				if( s.length==0 )
					return false;
				w = advance(w);
				s = advance(s);
				break;
			case '*':
				if( s.length==0 )
					return false;
				w = advance(w);
				if( w.length == 0 )
					return true;
				for( ; s.length!=0; s=advance(s) )
					if( wild_match_nopath(w,s) )
						return true;
				return false;
			default:
				if( s.length==0 )
					return false;
				if( isDL(w[0]) )
				  { if( w[0..2] != s[0..2] ) return false; }
				else
				  { if( w[0]    != s[0]    ) return false; }
				w = advance(w);
				s = advance(s);
				break;
			}
		return s.length==0;
	}

	if( wild=="" || wild=="*.*" || wild=="*" || wild=="**" )
		return true;

	string[] wilds = split( replace_yen( toLower(wild) ), "/" );
	string[] names = split( replace_yen( toLower(name) ), "/" );

	if( wilds.length != names.length )
		return false;
	for(int i=0; i!=wilds.length; ++i)
		if( wilds[i]!="*.*" && wilds[i]!="*" && wilds[i]!="**" )
			if( !wild_match_nopath( wilds[i], names[i] ) )
				return false;
	return true;
}

string check_path( string in_path )
{
	char[] path = in_path.dup;

	// C:\ ==> C_\
	if( path.length>=2 && path[1]==':' )
		path[1] = '_';

	// \\hoge ==> hoge
	// /hoge  ==> hoge
	while( path.length>0 && (path[0]=='\\'||path[0]=='/') )
		path = path[1..$];

	// .. ==> __
	string[] paths = split( replace_yen(path.idup), "/" );
	L1:
	foreach( ref string pc ; paths )
		if( pc.length >= 2 )
		{
			foreach( char c ; pc )
				if( c != '.' )
					continue L1;
			pc = replace( pc, ".", "_" );
		}

	return join( paths, "\\" );
}



//----------------------------------------------------------------
// 簡易テスト＠もっとマジメに書かなきゃ…
//----------------------------------------------------------------

unittest
{
	assert( check_path(`\\\\hoge\fuga`)==`hoge\fuga` );
	assert( check_path(`/usr/local/`)==`usr\local\` );
	assert( check_path(`..\abc def\...\.\g`)==`__\abc def\___\.\g` );
	assert( wild_match(`a/b/c`,`A\b\C`) );
	assert( wild_match(`a/*.*/a?x`,`A\hoge\Afx`) );
	assert( Bga_impl(null,"a hoge") < 0 );
}
