#define NEED_D3D
#include "Common.h"
#include "InjectedSection.h"
#include "Scripting.h"
#include "Custom.h"
#include "Hooks.h"

#define MOV_EAX_IMM32 0xB8U
#define JMP_NEAR32 0xE9U
#define NOP 0x90U
#define COMMON_THUNK_SIZE 22
#define WINDOW_MODE_STYLE WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_CLIPSIBLINGS | WS_VISIBLE
#define FULLSCREEN_MODE_STYLE WS_POPUP | WS_VISIBLE | WS_CLIPSIBLINGS
#define EXIT_CODE_ERROR 0x10101010U

#pragma pack(push, 1)
struct RepeatingThunk
{
	BYTE movInstruction;		// put MOV_EAX_IMM32 in here
	DWORD v1;
	BYTE jumpInstruction;		// put JMP_NEAR32 in here
	ptrdiff_t v2;
};
#pragma pack(pop)

#ifdef DO_CALL_COUNTS
static DWORD callCounts[4];
#endif
static D3DDISPLAYMODE D3DDisplayMode = { 1024, 768, 0, D3DFMT_A8R8G8B8 };
static BYTE thunkBuffer1[268];		// used to ghost IDirect3D9
static BYTE thunkBuffer2[1696];		// used to ghost IDirect3DDevice9
static int bModeChange = 0;
static D3DPRESENT_PARAMETERS originalPPs;
static RECT rectangle;
static int repeatCount = -2;
static WNDPROC previousWndProc = NULL;

#ifndef HOOK_SCRIPT_FROM_PRESENT
static CallHook scriptTickHook;
#endif
#ifdef DO_GXT_HOOK
static StompHook readGxtHook;
#endif
#ifdef DO_FULL_THREAD_CONTROL
static StompHook fullThreadControlHook;
#endif

/*
 * Note: this function is 22 bytes long.
 *   If it's modified, COMMON_THUNK_SIZE
 *   needs to be updated.
 */
static void __declspec(naked) commonThunk()
{
	__asm {
		push ebx
		push edx
		mov ebx, dword ptr [esp+12]
		mov edx, dword ptr [ebx+4]
		mov dword ptr [esp+12], edx
		mov ebx, dword ptr [edx]
		pop edx
		mov eax, dword ptr [ebx+eax*4]
		pop ebx
		jmp eax
	};
}

static void buildThunk(
	PVOID originalObject,
	DWORD numExtraPtrs,
	DWORD numberThunkEntries,
	PVOID buffer,
	PVOID _commonThunk,
	DWORD commonThunkSize)
{
	PVOID* p = (PVOID*) buffer;
	PVOID* t = p + 2 + numExtraPtrs;
	RepeatingThunk* rtp = (RepeatingThunk*) (t + numberThunkEntries);
	PBYTE ctp = (PBYTE) (rtp + numberThunkEntries);
	*p = (PVOID) t;
	p[1] = originalObject;
//	memset(p + 2, 0, numExtraPtrs * sizeof(PVOID));
	for (DWORD i = 0; i < numberThunkEntries; i++) {
		RepeatingThunk* c = rtp + i;
		t[i] = c;
		c->movInstruction = MOV_EAX_IMM32;
		c->v1 = i;
		c->jumpInstruction = JMP_NEAR32;
		c->v2 = ctp - (PBYTE)(c + 1);
	}
	memcpy(
		ctp,
		_commonThunk,
		commonThunkSize);
}

#ifndef HOOK_SCRIPT_FROM_PRESENT
static void __stdcall scriptHandler(DWORD timer_delta);

static void __declspec(naked) scriptHandlerStub()
{
	__asm {
		push dword ptr [esp + 20]
		push scriptTickHook.pOriginal
		jmp scriptHandler
	}
}

static void __stdcall scriptHandler(DWORD timerDelta)
{
	if (GTASA::theScript.hasWorkToDo()) {
		GTASA::theScript.adjustLocalTimers(timerDelta);
		GTASA::theScript.execute();
	}
#ifndef DO_CUSTOM_SCRIPT_HOOK
	else if (repeatCount == -2) {
		/*
		 * Nothing more to do, unhook
		 */
		scriptTickHook.releaseHook(true);
		return;
	}
#else
	customScriptHook(timerDelta);
#endif
}
#endif /* HOOK_SCRIPT_FROM_PRESENT */

