#include "jumbot.h"

static float AngleNormalize360(float angle) {
	return (float) ((360.0 / 65536) * ((int) (angle * (65536 / 360.0)) & 65535));
}

static float AngleNormalize180(float angle) {

	angle = AngleNormalize360(angle);

	if (angle > 180.0) {
		angle -= 360;
	}

	return angle;

}

static float AngleDelta(float angle1, float angle2) {
	return AngleNormalize180(angle1 - angle2);
}

static inline float rad2deg(double a) {
	return (float) ((a * 180.0f) / M_PI);
}

/**
 * Adjusts view angle.
 */
static void jumbot_angle() {

	float accelCoef = *jumbot.groundEntityNum != ENTITYNUM_NONE ? 10.0f : 1.0f;

	usercmd_t cmd;
	int cmd_num = jumbot.syscall(CG_GETCURRENTCMDNUMBER);
	jumbot.syscall(CG_GETUSERCMD, cmd_num, &cmd);

	float velSize = (float) sqrt(jumbot.velocity->x * jumbot.velocity->x + jumbot.velocity->y * jumbot.velocity->y);

	if ((MIN_SPEED - MIN_SPEED / 125.0f * accelCoef) / velSize * 1.1 > 1) {
		return;
	}

	float perAngle   = AngleNormalize180(rad2deg(acos((MIN_SPEED - MIN_SPEED / 125.0f * accelCoef) / velSize * 1.1)));
	float velAngle   = rad2deg(atan2(jumbot.velocity->y, jumbot.velocity->x));
	float accelAngle = rad2deg(atan2(-cmd.rightmove, cmd.forwardmove));
	float viewXAccel = jumbot.view->x + accelAngle;

	if (cmd.forwardmove == 0) {

		// Forward right half-beat.
		if (cmd.rightmove > 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(viewXAccel, velAngle - perAngle) - MOUSE_SAFE_PAD);
		}

		// Forward left half-beat.
		else if (cmd.rightmove < 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(viewXAccel, velAngle + perAngle) + MOUSE_SAFE_PAD);
		}

	} else if (cmd.forwardmove > 0) {

		// Right strafe.
		if (cmd.rightmove > 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(viewXAccel, velAngle - perAngle) - MOUSE_SAFE_PAD);
		}

		// Left strafe.
		else if (cmd.rightmove < 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(viewXAccel, velAngle + perAngle) + MOUSE_SAFE_PAD);
		}

	} else if (cmd.forwardmove < 0) {

		// Backward right half-beat.
		if (cmd.rightmove > 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(viewXAccel, velAngle + perAngle) + MOUSE_SAFE_PAD);
		}

		// Backward left half-beat.
		else if (cmd.rightmove < 0) {
			*jumbot.mouseX = (float) (*jumbot.mouseX - AngleDelta(jumbot.view->x + accelAngle, velAngle - perAngle) - MOUSE_SAFE_PAD);
		}

	}

}

/**
 * Automatically jump when touching ground.
 */
static void jumbot_autojump() {

	if (*jumbot.groundEntityNum != ENTITYNUM_NONE) {
		*(int *) ADDR_KEY_UP = TRUE;
	} else {
		*(int *) ADDR_KEY_UP = FALSE;
	}

}

/**
 * Hooked vmMain.
 */
static int jumbot_CG_vmMain(int command, int a0, int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9, int aa, int ab) {

	int ret;

	switch (command) {

		case CG_INIT:

			jumbot.syscall(CG_ADDCOMMAND, CMD_JUMBOT_ON);
			jumbot.syscall(CG_ADDCOMMAND, CMD_JUMBOT_OFF);
			jumbot.syscall(CG_ADDCOMMAND, CMD_AUTOJUMP_ON);
			jumbot.syscall(CG_ADDCOMMAND, CMD_AUTOJUMP_OFF);

			unsigned int offset = (unsigned int) jumbot.cgame + jumbot.mod->offset;

			jumbot.velocity        = (struct vector3 *) (OFFSET_VELOCITY          + offset);
			jumbot.view            = (struct vector2 *) (OFFSET_VIEW              + offset);
			jumbot.groundEntityNum = (int *)            (OFFSET_GROUND_ENTITY_NUM + offset);
			jumbot.mouseX          = (float *) ADDR_MOUSE_X;

			break;

		case CG_CONSOLE_COMMAND:

			const char *cmd = Cmd_Argv(0);

			if (!strcmp(cmd, CMD_JUMBOT_ON)) {
				jumbot.enabled = TRUE;
				return TRUE;
			} else if (!strcmp(cmd, CMD_JUMBOT_OFF)) {
				jumbot.enabled = FALSE;
				return TRUE;
			} else if (!strcmp(cmd, CMD_AUTOJUMP_ON)) {
				jumbot.autojump = TRUE;
				return TRUE;
			} else if (!strcmp(cmd, CMD_AUTOJUMP_OFF)) {
				*(int *) ADDR_KEY_UP = FALSE;
				jumbot.autojump = FALSE;
				return TRUE;
			}

			break;

	}

	// Temporarily change vmMain location and maintain poker face.
	if (jumbot.ETPro) {
		*(void **) ETPRO_AC2_VM_LOCATION = (void *) ETPRO_AC2_VM_ORIG;
		ret = jumbot.cg_vmMain(command, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab);
		*(void **) ETPRO_AC2_VM_LOCATION = jumbot_CG_vmMain;
	} else {
		ret = jumbot.cg_vmMain(command, a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, aa, ab);
	}

	// TODO: Aren't we one frame late here?
	if (command == CG_DRAW_ACTIVE_FRAME) {

		if (jumbot.enabled) {
			jumbot_angle();
		}

		if (jumbot.autojump) {
			jumbot_autojump();
		}

	}

	return ret;

}

