#include "gxruntime.hpp"
#include <algorithm>
#include <map>
#include "graphicsruntime.hpp"
#include "gxaudio.hpp"
#include "gxcanvas.hpp"
#include "gxfilesystem.hpp"
#include "gxgraphics.hpp"
#include "gxinput.hpp"
#include "gxtimer.hpp"

#include <stdutil.hpp>
#include <Windows.h>
#include <mmsystem.h>
#include <shellapi.h>
#include <zmouse.h>

extern "C" {
#include <fmod.h>
}

struct gxRuntime::GfxMode {
	DDSURFACEDESC2 desc;
};
struct gxRuntime::GfxDriver {
	GUID*                 guid;
	std::string           name;
	std::vector<GfxMode*> modes;
	D3DDEVICEDESC7        d3d_desc;
};

static const int static_ws = WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX;
static const int scaled_ws = WS_VISIBLE | WS_CAPTION | WS_SYSMENU | WS_SIZEBOX | WS_MINIMIZEBOX | WS_MAXIMIZEBOX;

static std::string    app_title;
static std::string    app_close;
static gxRuntime*     runtime;
static bool           busy, suspended;
static volatile bool  run_flag;
static DDSURFACEDESC2 desktop_desc;

typedef int(_stdcall* LibFunc)(const void* in, int in_sz, void* out, int out_sz);

struct gxDll {
	HINSTANCE                      hinst;
	std::map<std::string, LibFunc> funcs;
};

static std::map<std::string, gxDll*> libs;

static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam);

//current gfx mode
//
//0=NONE
//1=SCALED WINDOW
//2=FIXED SIZE WINDOW
//3=EXCLUSIVE
//
static int  gfx_mode;
static bool gfx_lost;
static bool auto_suspend;

//for modes 1 and 2
static int                  mod_cnt;
static MMRESULT             timerID;
static IDirectDrawClipper*  clipper;
static IDirectDrawSurface7* primSurf;
//static Debugger *debugger;

static std::set<gxTimer*> timers;

enum { WM_STOP = WM_USER + 1, WM_RUN, WM_END };

////////////////////
// STATIC STARTUP //
////////////////////
gxRuntime* gxRuntime::openRuntime(HINSTANCE hinst, const std::string& cmd_line, void* d)
{
	if (runtime)
		return 0;

	//create debugger
	//debugger = d;

	//create WNDCLASS
	WNDCLASS wndclass;
	memset(&wndclass, 0, sizeof(wndclass));
	wndclass.style         = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
	wndclass.lpfnWndProc   = ::windowProc;
	wndclass.hInstance     = hinst;
	wndclass.lpszClassName = TEXT("Blitz Runtime Class");
	wndclass.hCursor       = (HCURSOR)LoadCursor(0, IDC_ARROW);
	wndclass.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
	RegisterClass(&wndclass);

	gfx_mode = 0;
	clipper  = 0;
	primSurf = 0;
	busy = suspended = false;
	run_flag         = true;

	const char* app_t = " ";
	int         ws = WS_CAPTION, ws_ex = 0;

	HWND hwnd = CreateWindowEx(ws_ex, "Blitz Runtime Class", app_t, ws, 0, 0, 0, 0, 0, 0, 0, 0);

	UpdateWindow(hwnd);

	runtime = new gxRuntime(hinst, cmd_line, hwnd);
	return runtime;
}

void gxRuntime::closeRuntime(gxRuntime* r)
{
	if (!runtime || runtime != r)
		return;

	std::map<std::string, gxDll*>::const_iterator it;
	for (it = libs.begin(); it != libs.end(); ++it) {
		FreeLibrary(it->second->hinst);
	}
	libs.clear();

	delete runtime;
	runtime = 0;
}

//////////////////////////
// RUNTIME CONSTRUCTION //
//////////////////////////
typedef int(_stdcall* SetAppCompatDataFunc)(int x, int y);

gxRuntime::gxRuntime(HINSTANCE hi, const std::string& cl, HWND hw)
	: hinst(hi), cmd_line(cl), hwnd(hw), curr_driver(0), enum_all(false), pointer_visible(true), audio(0), input(0),
	  graphics(0), fileSystem(0), use_di(false)
{
	CoInitialize(0);

	enumGfx();
	TIMECAPS tc;
	timeGetDevCaps(&tc, sizeof(tc));
	timeBeginPeriod(tc.wPeriodMin);

	HMODULE ddraw = LoadLibraryA("ddraw.dll");
	if (ddraw) {
		SetAppCompatDataFunc SetAppCompatData = (SetAppCompatDataFunc)GetProcAddress(ddraw, "SetAppCompatData");
		if (SetAppCompatData)
			SetAppCompatData(12, 0);
		FreeLibrary(ddraw);
	}
}