#ifdef DO_FULL_THREAD_CONTROL
static void __stdcall fullThreadLoop(PGTASA_SCRIPT_THREAD pGst, DWORD timerDelta);

static void __declspec(naked) fullThreadLoopStub()
{
	__asm {
		mov eax, dword ptr [esp + 20]
		xchg eax, dword ptr [esp]
		push esi
		add eax, 57
		push eax
		jmp fullThreadLoop
	}
}

static void __stdcall fullThreadLoop(PGTASA_SCRIPT_THREAD pGst, DWORD timerDelta)
{
	PVOID processOneCommand = GA(PROCESS_ONE_COMMAND);
	for (;pGst && pGst->bStartNewScript; pGst = (PGTASA_SCRIPT_THREAD) pGst->pNext) {
		/*
		 * Disabling the thread that runs at SCRIPT_BASE crashes the game
		 */
		if ((PDWORD) pGst->dwScriptIP != GA(SCRIPT_BASE) &&
			!customThreadFilter(pGst))
			continue;
		(*GA(THREAD_LOOP_COUNTER))++;
		pGst->dwLocalVar[32] += timerDelta;
		pGst->dwLocalVar[33] += timerDelta;
		__asm {
			push ecx
			mov ecx, dword ptr pGst
			call processOneCommand
			pop ecx
		}
	}
}
#endif /* DO_FULL_THREAD_CONTROL */

#ifdef DO_GXT_HOOK
static void __declspec(naked) gxtHookStub()
{
	__asm {
		mov eax, dword ptr [esp + 8]
		test eax, eax
		jz short l1
		push ecx
		push eax
		call customStrings
		pop ecx
		test eax, eax
		jz short l1
		add esp, 4
		ret 4
l1:
		pop eax
		sub esp, 32
		push esi
		push edi
		jmp eax
	}
}
#endif

#ifdef DO_HOTKEYS
static int tooBig(UINT width, UINT height);
static LRESULT CALLBACK myWndProc(
	HWND hWnd,
	UINT uMsg,
	WPARAM wParam,
	LPARAM lParam)
{
	if (uMsg != WM_KEYDOWN && uMsg != WM_SYSKEYDOWN)
		goto fin2;
	/*
	 * Check that ALT is held and that this is the 1st rep
	 */
	if ((lParam & 0x60000000U) != 0x20000000U)
		goto fin1;
	switch (wParam) {
	case VK_RETURN:
		/*
		 * Alt-Enter
		 */
		if (!GetClientRect(hWnd, &rectangle))
			break;
		if (bWindow) {
			bWindow = 0;
			bModeChange = 2;
			SetWindowLong(
				hWnd,
				GWL_STYLE,
				FULLSCREEN_MODE_STYLE);
		} else {
			if (tooBig(
					rectangle.right - rectangle.left,
					rectangle.bottom - rectangle.top))
				break;
			bWindow = 1;
			SetWindowLong(
				hWnd,
				GWL_STYLE,
				WINDOW_MODE_STYLE);
			SetWindowPos(
				hWnd,
				HWND_NOTOPMOST,
				0,
				0,
				0,
				0,
				SWP_SHOWWINDOW | SWP_NOSIZE | SWP_FRAMECHANGED | SWP_NOSENDCHANGING | SWP_NOCOPYBITS);
		}
		return 0;
	case 'S':
		/*
		 * Alt-S
		 */
		if (repeatCount == -2) {
			if (GTASA::theScript.hasWorkToDo()) {
				repeatCount = GTASA::theScript.getRepeatCount();
				GTASA::theScript.setRepeatCount(0);
			} else
				break;
		} else {
			GTASA::theScript.setRepeatCount(repeatCount);
			repeatCount = -2;
		}
		return 0;
	}
fin1:
#ifdef DO_CUSTOM_KEYS
	if (customKeyHook(hWnd, uMsg, wParam, lParam))
		return 0;
#endif
fin2:
	return CallWindowProc(previousWndProc, hWnd, uMsg, wParam, lParam);
}
#endif /* DO_HOTKEYS */

/*
 * Initialization common to both hook() and lateHook()
 */
