#include "framework.h"
#include <detours.h>
#pragma comment(lib, "detours.lib")
#include <GL\freeglut.h>
#include <GL\GL.h>
#include <wingdi.h>
#include <windows.h>
#include <timeapi.h>
#include <iostream>
#include <thread>

#include <chrono>

#include "PluginConfigApi.h"

using namespace std::chrono;

unsigned short game_version = 710;

void exitGameClean(int)
{
	switch (game_version)
	{
	case 600:
		*(bool*)0x0000000140E8DF20 = true; // 6.00
		break;
	default:
		*(bool*)0x0000000140EDA6B0 = true; // 7.10
	}
}

int hookedCreateWindow(const char* title, void(__cdecl* exit_function)(int))
{
	SetProcessDPIAware();

	// trick Optimus into switching to the NVIDIA GPU
	if (cuInit != NULL) cuInit(0);
	
	if (nDisplay == 2) // fullscreen
	{
		char GameModeString[24];
		sprintf_s(GameModeString, sizeof(GameModeString), "%dx%d:%d@%d", nWidth, nHeight, nBitDepth, nRefreshRate);
		glutGameModeString(GameModeString);
		
		if (glutGameModeGet(GLUT_GAME_MODE_POSSIBLE)) {
			printf("[Render Manager] Game mode (exclusive fullscreen) enabled.\n");
			printf(GameModeString);
			printf("\n");
			glutEnterGameMode();
		}
		else {
			printf("[Render Manager] Requested display mode not supported. Using non-exclusive fullscreen instead.\n");
			printf(GameModeString);
			printf("\n");
			switch (game_version)
			{
			case 600:
				*fullScreenFlag_600 = 1;
				break;
			default:
				*fullScreenFlag_710 = 1;
			}
			__glutCreateWindowWithExit(title, exitGameClean);
		}
	}
	else // windowed or borderless
	{
		switch (game_version)
		{
		case 600:
			*fullScreenFlag_600 = 0;
			break;
		default:
			*fullScreenFlag_710 = 0;
		}

		RECT xy;
		SystemParametersInfo(SPI_GETWORKAREA, 0, &xy, 0);
		int screenWidth = xy.right - xy.left;
		int screenHeight = xy.bottom - xy.top;
		if (nWidth > screenWidth)
		{
			screenWidth = GetSystemMetrics(SM_CXSCREEN);
			xy.left = 0;
		}

		if (nHeight > screenHeight)
		{
			screenHeight = GetSystemMetrics(SM_CYSCREEN);
			xy.top = 0;
		}
		int wndX = xy.left + (screenWidth - nWidth) / 2;
		int wndY = xy.top + (screenHeight - nHeight) / 2;

		glutInitDisplayMode(GLUT_RGBA | GLUT_DOUBLE | GLUT_DEPTH);

		glutInitWindowSize(nWidth, nHeight);
		glutInitWindowPosition(wndX, wndY);
		__glutCreateWindowWithExit(title, exitGameClean);

		if (nDisplay == 1 || nDisplay == 3) // borderless mode
		{
			// support borderless mode even with original copy of glut
			HDC hDev = wglGetCurrentDC(); // get handle to current opengl device context
			HWND hWnd = WindowFromDC(hDev); // convert it to a window handle

			LONG_PTR wndStyle = WS_VISIBLE | WS_CLIPSIBLINGS | WS_CLIPCHILDREN;  // set no border (visible to ensure something is shown immediately)

			if (nDisplay == 1)
			{
				wndStyle |= WS_POPUP;
				printf("[Render Manager] Popup enabled.\n");
			}

			SetWindowLongPtr(hWnd, GWL_STYLE, wndStyle);
			SetWindowPos(hWnd, HWND_TOP, wndX, wndY, nWidth, nHeight, SWP_FRAMECHANGED); // adjust position to apply new style

			printf("[Render Manager] Borderless mode.\n");
		}
		else
		{
			printf("[Render Manager] Windowed mode.\n");
		}
	}

	return true;
}