gxRuntime::~gxRuntime()
{
	while (timers.size())
		freeTimer(*timers.begin());
	if (audio)
		closeAudio(audio);
	if (graphics)
		closeGraphics(graphics);
	if (input)
		closeInput(input);
	TIMECAPS tc;
	timeGetDevCaps(&tc, sizeof(tc));
	timeEndPeriod(tc.wPeriodMin);
	denumGfx();
	DestroyWindow(hwnd);
	UnregisterClass("Blitz Runtime Class", hinst);

	CoUninitialize();
}

void gxRuntime::pauseAudio()
{
	if (audio)
		audio->pause();
}

void gxRuntime::resumeAudio()
{
	if (audio)
		audio->resume();
}

void gxRuntime::backupGraphics()
{
	if (auto_suspend) {
		graphics->backup();
	}
}

void gxRuntime::restoreGraphics()
{
	if (auto_suspend) {
		if (!graphics->restore())
			gfx_lost = true;
	}
}

void gxRuntime::resetInput()
{
	if (input)
		input->reset();
}

void gxRuntime::acquireInput()
{
	if (!input)
		return;
	if (gfx_mode == 3) {
		if (use_di) {
			use_di = input->acquire();
		} else {
		}
	}
	input->reset();
}

void gxRuntime::unacquireInput()
{
	if (!input)
		return;
	if (gfx_mode == 3 && use_di)
		input->unacquire();
	input->reset();
}

/////////////
// SUSPEND //
/////////////
void gxRuntime::suspend()
{
	busy = true;
	pauseAudio();
	backupGraphics();
	unacquireInput();
	suspended = true;
	busy      = false;

	if (gfx_mode == 3)
		ShowCursor(1);

	//if (debugger) debugger->debugStop();
}

////////////
// RESUME //
////////////
void gxRuntime::resume()
{
	if (gfx_mode == 3)
		ShowCursor(0);
	busy = true;
	acquireInput();
	restoreGraphics();
	resumeAudio();
	suspended = false;
	busy      = false;

	//if (debugger) debugger->debugRun();
}

///////////////////
// FORCE SUSPEND //
///////////////////
void gxRuntime::forceSuspend()
{
	if (gfx_mode == 3) {
		SetForegroundWindow(GetDesktopWindow());
		ShowWindow(GetDesktopWindow(), SW_SHOW);
	} else {
		suspend();
	}
}

//////////////////
// FORCE RESUME //
//////////////////
void gxRuntime::forceResume()
{
	if (gfx_mode == 3) {
		SetForegroundWindow(hwnd);
		ShowWindow(hwnd, SW_SHOWMAXIMIZED);
	} else {
		resume();
	}
}

///////////
// PAINT //
///////////
void gxRuntime::paint()
{
	switch (gfx_mode) {
	case 0: {
	} break;
	case 1:
	case 2: //scaled windowed mode.
	{
		RECT src, dest;
		src.left = src.top = 0;
		GetClientRect(hwnd, &dest);
		src.right  = gfx_mode == 1 ? graphics->getWidth() : dest.right;
		src.bottom = gfx_mode == 1 ? graphics->getHeight() : dest.bottom;
		POINT p;
		p.x = p.y = 0;
		ClientToScreen(hwnd, &p);
		dest.left += p.x;
		dest.right += p.x;
		dest.top += p.y;
		dest.bottom += p.y;
		gxCanvas* f = graphics->getFrontCanvas();
		primSurf->Blt(&dest, f->getSurface(), &src, 0, 0);
	} break;
	case 3: {
		//exclusive mode
	} break;
	}
}

//////////
// FLIP //
//////////
void gxRuntime::flip(bool vwait)
{
	gxCanvas* b = graphics->getBackCanvas();
	gxCanvas* f = graphics->getFrontCanvas();
	int       n;
	switch (gfx_mode) {
	case 1:
	case 2:
		if (vwait)
			graphics->vwait();
		f->setModify(b->getModify());
		if (f->getModify() != mod_cnt) {
			mod_cnt = f->getModify();
			paint();
		}
		break;
	case 3:
		if (vwait) {
			BOOL vb;
			while (graphics->dirDraw->GetVerticalBlankStatus(&vb) >= 0 && vb) {
			}
			n = f->getSurface()->Flip(0, DDFLIP_WAIT);
		} else {
			n = f->getSurface()->Flip(0, DDFLIP_NOVSYNC | DDFLIP_WAIT);
		}
		if (n >= 0)
			return;
		std::string t = "Flip Failed! Return code:" + itoa(n & 0x7fff);
		debugLog(t.c_str());
		break;
	}
}

////////////////
// MOVE MOUSE //
////////////////
void gxRuntime::moveMouse(int x, int y)
{
	POINT p;
	RECT  rect;
	switch (gfx_mode) {
	case 1:
		GetClientRect(hwnd, &rect);
		x = x * (rect.right - rect.left) / graphics->getWidth();
		y = y * (rect.bottom - rect.top) / graphics->getHeight();
	case 2:
		p.x = x;
		p.y = y;
		ClientToScreen(hwnd, &p);
		x = p.x;
		y = p.y;
		break;
	case 3:
		if (use_di)
			return;
		break;
	default:
		return;
	}
	SetCursorPos(x, y);
}