static void commonInit()
{
#ifndef HOOK_SCRIPT_FROM_PRESENT
	/*
	 * Install script hook
	 *
	 * 46A22E - end of script thread loop, good place to hook
	 *   0046A22E: E8 CD 6A 12 00     call        00590D00
	 */
#ifdef DO_CUSTOM_SCRIPT_HOOK
	scriptTickHook.initialize("\xE8\xCD\x6A\x12", GA(SCRIPT_TICK_HOOK_POINT));
	scriptTickHook.installHook(scriptHandlerStub, true);
#else
	if (GTASA::theScript.hasWorkToDo()) {
		scriptTickHook.initialize("\xE8\xCD\x6A\x12", GA(SCRIPT_TICK_HOOK_POINT));
		scriptTickHook.installHook(scriptHandlerStub, true);
	}
#endif
#endif
#ifdef DO_GXT_HOOK
	readGxtHook.initialize("\x83\xEC\x20\x56\x57", 5, GA(READ_GXT));
	readGxtHook.installHook(gxtHookStub, false, true);
#endif
#ifdef DO_FULL_THREAD_CONTROL
	fullThreadControlHook.initialize("\x66\xFF\5\xF8\x47\xA4", 7, GA(THREAD_LOOP));
	fullThreadControlHook.installHook(fullThreadLoopStub, false, true);
#endif
}

/*
 * Disables some of the annoying splash screens when
 *   gta_sa fires up.
 */
static void disableSplashScreens()
{
	static BYTE buffer[] = {NOP, NOP, NOP, NOP, NOP, NOP};
	DWORD d;

	if (!VirtualProtect(GA(SPLASH_CODE_DISABLE), sizeof(buffer), PAGE_EXECUTE_READWRITE, &d))
		return;
	*GA(MAIN_LOOP_GAME_STATE) = 5U;
	memcpy(GA(SPLASH_CODE_DISABLE), buffer, sizeof(buffer));
	VirtualProtect(GA(SPLASH_CODE_DISABLE), sizeof(buffer), d, &d);
}

static void translateDisplayFormat()
{
	switch (D3DDisplayMode.Format) {
	case D3DFMT_X8R8G8B8:
		D3DDisplayMode.Format = D3DFMT_A8R8G8B8;
		break;
	case D3DFMT_X1R5G5B5:
		D3DDisplayMode.Format = D3DFMT_A1R5G5B5;
		break;
	case D3DFMT_X8B8G8R8:
		D3DDisplayMode.Format = D3DFMT_A8B8G8R8;
		break;
	}
}

static int tooBig(UINT width, UINT height)
{
	if (width >= D3DDisplayMode.Width ||
		height >= D3DDisplayMode.Height)
		return 1;
	return 0;
}

#if defined(HOOK_SCRIPT_FROM_PRESENT) || defined(DO_CUSTOM_PRESENT)
static HRESULT __stdcall myPresent(
	PVOID* _this,
	CONST RECT* pSourceRect,
	CONST RECT* pDestRect,
	HWND hDestWindowOverride,
	CONST RGNDATA* pDirtyRegion)	// index 17 in IDirect3DDevice9
{
	PDIRECT3DDEVICE9 pD3DDevice = (PDIRECT3DDEVICE9) _this[1];
#ifdef DO_CALL_COUNTS
	callCounts[3]++;
#endif
#ifdef HOOK_SCRIPT_FROM_PRESENT
	if (GTASA::theScript.hasWorkToDo()) {
		/*
		 * IN_MENU tells us if the game is in the
		 *   main menu.  The problem is that it's
		 *   set to 0 in the data section and only
		 *   initialized to 1 in the first few
		 *   seconds of the game.  So we use a
		 *   somewhat dubious test on the timer
		 *   to qualify it.  Ideally, the routine
		 *   at 0x5744D0 which initializes IN_MENU
		 *   should be hooked.
		 *   Also, if we set IN_MENU to 1 in
		 *   hook(), the game wipes it back to 0
		 *   before initializing it and going
		 *   to the menu!
		 * Note: can probably test *GA(MAIN_LOOP_GAME_STATE) == 9
		 *   instead of the timer test
		 * Note: it's not possible to correctly update the
		 *   thread's local timers when hooking from here.
		 */
		if (*GA(GLOBAL_TIMER) > 5000U && !*GA(IN_MENU))
			GTASA::theScript.execute();
	}
#if !defined(DO_CUSTOM_SCRIPT_HOOK) && !defined(DO_CUSTOM_PRESENT)
	else if (repeatCount == -2) {
		/*
		 * Nothing more to do, unhook Present()
		 */
		_this[19]= (PBYTE) _this[20] - sizeof(RepeatingThunk);
	}
#endif
#ifdef DO_CUSTOM_SCRIPT_HOOK
	customScriptHook(0);
#endif
#endif /* HOOK_SCRIPT_FROM_PRESENT */
#ifdef DO_CUSTOM_PRESENT
	customPresent(pSourceRect, pDestRect, hDestWindowOverride, pDirtyRegion);
#endif
	return pD3DDevice->Present(
		pSourceRect,
		pDestRect,
		hDestWindowOverride,
		pDirtyRegion);
}
#endif /* defined(HOOK_SCRIPT_FROM_PRESENT) || defined(DO_CUSTOM_PRESENT) */

