#include "crash.h"
#include "git.h"
#include "vm_local.h"
#include <sys/stat.h>


typedef struct {
	char			gitHeadHash[24]; // SHA-1 -> 160 bits -> 20 hex chars
	vm_t*			vm;
	unsigned int	crc32;
} vmCrash_t;

typedef struct {
	vmCrash_t	vm[VM_COUNT];
	char		modName[64];
	char		modVersion[64];
} crash_t;


static crash_t crash;


#if defined(_WIN32)
#	define NEWLINE	"\n"
#else
#	define NEWLINE	"\r\n"
#endif


static qbool IsVMIndexValid(vmIndex_t vmIndex)
{
	return vmIndex >= 0 && vmIndex < VM_COUNT;
}

static const char* GetVMName(vmIndex_t vmIndex)
{
	switch (vmIndex) {
		case VM_CGAME: return "cgame";
		case VM_GAME: return "game";
		case VM_UI: return "ui";
		default: return "unknown";
	}
}

void Crash_SaveQVMPointer(vmIndex_t vmIndex, vm_t* vm)
{
	if (IsVMIndexValid(vmIndex))
		crash.vm[vmIndex].vm = vm;
}

void Crash_SaveQVMChecksum(vmIndex_t vmIndex, unsigned int crc32)
{
	if (IsVMIndexValid(vmIndex))
		crash.vm[vmIndex].crc32 = crc32;
}

void Crash_SaveQVMGitString(const char* varName, const char* varValue)
{
	if (strstr(varName, "gitHeadHash") == NULL)
		return;

	vmIndex_t index;
	if (strstr(varName, "cg_") == varName)
		index = VM_CGAME;
	else if (strstr(varName, "g_") == varName)
		index = VM_GAME;
	else if (strstr(varName, "ui_") == varName)
		index = VM_UI;
	else
		return;

	Q_strncpyz(crash.vm[index].gitHeadHash, varValue, sizeof(crash.vm[index].gitHeadHash));
}

void Crash_SaveModName(const char* modName)
{
	if (modName && *modName != '\0')
		Q_strncpyz(crash.modName, modName, sizeof(crash.modName));
}

void Crash_SaveModVersion(const char* modVersion)
{
	if (modVersion && *modVersion != '\0')
		Q_strncpyz(crash.modVersion, modVersion, sizeof(crash.modVersion));
}

static void PrintQVMInfo(vmIndex_t vmIndex)
{
	static char callStack[MAX_VM_CALL_STACK_DEPTH * 12];

	vmCrash_t* vm = &crash.vm[vmIndex];
	if (vm->crc32 == 0) {
		return;
	}

	JSONW_BeginObject();

	JSONW_IntegerValue("index", vmIndex);
	JSONW_StringValue("name", GetVMName(vmIndex));
	JSONW_BooleanValue("loaded", vm->vm != NULL);
	if (vm->crc32)
		JSONW_HexValue("crc32", vm->crc32);
	JSONW_StringValue("git_head_hash", vm->gitHeadHash);

	if (vm->vm != NULL) {
		vm_t* const vmp = vm->vm;
		JSONW_IntegerValue("call_stack_depth_current", vmp->callStackDepth);
		JSONW_IntegerValue("call_stack_depth_previous", vmp->lastCallStackDepth);

		int d = vmp->callStackDepth;
		qbool current = qtrue;
		if (d <= 0 || d > MAX_VM_CALL_STACK_DEPTH) {
			d = vmp->lastCallStackDepth;
			current = qfalse;
		}

		if (d <= 0 || d > MAX_VM_CALL_STACK_DEPTH)
			d = 0;

		if (d > 0) {
			JSONW_BooleanValue("call_stack_current", current);
			JSONW_BooleanValue("call_stack_limit_reached", d == MAX_VM_CALL_STACK_DEPTH);

			callStack[0] = '\0';
			for (int i = 0; i < d; i++) {
				Q_strcat(callStack, sizeof(callStack), Q_itohex(vmp->callStack[i], qtrue, qtrue));
				if (i - 1 < d) 
					Q_strcat(callStack, sizeof(callStack), " ");
			}
			JSONW_StringValue("call_stack", callStack);
		}
	}

	JSONW_EndObject();
}

static qbool IsAnyVMLoaded()
{
	for (int i = 0; i < VM_COUNT; i++) {
		if (crash.vm[i].crc32 != 0)
			return qtrue;
	}

	return qfalse;
}