/////////////////
// WINDOW PROC //
/////////////////
LRESULT gxRuntime::windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	if (busy) {
		return DefWindowProc(hwnd, msg, wparam, lparam);
	}

	PAINTSTRUCT ps;

	//handle 'special' messages!
	switch (msg) {
	case WM_PAINT:
		if (gfx_mode && !auto_suspend) {
			if (!graphics->restore())
				gfx_lost = true;
		}
		BeginPaint(hwnd, &ps);
		paint();
		EndPaint(hwnd, &ps);
		return DefWindowProc(hwnd, msg, wparam, lparam);
	case WM_ERASEBKGND:
		return gfx_mode ? 1 : DefWindowProc(hwnd, msg, wparam, lparam);
	case WM_CLOSE:
		if (app_close.size()) {
			int n = MessageBox(hwnd, app_close.c_str(), app_title.c_str(),
							   MB_OKCANCEL | MB_ICONWARNING | MB_SETFOREGROUND | MB_TOPMOST);
			if (n != IDOK)
				return 0;
		}
		asyncEnd();
		return 0;
	case WM_SETCURSOR:
		if (!suspended) {
			if (gfx_mode == 3) {
				SetCursor(0);
				return 1;
			} else if (!pointer_visible) {
				POINT p;
				GetCursorPos(&p);
				ScreenToClient(hwnd, &p);
				RECT r;
				GetClientRect(hwnd, &r);
				if (p.x >= 0 && p.y >= 0 && p.x < r.right && p.y < r.bottom) {
					SetCursor(0);
					return 1;
				}
			}
		}
		break;
	case WM_ACTIVATEAPP:
		if (auto_suspend) {
			if (wparam) {
				if (suspended)
					resume();
			} else {
				if (!suspended)
					suspend();
			}
		}
		break;
	}

	if (!input || suspended)
		return DefWindowProc(hwnd, msg, wparam, lparam);

	if (gfx_mode == 3 && use_di) {
		use_di = input->acquire();
		return DefWindowProc(hwnd, msg, wparam, lparam);
	}

	static const int MK_ALLBUTTONS = MK_LBUTTON | MK_RBUTTON | MK_MBUTTON;

	//handle input messages
	switch (msg) {
	case WM_LBUTTONDOWN:
		input->wm_mousedown(1);
		SetCapture(hwnd);
		break;
	case WM_LBUTTONUP:
		input->wm_mouseup(1);
		if (!(wparam & MK_ALLBUTTONS))
			ReleaseCapture();
		break;
	case WM_RBUTTONDOWN:
		input->wm_mousedown(2);
		SetCapture(hwnd);
		break;
	case WM_RBUTTONUP:
		input->wm_mouseup(2);
		if (!(wparam & MK_ALLBUTTONS))
			ReleaseCapture();
		break;
	case WM_MBUTTONDOWN:
		input->wm_mousedown(3);
		SetCapture(hwnd);
		break;
	case WM_MBUTTONUP:
		input->wm_mouseup(3);
		if (!(wparam & MK_ALLBUTTONS))
			ReleaseCapture();
		break;
	case WM_MOUSEMOVE:
		if (!graphics)
			break;
		if (gfx_mode == 3 && !use_di) {
			POINT p;
			GetCursorPos(&p);
			input->wm_mousemove(p.x, p.y);
		} else {
			int x = (short)(lparam & 0xffff), y = lparam >> 16;
			if (gfx_mode == 1) {
				RECT rect;
				GetClientRect(hwnd, &rect);
				x = x * graphics->getWidth() / (rect.right - rect.left);
				y = y * graphics->getHeight() / (rect.bottom - rect.top);
			}
			if (x < 0)
				x = 0;
			else if (x >= graphics->getWidth())
				x = graphics->getWidth() - 1;
			if (y < 0)
				y = 0;
			else if (y >= graphics->getHeight())
				y = graphics->getHeight() - 1;
			input->wm_mousemove(x, y);
		}
		break;
	case WM_MOUSEWHEEL:
		input->wm_mousewheel((short)HIWORD(wparam));
		break;
	case WM_KEYDOWN:
	case WM_SYSKEYDOWN:
		if (lparam & 0x40000000)
			break;
		if (int n = ((lparam >> 17) & 0x80) | ((lparam >> 16) & 0x7f))
			input->wm_keydown(n);
		break;
	case WM_KEYUP:
	case WM_SYSKEYUP:
		if (int n = ((lparam >> 17) & 0x80) | ((lparam >> 16) & 0x7f))
			input->wm_keyup(n);
		break;
	default:
		return DefWindowProc(hwnd, msg, wparam, lparam);
	}

	return 0;
}

static LRESULT CALLBACK windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam)
{
	if (runtime)
		return runtime->windowProc(hwnd, msg, wparam, lparam);
	return DefWindowProc(hwnd, msg, wparam, lparam);
}

