#include "gui/guiWrapper.h"
#include "gui/wxgui.h"
#include "util/crypto/aes128.h"
#include "gui/MainWindow.h"
#include "Cafe/OS/RPL/rpl.h"
#include "Cafe/OS/RPL/rpl_symbol_storage.h"
#include "Cafe/OS/libs/gx2/GX2.h"
#include "Cafe/GameProfile/GameProfile.h"
#include "Cafe/GraphicPack/GraphicPack.h"
#include "config/CemuConfig.h"
#include "gui/CemuApp.h"
#include "Cafe/HW/Latte/Core/LatteOverlay.h"
#include "config/LaunchSettings.h"
#include "Cafe/OS/libs/coreinit/coreinit_Thread.h"

#include "Cafe/CafeSystem.h"
#include "Cafe/TitleList/TitleList.h"
#include "Cafe/TitleList/SaveList.h"

#include "Common/ExceptionHandler/ExceptionHandler.h"

#include <wx/setup.h>
#include "util/helpers/helpers.h"
#include "config/ActiveSettings.h"
#include "Cafe/HW/Latte/Renderer/Vulkan/VsyncDriver.h"

#include "Cafe/IOSU/legacy/iosu_crypto.h"
#include "Cafe/OS/libs/vpad/vpad.h"

#include "audio/IAudioAPI.h"
#if BOOST_OS_WINDOWS > 0
#pragma comment(lib,"Dbghelp.lib")
#endif

#define SDL_MAIN_HANDLED
#include <SDL.h>

#if BOOST_OS_LINUX > 0
#define _putenv(__s) putenv((char*)(__s))
#endif

extern "C"
{
	__declspec(dllexport) int AmdPowerXpressRequestHighPerformance = 1;
	__declspec(dllexport) DWORD NvOptimusEnablement = 0x00000001;
}

bool _cpuExtension_SSSE3 = false;
bool _cpuExtension_SSE4_1 = false;
bool _cpuExtension_AVX2 = false;

std::atomic_bool g_isGPUInitFinished = false;

std::wstring executablePath;

bool g_cemuhook_loaded = false;
bool IsCemuhookLoaded()
{
	return g_cemuhook_loaded;
}

void checkForCemuhook()
{
	#if BOOST_OS_WINDOWS > 0
	// check if there is a dbghelp.dll in the current working directory
	if (!fs::exists(ActiveSettings::GetPath("cemuhook.dll")))
		return;	
	// check if Cemuhook can be detected
	DWORD verHandle;
	DWORD verLen = GetFileVersionInfoSizeW(L"cemuhook.dll", &verHandle);
	if (verLen == 0)
		return;
	uint8* verData = (uint8*)malloc(verLen);
	GetFileVersionInfoW(L"cemuhook.dll", 0, verLen, verData);
	// get version
	LPVOID lpBuffer;
	UINT size;
	if (VerQueryValueW(verData, L"\\", (LPVOID*)&lpBuffer, (PUINT)&size))
	{
		if (size)
		{
			VS_FIXEDFILEINFO *verInfo = (VS_FIXEDFILEINFO *)lpBuffer;
			if (verInfo->dwSignature == 0xfeef04bd)
			{
				forceLog_printf("Cemuhook version: %d.%d.%d.%d", (verInfo->dwFileVersionMS >> 16) & 0xFFFF, (verInfo->dwFileVersionMS >> 0) & 0xFFFF, (verInfo->dwFileVersionLS >> 16) & 0xFFFF, (verInfo->dwFileVersionLS >> 0) & 0xFFFF);
				g_cemuhook_loaded = true;
			}
		}
	}
	free(verData);
	#endif
}

