//Module name: Floating IPS, Windows frontend
//Author: Alcaro
//Date: See Git history
//Licence: GPL v3.0 or higher

#include "flips.h"

#ifdef FLIPS_WINDOWS
class file_w32 : public file {
	size_t size;
	HANDLE io;
	
public:
	static file* create(LPCWSTR filename)
	{
		HANDLE io = CreateFile(filename, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
		if (io==INVALID_HANDLE_VALUE) return NULL;
		return new file_w32(io, (size_t)0);
	}
	
private:
	file_w32(HANDLE io, uint32_t sizetsize) : io(io)
	{
		size = GetFileSize(io, NULL);
	}
	
	file_w32(HANDLE io, uint64_t sizetsize) : io(io)
	{
		GetFileSizeEx(io, (PLARGE_INTEGER)&size);
	}
	
public:
	size_t len() { return size; }
	
	bool read(uint8_t* target, size_t start, size_t len)
	{
		OVERLAPPED ov = {0};
		ov.Offset = start;
		ov.OffsetHigh = start>>16>>16;
		DWORD actuallen;
		return (ReadFile(io, target, len, &actuallen, &ov) && len==actuallen);
	}
	
	~file_w32() { CloseHandle(io); }
};

file* file::create(LPCWSTR filename) { return file_w32::create(filename); }
bool file::exists(LPCWSTR filename) { return GetFileAttributes(filename) != INVALID_FILE_ATTRIBUTES; }


class filewrite_w32 : public filewrite {
	HANDLE io;
	
public:
	static filewrite* create(LPCWSTR filename)
	{
		HANDLE io = CreateFile(filename, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
		if (!io) return NULL;
		return new filewrite_w32(io);
	}
	
private:
	filewrite_w32(HANDLE io) : io(io) {}
	
public:
	bool append(const uint8_t* data, size_t len)
	{
		DWORD truelen;
		return (WriteFile(io, data, len, &truelen, NULL) && truelen==len);
	}
	
	~filewrite_w32() { CloseHandle(io); }
};

filewrite* filewrite::create(LPCWSTR filename) { return filewrite_w32::create(filename); }


//TODO: implement properly
//also ensure input==output works if implementing this, rather than getting a file sharing violation
//applies even when selecting multiple patches, of which one overwrites input
filemap* filemap::create(LPCWSTR filename) { return filemap::create_fallback(filename); }


HWND hwndMain=NULL;
HWND hwndSettings=NULL;

struct {
	char signature[9];
	unsigned char cfgversion;
	unsigned char lastRomType;
	bool openInEmulatorOnAssoc;
	bool enableAutoRomSelector;
	enum patchtype lastPatchType;
	int windowleft;
	int windowtop;
} static state;
#define mycfgversion 2
WCHAR * st_emulator=NULL;
void set_st_emulator_len(LPCWSTR newemu, int len)
{
	free(st_emulator);
	st_emulator=(WCHAR*)malloc((len+1)*sizeof(WCHAR));
	if (newemu) memcpy(st_emulator, newemu, len*sizeof(WCHAR));
	st_emulator[len]='\0';
}
void set_st_emulator(LPCWSTR newemu)
{
	set_st_emulator_len(newemu, wcslen(newemu));
}


HWND hwndProgress;
LRESULT CALLBACK bpsdProgressWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

bool bpsdCancel;

void bpsdeltaBegin()
{
	bpsdCancel=false;
	RECT mainwndpos;
	GetWindowRect(hwndMain, &mainwndpos);
	hwndProgress=CreateWindowA(
						"floatingmunchers", flipsversion,
						WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_BORDER,
						mainwndpos.left+53, mainwndpos.top+27, 101, 39, hwndMain, NULL, GetModuleHandle(NULL), NULL);
	SetWindowLongPtrA(hwndProgress, GWLP_WNDPROC, (LONG_PTR)bpsdProgressWndProc);
	
	ShowWindow(hwndProgress, SW_SHOW);
	EnableWindow(hwndMain, FALSE);
	
	bpsdeltaProgress(NULL, 0, 1);
}

bool bpsdeltaProgress(void* userdata, size_t done, size_t total)
{
	if (!bpsdeltaGetProgress(done, total)) return !bpsdCancel;
	if (hwndProgress) InvalidateRect(hwndProgress, NULL, false);
	MSG Msg;
	while (PeekMessage(&Msg, NULL, 0, 0, PM_REMOVE)) DispatchMessage(&Msg);
	return !bpsdCancel;
}

LRESULT CALLBACK bpsdProgressWndProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_ERASEBKGND: return TRUE;
	case WM_PAINT:
		{
			PAINTSTRUCT ps;
			RECT rc;
			BeginPaint(hwnd, &ps);
			GetClientRect(hwnd, &rc);
			FillRect(ps.hdc, &rc, GetSysColorBrush(COLOR_3DFACE));
			SetBkColor(ps.hdc, GetSysColor(COLOR_3DFACE));
			SelectObject(ps.hdc, (HFONT)GetStockObject(DEFAULT_GUI_FONT));
			DrawTextA(ps.hdc, bpsdProgStr, -1, &rc, DT_CENTER | DT_NOCLIP);
			EndPaint(hwnd, &ps);
		}
		break;
	case WM_CLOSE:
		bpsdCancel=true;
		break;
	default:
		return DefWindowProcA(hwnd, uMsg, wParam, lParam);
	}
	return 0;
}