static unsigned int CRC32_HashFile(const char* filePath)
{
	enum { BUFFER_SIZE = 16 << 10 }; // 16 KB
	static byte buffer[BUFFER_SIZE];

	struct stat st;
	if (stat(filePath, &st) != 0 || st.st_size == 0)
		return 0;

	FILE* const file = fopen(filePath, "rb");
	if (file == NULL)
		return 0;

	const unsigned int fileSize = (unsigned int)st.st_size;
	const unsigned int fullBlocks = fileSize / (unsigned int)BUFFER_SIZE;
	const unsigned int lastBlockSize = fileSize - fullBlocks * (unsigned int)BUFFER_SIZE;

	unsigned int crc32 = 0;
	CRC32_Begin(&crc32);

	for(unsigned int i = 0; i < fullBlocks; ++i) {
		if (fread(buffer, BUFFER_SIZE, 1, file) != 1) {
			fclose(file);
			return 0;
		}
		CRC32_ProcessBlock(&crc32, buffer, BUFFER_SIZE);
	}

	if(lastBlockSize > 0) {
		if (fread(buffer, lastBlockSize, 1, file) != 1) {
			fclose(file);
			return 0;
		}
		CRC32_ProcessBlock(&crc32, buffer, lastBlockSize);
	}

	CRC32_End(&crc32);

	fclose(file);

	return crc32;
}

void Crash_PrintToFile(const char* engineFilePath)
{
	JSONW_StringValue("engine_version", Q3_VERSION);
	JSONW_StringValue("engine_build_date", __DATE__);
	JSONW_StringValue("engine_arch", ARCH_STRING);
#ifdef DEDICATED
	JSONW_BooleanValue("engine_ded_server", qtrue);
#else
	JSONW_BooleanValue("engine_ded_server", qfalse);
#endif
#ifdef DEBUG
	JSONW_BooleanValue("engine_debug", qtrue);
#else
	JSONW_BooleanValue("engine_debug", qfalse);
#endif
#ifdef CNQ3_DEV
	JSONW_BooleanValue("engine_dev_build", qtrue);
#else
	JSONW_BooleanValue("engine_dev_build", qfalse);
#endif
	JSONW_StringValue("engine_git_branch", GIT_BRANCH);
	JSONW_StringValue("engine_git_commit", GIT_COMMIT);
	const unsigned int crc32 = CRC32_HashFile(engineFilePath);
	if (crc32)
		JSONW_HexValue("engine_crc32", crc32);
	JSONW_StringValue("mod_name", crash.modName);
	JSONW_StringValue("mod_version", crash.modVersion);

	if (IsAnyVMLoaded()) {
		JSONW_BeginNamedArray("vms");
		for (int i = 0; i < VM_COUNT; i++) {
			PrintQVMInfo((vmIndex_t)i);
		}
		JSONW_EndArray();
	}
}


#if defined(__linux__)

#include <unistd.h>

static void Print(int fd, const char* msg)
{
	write(fd, msg, strlen(msg));
}

static void PrintVMStackTrace(int fd, vmIndex_t vmIndex)
{
	Print(fd, GetVMName(vmIndex));
	Print(fd, ": ");

	vmCrash_t* const vmc = &crash.vm[vmIndex];
	vm_t* const vm = vmc->vm;
	if (vm == NULL) {
		Print(fd, "not loaded\r\n");
		return;
	}

	Print(fd, "crc32 ");
	Print(fd, Q_itohex(vmc->crc32, qtrue, qtrue));
	Print(fd, " - ");

	int d = vm->callStackDepth;
	qbool current = qtrue;
	if (d <= 0 || d > MAX_VM_CALL_STACK_DEPTH) {
		d = vm->lastCallStackDepth;
		current = qfalse;
	}

	if (d <= 0 || d > MAX_VM_CALL_STACK_DEPTH)
		d = 0;

	Print(fd, current ? "current " : "old ");
	if (d > 0) {
		for (int i = 0; i < d; i++) {
			Print(fd, Q_itohex(vm->callStack[i], qtrue, qtrue));
			if (i - 1 < d) 
				Print(fd, " ");
		}		
	}
	Print(fd, "\r\n");
}

void Crash_PrintVMStackTracesASS(int fd)
{
	if (crash.modName[0] != '\0') {
		Print(fd, "mod: ");
		Print(fd, crash.modName);
		Print(fd, " ");
		Print(fd, crash.modVersion);
		Print(fd, "\r\n");	
	}

	for (int i = 0; i < VM_COUNT; i++) {
		PrintVMStackTrace(fd, (vmIndex_t)i);
	}
}

#endif