__int64 hookedParseParameters(int a1, __int64* a2)
{
	// Force -wqhd if Custom Internal Resolution is enabled
	if (nIntRes)
		switch (game_version)
		{
		case 600:
			*resolutionType_600 = 15;
			break;
		default:
			*resolutionType_710 = 15;
		}
	// Return to the original function
	switch (game_version)
	{
	case 600:
		return divaParseParameters_600(a1, a2);
		break;
	default:
		return divaParseParameters_710(a1, a2);
	}
}

time_point<high_resolution_clock> sleepUntil = high_resolution_clock::now();
time_point<high_resolution_clock> nextUpdate = high_resolution_clock::now();
nanoseconds expectedFrameDuration;
nanoseconds sleepWindow(2500000);

__int64 __fastcall limiterFuncNormal(__int64 a1)
{
	auto timeNow = high_resolution_clock::now();

	if (timeNow < nextUpdate)
		return 0;

	timeNow = high_resolution_clock::now();

	// increment nextUpdate by fixed interval to unsure timing accuracy between frames
	// (ignore time spent outside the update)
	nextUpdate += expectedFrameDuration;

	// if dropping frames, run as fast as possible
	// and also change nextUpdate's timing to match current time
	// (so frame n+2 will be at least 1/nFpsLimit seconds from frame n+1)
	if (nextUpdate < timeNow)
		nextUpdate = timeNow;

	switch (game_version)
	{
	case 600:
		return divaEngineUpdate_600(a1);
	default:
		return divaEngineUpdate_710(a1);
	}
}

__int64 __fastcall limiterFuncLight(__int64 a1)
{
	auto timeNow = high_resolution_clock::now();

	// sleep until the approximate correct time to save CPU usage,
	// then change to instantly returning when timing is more critical
	if (timeNow < sleepUntil)
	{
		std::this_thread::sleep_until(sleepUntil);

		if (nVerboseLimiter)
		{
			timeNow = high_resolution_clock::now();
			nanoseconds timeDifference = (timeNow - sleepUntil);
			if (timeDifference > sleepWindow) // no need for abs because it doesn't matter if this is early
			{
				printf("FPS LIMITER SLEEP TIME OUTSIDE ALLOWED WINDOW\n");
				printf(" Target sleep until time: %lld, Actual time: %lld\n", sleepUntil.time_since_epoch().count(), timeNow.time_since_epoch().count());
				printf(" (Difference: %lld, Window: %lld)\n", timeDifference.count(), sleepWindow.count());
			}
		}
	}

	if (timeNow < nextUpdate)
		return 0;

	timeNow = high_resolution_clock::now();

	// increment nextUpdate by fixed interval to unsure timing accuracy between frames
	// (ignore time spent outside the update)
	nextUpdate += expectedFrameDuration;

	// if dropping frames, run as fast as possible
	// and also change nextUpdate's timing to match current time
	// (so frame n+2 will be at least 1/nFpsLimit seconds from frame n+1)
	if (nextUpdate < timeNow)
		nextUpdate = timeNow;

	sleepUntil = nextUpdate - sleepWindow;

	switch (game_version)
	{
	case 600:
		return divaEngineUpdate_600(a1);
	default:
		return divaEngineUpdate_710(a1);
	}
}

__int64 __fastcall hookedEngineUpdate(__int64 a1)
{
	if (nFpsLimit < 1)
		switch (game_version)
		{
		case 600:
			return divaEngineUpdate_600(a1);
		default:
			return divaEngineUpdate_710(a1);
		}
	else if (nUseLightLimiter)
		return limiterFuncLight(a1);
	else
		return limiterFuncNormal(a1);
}

extern "C" __declspec(dllexport) int getFramerateLimit(void)
{
	return nFpsLimit;
}

extern "C" __declspec(dllexport) void setFramerateLimit(int framerate)
{
	nFpsLimit = framerate;
	if (nFpsLimit < 1)
		expectedFrameDuration = nanoseconds(0);
	else
		expectedFrameDuration = nanoseconds(1000000000 / nFpsLimit);
	return;
}