void bpsdeltaEnd()
{
	EnableWindow(hwndMain, TRUE);
	DestroyWindow(hwndProgress);
	hwndProgress=NULL;
}


bool SelectRom(LPWSTR filename, LPCWSTR title, bool output)
{
	OPENFILENAME ofn;
	ZeroMemory(&ofn, sizeof(ofn));
	ofn.lStructSize=sizeof(ofn);
	ofn.hwndOwner=hwndMain;
	ofn.lpstrFilter=TEXT("Most Common ROM Files\0*.smc;*.sfc;*.nes;*.gb;*.gbc;*.gba;*.nds;*.vb;*.sms;*.smd;*.md;*.ngp;*.n64;*.z64\0All Files (*.*)\0*.*\0");
	ofn.lpstrFile=filename;
	ofn.nMaxFile=MAX_PATH;
	ofn.nFilterIndex=state.lastRomType;
	ofn.lpstrTitle=title;
	ofn.Flags=OFN_PATHMUSTEXIST|(output?OFN_HIDEREADONLY|OFN_OVERWRITEPROMPT:OFN_FILEMUSTEXIST);
	ofn.lpstrDefExt=TEXT("smc");
	if (!output && !GetOpenFileName(&ofn)) return false;
	if ( output && !GetSaveFileName(&ofn)) return false;
	state.lastRomType=ofn.nFilterIndex;
	return true;
}

UINT mboxtype[]={ MB_OK, MB_OK, MB_OK|MB_ICONWARNING, MB_OK|MB_ICONWARNING, MB_OK|MB_ICONERROR, MB_OK|MB_ICONERROR };

LPCWSTR patchextensions[]={
	NULL,//unused, ty_null
	TEXT("bps"),
	TEXT("ips"),
};

static struct errorinfo error(errorlevel level, const char * text)
{
	struct errorinfo errinf = { level, text };
	return errinf;
}