/**
 * ETPro checksum circumvention.
 */
static void jumbot_ETPro_AC(void *a, void *b, void *c, int checksum, void *e, char *orig_guid) {
	jumbot.ETPro_AC(a, b, c, ETPRO_AC_CHECKSUM, e, orig_guid);
}

/**
 * Tricks ETPro into thinking that we haven't hooked our vmMain.
 */
static void* jumbot_ETPro_AC2(DWORD a) {
	*(void **) ETPRO_AC2_VM_LOCATION = (void *) ETPRO_AC2_VM_ORIG;
	void *p = jumbot.ETPro_AC2(a);
	*(void **) ETPRO_AC2_VM_LOCATION = jumbot_CG_vmMain;
	*(int *) ETPRO_AC2_SECURITY = 0;
	return p;
}

/**
 * Catches syscall pointer.
 */
static void jumbot_CG_dllEntry(int(*syscallptr)(int arg, ...)) {
	jumbot.syscall = syscallptr;
	jumbot.cg_dllEntry(syscallptr);
}

/**
 * CGame load & ETPro AC detours.
 */
static HINSTANCE __stdcall jumbot_LoadLibraryA(LPCSTR lpLibName) {

	HMODULE ret = jumbot.LoadLibraryA(lpLibName);

	if (strstr(lpLibName, "cgame_mp_x86.dll")) {

		jumbot.ETPro = FALSE;
		jumbot.mod   = NULL;
		jumbot.cgame = NULL;

		if (ETPRO_AC_LOCATION && strstr(lpLibName, "etpro\\cgame_mp_x86.dll")) {
			jumbot.ETPro = TRUE;
			jumbot.ETPro_AC = (void (*)(void *, void *, void *, int, void *, char *)) DetourFunction((PBYTE) ETPRO_AC_LOCATION, (PBYTE) jumbot_ETPro_AC);
			jumbot.ETPro_AC2 = (void* (*)(DWORD)) DetourFunction((PBYTE) ETPRO_AC2_LOCATION, (PBYTE) jumbot_ETPro_AC2);
		}

		char string[128];

		for (int i = 0; i < sizeof(mods) / sizeof(mods[0]); i++) {

			memset(string, 0, sizeof(string));
			strcat(string, "\\");
			strcat(string, mods[i].name);
			strcat(string, "\\cgame_mp_x86.dll");

			if (strstr(lpLibName, string)) {
				jumbot.mod = &mods[i];
				jumbot.cgame = ret;
			}

		}

	}

	return ret;

}

/**
 * Hooks vmMain & dllEntry.
 */
static FARPROC __stdcall jumbot_GetProcAddress(HMODULE hModule, LPCSTR lpProcName) {

	FARPROC ret = jumbot.GetProcAddress(hModule, lpProcName);

	if (hModule == jumbot.cgame) {

		if (!strcmp(lpProcName, "vmMain")) {
			jumbot.cg_vmMain = (int(*)(int, ...)) ret;
			ret = (void*) jumbot_CG_vmMain;
		} else if (!strcmp(lpProcName, "dllEntry")) {
			jumbot.cg_dllEntry = (void(*)(int(*)(int, ...))) ret;
			ret = (void*) jumbot_CG_dllEntry;
		}

	}

	return ret;

}

/**
 * DLL entry point.
 */
static BOOL __stdcall DllMain(HMODULE module, DWORD reason, LPVOID reserved) {

	if (reason == DLL_PROCESS_ATTACH) {

		jumbot.LoadLibraryA   = (void *) DetourFunction((void *) LoadLibraryA, (void *) jumbot_LoadLibraryA);
		jumbot.GetProcAddress = (void *) DetourFunction((void *) GetProcAddress, (void *) jumbot_GetProcAddress);

		Cmd_Argv   = (char* (*)(int)) ADDR_COM_ARGV;

	}

	return TRUE;

}