void logCPUAndMemoryInfo()
{
	#if BOOST_OS_WINDOWS > 0
	int CPUInfo[4] = { -1 };
	unsigned   nExIds, i = 0;
	char CPUBrandString[0x40];
	// Get the information associated with each extended ID.
	__cpuid(CPUInfo, 0x80000000);
	nExIds = CPUInfo[0];
	for (i = 0x80000000; i <= nExIds; ++i)
	{
		__cpuid(CPUInfo, i);
		// Interpret CPU brand string
		if (i == 0x80000002)
			memcpy(CPUBrandString, CPUInfo, sizeof(CPUInfo));
		else if (i == 0x80000003)
			memcpy(CPUBrandString + 16, CPUInfo, sizeof(CPUInfo));
		else if (i == 0x80000004)
			memcpy(CPUBrandString + 32, CPUInfo, sizeof(CPUInfo));
	}
	forceLog_printf("CPU: %s", CPUBrandString);

	MEMORYSTATUSEX statex;
	statex.dwLength = sizeof(statex);
	GlobalMemoryStatusEx(&statex);
	uint32 memoryInMB = (uint32)(statex.ullTotalPhys / 1024LL / 1024LL);
	forceLog_printf("RAM: %uMB", memoryInMB);
	#endif
}

bool g_running_in_wine = false;
bool IsRunningInWine()
{
	return g_running_in_wine;
}

void checkForWine()
{
	#if BOOST_OS_WINDOWS > 0
	const HMODULE hmodule = GetModuleHandleA("ntdll.dll");
	if (!hmodule)
		return;

	const auto pwine_get_version = (const char*(__cdecl*)())GetProcAddress(hmodule, "wine_get_version");
	if (pwine_get_version)
	{
		g_running_in_wine = true;
		forceLog_printf("Wine version: %s", pwine_get_version());
	}
	#else
	g_running_in_wine = false;
	#endif
}

void infoLog_cemuStartup()
{
	cemuLog_force("------- Init {} {}.{}{} -------", EMULATOR_NAME, EMULATOR_VERSION_LEAD, EMULATOR_VERSION_MAJOR, EMULATOR_VERSION_SUFFIX);
	cemuLog_force("Init Wii U memory space (base: 0x{:016x})", (size_t)memory_base);
	cemuLog_force(u8"mlc01 path: {}", ActiveSettings::GetMlcPath().generic_u8string());
	// check if Cemuhook is installed
	checkForCemuhook();
	// check for wine version
	checkForWine();
	// CPU and RAM info
	logCPUAndMemoryInfo();
	// extensions that Cemu uses
	char cpuExtensionStr[256];
	strcpy(cpuExtensionStr, "");
	if (_cpuExtension_SSSE3)
	{
		strcat(cpuExtensionStr, "SSSE3");
	}
	if (_cpuExtension_SSE4_1)
	{
		if (cpuExtensionStr[0] != '\0')
			strcat(cpuExtensionStr, ", ");
		strcat(cpuExtensionStr, "SSE4.1");
	}
	if (_cpuExtension_AVX2)
	{
		if (cpuExtensionStr[0] != '\0')
			strcat(cpuExtensionStr, ", ");
		strcat(cpuExtensionStr, "AVX2");
	}
	if (AES128_useAESNI())
	{
		if (cpuExtensionStr[0] != '\0')
			strcat(cpuExtensionStr, ", ");
		strcat(cpuExtensionStr, "AES-NI");
	}
	cemuLog_force("Used CPU extensions: {}", cpuExtensionStr);
}

// some implementations of _putenv dont copy the string and instead only store a pointer
// thus we use a helper to keep a permanent copy
std::vector<std::string*> sPutEnvMap;

void _putenvSafe(const char* c)
{
    auto s = new std::string(c);
    sPutEnvMap.emplace_back(s);
    _putenv(s->c_str());
}

void reconfigureGLDrivers()
{
	// reconfigure GL drivers to store 
	const fs::path nvCacheDir = ActiveSettings::GetPath("shaderCache/driver/nvidia/");

	std::error_code err;
	fs::create_directories(nvCacheDir, err);

	std::string nvCacheDirEnvOption("__GL_SHADER_DISK_CACHE_PATH=");
	nvCacheDirEnvOption.append(_utf8Wrapper(nvCacheDir));

#if BOOST_OS_WINDOWS > 0
	std::wstring tmpW = boost::nowide::widen(nvCacheDirEnvOption);
	_wputenv(tmpW.c_str());
#else
    _putenvSafe(nvCacheDirEnvOption.c_str());
#endif
    _putenvSafe("__GL_SHADER_DISK_CACHE_SKIP_CLEANUP=1");

}

