#include "crashhandler.h"
#include "dedicated/dedicated.h"
#include "config/profile.h"
#include "util/version.h"
#include "mods/modmanager.h"

#include <minidumpapiset.h>

HANDLE hExceptionFilter;

std::shared_ptr<ExceptionLog> storedException {};

#define RUNTIME_EXCEPTION 3765269347
// clang format did this :/
std::map<int, std::string> ExceptionNames = {
	{EXCEPTION_ACCESS_VIOLATION, "Access Violation"},			{EXCEPTION_IN_PAGE_ERROR, "Access Violation"},
	{EXCEPTION_ARRAY_BOUNDS_EXCEEDED, "Array bounds exceeded"}, {EXCEPTION_DATATYPE_MISALIGNMENT, "Datatype misalignment"},
	{EXCEPTION_FLT_DENORMAL_OPERAND, "Denormal operand"},		{EXCEPTION_FLT_DIVIDE_BY_ZERO, "Divide by zero (float)"},
	{EXCEPTION_FLT_INEXACT_RESULT, "Inexact float result"},		{EXCEPTION_FLT_INVALID_OPERATION, "Invalid operation"},
	{EXCEPTION_FLT_OVERFLOW, "Numeric overflow (float)"},		{EXCEPTION_FLT_STACK_CHECK, "Stack check"},
	{EXCEPTION_FLT_UNDERFLOW, "Numeric underflow (float)"},		{EXCEPTION_ILLEGAL_INSTRUCTION, "Illegal instruction"},
	{EXCEPTION_INT_DIVIDE_BY_ZERO, "Divide by zero (int)"},		{EXCEPTION_INT_OVERFLOW, "Numeric overfloat (int)"},
	{EXCEPTION_INVALID_DISPOSITION, "Invalid disposition"},		{EXCEPTION_NONCONTINUABLE_EXCEPTION, "Non-continuable exception"},
	{EXCEPTION_PRIV_INSTRUCTION, "Priviledged instruction"},	{EXCEPTION_STACK_OVERFLOW, "Stack overflow"},
	{RUNTIME_EXCEPTION, "Uncaught runtime exception:"},
};

void PrintExceptionLog(ExceptionLog& exc)
{
	// General crash message
	spdlog::error("Northstar version: {}", version);
	spdlog::error("Northstar has crashed! a minidump has been written and exception info is available below:");
	if (g_pModManager)
	{
		spdlog::error("Loaded mods: ");
		for (const auto& mod : g_pModManager->m_LoadedMods)
		{
			if (mod.m_bEnabled)
			{
				spdlog::error("{} {}", mod.Name, mod.Version);
			}
		}
	}
	spdlog::error(exc.cause);
	// If this was a runtime error, print the message
	if (exc.runtimeInfo.length() != 0)
		spdlog::error("\"{}\"", exc.runtimeInfo);
	spdlog::error("At: {} + {}", exc.trace[0].name, exc.trace[0].relativeAddress);
	spdlog::error("");
	spdlog::error("Stack trace:");

	// Generate format string for stack trace
	std::stringstream formatString;
	formatString << "    {:<" << exc.longestModuleNameLength + 2 << "} {:<" << exc.longestRelativeAddressLength << "} {}";
	std::string guide = fmt::format(formatString.str(), "Module Name", "Offset", "Full Address");
	std::string line(guide.length() + 2, '-');
	spdlog::error(guide);
	spdlog::error(line);

	for (const auto& module : exc.trace)
		spdlog::error(formatString.str(), module.name, module.relativeAddress, module.address);

	// Print dump of most CPU registers
	spdlog::error("");
	for (const auto& reg : exc.registerDump)
		spdlog::error(reg);

	if (!IsDedicatedServer())
		MessageBoxA(
			0,
			"Northstar has crashed! Crash info can be found in R2Northstar/logs",
			"Northstar has crashed!",
			MB_ICONERROR | MB_OK | MB_SYSTEMMODAL);

	NS::log::FlushLoggers();
}

std::string GetExceptionName(ExceptionLog& exc)
{
	const DWORD exceptionCode = exc.exceptionRecord.ExceptionCode;
	auto name = ExceptionNames[exceptionCode];
	if (exceptionCode == EXCEPTION_ACCESS_VIOLATION || exceptionCode == EXCEPTION_IN_PAGE_ERROR)
	{
		std::stringstream returnString;
		returnString << name << ": ";

		auto exceptionInfo0 = exc.exceptionRecord.ExceptionInformation[0];
		auto exceptionInfo1 = exc.exceptionRecord.ExceptionInformation[1];

		if (!exceptionInfo0)
			returnString << "Attempted to read from: 0x" << (void*)exceptionInfo1;
		else if (exceptionInfo0 == 1)
			returnString << "Attempted to write to: 0x" << (void*)exceptionInfo1;
		else if (exceptionInfo0 == 8)
			returnString << "Data Execution Prevention (DEP) at: 0x" << (void*)std::hex << exceptionInfo1;
		else
			returnString << "Unknown access violation at: 0x" << (void*)exceptionInfo1;
		return returnString.str();
	}
	return name;
}

