#include <STDInclude.hpp>
#include "GSC/Script.hpp"

namespace Components
{
	Dvar::Var PlayerMovement::BGRocketJump;
	Dvar::Var PlayerMovement::BGRocketJumpScale;
	Dvar::Var PlayerMovement::BGPlayerEjection;
	Dvar::Var PlayerMovement::BGPlayerCollision;
	const Game::dvar_t* PlayerMovement::CGNoclipScaler;
	const Game::dvar_t* PlayerMovement::CGUfoScaler;
	const Game::dvar_t* PlayerMovement::PlayerSpectateSpeedScale;
	const Game::dvar_t* PlayerMovement::BGBounces;
	const Game::dvar_t* PlayerMovement::BGBouncesAllAngles;
	const Game::dvar_t* PlayerMovement::PlayerDuckedSpeedScale;
	const Game::dvar_t* PlayerMovement::PlayerProneSpeedScale;

	__declspec(naked) void PlayerMovement::PM_PlayerDuckedSpeedScaleStub()
	{
		__asm
		{
			push eax
			mov eax, PlayerDuckedSpeedScale
			fld dword ptr [eax + 0x10] // dvar_t.current.value
			pop eax

			// Game's code
			pop ecx
			ret
		}
	}

	__declspec(naked) void PlayerMovement::PM_PlayerProneSpeedScaleStub()
	{
		__asm
		{
			push eax
			mov eax, PlayerProneSpeedScale
			fld dword ptr [eax + 0x10] // dvar_t.current.value
			pop eax

			// Game's code
			pop ecx
			ret
		}
	}

	__declspec(naked) void PlayerMovement::PM_MoveScale_Noclip()
	{
		__asm
		{
			mov eax, CGNoclipScaler
			fld dword ptr [eax + 0x10] // dvar_t.current.value
			fmul dword ptr [esp + 0xC]
			fstp dword ptr [esp + 0xC]

			push 0x56F43A
			ret
		}
	}

	__declspec(naked) void PlayerMovement::PM_MoveScale_Ufo()
	{
		__asm
		{
			mov eax, CGUfoScaler
			fld dword ptr [eax + 0x10] // dvar_t.current.value
			fmul dword ptr [esp + 0xC]
			fstp dword ptr [esp + 0xC]

			push 0x56F44D
			ret
		}
	}

	__declspec(naked) void PlayerMovement::PM_MoveScale_Spectate()
	{
		__asm
		{
			mov eax, PlayerSpectateSpeedScale
			fld dword ptr [eax + 0x10] // dvar_t.current.value
			fmul dword ptr [esp + 0xC]
			fstp dword ptr [esp + 0xC]

			push 0x56F462
			ret
		}
	}

	__declspec(naked) void PlayerMovement::PM_StepSlideMoveStub()
	{
		__asm
		{
			// Check the value of BGBounces
			push ecx
			push eax

			mov eax, BGBounces
			mov ecx, dword ptr [eax + 0x10]
			test ecx, ecx

			pop eax
			pop ecx

			// Do not bounce if BGBounces is 0
			jle noBounce

			// Bounce
			push 0x4B1B34
			ret

		noBounce:
			// Original game code
			cmp dword ptr [esp + 0x24], 0
			push 0x4B1B48
			ret
		}
	}

	// Double bounces
	void PlayerMovement::Jump_ClearState_Hk(Game::playerState_s* ps)
	{
		if (BGBounces->current.integer != DOUBLE)
		{
			Game::Jump_ClearState(ps);
		}
	}

	__declspec(naked) void PlayerMovement::PM_ProjectVelocityStub()
	{
		__asm
		{
			push eax
			mov eax, BGBouncesAllAngles
			cmp byte ptr [eax + 0x10], 1
			pop eax

			je bounce

			fstp ST(0)
			pop esi
			add esp, 0x10
			ret

		bounce:
			push 0x417B6F
			ret
		}
	}

	Game::gentity_s* PlayerMovement::Weapon_RocketLauncher_Fire_Hk(Game::gentity_s* ent, unsigned int weaponIndex,
		float spread, Game::weaponParms* wp, const float* gunVel, Game::lockonFireParms* lockParms, bool magicBullet)
	{
		auto* result = Game::Weapon_RocketLauncher_Fire(ent, weaponIndex, spread, wp, gunVel, lockParms, magicBullet);

		if (ent->client != nullptr && BGRocketJump.get<bool>() &&
			wp->weapDef->inventoryType != Game::WEAPINVENTORY_EXCLUSIVE)
		{
			const auto scale = BGRocketJumpScale.get<float>();
			ent->client->ps.velocity[0] += (0.0f - wp->forward[0]) * scale;
			ent->client->ps.velocity[1] += (0.0f - wp->forward[1]) * scale;
			ent->client->ps.velocity[2] += (0.0f - wp->forward[2]) * scale;
		}

		return result;
	}

	int PlayerMovement::StuckInClient_Hk(Game::gentity_s* self)
	{
		if (BGPlayerEjection.get<bool>())
		{
			return Utils::Hook::Call<int(Game::gentity_s*)>(0x402D30)(self); // StuckInClient
		}

		return 0;
	}

	void PlayerMovement::CM_TransformedCapsuleTrace_Hk(Game::trace_t* results, const float* start, const float* end,
		const Game::Bounds* bounds, const Game::Bounds* capsule, int contents, const float* origin, const float* angles)
	{
		if (BGPlayerCollision.get<bool>())
		{
			Utils::Hook::Call<void(Game::trace_t*, const float*, const float*,
				const Game::Bounds*, const Game::Bounds*, int, const float*, const float*)>
				(0x478300)
				(results, start, end, bounds, capsule, contents, origin, angles); // CM_TransformedCapsuleTrace
		}
	}