BOOL APIENTRY DllMain(HMODULE hModule,
	DWORD  ul_reason_for_call,
	LPVOID lpReserved
)
{
	if (ul_reason_for_call == DLL_PROCESS_ATTACH)
	{
		if (*(char*)0x140A570F0 == '6') game_version = 600;

		DisableThreadLibraryCalls(hModule);
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		switch (game_version)
		{
		case 600:
			DetourAttach(&(PVOID&)divaCreateWindow_600, hookedCreateWindow);
			break;
		default:
			DetourAttach(&(PVOID&)divaCreateWindow_710, hookedCreateWindow);
		}
		DetourTransactionCommit();

		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		switch (game_version)
		{
		case 600:
			DetourAttach(&(PVOID&)divaParseParameters_600, hookedParseParameters);
			break;
		default:
			DetourAttach(&(PVOID&)divaParseParameters_710, hookedParseParameters);
		}
		DetourTransactionCommit();


		// set sleep time resolution to 2ms or device minimum (whichever's lower)
		TIMECAPS tc;

		if (timeGetDevCaps(&tc, sizeof(TIMECAPS)) == MMSYSERR_NOERROR)
		{
			UINT wTimerRes = min(max(tc.wPeriodMin, 2), tc.wPeriodMax);
			timeBeginPeriod(wTimerRes);
		}

		setFramerateLimit(nFpsLimit);
		DetourTransactionBegin();
		DetourUpdateThread(GetCurrentThread());
		switch (game_version)
		{
		case 600:
			DetourAttach(&(PVOID&)divaEngineUpdate_600, hookedEngineUpdate);
			break;
		default:
			DetourAttach(&(PVOID&)divaEngineUpdate_710, hookedEngineUpdate);
		}
		DetourTransactionCommit();

		// if window is set to screen res, process that now so it can be used for auto internal res
		if (nWidth == -1 || nHeight == -1)
		{
			nWidth = GetSystemMetrics(SM_CXSCREEN);
			nHeight = GetSystemMetrics(SM_CYSCREEN);
		}

		if (nIntRes)
		{
			// if -1 use window resolution
			if (nIntResWidth == -1 || nIntResHeight == -1)
			{
				nIntResWidth = nWidth;
				nIntResHeight = nHeight;
			}

			printf("[Render] Custom internal resolution enabled\n");
			printf("[Render] X: %d Y: %d\n", nIntResWidth, nIntResHeight);
			switch (game_version)
			{
			case 600:
			{
				DWORD oldProtect, bck;
					VirtualProtect((BYTE*)0x0000000140980954, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
				*((int*)0x0000000140980954) = nIntResWidth;
				VirtualProtect((BYTE*)0x0000000140980954, 6, oldProtect, &bck);
			}
			{
				DWORD oldProtect, bck;
				VirtualProtect((BYTE*)0x0000000140980958, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
				*((int*)0x0000000140980958) = nIntResHeight;
				VirtualProtect((BYTE*)0x0000000140980958, 6, oldProtect, &bck);
			}
			break;
			default:
				{
					DWORD oldProtect, bck;
					VirtualProtect((BYTE*)0x00000001409B8B68, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
					*((int*)0x00000001409B8B68) = nIntResWidth;
					VirtualProtect((BYTE*)0x00000001409B8B68, 6, oldProtect, &bck);
				}
				{
					DWORD oldProtect, bck;
					VirtualProtect((BYTE*)0x00000001409B8B6C, 4, PAGE_EXECUTE_READWRITE, &oldProtect);
					*((int*)0x00000001409B8B6C) = nIntResHeight;
					VirtualProtect((BYTE*)0x00000001409B8B6C, 6, oldProtect, &bck);
				}

				//*((int*)0x00000001409B8B6C) = maxHeight;
				//*((int*)0x00000001409B8B14) = maxWidth;
				//*((int*)0x00000001409B8B18) = maxHeight;

				//*((int*)0x00000001409B8B1C) = maxWidth; // No parameters width?
				//*((int*)0x00000001409B8B20) = maxHeight; // No parameters height?
			}
		}
	}
	return TRUE;
}


using namespace PluginConfig;

extern "C" __declspec(dllexport) LPCWSTR GetPluginName(void)
{
	return L"Render";
}

extern "C" __declspec(dllexport) LPCWSTR GetPluginDescription(void)
{
	return L"Applies window mode/size and FPS limiting.";
}