//////////////////////////////
//STOP FROM EXTERNAL SOURCE //
//////////////////////////////
void gxRuntime::asyncStop()
{
	PostMessage(hwnd, WM_STOP, 0, 0);
}

//////////////////////////////
//RUN  FROM EXTERNAL SOURCE //
//////////////////////////////
void gxRuntime::asyncRun()
{
	PostMessage(hwnd, WM_RUN, 0, 0);
}

//////////////////////////////
// END FROM EXTERNAL SOURCE //
//////////////////////////////
void gxRuntime::asyncEnd()
{
	PostMessage(hwnd, WM_END, 0, 0);
}

//////////
// IDLE //
//////////
bool gxRuntime::idle()
{
	for (;;) {
		MSG msg;
		if (suspended && run_flag) {
			GetMessage(&msg, 0, 0, 0);
		} else {
			if (!PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
				return run_flag;
		}
		switch (msg.message) {
		case WM_STOP:
			if (!suspended)
				forceSuspend();
			break;
		case WM_RUN:
			if (suspended)
				forceResume();
			break;
		case WM_END:
			//debugger = 0;
			run_flag = false;
			break;
		default:
			DispatchMessage(&msg);
		}
	}
	return run_flag;
}

///////////
// DELAY //
///////////
bool gxRuntime::delay(int ms)
{
	int t = timeGetTime() + ms;
	for (;;) {
		if (!idle())
			return false;
		int d = t - timeGetTime(); //how long left to wait
		if (d <= 0)
			return true;
		if (d > 100)
			d = 100;
		Sleep(d);
	}
}

///////////////
// DEBUGSTMT //
///////////////
void gxRuntime::debugStmt(int pos, const char* file)
{
	//if (debugger) debugger->debugStmt(pos, file);
}

///////////////
// DEBUGSTOP //
///////////////
void gxRuntime::debugStop()
{
	//if (!suspended) forceSuspend();
}

////////////////
// DEBUGENTER //
////////////////
void gxRuntime::debugEnter(void* frame, void* env, const char* func)
{
	//if (debugger) debugger->debugEnter(frame, env, func);
}

////////////////
// DEBUGLEAVE //
////////////////
void gxRuntime::debugLeave()
{
	//if (debugger) debugger->debugLeave();
}

////////////////
// DEBUGERROR //
////////////////
void gxRuntime::debugError(const char* t)
{
	/*if (!debugger) return;
	void*d = debugger;
	asyncEnd();
	if (!suspended) {
		forceSuspend();
	}
	d->debugMsg(t, true);*/
}

///////////////
// DEBUGINFO //
///////////////
void gxRuntime::debugInfo(const char* t)
{
	/*if (!debugger) return;
	Debugger *d = debugger;
	asyncEnd();
	if (!suspended) {
		forceSuspend();
	}
	d->debugMsg(t, false);*/
}

//////////////
// DEBUGLOG //
//////////////
void gxRuntime::debugLog(const char* t)
{
	//if (debugger) debugger->debugLog(t);
}

/////////////////////////
// RETURN COMMAND LINE //
/////////////////////////
std::string gxRuntime::commandLine()
{
	return cmd_line;
}

/////////////
// EXECUTE //
/////////////
bool gxRuntime::execute(const std::string& cmd_line)
{
	if (!cmd_line.size())
		return false;

	//convert cmd_line to cmd and params
	std::string cmd = cmd_line, params;
	while (cmd.size() && cmd[0] == ' ')
		cmd = cmd.substr(1);
	if (cmd.find('\"') == 0) {
		int n = cmd.find('\"', 1);
		if (n != std::string::npos) {
			params = cmd.substr(n + 1);
			cmd    = cmd.substr(1, n - 1);
		}
	} else {
		int n = cmd.find(' ');
		if (n != std::string::npos) {
			params = cmd.substr(n + 1);
			cmd    = cmd.substr(0, n);
		}
	}
	while (params.size() && params[0] == ' ')
		params = params.substr(1);
	while (params.size() && params[params.size() - 1] == ' ')
		params = params.substr(0, params.size() - 1);

	SetForegroundWindow(GetDesktopWindow());

	return (int)ShellExecute(GetDesktopWindow(), 0, cmd.c_str(), params.size() ? params.c_str() : 0, 0, SW_SHOW) > 32;
}

///////////////
// APP TITLE //
///////////////
void gxRuntime::setTitle(const std::string& t, const std::string& e)
{
	app_title = t;
	app_close = e;
	SetWindowText(hwnd, app_title.c_str());
}

//////////////////
// GETMILLISECS //
//////////////////
int gxRuntime::getMilliSecs()
{
	return timeGetTime();
}

/////////////////////
// POINTER VISIBLE //
/////////////////////
void gxRuntime::setPointerVisible(bool vis)
{
	if (pointer_visible == vis)
		return;

	pointer_visible = vis;
	if (gfx_mode == 3)
		return;

	//force a WM_SETCURSOR
	POINT pt;
	GetCursorPos(&pt);
	SetCursorPos(pt.x, pt.y);
}

/////////////////
// AUDIO SETUP //
/////////////////
gxAudio* gxRuntime::openAudio(int flags)
{
	if (audio)
		return 0;

	int f_flags = FSOUND_INIT_GLOBALFOCUS | FSOUND_INIT_USEDEFAULTMIDISYNTH;

	FSOUND_SetHWND(hwnd);
	if (!FSOUND_Init(44100, 1024, f_flags)) {
		return 0;
	}

	audio = new gxAudio(this);
	return audio;
}

void gxRuntime::closeAudio(gxAudio* a)
{
	if (!audio || audio != a)
		return;
	delete audio;
	audio = 0;
}

/////////////////
// INPUT SETUP //
/////////////////
gxInput* gxRuntime::openInput(int flags)
{
	if (input)
		return 0;
	LPDIRECTINPUT8 di;
	if (DirectInput8Create(hinst, DIRECTINPUT_VERSION, IID_IDirectInput8, (void**)&di, NULL) >= 0) {
		//if (DirectInput8Create(hinst, DIRECTINPUT_VERSION, (void**)&di, NULL) >= 0) {
		input = new gxInput(this, di);
		acquireInput();
	} else {
		debugInfo("Create DirectInput failed");
	}
	return input;
}

void gxRuntime::closeInput(gxInput* i)
{
	if (!input || input != i)
		return;
	unacquireInput();
	delete input;
	input = 0;
}

/////////////////////////////////////////////////////
// TIMER CALLBACK FOR AUTOREFRESH OF WINDOWED MODE //
/////////////////////////////////////////////////////
static void CALLBACK timerCallback(UINT id, UINT msg, DWORD user, DWORD dw1, DWORD dw2)
{
	if (gfx_mode) {
		gxCanvas* f = runtime->graphics->getFrontCanvas();
		if (f->getModify() != mod_cnt) {
			mod_cnt = f->getModify();
			InvalidateRect(runtime->hwnd, 0, false);
		}
	}
}

////////////////////
// GRAPHICS SETUP //
////////////////////
void gxRuntime::backupWindowState()
{
	GetWindowRect(hwnd, &t_rect);
	t_style = GetWindowLong(hwnd, GWL_STYLE);
}

void gxRuntime::restoreWindowState()
{
	SetWindowLong(hwnd, GWL_STYLE, t_style);
	SetWindowPos(hwnd, 0, t_rect.left, t_rect.top, t_rect.right - t_rect.left, t_rect.bottom - t_rect.top,
				 SWP_NOZORDER | SWP_FRAMECHANGED);
}

bool gxRuntime::setDisplayMode(int w, int h, int d, bool d3d, IDirectDraw7* dirDraw)
{
	if (d)
		return dirDraw->SetDisplayMode(w, h, d, 0, 0) >= 0;

	int best_d = 0;

	if (d3d) {
		int bd = curr_driver->d3d_desc.dwDeviceRenderBitDepth;
		if (bd & DDBD_32)
			best_d = 32;
		else if (bd & DDBD_24)
			best_d = 24;
		else if (bd & DDBD_16)
			best_d = 16;
	} else {
		int best_n = 0;
		for (d = 16; d <= 32; d += 8) {
			if (dirDraw->SetDisplayMode(w, h, d, 0, 0) < 0)
				continue;
			DDCAPS caps = {sizeof(caps)};
			dirDraw->GetCaps(&caps, 0);
			int n = 0;
			if (caps.dwCaps & DDCAPS_BLT)
				++n;
			if (caps.dwCaps & DDCAPS_BLTCOLORFILL)
				++n;
			if (caps.dwCKeyCaps & DDCKEYCAPS_SRCBLT)
				++n;
			if (caps.dwCaps2 & DDCAPS2_WIDESURFACES)
				++n;
			if (n == 4)
				return true;
			if (n > best_n) {
				best_d = d;
				best_n = n;
			}
			dirDraw->RestoreDisplayMode();
		}
	}
	return best_d ? dirDraw->SetDisplayMode(w, h, best_d, 0, 0) >= 0 : false;
}

gxGraphics* gxRuntime::openWindowedGraphics(int w, int h, int d, bool d3d)
{
	IDirectDraw7* dd;
	if (DirectDrawCreateEx(curr_driver->guid, (void**)&dd, IID_IDirectDraw7, 0) < 0)
		return 0;

	//set coop level
	if (dd->SetCooperativeLevel(hwnd, DDSCL_NORMAL) >= 0) {
		//create primary surface
		IDirectDrawSurface7* ps;
		DDSURFACEDESC2       desc = {sizeof(desc)};
		desc.dwFlags              = DDSD_CAPS;
		desc.ddsCaps.dwCaps       = DDSCAPS_PRIMARYSURFACE;
		if (dd->CreateSurface(&desc, &ps, 0) >= 0) {
			//create clipper
			IDirectDrawClipper* cp;
			if (dd->CreateClipper(0, &cp, 0) >= 0) {
				//attach clipper
				if (ps->SetClipper(cp) >= 0) {
					//set clipper HWND
					if (cp->SetHWnd(0, hwnd) >= 0) {
						//create front buffer
						IDirectDrawSurface7* fs;
						DDSURFACEDESC2       desc = {sizeof(desc)};
						desc.dwFlags              = DDSD_WIDTH | DDSD_HEIGHT | DDSD_CAPS;
						desc.dwWidth              = w;
						desc.dwHeight             = h;
						desc.ddsCaps.dwCaps       = DDSCAPS_OFFSCREENPLAIN;

						if (d3d)
							desc.ddsCaps.dwCaps |= DDSCAPS_3DDEVICE;

						if (dd->CreateSurface(&desc, &fs, 0) >= 0) {
							if (timerID = timeSetEvent(100, 10, timerCallback, 0, TIME_PERIODIC)) {
								//Success!
								clipper  = cp;
								primSurf = ps;
								mod_cnt  = 0;
								fs->AddRef();
								return new gxGraphics(this, dd, fs, fs, d3d);
							}
							fs->Release();
						}
					}
				}
				cp->Release();
			}
			ps->Release();
		}
	}
	dd->Release();
	return 0;
}

gxGraphics* gxRuntime::openExclusiveGraphics(int w, int h, int d, bool d3d)
{
	IDirectDraw7* dd;
	if (DirectDrawCreateEx(curr_driver->guid, (void**)&dd, IID_IDirectDraw7, 0) < 0)
		return 0;

	//Set coop level
	if (dd->SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN | DDSCL_ALLOWREBOOT) >= 0) {
		//Set display mode
		if (setDisplayMode(w, h, d, d3d, dd)) {
			//create primary surface
			IDirectDrawSurface7* ps;
			DDSURFACEDESC2       desc = {sizeof(desc)};
			desc.dwFlags              = DDSD_CAPS | DDSD_BACKBUFFERCOUNT;
			desc.ddsCaps.dwCaps       = DDSCAPS_PRIMARYSURFACE | DDSCAPS_COMPLEX | DDSCAPS_FLIP;

			desc.dwBackBufferCount = 1;
			if (d3d)
				desc.ddsCaps.dwCaps |= DDSCAPS_3DDEVICE;

			if (dd->CreateSurface(&desc, &ps, 0) >= 0) {
				//find back surface
				IDirectDrawSurface7* bs;
				DDSCAPS2             caps = {sizeof caps};
				caps.dwCaps               = DDSCAPS_BACKBUFFER;
				if (ps->GetAttachedSurface(&caps, &bs) >= 0) {
					return new gxGraphics(this, dd, ps, bs, d3d);
				}
				ps->Release();
			}
			dd->RestoreDisplayMode();
		}
	}
	dd->Release();
	return 0;
}