static HRESULT __stdcall myGetDirect3D(PVOID* _this, IDirect3D9** ppD3D9)	//index 6 in IDirect3DDevice9
{
	*ppD3D9 = (PDIRECT3D9) thunkBuffer1;
	return D3D_OK;
}

static ULONG __stdcall myRelease(PVOID* _this)	// index 2 in IUnknown
{
	LPUNKNOWN pUnknown = (LPUNKNOWN) _this[1];
	if (pUnknown->Release() <= 0) {
		*_this = NULL;
	}
	return 0;
}

static HRESULT __stdcall myReset(
	PVOID* _this,
	D3DPRESENT_PARAMETERS* pPresentationParameters)	// index 16 in IDirect3DDevice9
{
	PDIRECT3DDEVICE9 pD3DDevice = (PDIRECT3DDEVICE9) _this[1];
	PDIRECT3D9 pD3D;
	D3DDEVICE_CREATION_PARAMETERS creationParameters;
	RECT r;
#ifdef DO_CALL_COUNTS
	callCounts[2]++;
#endif
	if (!bModeChange) {
		/*
		 * This part is done if we enter through lateHook
		 */
		memcpy(&originalPPs, pPresentationParameters, sizeof(D3DPRESENT_PARAMETERS));
		if (SUCCEEDED(pD3DDevice->GetDirect3D(&pD3D))) {
			if (FAILED(pD3DDevice->GetCreationParameters(&creationParameters)))
				creationParameters.AdapterOrdinal = D3DADAPTER_DEFAULT;
			pD3D->GetAdapterDisplayMode(creationParameters.AdapterOrdinal, &D3DDisplayMode);
			pD3D->Release();
			translateDisplayFormat();
		}
		bModeChange = 1;
	}
	if (bWindow && tooBig(
			pPresentationParameters->BackBufferWidth,
			pPresentationParameters->BackBufferHeight)) {
		bWindow = 0;
		if (pPresentationParameters->Windowed) {
			SetWindowLong(
				pPresentationParameters->hDeviceWindow,
				GWL_STYLE,
				FULLSCREEN_MODE_STYLE);
			goto switchToFullScreen;
		}
	}
	if (bWindow) {
		pPresentationParameters->Windowed = 1;
		/*
		 * See note below about SwapEffect
		 */
		pPresentationParameters->SwapEffect = D3DSWAPEFFECT_DISCARD;
		pPresentationParameters->FullScreen_RefreshRateInHz = 0;
//		pPresentationParameters->PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

		pPresentationParameters->BackBufferFormat = D3DDisplayMode.Format;

		/*
		 * Note: change WINDOW_MODE_STYLE to
		 *   FULLSCREEN_MODE_STYLE to get a borderless window
		 */
		SetWindowLong(
			pPresentationParameters->hDeviceWindow,
			GWL_STYLE,
			WINDOW_MODE_STYLE);
		r.left = (int) (D3DDisplayMode.Width - pPresentationParameters->BackBufferWidth) / 2;
		r.top  = (int) (D3DDisplayMode.Height - pPresentationParameters->BackBufferHeight) / 2;
		r.right = r.left + pPresentationParameters->BackBufferWidth;
		r.bottom = r.top + pPresentationParameters->BackBufferHeight;
		if (AdjustWindowRect(&r, WINDOW_MODE_STYLE, FALSE)) {
			if (r.left < 0) r.left = 0;
			if (r.top < 0) r.top = 0;
			if (r.right > (int) D3DDisplayMode.Width) r.right = D3DDisplayMode.Width;
			if (r.bottom > (int) D3DDisplayMode.Height) r.bottom = D3DDisplayMode.Height;
			SetWindowPos(
				pPresentationParameters->hDeviceWindow,
				HWND_NOTOPMOST,
				r.left,
				r.top,
				r.right - r.left,
				r.bottom - r.top,
				SWP_SHOWWINDOW | SWP_FRAMECHANGED | SWP_NOSENDCHANGING | SWP_NOCOPYBITS);
		}
	} else if (bModeChange == 2) {
		pPresentationParameters->BackBufferWidth = rectangle.right - rectangle.left;
		pPresentationParameters->BackBufferHeight = rectangle.bottom - rectangle.top;
		bModeChange = 1;
switchToFullScreen:
		pPresentationParameters->Windowed = 0;
		pPresentationParameters->SwapEffect = originalPPs.SwapEffect;
		pPresentationParameters->FullScreen_RefreshRateInHz = originalPPs.FullScreen_RefreshRateInHz;
//		pPresentationParameters->PresentationInterval = originalPPs.PresentationInterval;
		pPresentationParameters->BackBufferFormat = originalPPs.BackBufferFormat;
	}
	return pD3DDevice->Reset(pPresentationParameters);
}