int a_ApplyPatch(LPCWSTR clipatchname)
{
	WCHAR patchnames[65536];
	patchnames[0]='\0';
	bool multiplePatches;
	if (clipatchname)
	{
		multiplePatches=false;
		wcscpy(patchnames, clipatchname);
	}
	else
	{
		//get patch names
		OPENFILENAME ofn;
		ZeroMemory(&ofn, sizeof(ofn));
		ofn.lStructSize=sizeof(ofn);
		ofn.hwndOwner=hwndMain;
		ofn.lpstrFilter=TEXT("All supported patches (*.ips, *.bps)\0*.ips;*.bps;*.ups\0All files (*.*)\0*.*\0");
		ofn.lpstrFile=patchnames;
		ofn.nMaxFile=65535;
		ofn.lpstrTitle=TEXT("Select Patches to Use");
		ofn.Flags=OFN_HIDEREADONLY|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_ALLOWMULTISELECT|OFN_EXPLORER;
		ofn.lpstrDefExt=patchextensions[state.lastPatchType];
		if (!GetOpenFileName(&ofn)) return 0;
		multiplePatches=(ofn.nFileOffset && patchnames[ofn.nFileOffset-1]=='\0');
	}
	
	//get rom name and apply
	
	if (!multiplePatches)
	{
		file* patch = file::create(patchnames);
		if (patch)
		{
		
		WCHAR inromname_buf[MAX_PATH];
		LPCWSTR inromname=NULL;
		if (state.enableAutoRomSelector) inromname=FindRomForPatch(patch, NULL);
		if (!inromname)
		{
			inromname=inromname_buf;
			inromname_buf[0]='\0';
			if (!SelectRom(inromname_buf, TEXT("Select File to Patch"), false)) goto cancel;
		}
		WCHAR outromname[MAX_PATH];
		wcscpy(outromname, inromname);
		LPWSTR outrompath=GetBaseName(outromname);
		LPWSTR patchpath=GetBaseName(patchnames);
		if (outrompath && patchpath)
		{
			wcscpy(outrompath, patchpath);
			LPWSTR outromext=GetExtension(outrompath);
			LPWSTR inromext=GetExtension(inromname);
			if (*inromext && *outromext) wcscpy(outromext, inromext);
		}
		if (!SelectRom(outromname, TEXT("Select Output File"), true)) goto cancel;
		struct errorinfo errinf=ApplyPatchMem(patch, inromname, true, outromname, NULL, state.enableAutoRomSelector);
		delete patch;
		MessageBoxA(hwndMain, errinf.description, flipsversion, mboxtype[errinf.level]);
		return errinf.level;
		}
	cancel:
		delete patch;
		return 0;
	}
	else
	{
#define max(a, b) (a > b ? a : b)
		if (state.enableAutoRomSelector)
		{
			LPCWSTR foundRom=NULL;
			bool canUseFoundRom=true;
			bool usingFoundRom=false;
			
		redo: ;
			WCHAR thisFileNameWithPath[MAX_PATH];
			bool anySuccess=false;
			enum { e_none, e_notice, e_warning, e_invalid, e_io_rom_write, e_io_rom_read, e_no_auto, e_io_read_patch } worsterror=e_none;
			LPCSTR messages[8]={
					"The patches were applied successfully!",//e_none
					"The patches were applied successfully!",//e_notice (ignore)
					"The patches were applied, but one or more may be mangled or improperly created...",//e_warning
					"Some patches were applied, but not all of the given patches are valid...",//e_invalid
					"Some patches were applied, but not all of the desired ROMs could be created...",//e_rom_io_write
					"Some patches were applied, but not all of the input ROMs could be read...",//e_io_rom_read
					"Some patches were applied, but not all of the required input ROMs could be located...",//e_no_auto
					"Some patches were applied, but not all of the given patches could be read...",//e_io_read_patch
				};
			
			wcscpy(thisFileNameWithPath, patchnames);
			LPWSTR thisFileName=wcschr(thisFileNameWithPath, '\0');
			*thisFileName='\\';
			thisFileName++;
			
			LPWSTR thisPatchName=wcschr(patchnames, '\0')+1;
			while (*thisPatchName)
			{
				wcscpy(thisFileName, thisPatchName);
				file* patch = file::create(thisFileNameWithPath);
				{
				if (!patch)
				{
					worsterror=max(worsterror, e_io_read_patch);
					canUseFoundRom=false;
					goto multi_auto_next;
				}
				bool possible;
				LPCWSTR romname=FindRomForPatch(patch, &possible);
				if (usingFoundRom)
				{
					if (!romname) romname=foundRom;
					else goto multi_auto_next;
				}
				else
				{
					if (!romname)
					{
						if (possible) canUseFoundRom=false;
						worsterror=max(worsterror, e_no_auto);
						goto multi_auto_next;
					}
				}
				if (!foundRom) foundRom=romname;
				if (foundRom!=romname) canUseFoundRom=false;
				
				wcscpy(GetExtension(thisFileName), GetExtension(romname));
				struct errorinfo errinf=ApplyPatchMem(patch, romname, true, thisFileNameWithPath, NULL, true);
				
				if (errinf.level==el_broken) worsterror=max(worsterror, e_invalid);
				if (errinf.level==el_notthis) worsterror=max(worsterror, e_no_auto);
				if (errinf.level==el_warning) worsterror=max(worsterror, e_warning);
				if (errinf.level<el_notthis) anySuccess=true;
				else canUseFoundRom=false;
				}
			multi_auto_next:
				delete patch;
				thisPatchName=wcschr(thisPatchName, '\0')+1;
			}
			if (anySuccess)
			{
				if (worsterror==e_no_auto && foundRom && canUseFoundRom && !usingFoundRom)
				{
					usingFoundRom=true;
					goto redo;
				}
				int severity=(worsterror==e_none ? el_ok : el_warning);
				MessageBoxA(hwndMain, messages[worsterror], flipsversion, mboxtype[severity]);
				return severity;
			}
		}
		WCHAR inromname[MAX_PATH];
		inromname[0]='\0';
		if (!SelectRom(inromname, TEXT("Select Base File"), false)) return 0;
		WCHAR thisFileNameWithPath[MAX_PATH];
		wcscpy(thisFileNameWithPath, patchnames);
		LPWSTR thisFileName=wcschr(thisFileNameWithPath, '\0');
		*thisFileName='\\';
		thisFileName++;
		LPWSTR thisPatchName=wcschr(patchnames, '\0')+1;
		LPCWSTR romExtension=GetExtension(inromname);
		filemap* inrommap=filemap::create(inromname);
		struct mem inrom=inrommap->get();
		bool anySuccess=false;
		enum { e_none, e_notice, e_warning, e_invalid_this, e_invalid, e_io_write, e_io_read, e_io_read_rom } worsterror=e_none;
		enum errorlevel severity[2][8]={
				 { el_ok,  el_ok,  el_warning,el_broken,      el_broken,  el_broken, el_broken, el_broken },
				 { el_ok,  el_ok,  el_warning,el_warning,     el_warning, el_warning,el_warning,el_broken },
			};
		LPCSTR messages[2][8]={
				{
					//no error-free
					NULL,//e_none
					NULL,//e_notice
					NULL,//e_warning
					"None of these are valid patches for this ROM!",//e_invalid_this
					"None of these are valid patches!",//e_invalid
					"Couldn't write any ROMs. Are you on a read-only medium?",//e_io_write
					"Couldn't read any patches. What exactly are you doing?",//e_io_read
					"Couldn't read the input ROM. What exactly are you doing?",//e_io_read_rom
				},{
					//at least one error-free
					"The patches were applied successfully!",//e_none
					"The patches were applied successfully!",//e_notice
					"The patches were applied, but one or more may be mangled or improperly created...",//e_warning
					"Some patches were applied, but not all of the given patches are valid for this ROM...",//e_invalid_this
					"Some patches were applied, but not all of the given patches are valid...",//e_invalid
					"Some patches were applied, but not all of the desired ROMs could be created...",//e_io_write
					"Some patches were applied, but not all of the given patches could be read...",//e_io_read
					NULL,//e_io_read_rom
				},
			};
		if (inrom.ptr)
		{
			bool removeheaders=shouldRemoveHeader(inromname, inrom.len);
			while (*thisPatchName)
			{
				wcscpy(thisFileName, thisPatchName);
				file* patch = file::create(thisFileNameWithPath);
				if (patch)
				{
					LPWSTR patchExtension=GetExtension(thisFileName);
					wcscpy(patchExtension, romExtension);
					struct errorinfo errinf=ApplyPatchMem2(patch, inrom, removeheaders, true, thisFileNameWithPath, NULL);
					
					if (errinf.level==el_broken) worsterror=max(worsterror, e_invalid);
					if (errinf.level==el_notthis) worsterror=max(worsterror, e_invalid_this);
					if (errinf.level==el_warning) worsterror=max(worsterror, e_warning);
					if (errinf.level<el_notthis)
					{
						if (state.enableAutoRomSelector && !anySuccess) AddToRomList(patch, inromname);
						anySuccess=true;
					}
					delete patch;
				}
				else worsterror=max(worsterror, e_io_read);
				thisPatchName=wcschr(thisPatchName, '\0')+1;
			}
		}
		else worsterror=e_io_read_rom;
		delete inrommap;
		MessageBoxA(hwndMain, messages[anySuccess][worsterror], flipsversion, mboxtype[severity[anySuccess][worsterror]]);
		return severity[anySuccess][worsterror];
#undef max
	}
}