	void PlayerMovement::GScr_IsSprinting(const Game::scr_entref_t entref)
	{
		const auto* client = Game::GetEntity(entref)->client;
		if (!client)
		{
			Game::Scr_Error("IsSprinting can only be called on a player");
			return;
		}

		Game::Scr_AddBool(Game::PM_IsSprinting(&client->ps));
	}

	const Game::dvar_t* PlayerMovement::Dvar_RegisterSpectateSpeedScale(const char* dvarName, float value,
		float min, float max, unsigned __int16 /*flags*/, const char* description)
	{
		PlayerSpectateSpeedScale = Game::Dvar_RegisterFloat(dvarName, value,
			min, max, Game::DVAR_CHEAT | Game::DVAR_CODINFO, description);

		return PlayerSpectateSpeedScale;
	}

	void PlayerMovement::RegisterMovementDvars()
	{
		PlayerDuckedSpeedScale = Game::Dvar_RegisterFloat("player_duckedSpeedScale",
			0.65f, 0.0f, 5.0f, Game::DVAR_CHEAT | Game::DVAR_CODINFO,
			"The scale applied to the player speed when ducking");

		PlayerProneSpeedScale = Game::Dvar_RegisterFloat("player_proneSpeedScale",
			0.15f, 0.0f, 5.0f, Game::DVAR_CHEAT | Game::DVAR_CODINFO,
			"The scale applied to the player speed when crawling");

		// 3arc naming convention
		CGUfoScaler = Game::Dvar_RegisterFloat("cg_ufo_scaler",
			6.0f, 0.001f, 1000.0f, Game::DVAR_CHEAT | Game::DVAR_CODINFO,
			"The speed at which ufo camera moves");

		CGNoclipScaler = Game::Dvar_RegisterFloat("cg_noclip_scaler",
			3.0f, 0.001f, 1000.0f, Game::DVAR_CHEAT | Game::DVAR_CODINFO,
			"The speed at which noclip camera moves");

		BGBouncesAllAngles = Game::Dvar_RegisterBool("bg_bouncesAllAngles",
			false, Game::DVAR_CODINFO, "Force bounce from all angles");

		BGRocketJump = Dvar::Register<bool>("bg_rocketJump",
			false, Game::DVAR_CODINFO, "Enable CoD4 rocket jumps");

		BGRocketJumpScale = Dvar::Register<float>("bg_rocketJumpScale",
			64.0f, 1.0f, std::numeric_limits<float>::max(), Game::DVAR_CODINFO,
			"The scale applied to the pushback force of a rocket");

		BGPlayerEjection = Dvar::Register<bool>("bg_playerEjection",
			true, Game::DVAR_CODINFO, "Push intersecting players away from each other");

		BGPlayerCollision = Dvar::Register<bool>("bg_playerCollision",
			true, Game::DVAR_CODINFO, "Push intersecting players away from each other");
	}

	PlayerMovement::PlayerMovement()
	{
		AssertOffset(Game::playerState_s, eFlags, 0xB0);
		AssertOffset(Game::playerState_s, pm_flags, 0xC);

		Scheduler::Once([]
		{
			static const char* bg_bouncesValues[] =
			{
				"disabled",
				"enabled",
				"double",
				nullptr
			};

			BGBounces = Game::Dvar_RegisterEnum("bg_bounces",
				bg_bouncesValues, DISABLED, Game::DVAR_CODINFO, "Bounce glitch settings");
		}, Scheduler::Pipeline::MAIN);

		// Hook Dvar_RegisterFloat. Only thing that's changed is that the 0x80 flag is not used.
		Utils::Hook(0x448990, Dvar_RegisterSpectateSpeedScale, HOOK_CALL).install()->quick();

		// PM_CmdScaleForStance
		Utils::Hook(0x572D9B, PM_PlayerDuckedSpeedScaleStub, HOOK_JUMP).install()->quick();
		Utils::Hook(0x572DA5, PM_PlayerProneSpeedScaleStub, HOOK_JUMP).install()->quick();

		// Hook PM_MoveScale so we can add custom speed scale for Ufo and Noclip
		Utils::Hook(0x56F42C, PM_MoveScale_Noclip, HOOK_JUMP).install()->quick();
		Utils::Hook(0x56F43F, PM_MoveScale_Ufo, HOOK_JUMP).install()->quick();
		Utils::Hook(0x56F452, PM_MoveScale_Spectate, HOOK_JUMP).install()->quick();

		// Bounce logic
		Utils::Hook(0x4B1B2D, PM_StepSlideMoveStub, HOOK_JUMP).install()->quick();
		Utils::Hook(0x57383E, Jump_ClearState_Hk, HOOK_CALL).install()->quick();
		Utils::Hook(0x417B66, PM_ProjectVelocityStub, HOOK_JUMP).install()->quick();

		// Rocket jump
		Utils::Hook(0x4A4F9B, Weapon_RocketLauncher_Fire_Hk, HOOK_CALL).install()->quick(); //  FireWeapon        

		// Hook StuckInClient & CM_TransformedCapsuleTrace 
		// so we can prevent intersecting players from being pushed away from each other
		Utils::Hook(0x5D8153, StuckInClient_Hk, HOOK_CALL).install()->quick();
		Utils::Hook(0x45A5BF, CM_TransformedCapsuleTrace_Hk, HOOK_CALL).install()->quick(); // SV_ClipMoveToEntity
		Utils::Hook(0x5A0CAD, CM_TransformedCapsuleTrace_Hk, HOOK_CALL).install()->quick(); // CG_ClipMoveToEntity

		Script::AddMethod("IsSprinting", GScr_IsSprinting);

		RegisterMovementDvars();
	}
}