void reconfigureVkDrivers()
{
    _putenvSafe("DISABLE_LAYER_AMD_SWITCHABLE_GRAPHICS_1=1");
    _putenvSafe("DISABLE_VK_LAYER_VALVE_steam_fossilize_1=1");
}

void mainEmulatorCommonInit()
{
	reconfigureGLDrivers();
	reconfigureVkDrivers();
	// crypto init
	AES128_init();
	// init PPC timer (call this as early as possible because it measures frequency of RDTSC using an asynchronous thread over 3 seconds)
	PPCTimer_init();
	// check available CPU extensions
	int cpuInfo[4];
	__cpuid(cpuInfo, 0x1);
	_cpuExtension_SSSE3 = ((cpuInfo[2] >> 9) & 1) != 0;
	_cpuExtension_SSE4_1 = ((cpuInfo[2] >> 19) & 1) != 0;

	__cpuidex(cpuInfo, 0x7, 0);
	_cpuExtension_AVX2 = ((cpuInfo[1] >> 5) & 1) != 0;

#if BOOST_OS_WINDOWS > 0
	executablePath.resize(4096);
	int i = GetModuleFileName(NULL, executablePath.data(), executablePath.size());
	if(i >= 0)
		executablePath.resize(i);
	else
		executablePath.clear();
	SetCurrentDirectory(executablePath.c_str());

	// set high priority
	SetPriorityClass(GetCurrentProcess(), ABOVE_NORMAL_PRIORITY_CLASS);
#endif
    ExceptionHandler_init();
	// read config
	g_config.Load();
	// symbol storage
	rplSymbolStorage_init();
	// static initialization
	IAudioAPI::InitializeStatic();
	// load graphic packs (must happen before config is loaded)
	graphicPack_loadAll();
	// initialize file system
	fsc_init();
}

void mainEmulatorLLE();
void ppcAsmTest();
void gx2CopySurfaceTest();
void ExpressionParser_test();
void FSTVolumeTest();

void unitTests()
{
	ExpressionParser_test();
	gx2CopySurfaceTest();
	ppcAsmTest();
	FSTVolumeTest();
}

int mainEmulatorHLE()
{
	if (!TestWriteAccess(ActiveSettings::GetPath()))
		wxMessageBox("Cemu doesn't have write access to it's own directory.\nPlease move it to a different location or run Cemu as administrator!", "Warning", wxOK|wxICON_ERROR); // todo - different error messages per OS
	LatteOverlay_init();
	// run a couple of tests if in non-release mode
#ifndef PUBLIC_RELEASE
	unitTests();
#endif
	// init common
	mainEmulatorCommonInit();
	// reserve memory (no allocations yet)
	memory_init();
	// init ppc core
	PPCCore_init();
	// log Cemu startup info
	infoLog_cemuStartup();
	// init RPL loader
	RPLLoader_InitState();
	// init IOSU components
	iosuCrypto_init();
	// init Cafe system (todo - the stuff above should be part of this too)
	CafeSystem::Initialize();
	// init title list
	CafeTitleList::Initialize(ActiveSettings::GetPath("title_list_cache.xml"));
	for (auto& it : GetConfig().game_paths)
		CafeTitleList::AddScanPath(it);
	fs::path mlcPath = ActiveSettings::GetMlcPath();
	if (!mlcPath.empty())
		CafeTitleList::SetMLCPath(mlcPath);
	CafeTitleList::Refresh();
	// init save list
	CafeSaveList::Initialize();
	if (!mlcPath.empty())
	{
		CafeSaveList::SetMLCPath(mlcPath);
		CafeSaveList::Refresh();
	}
	// Create UI
	gui_create();
	return 0;
}