void a_CreatePatch()
{
	//pick roms
	WCHAR romnames[2][MAX_PATH];
	WCHAR patchname[MAX_PATH];
	
	romnames[0][0]='\0';
	romnames[1][0]='\0';
	if (!SelectRom(romnames[0], TEXT("Select ORIGINAL UNMODIFIED File to Use"), false)) return;
	if (!SelectRom(romnames[1], TEXT("Select NEW MODIFIED File to Use"), false)) return;
	
	if (!wcsicmp(romnames[0], romnames[1]))
	{
		MessageBoxA(hwndMain, "That's the same file! You should really use two different files.", flipsversion, mboxtype[el_broken]);
		return;
	}
	
	//pick patch name and type
	wcscpy(patchname, romnames[1]);
	LPWSTR extension=GetExtension(patchname);
	if (extension) *extension='\0';//wcscpy(extension+1, extensions[state.lastPatchType]);
	OPENFILENAME ofn;
	ZeroMemory(&ofn, sizeof(ofn));
	ofn.lStructSize=sizeof(ofn);
	ofn.hwndOwner=hwndMain;
	ofn.lpstrFilter =
		TEXT("BPS Patch File (*.bps)\0*.bps\0")
		//TEXT("BPS Patch File (Favor Creation Speed) (*.bps)\0*.bps\0")
		TEXT("IPS Patch File (*.ips)\0*.ips\0");
	ofn.lpstrFile=patchname;
	ofn.nMaxFile=MAX_PATH;
	ofn.nFilterIndex=state.lastPatchType;
	ofn.lpstrTitle=TEXT("Select File to Save As");
	ofn.Flags=OFN_HIDEREADONLY|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST|OFN_OVERWRITEPROMPT;
	ofn.lpstrDefExt=patchextensions[state.lastPatchType];
	if (!GetSaveFileName(&ofn))
	{
		state.lastPatchType=(enum patchtype)ofn.nFilterIndex;
		return;
	}
	state.lastPatchType=(enum patchtype)ofn.nFilterIndex;
	
	bpsdCancel=false;
	struct errorinfo errinf=CreatePatch(romnames[0], romnames[1], (enum patchtype)ofn.nFilterIndex, NULL, patchname);
	if (!bpsdCancel) MessageBoxA(hwndMain, errinf.description, flipsversion, mboxtype[errinf.level]);
}

bool a_SetEmulator()
{
	WCHAR newemupath[MAX_PATH];
	*newemupath='\0';
	OPENFILENAME ofn;
	ZeroMemory(&ofn, sizeof(ofn));
	ofn.lStructSize=sizeof(ofn);
	ofn.hwndOwner=hwndMain;
	ofn.lpstrFilter=TEXT("Emulator Files (*.exe)\0*.exe\0All files (*.*)\0*.*\0");
	ofn.lpstrFile=newemupath;
	ofn.nMaxFile=MAX_PATH;
	ofn.lpstrTitle=TEXT("Select Emulator to Use");
	ofn.Flags=OFN_HIDEREADONLY|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST;
	ofn.lpstrDefExt=TEXT("exe");
	if (!GetOpenFileName(&ofn)) return false;
	set_st_emulator(newemupath);
	return true;
}