static HRESULT __stdcall myCreateDevice(
	PVOID* _this,
	UINT Adapter,
	D3DDEVTYPE DeviceType,
	HWND hFocusWindow,
	DWORD BehaviorFlags,
	D3DPRESENT_PARAMETERS* pPresentationParameters,
	IDirect3DDevice9** ppReturnedDeviceInterface)	// index 16 in IDirect3D9
{
	PDIRECT3D9 pD3D = (PDIRECT3D9) _this[1];
#ifdef DO_CALL_COUNTS
	callCounts[1]++;
#endif
	if (*(PVOID*) thunkBuffer2)
		return pD3D->CreateDevice(
			Adapter,
			DeviceType,
			hFocusWindow,
			BehaviorFlags,
			pPresentationParameters,
			ppReturnedDeviceInterface);
	if (!bModeChange) {
		memcpy(&originalPPs, pPresentationParameters, sizeof(D3DPRESENT_PARAMETERS));
		pD3D->GetAdapterDisplayMode(Adapter, &D3DDisplayMode);
		translateDisplayFormat();
		bModeChange = 1;
	}
#ifdef DO_HOTKEYS
	if (!previousWndProc)
		previousWndProc = (WNDPROC) SetWindowLong(
			pPresentationParameters->hDeviceWindow,
			GWL_WNDPROC,
			(LONG) myWndProc);
#endif
	if (bWindow && tooBig(
			pPresentationParameters->BackBufferWidth,
			pPresentationParameters->BackBufferHeight))
		bWindow = 0;
	if (bWindow) {
		pPresentationParameters->Windowed = 1;
		/*
		 * Note: gta_sa requests D3DSWAPEFFECT_FLIP which
		 *   causes a lot of flicker in borderless window mode.
		 *   Changing to discard suppresses the flicker and doesn't
		 *   seem to bother the game.
		 */
		pPresentationParameters->SwapEffect = D3DSWAPEFFECT_DISCARD;
		pPresentationParameters->FullScreen_RefreshRateInHz = 0;
//		pPresentationParameters->PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;

		pPresentationParameters->BackBufferFormat = D3DDisplayMode.Format;

		SetWindowLong(
			pPresentationParameters->hDeviceWindow,
			GWL_STYLE,
			FULLSCREEN_MODE_STYLE);
		SetWindowPos(
			pPresentationParameters->hDeviceWindow,
			HWND_NOTOPMOST,
			0,
			0,
			pPresentationParameters->BackBufferWidth,
			pPresentationParameters->BackBufferHeight,
			SWP_SHOWWINDOW | SWP_NOMOVE | SWP_FRAMECHANGED | SWP_NOSENDCHANGING | SWP_NOCOPYBITS);
	}

	HRESULT hr = pD3D->CreateDevice(
		Adapter,
		DeviceType,
		hFocusWindow,
		BehaviorFlags,
		pPresentationParameters,
		ppReturnedDeviceInterface);
	if (FAILED(hr))
		return hr;
	/*
	 * create a ghost for the IDirect3DDevice9 object
	 */
	buildThunk(
		*ppReturnedDeviceInterface,
		0,
		119,
		thunkBuffer2,
		commonThunk,
		COMMON_THUNK_SIZE);
	PVOID* p = (PVOID*) thunkBuffer2 + 2;
	p[2]     = myRelease;					// hook Release()
	p[6]     = myGetDirect3D;				// hook GetDirect3D()
#if 0
	p[13]    = NULL;	// to see if CreateAdditionalSwapChain() gets called (doesn't seem to)
#endif
	p[16]    = myReset;						// hook Reset()
#ifdef DO_CUSTOM_PRESENT
	p[17]    = myPresent;					// hook Present()
#elif defined(HOOK_SCRIPT_FROM_PRESENT)
#ifdef DO_CUSTOM_SCRIPT_HOOK
	p[17]    = myPresent;					// hook Present()
#else
	/*
	 * Hook Present() only if there's a script to inject
	 */
	if (GTASA::theScript.hasWorkToDo())
		p[17]= myPresent;					// hook Present()
#endif
#endif
	*ppReturnedDeviceInterface = (PDIRECT3DDEVICE9) thunkBuffer2;
	return hr;
}