gxGraphics* gxRuntime::openGraphics(int w, int h, int d, int driver, int flags)
{
	if (graphics)
		return 0;

	busy = true;

	bool d3d      = flags & gxGraphics::GRAPHICS_3D ? true : false;
	bool windowed = flags & gxGraphics::GRAPHICS_WINDOWED ? true : false;

	if (windowed)
		driver = 0;

	curr_driver = drivers[driver];

	if (windowed) {
		if (graphics = openWindowedGraphics(w, h, d, d3d)) {
			gfx_mode     = (flags & gxGraphics::GRAPHICS_SCALED) ? 1 : 2;
			auto_suspend = (flags & gxGraphics::GRAPHICS_AUTOSUSPEND) ? true : false;
			int ws, ww, hh;
			if (gfx_mode == 1) {
				ws = scaled_ws;
				RECT c_r;
				GetClientRect(hwnd, &c_r);
				ww = c_r.right - c_r.left;
				hh = c_r.bottom - c_r.top;
			} else {
				ws = static_ws;
				ww = w;
				hh = h;
			}

			SetWindowLong(hwnd, GWL_STYLE, ws);
			SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);

			RECT w_r, c_r;
			GetWindowRect(hwnd, &w_r);
			GetClientRect(hwnd, &c_r);
			int   tw = (w_r.right - w_r.left) - (c_r.right - c_r.left);
			int   th = (w_r.bottom - w_r.top) - (c_r.bottom - c_r.top);
			int   cx = (GetSystemMetrics(SM_CXSCREEN) - ww) / 2;
			int   cy = (GetSystemMetrics(SM_CYSCREEN) - hh) / 2;
			POINT zz = {0, 0};
			ClientToScreen(hwnd, &zz);
			int bw = zz.x - w_r.left, bh = zz.y - w_r.top;
			int wx = cx - bw, wy = cy - bh;
			if (wy < 0)
				wy = 0; //not above top!
			MoveWindow(hwnd, wx, wy, ww + tw, hh + th, true);
		}
	} else {
		backupWindowState();

		SetWindowLong(hwnd, GWL_STYLE, WS_VISIBLE | WS_POPUP);
		SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);

		ShowCursor(0);
		if (graphics = openExclusiveGraphics(w, h, d, d3d)) {
			gfx_mode     = 3;
			auto_suspend = true;
			SetCursorPos(0, 0);
			acquireInput();
		} else {
			ShowCursor(1);
			restoreWindowState();
		}
	}

	if (!graphics)
		curr_driver = 0;

	gfx_lost = false;

	busy = false;

	return graphics;
}