// Custom formatter for the Xmm registers
template <> struct fmt::formatter<M128A> : fmt::formatter<string_view>
{
	template <typename FormatContext> auto format(const M128A& obj, FormatContext& ctx)
	{
		// Masking the top and bottom half of the long long
		int v1 = obj.Low & INT_MAX;
		int v2 = obj.Low >> 32;
		int v3 = obj.High & INT_MAX;
		int v4 = obj.High >> 32;
		return fmt::format_to(
			ctx.out(),
			"[ {:G}, {:G}, {:G}, {:G}], [ 0x{:x}, 0x{:x}, 0x{:x}, 0x{:x} ]",
			*reinterpret_cast<float*>(&v1),
			*reinterpret_cast<float*>(&v2),
			*reinterpret_cast<float*>(&v3),
			*reinterpret_cast<float*>(&v4),
			v1,
			v2,
			v3,
			v4);
	}
};

void GenerateTrace(ExceptionLog& exc, bool skipErrorHandlingFrames = true, int numSkipFrames = 0)
{

	MODULEINFO crashedModuleInfo;
	GetModuleInformation(GetCurrentProcess(), exc.crashedModule, &crashedModuleInfo, sizeof(crashedModuleInfo));

	char crashedModuleFullName[MAX_PATH];
	GetModuleFileNameExA(GetCurrentProcess(), exc.crashedModule, crashedModuleFullName, MAX_PATH);
	char* crashedModuleName = strrchr(crashedModuleFullName, '\\') + 1;

	DWORD64 crashedModuleOffset = ((DWORD64)exc.exceptionRecord.ExceptionAddress) - ((DWORD64)crashedModuleInfo.lpBaseOfDll);

	PVOID framesToCapture[62];
	int frames = RtlCaptureStackBackTrace(0, 62, framesToCapture, NULL);
	bool haveSkippedErrorHandlingFrames = false;

	for (int i = 0; i < frames; i++)
	{

		HMODULE backtraceModuleHandle;
		GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(framesToCapture[i]), &backtraceModuleHandle);

		char backtraceModuleFullName[MAX_PATH];
		GetModuleFileNameExA(GetCurrentProcess(), backtraceModuleHandle, backtraceModuleFullName, MAX_PATH);
		char* backtraceModuleName = strrchr(backtraceModuleFullName, '\\') + 1;

		if (!haveSkippedErrorHandlingFrames)
		{
			if (!strncmp(backtraceModuleFullName, crashedModuleFullName, MAX_PATH) &&
				!strncmp(backtraceModuleName, crashedModuleName, MAX_PATH))
			{
				haveSkippedErrorHandlingFrames = true;
			}
			else
			{
				continue;
			}
		}

		if (numSkipFrames > 0)
		{
			numSkipFrames--;
			continue;
		}

		void* actualAddress = (void*)framesToCapture[i];
		void* relativeAddress = (void*)(uintptr_t(actualAddress) - uintptr_t(backtraceModuleHandle));
		std::string s_moduleName {backtraceModuleName};
		std::string s_relativeAddress {fmt::format("{}", relativeAddress)};
		// These are used for formatting later on
		if (s_moduleName.length() > exc.longestModuleNameLength)
		{
			exc.longestModuleNameLength = s_moduleName.length();
		}
		if (s_relativeAddress.length() > exc.longestRelativeAddressLength)
		{
			exc.longestRelativeAddressLength = s_relativeAddress.length();
		}

		exc.trace.push_back(BacktraceModule {s_moduleName, s_relativeAddress, fmt::format("{}", actualAddress)});
	}

	CONTEXT* exceptionContext = &exc.contextRecord;

	exc.registerDump.push_back(fmt::format("Flags: 0b{0:b}", exceptionContext->ContextFlags));
	exc.registerDump.push_back(fmt::format("RIP: 0x{0:x}", exceptionContext->Rip));
	exc.registerDump.push_back(fmt::format("CS : 0x{0:x}", exceptionContext->SegCs));
	exc.registerDump.push_back(fmt::format("DS : 0x{0:x}", exceptionContext->SegDs));
	exc.registerDump.push_back(fmt::format("ES : 0x{0:x}", exceptionContext->SegEs));
	exc.registerDump.push_back(fmt::format("SS : 0x{0:x}", exceptionContext->SegSs));
	exc.registerDump.push_back(fmt::format("FS : 0x{0:x}", exceptionContext->SegFs));
	exc.registerDump.push_back(fmt::format("GS : 0x{0:x}", exceptionContext->SegGs));

	exc.registerDump.push_back(fmt::format("RAX: 0x{0:x}", exceptionContext->Rax));
	exc.registerDump.push_back(fmt::format("RBX: 0x{0:x}", exceptionContext->Rbx));
	exc.registerDump.push_back(fmt::format("RCX: 0x{0:x}", exceptionContext->Rcx));
	exc.registerDump.push_back(fmt::format("RDX: 0x{0:x}", exceptionContext->Rdx));
	exc.registerDump.push_back(fmt::format("RSI: 0x{0:x}", exceptionContext->Rsi));
	exc.registerDump.push_back(fmt::format("RDI: 0x{0:x}", exceptionContext->Rdi));
	exc.registerDump.push_back(fmt::format("RBP: 0x{0:x}", exceptionContext->Rbp));
	exc.registerDump.push_back(fmt::format("RSP: 0x{0:x}", exceptionContext->Rsp));
	exc.registerDump.push_back(fmt::format("R8 : 0x{0:x}", exceptionContext->R8));
	exc.registerDump.push_back(fmt::format("R9 : 0x{0:x}", exceptionContext->R9));
	exc.registerDump.push_back(fmt::format("R10: 0x{0:x}", exceptionContext->R10));
	exc.registerDump.push_back(fmt::format("R11: 0x{0:x}", exceptionContext->R11));
	exc.registerDump.push_back(fmt::format("R12: 0x{0:x}", exceptionContext->R12));
	exc.registerDump.push_back(fmt::format("R13: 0x{0:x}", exceptionContext->R13));
	exc.registerDump.push_back(fmt::format("R14: 0x{0:x}", exceptionContext->R14));
	exc.registerDump.push_back(fmt::format("R15: 0x{0:x}", exceptionContext->R15));

	exc.registerDump.push_back(fmt::format("Xmm0 : {}", exceptionContext->Xmm0));
	exc.registerDump.push_back(fmt::format("Xmm1 : {}", exceptionContext->Xmm1));
	exc.registerDump.push_back(fmt::format("Xmm2 : {}", exceptionContext->Xmm2));
	exc.registerDump.push_back(fmt::format("Xmm3 : {}", exceptionContext->Xmm3));
	exc.registerDump.push_back(fmt::format("Xmm4 : {}", exceptionContext->Xmm4));
	exc.registerDump.push_back(fmt::format("Xmm5 : {}", exceptionContext->Xmm5));
	exc.registerDump.push_back(fmt::format("Xmm6 : {}", exceptionContext->Xmm6));
	exc.registerDump.push_back(fmt::format("Xmm7 : {}", exceptionContext->Xmm7));
	exc.registerDump.push_back(fmt::format("Xmm8 : {}", exceptionContext->Xmm8));
	exc.registerDump.push_back(fmt::format("Xmm9 : {}", exceptionContext->Xmm9));
	exc.registerDump.push_back(fmt::format("Xmm10: {}", exceptionContext->Xmm10));
	exc.registerDump.push_back(fmt::format("Xmm11: {}", exceptionContext->Xmm11));
	exc.registerDump.push_back(fmt::format("Xmm12: {}", exceptionContext->Xmm12));
	exc.registerDump.push_back(fmt::format("Xmm13: {}", exceptionContext->Xmm13));
	exc.registerDump.push_back(fmt::format("Xmm14: {}", exceptionContext->Xmm14));
	exc.registerDump.push_back(fmt::format("Xmm15: {}", exceptionContext->Xmm15));
}