int a_ApplyRun(LPCWSTR clipatchname)
{
	struct mem rommem={NULL,0};
	struct mem patchedmem={NULL,0};
	
	WCHAR patchpath[MAX_PATH];
	*patchpath='\0';
	if (clipatchname)
	{
		wcscpy(patchpath, clipatchname);
	}
	else
	{
		OPENFILENAME ofn;
		ZeroMemory(&ofn, sizeof(ofn));
		ofn.lStructSize=sizeof(ofn);
		ofn.hwndOwner=hwndMain;
		ofn.lpstrFilter=TEXT("All supported patches (*.bps, *.ips)\0*.bps;*.ips;*.ups\0All files (*.*)\0*.*\0");
		ofn.lpstrFile=patchpath;
		ofn.nMaxFile=MAX_PATH;
		ofn.lpstrTitle=TEXT("Select Patch to Use");
		ofn.Flags=OFN_HIDEREADONLY|OFN_FILEMUSTEXIST|OFN_PATHMUSTEXIST;
		ofn.lpstrDefExt=TEXT("bps");
		if (!GetOpenFileName(&ofn)) return 0;
	}
	
	struct errorinfo errinf;
	file* patch = file::create(patchpath);
	if (!patch)
	{
		errinf=error(el_broken, "Couldn't read input patch. What exactly are you doing?");
		goto error;
	}
	
	LPCWSTR romname;
	romname=NULL;
	if (state.enableAutoRomSelector) romname=FindRomForPatch(patch, NULL);
	WCHAR romname_base[MAX_PATH];
	if (!romname)
	{
		romname_base[0]='\0';
		if (!SelectRom(romname_base, TEXT("Select Base File"), false))
		{
			delete patch;
			return 0;
		}
		romname=romname_base;
	}
	
	if (!*st_emulator && !a_SetEmulator())
	{
		delete patch;
		return 0;
	}
	
	//WCHAR tempfilepath[MAX_PATH];
	//WCHAR tempfilename[MAX_PATH];
	//if (!GetTempPath(MAX_PATH, tempfilepath)) wcscpy(tempfilepath, TEXT("."));
	//if (!GetTempFileName(tempfilepath, TEXT("rom"), 0, tempfilename)) wcscpy(tempfilename, TEXT("temprom.tmp"));
	
	WCHAR outfilename_rel[MAX_PATH];
	wcscpy(outfilename_rel, patchpath);
	wcscpy(GetExtension(outfilename_rel), GetExtension(romname));
	WCHAR outfilename[MAX_PATH];
	GetFullPathName(outfilename_rel, MAX_PATH, outfilename, NULL);
	
	errinf=ApplyPatchMem(patch, romname, true, outfilename, NULL, state.enableAutoRomSelector);
error:
	
	if (errinf.level!=el_ok) MessageBoxA(hwndMain, errinf.description, flipsversion, mboxtype[errinf.level]);
	if (errinf.level>=el_notthis) return el_broken;
	
	delete patch;
	if (rommem.ptr) FreeFileMemory(rommem);
	if (patchedmem.ptr) free(patchedmem.ptr);
	
	WCHAR cmdline[1+MAX_PATH+3+MAX_PATH+1+1];
	swprintf(cmdline, 1+MAX_PATH+3+MAX_PATH+1+1, TEXT("\"%ls\" \"%ls\""), st_emulator, outfilename);
	WCHAR * dirend=GetBaseName(patchpath);
	if (dirend) *dirend='\0';
	STARTUPINFO startupinfo;
	ZeroMemory(&startupinfo, sizeof(STARTUPINFO));
	PROCESS_INFORMATION processinformation;
	if (!CreateProcess(NULL, cmdline, NULL, NULL, FALSE, 0, NULL, patchpath, &startupinfo, &processinformation))
	{
		MessageBoxA(hwndMain, "Couldn't open emulator.", flipsversion, mboxtype[el_broken]);
		//DeleteFile(tempfilename);
		return el_broken;
	}
	
	//I don't clean up the temp file when the emulator is done.
	//- It would just force me to keep track of a bunch of state.
	//- It'd force me to not exit when the window is closed.
	//- The bsnes profile selector would confuse it.
	//- The emulator may have created a bunch of other files, for example SRAM.
	//Few other apps clean up anyways.
	CloseHandle(processinformation.hProcess);
	CloseHandle(processinformation.hThread);
	return errinf.level;
}

void a_AssignFileTypes(bool checkonly);