void gxRuntime::closeGraphics(gxGraphics* g)
{
	if (!graphics || graphics != g)
		return;

	auto_suspend = false;

	busy = true;

	unacquireInput();
	if (timerID) {
		timeKillEvent(timerID);
		timerID = 0;
	}
	if (clipper) {
		clipper->Release();
		clipper = 0;
	}
	if (primSurf) {
		primSurf->Release();
		primSurf = 0;
	}
	delete graphics;
	graphics = 0;

	if (gfx_mode == 3) {
		ShowCursor(1);
		restoreWindowState();
	}
	gfx_mode = 0;

	gfx_lost = false;

	busy = false;
}

bool gxRuntime::graphicsLost()
{
	return gfx_lost;
}

gxFileSystem* gxRuntime::openFileSystem(int flags)
{
	if (fileSystem)
		return 0;

	fileSystem = new gxFileSystem();
	return fileSystem;
}

void gxRuntime::closeFileSystem(gxFileSystem* f)
{
	if (!fileSystem || fileSystem != f)
		return;

	delete fileSystem;
	fileSystem = 0;
}

////////////////////
// GFX ENUM STUFF //
////////////////////
static HRESULT WINAPI enumMode(DDSURFACEDESC2* desc, void* context)
{
	int dp = desc->ddpfPixelFormat.dwRGBBitCount;
	if (dp == 16 || dp == 24 || dp == 32) {
		gxRuntime::GfxMode* m   = new gxRuntime::GfxMode;
		m->desc                 = *desc;
		gxRuntime::GfxDriver* d = (gxRuntime::GfxDriver*)context;
		d->modes.push_back(m);
	}
	return DDENUMRET_OK;
}