void CreateMiniDump(EXCEPTION_POINTERS* exceptionInfo)
{
	time_t time = std::time(nullptr);
	tm currentTime = *std::localtime(&time);
	std::stringstream stream;
	stream << std::put_time(&currentTime, (GetNorthstarPrefix() + "/logs/nsdump%Y-%m-%d %H-%M-%S.dmp").c_str());

	auto hMinidumpFile = CreateFileA(stream.str().c_str(), GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
	if (hMinidumpFile)
	{
		MINIDUMP_EXCEPTION_INFORMATION dumpExceptionInfo;
		dumpExceptionInfo.ThreadId = GetCurrentThreadId();
		dumpExceptionInfo.ExceptionPointers = exceptionInfo;
		dumpExceptionInfo.ClientPointers = false;

		MiniDumpWriteDump(
			GetCurrentProcess(),
			GetCurrentProcessId(),
			hMinidumpFile,
			MINIDUMP_TYPE(MiniDumpWithIndirectlyReferencedMemory | MiniDumpScanMemory),
			&dumpExceptionInfo,
			nullptr,
			nullptr);
		CloseHandle(hMinidumpFile);
	}
	else
		spdlog::error("Failed to write minidump file {}!", stream.str());
}

long GenerateExceptionLog(EXCEPTION_POINTERS* exceptionInfo)
{
	storedException->exceptionRecord = *exceptionInfo->ExceptionRecord;
	storedException->contextRecord = *exceptionInfo->ContextRecord;
	const DWORD exceptionCode = exceptionInfo->ExceptionRecord->ExceptionCode;

	void* exceptionAddress = exceptionInfo->ExceptionRecord->ExceptionAddress;

	storedException->cause = GetExceptionName(*storedException);

	HMODULE crashedModuleHandle;
	GetModuleHandleExA(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, static_cast<LPCSTR>(exceptionAddress), &crashedModuleHandle);

	storedException->crashedModule = crashedModuleHandle;

	// When encountering a runtime exception, we store the exception to be displayed later
	// We then have to return EXCEPTION_CONTINUE_SEARCH so that our runtime handler may be called
	// This might possibly cause some issues if client and server are crashing at the same time, but honestly i don't care
	if (exceptionCode == RUNTIME_EXCEPTION)
	{
		GenerateTrace(*storedException, false, 2);
		storedException = storedException;
		return EXCEPTION_CONTINUE_SEARCH;
	}

	GenerateTrace(*storedException, true, 0);
	CreateMiniDump(exceptionInfo);
	PrintExceptionLog(*storedException);
	return EXCEPTION_EXECUTE_HANDLER;
}

long __stdcall ExceptionFilter(EXCEPTION_POINTERS* exceptionInfo)
{
	if (!IsDebuggerPresent())
	{
		// Check if we are capable of handling this type of exception
		if (ExceptionNames.find(exceptionInfo->ExceptionRecord->ExceptionCode) == ExceptionNames.end())
			return EXCEPTION_CONTINUE_SEARCH;
		if (exceptionInfo->ExceptionRecord->ExceptionCode == RUNTIME_EXCEPTION)
		{
			storedException = std::make_shared<ExceptionLog>();
			storedException->exceptionRecord = *exceptionInfo->ExceptionRecord;
			storedException->contextRecord = *exceptionInfo->ContextRecord;
		}
		else
		{
			CreateMiniDump(exceptionInfo);
			return GenerateExceptionLog(exceptionInfo);
		}
	}

	return EXCEPTION_EXECUTE_HANDLER;
}

void RuntimeExceptionHandler()
{
	auto ptr = std::current_exception();
	if (ptr)
	{
		try
		{
			// This is to generate an actual std::exception object that we can then inspect
			std::rethrow_exception(ptr);
		}
		catch (std::exception& e)
		{
			storedException->runtimeInfo = e.what();
		}
		catch (...)
		{
			storedException->runtimeInfo = "Unknown runtime exception type";
		}
		EXCEPTION_POINTERS test {};
		test.ContextRecord = &storedException->contextRecord;
		test.ExceptionRecord = &storedException->exceptionRecord;
		CreateMiniDump(&test);
		GenerateExceptionLog(&test);
		PrintExceptionLog(*storedException);
		exit(-1);
	}
	else
	{
		spdlog::error(
			"std::current_exception() returned nullptr while being handled by RuntimeExceptionHandler. This should never happen!");
		std::abort();
	}
}

BOOL WINAPI ConsoleHandlerRoutine(DWORD eventCode)
{
	switch (eventCode)
	{
	case CTRL_CLOSE_EVENT:
		// User closed console, shut everything down
		spdlog::info("Exiting due to console close...");
		RemoveCrashHandler();
		exit(EXIT_SUCCESS);
		return FALSE;
	}

	return TRUE;
}

void InitialiseCrashHandler()
{
	hExceptionFilter = AddVectoredExceptionHandler(TRUE, ExceptionFilter);
	SetConsoleCtrlHandler(ConsoleHandlerRoutine, true);
	std::set_terminate(RuntimeExceptionHandler);
}

void RemoveCrashHandler()
{
	RemoveVectoredExceptionHandler(hExceptionFilter);
}