HWND assocText;
HWND assocButton;
void a_ShowSettings()
{
	if (hwndSettings)
	{
		SetActiveWindow(hwndSettings);
		return;
	}
	
	hwndSettings=CreateWindowA(
		"floatingmunchers", flipsversion,
		WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_BORDER|WS_MINIMIZEBOX,
		CW_USEDEFAULT, CW_USEDEFAULT, 3+6+202+6+3, 21 + 6+23+6+23+3+13+1+17+4+17+6 + 3, NULL, NULL, GetModuleHandle(NULL), NULL);
	
	HFONT hfont=(HFONT)GetStockObject(DEFAULT_GUI_FONT);
	HWND item;
	
	int x=6;
	int y=6;
	int lineheight;
	
#define endline(padding) do { x=6; y+=lineheight+padding; } while(0)
#define line(height) lineheight=height
	
#define widget(type, style, text, w, h, action) \
	do { \
		int thisy=y+(lineheight-h)/2; \
		item=CreateWindowA(type, text, WS_CHILD|WS_TABSTOP|WS_VISIBLE|style, \
		                   x, thisy, w, h, hwndSettings, (HMENU)(action), GetModuleHandle(NULL), NULL); \
		SendMessage(item, WM_SETFONT, (WPARAM)hfont, 0); \
		x+=w+6; \
	} while(0)
	
#define firstbutton(text, w, h, action) \
	widget(WC_BUTTONA, WS_GROUP|BS_DEFPUSHBUTTON, text, w, h, action)
	
#define button(text, w, h, action) \
	widget(WC_BUTTONA, BS_PUSHBUTTON, text, w, h, action)
	
#define labelL(text, w, h, action) \
	widget(WC_STATICA, SS_LEFT, text, w, h, action)
#define labelC(text, w, h, action) \
	widget(WC_STATICA, SS_CENTER, text, w, h, action)
	
#define radio(text, w, h, action) \
	widget(WC_BUTTONA, BS_AUTORADIOBUTTON, text, w, h, action)
#define check(text, w, h, action) \
	widget(WC_BUTTONA, BS_AUTOCHECKBOX, text, w, h, action)
	
	line(23);
	firstbutton("Select emulator", 202/*94*/, 23, 101);
	endline(6);
	
	line(23);
	button("Assign file types", 98, 23, 102); assocButton=item;
	labelL("(can not be undone)", 98, 13, 0); assocText=item;
	endline(3);
	
	line(13);
	labelC("When opening through associations:", 175, 13, 0);
	endline(1);
	line(17);
	radio("Create ROM", 79, 17, 103); Button_SetCheck(item, (state.openInEmulatorOnAssoc==false));
	radio("Run in emulator", 95, 17, 104); Button_SetCheck(item, (state.openInEmulatorOnAssoc==true));
	endline(4);
	
	line(17);
	check("Enable automatic ROM selector", 202, 17, 105); Button_SetCheck(item, (state.enableAutoRomSelector));
	endline(3);
	
	ShowWindow(hwndSettings, SW_SHOW);
#undef firstbutton
#undef button
#undef label
#undef radio
	//if (!fileTypesAssigned) button(6,68,200,23, "Assign File Types");
}

void key_core(bool checkonly, LPCWSTR path, LPCWSTR value, bool * p_hasExts, bool * p_refresh)
{
	HKEY hkey;
	DWORD type;
	WCHAR truepath[60];
	wcscpy(truepath, TEXT("Software\\Classes\\"));
	wcscat(truepath, path);
	WCHAR regval[MAX_PATH+30];
	DWORD regvallen;
	bool hasExts=true;
	if (checkonly)
	{
		regvallen=sizeof(regval);
		if (RegOpenKeyEx(HKEY_CURRENT_USER, truepath, 0, KEY_READ, &hkey)!=ERROR_SUCCESS) goto add;
		if (value && RegQueryValueEx(hkey, NULL, NULL, &type, (LPBYTE)regval, &regvallen)!=ERROR_SUCCESS) hasExts=false;
		RegCloseKey(hkey);
		if (!hasExts) goto add;
		if (value && wcsncmp(regval, value, sizeof(regval)/sizeof(*regval))) *p_hasExts=false;
		return;
	}
	else
	{
	add:
		regvallen=sizeof(regval);
		if (RegCreateKeyExW(HKEY_CURRENT_USER, truepath, 0, NULL, 0, KEY_WRITE, NULL, &hkey, NULL)==ERROR_SUCCESS)
		{
			if (value) RegSetValueExW(hkey, NULL, 0, REG_SZ, (BYTE*)value, (wcslen(value)+1)*sizeof(WCHAR));
			RegCloseKey(hkey);
		}
		if (path[0]=='.') *p_refresh=true;
		return;
	}
}