static int     maxDevType;
static HRESULT CALLBACK enumDevice(char* desc, char* name, D3DDEVICEDESC7* devDesc, void* context)
{
	int  t    = 0;
	GUID guid = devDesc->deviceGUID;
	if (guid == IID_IDirect3DRGBDevice)
		t = 1;
	else if (guid == IID_IDirect3DHALDevice)
		t = 2;
	else if (guid == IID_IDirect3DTnLHalDevice)
		t = 3;
	if (t > 1 && t > maxDevType) {
		maxDevType              = t;
		gxRuntime::GfxDriver* d = (gxRuntime::GfxDriver*)context;
		d->d3d_desc             = *devDesc;
	}
	return D3DENUMRET_OK;
}

static BOOL WINAPI enumDriver(GUID FAR* guid, LPSTR desc, LPSTR name, LPVOID context, HMONITOR hm)
{
	IDirectDraw7* dd;
	if (DirectDrawCreateEx(guid, (void**)&dd, IID_IDirectDraw7, 0) < 0)
		return 0;

	if (!guid && !desktop_desc.ddpfPixelFormat.dwRGBBitCount) {
		desktop_desc.dwSize = sizeof(desktop_desc);
		dd->GetDisplayMode(&desktop_desc);
	}

	gxRuntime::GfxDriver* d = new gxRuntime::GfxDriver;

	d->guid = guid ? new GUID(*guid) : 0;
	d->name = desc; //string( name )+" "+string( desc );

	memset(&d->d3d_desc, 0, sizeof(d->d3d_desc));
	IDirect3D7* dir3d;
	if (dd->QueryInterface(IID_IDirect3D7, (void**)&dir3d) >= 0) {
		maxDevType = 0;
		dir3d->EnumDevices(enumDevice, d);
		dir3d->Release();
	}
	std::vector<gxRuntime::GfxDriver*>* drivers = (std::vector<gxRuntime::GfxDriver*>*)context;
	drivers->push_back(d);
	dd->EnumDisplayModes(0, 0, d, enumMode);
	dd->Release();
	return 1;
}

void gxRuntime::enumGfx()
{
	denumGfx();
	if (enum_all) {
		DirectDrawEnumerateEx(enumDriver, &drivers, DDENUM_ATTACHEDSECONDARYDEVICES | DDENUM_NONDISPLAYDEVICES);
	} else {
		DirectDrawEnumerateEx(enumDriver, &drivers, 0);
	}
}

void gxRuntime::denumGfx()
{
	for (size_t k = 0; k < drivers.size(); ++k) {
		gxRuntime::GfxDriver* d = drivers[k];
		for (size_t j = 0; j < d->modes.size(); ++j)
			delete d->modes[j];
		delete d->guid;
		delete d;
	}
	drivers.clear();
}

int gxRuntime::enumerateGraphicsDrivers()
{
	if (!enum_all) {
		enum_all = true;
		enumGfx();
	}
	return drivers.size();
}