PDIRECT3D9 WINAPI hook(UINT SDKVersion)
{
#ifdef DO_CALL_COUNTS
	callCounts[0]++;
#endif
	const GameAddresses* pGA = localDetectVersion();
	if (pGA)
		selectVersion(pGA);
	/*
	 * create a real D3D object
	 */
	PDIRECT3D9 pD3D = originalDirect3DCreate9(SDKVersion);
	/*
	 * rehook self
	 * this is necessary because d3d9.dll is a delay-loaded DLL
	 * and the first call modifies this IAT entry via the
	 * dynamic load function
	 */
	if (*GA(IAT_ENTRY_Direct3DCreate9) != hook) {
		originalDirect3DCreate9 = *GA(IAT_ENTRY_Direct3DCreate9);
		*GA(IAT_ENTRY_Direct3DCreate9) = hook;
	}
	/*
	 * Note: gta_sa actually calls this function twice.
	 *   The 2nd time is a transient call that's used
	 *   to discover adapters and modes, so we don't
	 *   hook it (doesn't call CreateDevice() anyway.)
	 */
	if (!pD3D || *(PVOID*) thunkBuffer1)
		return pD3D;

	commonInit();

	if (!bSplashScreens && pGA)
		disableSplashScreens();

	/*
	 * create a ghost for the IDirect3D9 object
	 */
	buildThunk(
		pD3D,
		0,
		17,
		thunkBuffer1,
		commonThunk,
		COMMON_THUNK_SIZE);
	PVOID* p = (PVOID*) thunkBuffer1 + 2;
	p[2]     = myRelease;		// hook Release()
	p[16]    = myCreateDevice;	// hook CreateDevice()
	return (PDIRECT3D9) thunkBuffer1;
}

#if _MSC_VER < 1300
typedef HANDLE (WINAPI *POpenThread)(DWORD dwDesiredAccess, BOOL bInheritHandle, DWORD dwThreadId);
#endif
DWORD WINAPI lateHook(PVOID lpThreadParameter)
{
	selectVersion(localDetectVersion());
	PDIRECT3DDEVICE9 pD3DDevice9 = *GA(GAME_DIRECT3DDEVICE9);
	/*
	 * create a ghost for the IDirect3DDevice9 object
	 */
	buildThunk(
		pD3DDevice9,
		0,
		119,
		thunkBuffer2,
		commonThunk,
		COMMON_THUNK_SIZE);
		PVOID* p = (PVOID*) thunkBuffer2 + 2;
		p[2]     = myRelease;	// hook Release()
		p[16]    = myReset;		// hook Reset()
		*GA(GAME_DIRECT3DDEVICE9) = (PDIRECT3DDEVICE9) thunkBuffer2;

	commonInit();

#ifdef DO_HOTKEYS
	if (!previousWndProc)
		previousWndProc = (WNDPROC) SetWindowLong(
			hWnd_GTASA,
			GWL_WNDPROC,
			(LONG) myWndProc);
#endif

	/*
	 * Resume window thread
	 */
#if _MSC_VER < 1300
	POpenThread pOpenThread = (POpenThread)
		GetProcAddress(GetModuleHandle("KERNEL32.DLL"), "OpenThread");
	HANDLE tHandle = pOpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId_GTASA);
#else
	HANDLE tHandle = OpenThread(THREAD_ALL_ACCESS, FALSE, dwThreadId_GTASA);
#endif
	if (!tHandle ||
		ResumeThread(tHandle) == (DWORD) -1) {
		/*
		 * No way to recover from this
		 */
		if (tHandle)
			CloseHandle(tHandle);
		ExitProcess(EXIT_CODE_ERROR);
	}
	CloseHandle(tHandle);
	ExitThread(0);
	return 0;
}