void a_AssignFileTypes(bool checkonly)
{
	WCHAR outstring[MAX_PATH+30];
	outstring[0]='"';
	GetModuleFileNameW(NULL, outstring+1, MAX_PATH);
	LPWSTR outstringend=wcschr(outstring, '\0');
	*outstringend='"';
	outstringend++;
	
	bool hasExts=true;
	bool refresh=false;
#define key(path, value) \
			key_core(checkonly, TEXT(path), TEXT(value), &hasExts, &refresh)
#define key_path(path, value) \
			wcscpy(outstringend, TEXT(value)); key_core(checkonly, TEXT(path), outstring, &hasExts, &refresh)
#define key_touch(path) \
			key_core(checkonly, TEXT(path), NULL, &hasExts, &refresh)
	
	key(".ips", "FloatingIPSFileIPS");
	key("FloatingIPSFileIPS", "Floating IPS File");
	key_path("FloatingIPSFileIPS\\DefaultIcon", ",1");
	key_touch("FloatingIPSFileIPS\\shell");
	key_touch("FloatingIPSFileIPS\\shell\\open");
	key_path("FloatingIPSFileIPS\\shell\\open\\command", " \"%1\"");
	
	key(".bps", "FloatingIPSFileBPS");
	key("FloatingIPSFileBPS", "Floating IPS File");
	key_path("FloatingIPSFileBPS\\DefaultIcon", ",2");
	key_touch("FloatingIPSFileBPS\\shell");
	key_touch("FloatingIPSFileBPS\\shell\\open");
	key_path("FloatingIPSFileBPS\\shell\\open\\command", " \"%1\"");
	
	if (refresh)
	{
		SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
		
		if (hwndMain)
		{
			RECT wndpos;
			GetWindowRect(hwndMain, &wndpos);
			MoveWindow(hwndMain, wndpos.left, wndpos.top, 218, 93, true);
		}
	}
	if (!checkonly || hasExts)
	{
		SetWindowText(assocText, TEXT("(already done)"));
		Button_Enable(assocButton, false);
	}
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
	switch (uMsg)
	{
	case WM_COMMAND:
		{
			if (wParam==1) a_ApplyPatch(NULL);
			if (wParam==2) a_CreatePatch();
			if (wParam==3) a_ApplyRun(NULL);
			if (wParam==4) a_ShowSettings();
			
			if (wParam==101) a_SetEmulator();
			if (wParam==102) a_AssignFileTypes(false);
			if (wParam==103) state.openInEmulatorOnAssoc=false;
			if (wParam==104) state.openInEmulatorOnAssoc=true;
			if (wParam==105) state.enableAutoRomSelector^=1;
		}
		break;
	case WM_CLOSE:
		{
			if (hwnd==hwndMain && !IsIconic(hwnd))
			{
				RECT wndpos;
				GetWindowRect(hwnd, &wndpos);
				state.windowleft=wndpos.left;
				state.windowtop=wndpos.top;
			}
			DestroyWindow(hwnd);
		}
		break;
	case WM_DESTROY:
		{
			if (hwnd==hwndMain) PostQuitMessage(0);
			if (hwnd==hwndSettings) hwndSettings=NULL;
			break;
		}
	default:
		return DefWindowProcA(hwnd, uMsg, wParam, lParam);
	}
	return 0;
}

static HFONT try_create_font(const char * name, int size)
{
	return CreateFontA(-size*96/72, 0, 0, 0, FW_NORMAL,
	                   FALSE, FALSE, FALSE, DEFAULT_CHARSET,
	                   OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY, DEFAULT_PITCH|FF_DONTCARE,
	                   name);
}

int ShowMainWindow(HINSTANCE hInstance, int nCmdShow)
{
	WNDCLASSA wc;
	wc.style=0;
	wc.lpfnWndProc=WindowProc;
	wc.cbClsExtra=0;
	wc.cbWndExtra=0;
	wc.hInstance=GetModuleHandle(NULL);
	wc.hIcon=LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(0));
	wc.hCursor=LoadCursor(NULL, IDC_ARROW);
	wc.hbrBackground=GetSysColorBrush(COLOR_3DFACE);//(HBRUSH)(COLOR_WINDOW + 1);
	wc.lpszMenuName=NULL;
	wc.lpszClassName="floatingmunchers";
	RegisterClassA(&wc);
	
	MSG msg;
	hwndMain=CreateWindowA(
				"floatingmunchers", flipsversion,
				WS_OVERLAPPED|WS_CAPTION|WS_SYSMENU|WS_BORDER|WS_MINIMIZEBOX,
				state.windowleft, state.windowtop, 204, 93, NULL, NULL, GetModuleHandle(NULL), NULL);
	
	HFONT hfont=try_create_font("Segoe UI", 9);
	if (!hfont) hfont=try_create_font("MS Shell Dlg 2", 8);
	if (!hfont) hfont=(HFONT)GetStockObject(DEFAULT_GUI_FONT);
	
	int buttonid=0;
	HWND lastbutton;
#define button(x,y,w,h, text) \
		do { \
			lastbutton=CreateWindowA("BUTTON", text, WS_CHILD|WS_TABSTOP|WS_VISIBLE|(buttonid==0?(BS_DEFPUSHBUTTON|WS_GROUP):(BS_PUSHBUTTON)), \
											x, y, w, h, hwndMain, (HMENU)(uintptr_t)(buttonid+1), GetModuleHandle(NULL), NULL); \
			SendMessage(lastbutton, WM_SETFONT, (WPARAM)hfont, 0); \
			buttonid++; \
		} while(0)
	button(6,  6,  90/*77*/,23, "Apply Patch"); SetActiveWindow(lastbutton);
	button(104,6,  90/*83*/,23, "Create Patch");
	button(6,  37, 90/*90*/,23, "Apply and Run");
	button(104,37, 90/*59*/,23, "Settings");
	
	ShowWindow(hwndMain, nCmdShow);
	
	a_AssignFileTypes(true);
	
	while (GetMessageA(&msg, NULL, 0, 0)>0)
	{
		if (!IsDialogMessageA(hwndMain, &msg))
		{
			TranslateMessage(&msg);
			DispatchMessageA(&msg);
		}
	}
	
	return msg.wParam;
}