void gxRuntime::graphicsDriverInfo(int driver, std::string* name, int* c)
{
	GfxDriver* g    = drivers[driver];
	int        caps = 0;
	if (g->d3d_desc.dwDeviceRenderBitDepth)
		caps |= GFXMODECAPS_3D;
	*name = g->name;
	*c    = caps;
}

int gxRuntime::countGraphicsModes(int driver)
{
	return drivers[driver]->modes.size();
}

void gxRuntime::graphicsModeInfo(int driver, int mode, int* w, int* h, int* d, int* c)
{
	GfxDriver* g    = drivers[driver];
	GfxMode*   m    = g->modes[mode];
	int        caps = 0;
	int        bd   = 0;
	switch (m->desc.ddpfPixelFormat.dwRGBBitCount) {
	case 16:
		bd = DDBD_16;
		break;
	case 24:
		bd = DDBD_24;
		break;
	case 32:
		bd = DDBD_32;
		break;
	}
	if (g->d3d_desc.dwDeviceRenderBitDepth & bd)
		caps |= GFXMODECAPS_3D;
	*w = m->desc.dwWidth;
	*h = m->desc.dwHeight;
	*d = m->desc.ddpfPixelFormat.dwRGBBitCount;
	*c = caps;
}

void gxRuntime::windowedModeInfo(int* c)
{
	int caps = 0;
	int bd   = 0;
	switch (desktop_desc.ddpfPixelFormat.dwRGBBitCount) {
	case 16:
		bd = DDBD_16;
		break;
	case 24:
		bd = DDBD_24;
		break;
	case 32:
		bd = DDBD_32;
		break;
	}
	if (drivers[0]->d3d_desc.dwDeviceRenderBitDepth & bd)
		caps |= GFXMODECAPS_3D;
	*c = caps;
}

gxTimer* gxRuntime::createTimer(int hertz)
{
	gxTimer* t = new gxTimer(this, hertz);
	timers.insert(t);
	return t;
}

void gxRuntime::freeTimer(gxTimer* t)
{
	if (!timers.count(t))
		return;
	timers.erase(t);
	delete t;
}

static std::string toDir(std::string t)
{
	if (t.size() && t[t.size() - 1] != '\\')
		t += '\\';
	return t;
}

std::string gxRuntime::systemProperty(const std::string& p)
{
	char        buff[MAX_PATH + 1];
	std::string t = tolower(p);
	if (t == "cpu") {
		return "Intel";
	} else if (t == "os") {
		return "Windows";
	} else if (t == "appdir") {
		if (GetModuleFileName(0, buff, MAX_PATH)) {
			std::string t = buff;
			int         n = t.find_last_of('\\');
			if (n != std::string::npos)
				t = t.substr(0, n);
			return toDir(t);
		}
	} else if (t == "apphwnd") {
		return itoa((int)hwnd);
	} else if (t == "apphinstance") {
		return itoa((int)hinst);
	} else if (t == "windowsdir") {
		if (GetWindowsDirectory(buff, MAX_PATH))
			return toDir(buff);
	} else if (t == "systemdir") {
		if (GetSystemDirectory(buff, MAX_PATH))
			return toDir(buff);
	} else if (t == "tempdir") {
		if (GetTempPath(MAX_PATH, buff))
			return toDir(buff);
	} else if (t == "direct3d7") {
		if (graphics)
			return itoa((int)graphics->dir3d);
	} else if (t == "direct3ddevice7") {
		if (graphics)
			return itoa((int)graphics->dir3dDev);
	} else if (t == "directdraw7") {
		if (graphics)
			return itoa((int)graphics->dirDraw);
	} else if (t == "directinput7") {
		if (input)
			return itoa((int)input->dirInput);
	}
	return "";
}

void gxRuntime::enableDirectInput(bool enable)
{
	if (use_di = enable) {
		acquireInput();
	} else {
		unacquireInput();
	}
}

int gxRuntime::callDll(const std::string& dll, const std::string& func, const void* in, int in_sz, void* out,
					   int out_sz)
{
	std::map<std::string, gxDll*>::const_iterator lib_it = libs.find(dll);

	if (lib_it == libs.end()) {
		HINSTANCE h = LoadLibrary(dll.c_str());
		if (!h)
			return 0;
		gxDll* t = new gxDll;
		t->hinst = h;
		lib_it   = libs.insert(make_pair(dll, t)).first;
	}

	gxDll*                                         t      = lib_it->second;
	std::map<std::string, LibFunc>::const_iterator fun_it = t->funcs.find(func);

	if (fun_it == t->funcs.end()) {
		LibFunc f = (LibFunc)GetProcAddress(t->hinst, func.c_str());
		if (!f)
			return 0;
		fun_it = t->funcs.insert(make_pair(func, f)).first;
	}

	static void* save_esp;

	_asm {
		mov[save_esp], esp
	}
	;

	int n = fun_it->second(in, in_sz, out, out_sz);

	_asm {
		mov esp, [save_esp]
	}
	;

	return n;
}