bool isConsoleConnected = false;
void requireConsole()
{
	#if BOOST_OS_WINDOWS > 0
	if (isConsoleConnected)
		return;

	if (AttachConsole(ATTACH_PARENT_PROCESS) != FALSE)
	{
		freopen("CONIN$", "r", stdin);
		freopen("CONOUT$", "w", stdout);
		freopen("CONOUT$", "w", stderr);
		isConsoleConnected = true;
	}
	#endif
}

void HandlePostUpdate()
{
	// finalize update process
	// delete update cemu.exe.backup if available
	const auto filename = ActiveSettings::GetFullPath().replace_extension("exe.backup");
	if (fs::exists(filename))
	{
#if BOOST_OS_WINDOWS > 0
		HANDLE lock;
		do
		{
			lock = CreateMutex(nullptr, TRUE, L"Global\\cemu_update_lock");
			std::this_thread::sleep_for(std::chrono::milliseconds(1));
		} while (lock == nullptr);
		const DWORD wait_result = WaitForSingleObject(lock, 2000);
		CloseHandle(lock);

		if (wait_result == WAIT_OBJECT_0)
		{
			std::this_thread::sleep_for(std::chrono::milliseconds(500));
			std::error_code ec;
			fs::remove(filename, ec);
		}
#else
		while( fs::exists(filename) )
		{
			std::error_code ec;
			fs::remove(filename, ec);		
			std::this_thread::sleep_for(std::chrono::milliseconds(1000));
		}
#endif
	}
}

void ToolShaderCacheMerger();

#if BOOST_OS_WINDOWS > 0

#ifndef PUBLIC_RELEASE
#include <crtdbg.h>
int wmain(int argc, wchar_t* argv[])
{
	SDL_SetMainReady();
	_CrtSetDbgFlag(_CRTDBG_CHECK_DEFAULT_DF);
	//ToolShaderCacheMerger();

	if (!LaunchSettings::HandleCommandline(argc, argv))
		return 0;	

	ActiveSettings::LoadOnce();
	
	HandlePostUpdate();
	return mainEmulatorHLE();
}
#else
int wWinMain( _In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPTSTR lpCmdLine, _In_ int nShowCmd)
{
	SDL_SetMainReady();

	if (!LaunchSettings::HandleCommandline(lpCmdLine))
		return 0;

	ActiveSettings::LoadOnce();

	HandlePostUpdate();
	return mainEmulatorHLE();
}

#endif

#else
int main(int argc, char *argv[])
{
    XInitThreads();
    if (!LaunchSettings::HandleCommandline(argc, argv))
		return 0;

	ActiveSettings::LoadOnce();

	HandlePostUpdate();
	return mainEmulatorHLE();
}
#endif

/* Cemuhook legacy API */

#pragma optimize("",off)

__declspec(dllexport) void gameMeta_loadForCurrent()
{
	int placeholderA = 0x11223344;
	int placeholderB = 0x55667788;
}

#pragma optimize("",on)

__declspec(dllexport) uint64 gameMeta_getTitleId()
{
	return CafeSystem::GetForegroundTitleId();
}

/* Cemuhook loading */
#if BOOST_OS_WINDOWS > 0
#pragma init_seg(".CRT$XCT")

HANDLE dbgLib;

int dbghelp_init(void)
{
	// load dbghelp.dll from the system folder instead of loading outdated cemuhook via dbghelp.dll
	WCHAR dllPath[MAX_PATH];
	GetSystemDirectoryW(dllPath, MAX_PATH);
	wcscat_s(dllPath, sizeof(dllPath) / sizeof(WCHAR), TEXT("\\dbghelp.dll"));

	dbgLib = LoadLibraryW(dllPath);
	if (dbgLib == NULL)
		return -1;

	return 0;
}

HMODULE _earlyInitFunction()
{
	dbghelp_init();
	return LoadLibraryA("cemuhook2.dll");
}

HMODULE _cemuHookDllHandle = _earlyInitFunction();
#endif