void GUIClaimConsole()
{
	//this one makes it act like a console app in all cases except it doesn't create a new console if
	// not launched from one (it'd swiftly go away on app exit anyways), and it doesn't like being
	// launched from cmd since cmd wants to run a new command if spawning a gui app (I can't make it
	// not be a gui app because that flashes a console; it acts sanely from batch files)
	
	bool claimstdin=(GetFileType(GetStdHandle(STD_INPUT_HANDLE))==FILE_TYPE_UNKNOWN);
	bool claimstdout=(GetFileType(GetStdHandle(STD_OUTPUT_HANDLE))==FILE_TYPE_UNKNOWN);
	bool claimstderr=(GetFileType(GetStdHandle(STD_ERROR_HANDLE))==FILE_TYPE_UNKNOWN);
	
	if (claimstdin || claimstdout || claimstderr) AttachConsole(ATTACH_PARENT_PROCESS);
	
	if (claimstdin) freopen("CONIN$", "rt", stdin);
	if (claimstdout) freopen("CONOUT$", "wt", stdout);
	if (claimstderr) freopen("CONOUT$", "wt", stderr);
	
	if (claimstdout) fputc('\r', stdout);
	if (claimstderr) fputc('\r', stderr);
}

HINSTANCE hInstance_;
int nCmdShow_;

WCHAR * get_cfgpath()
{
	static WCHAR cfgfname[MAX_PATH+8];
	GetModuleFileNameW(NULL, cfgfname, MAX_PATH);
	WCHAR * ext=GetExtension(cfgfname);
	if (ext) *ext='\0';
	wcscat(cfgfname, TEXT("cfg.bin"));
	return cfgfname;
}

void GUILoadConfig()
{
	memset(&state, 0, sizeof(state));
	struct mem configbin=ReadWholeFile(get_cfgpath());
	void* configbin_org=configbin.ptr;
	if (configbin.len >= sizeof(state))
	{
#define readconfig(target, size) \
	if (size<0 || configbin.len < size) goto badconfig; \
	memcpy(target, configbin.ptr, size); \
	configbin.ptr += size; \
	configbin.len -= size
		
		readconfig(&state, sizeof(state));
		if (memcmp(state.signature, "FlipscfgW", sizeof(state.signature))!=0 || state.cfgversion!=mycfgversion) goto badconfig;
		int emulen;
		readconfig(&emulen, sizeof(emulen));
		set_st_emulator_len(NULL, emulen);
		readconfig(st_emulator, (unsigned)emulen*sizeof(WCHAR));
		SetRomList(configbin);
	}
	else
	{
	badconfig:
		memcpy(state.signature, "FlipscfgW", sizeof(state.signature));
		state.cfgversion=mycfgversion;
		state.lastRomType=0;
		state.openInEmulatorOnAssoc=false;
		state.enableAutoRomSelector=false;
		state.lastPatchType=ty_bps;
		state.windowleft=CW_USEDEFAULT;
		state.windowtop=CW_USEDEFAULT;
		set_st_emulator(TEXT(""));
	}
	free(configbin_org);
}

int GUIShow(LPCWSTR filename)
{
	GUILoadConfig();
	
	INITCOMMONCONTROLSEX initctrls;
	initctrls.dwSize=sizeof(initctrls);
	initctrls.dwICC=ICC_STANDARD_CLASSES;
	InitCommonControlsEx(&initctrls);
	
	int ret;
	if (filename)
	{
		if (state.openInEmulatorOnAssoc==false) ret=a_ApplyPatch(filename);
		else ret=a_ApplyRun(filename);
	}
	else ret=ShowMainWindow(hInstance_, nCmdShow_);
	
	HANDLE file=CreateFile(get_cfgpath(), GENERIC_WRITE, FILE_SHARE_READ, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_HIDDEN, NULL);
	if (file!=INVALID_HANDLE_VALUE)
	{
		DWORD whocares;
		WriteFile(file, &state, sizeof(state), &whocares, NULL);
		int len=wcslen(st_emulator);
		WriteFile(file, &len, sizeof(len), &whocares, NULL);
		WriteFile(file, st_emulator, sizeof(WCHAR)*wcslen(st_emulator), &whocares, NULL);
		struct mem romlist=GetRomList();
		WriteFile(file, romlist.ptr, romlist.len, &whocares, NULL);
		CloseHandle(file);
	}
	
	return ret;
}

int CALLBACK WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
	hInstance_=hInstance;
	nCmdShow_=nCmdShow;
	int argc;
	wchar_t ** argv=CommandLineToArgvW(GetCommandLineW(), &argc);
	return flipsmain(argc, argv);
}
#endif
