#include <std_include.hpp>
#include "loader/component_loader.hpp"
#include "game/game.hpp"
#include "game/dvars.hpp"
#include "game/engine/sv_game.hpp"

#include "command.hpp"
#include "console.hpp"
#include "dvars.hpp"
#include "filesystem.hpp"
#include "scheduler.hpp"

#include <utils/hook.hpp>
#include <utils/nt.hpp>

namespace patches
{
	namespace
	{
		utils::hook::detour live_get_local_client_name_hook;

		const char* live_get_local_client_name()
		{
			return game::Dvar_FindVar("name")->current.string;
		}

		utils::hook::detour sv_kick_client_num_hook;

		void sv_kick_client_num(const int clientNum, const char* reason)
		{
			// Don't kick bot to equalize team balance.
			if (reason == "EXE_PLAYERKICKED_BOT_BALANCE"s)
			{
				return;
			}
			return sv_kick_client_num_hook.invoke<void>(clientNum, reason);
		}

		std::string get_login_username()
		{
			char username[UNLEN + 1];
			DWORD username_len = UNLEN + 1;
			if (!GetUserNameA(username, &username_len))
			{
				return "Unknown Soldier";
			}

			return std::string{ username, username_len - 1 };
		}

		utils::hook::detour com_register_dvars_hook;

		void com_register_dvars_stub()
		{
			if (game::environment::is_mp())
			{
				// Make name save
				game::Dvar_RegisterString("name", get_login_username().data(), game::DVAR_FLAG_SAVED, "Player name.");
			}

			return com_register_dvars_hook.invoke<void>();
		}

		game::dvar_t* register_network_fps_stub(const char* name, int, int, int, unsigned int flags,
		                                        const char* desc)
		{
			return game::Dvar_RegisterInt(name, 1000, 20, 1000, flags, desc);
		}

		bool cmd_exec_patch()
		{
			const command::params exec_params{};
			if (exec_params.size() == 2)
			{
				std::string file_name = exec_params.get(1);
				if (!file_name.ends_with(".cfg"))
				{
					file_name.append(".cfg");
				}

				const auto file = filesystem::file(file_name);
				if (file.exists())
				{
					game::Cbuf_ExecuteBufferInternal(0, 0, file.get_buffer().data(), game::Cmd_ExecuteSingleCommand);
					return true;
				}
			}

			return false;
		}

		auto cmd_exec_stub_mp = utils::hook::assemble([](utils::hook::assembler& a)
		{
			const auto success = a.newLabel();

			a.pushad64();
			a.call_aligned(cmd_exec_patch);
			a.test(al, al);
			a.popad64();

			a.jz(success);
			a.mov(edx, 0x18000);
			a.jmp(0x1403F7530);

			a.bind(success);
			a.jmp(0x1403F7574);
		});

		auto cmd_exec_stub_sp = utils::hook::assemble([](utils::hook::assembler& a)
		{
			const auto success = a.newLabel();

			a.pushad64();
			a.call_aligned(cmd_exec_patch);
			a.test(al, al);
			a.popad64();

			a.jz(success);
			a.mov(edx, 0x18000);
			a.jmp(0x1403B39C0);

			a.bind(success);
			a.jmp(0x1403B3A04);
		});

		int dvar_command_patch() // game makes this return an int and compares with eax instead of al -_-
		{
			const command::params args{};

			if (args.size() <= 0)
				return 0;

			auto* dvar = game::Dvar_FindVar(args.get(0));
			if (dvar)
			{
				if (args.size() == 1)
				{
					const std::string current = game::Dvar_ValueToString(dvar, dvar->current);
					const std::string reset = game::Dvar_ValueToString(dvar, dvar->reset);
					console::info("\"%s\" is: \"%s^7\" default: \"%s^7\"\n", dvar->name, current.data(), reset.data());
					console::info("   %s\n", dvars::dvar_get_domain(dvar->type, dvar->domain).data());
				}
				else
				{
					char command[0x1000]{};
					game::Dvar_GetCombinedString(command, 1);
					game::Dvar_SetCommand(args.get(0), command);
				}

				return 1;
			}

			return 0;
		}

		game::Font_s* get_chat_font_handle()
		{
			return game::R_RegisterFont("fonts/bigFont");
		}

		void aim_assist_add_to_target_list(void* a1, void* a2)
		{
			if (!dvars::aimassist_enabled->current.enabled)
				return;

			game::AimAssist_AddToTargetList(a1, a2);
		}

		game::dvar_t* register_cg_fov_stub(const char* name, const float value, const float min, [[maybe_unused]] const float max,
		                                   const unsigned int flags,
		                                   const char* description)
		{
			return game::Dvar_RegisterFloat(name, value, min, 160, flags, description);
		}

		void bsp_sys_error_stub(const char* error, const char* arg1)
		{
			if (game::environment::is_dedi())
			{
				game::Sys_Error(error, arg1);
			}
			else
			{
				scheduler::once([]()
				{
					command::execute("reconnect");
				}, scheduler::pipeline::main, 1s);
				game::Com_Error(game::ERR_DROP, error, arg1);
			}
		}

		utils::hook::detour cmd_lui_notify_server_hook;
		void cmd_lui_notify_server_stub(game::mp::gentity_s* ent)
		{
			command::params_sv params{};
			const auto menu_id = atoi(params.get(1));
			const auto client = &game::mp::svs_clients[ent->s.clientNum];

			// 9 => "end_game"
			if (menu_id == 9 && client->header.netchan.remoteAddress.type != game::NA_LOOPBACK)
			{
				return;
			}

			cmd_lui_notify_server_hook.invoke<void>(ent);
		}

		void update_last_seen_players_stub(utils::hook::assembler& a)
		{
			const auto safe_continue = a.newLabel();

			// (game's code)
			a.mov(rax, ptr(rbx)); // g_entities pointer

			// Avoid crash if pointer is nullptr
			a.test(rax, rax);
			a.jz(safe_continue);

			// Jump back in (game's code)
			a.mov(dword_ptr(rax, 0x34F8), 0);

			// Continue to next iter in this loop
			a.bind(safe_continue);
			a.jmp(0x1403A1A10);
		}
	}

	class component final : public component_interface
	{
	public:
		void post_unpack() override
		{
			// Increment ref-count on these
			LoadLibraryA("PhysXDevice64.dll");
			LoadLibraryA("PhysXUpdateLoader64.dll");

			// Unlock fps in main menu
			utils::hook::set<std::uint8_t>(SELECT_VALUE(0x140242DDB, 0x1402CF58B), 0xEB);

			// Unlock cg_fov
			utils::hook::call(SELECT_VALUE(0x1401F3E96, 0x14027273C), register_cg_fov_stub);
			if (game::environment::is_sp())
			{
				utils::hook::call(0x1401F3EC7, register_cg_fov_stub);
			}

			// changed max value from 85 -> 1000
			if (!game::environment::is_dedi())
			{
				dvars::override::register_int("com_maxfps", 85, 0, 1000, game::DVAR_FLAG_SAVED);
			}

			if (!game::environment::is_sp())
			{
				//increased max limit for sv_network_fps, the lower limit is the default one. Original range is from 20 to 200 times a second.
				utils::hook::call(0x140476F4F, register_network_fps_stub);
			}

			// register cg_gun_ dvars with new values and flags
			// maybe _x can stay usable within a reasonable range? it can make scoped weapons DRASTICALLY better on high FOVs
			dvars::override::register_float("cg_gun_x", 0.0f, -1.0f, 2.0f, game::DVAR_FLAG_SAVED);
			dvars::override::register_float("cg_gun_y", 0.0f, 0.0f, 0.0f, game::DVAR_FLAG_NONE);
			dvars::override::register_float("cg_gun_z", 0.0f, 0.0f, 0.0f, game::DVAR_FLAG_NONE);

			// Register cg_fovscale with new params
			dvars::override::register_float("cg_fovScale", 1.0f, 0.1f, 5.0f, game::DVAR_FLAG_SAVED);

			// Patch Dvar_Command to print out values how CoD4 does it
			utils::hook::jump(SELECT_VALUE(0x1403BFCB0, 0x140416A60), dvar_command_patch);

			// Allow executing custom cfg files with the "exec" command
			utils::hook::jump(SELECT_VALUE(0x1403B39BB, 0x1403F752B), SELECT_VALUE(0x1403B3A12, 0x1403F7582));
			//Use a relative jump to empty memory first
			utils::hook::jump(SELECT_VALUE(0x1403B3A12, 0x1403F7582), SELECT_VALUE(cmd_exec_stub_sp, cmd_exec_stub_mp), true);
			// Use empty memory to go to our stub first (can't do close jump, so need space for 12 bytes)

			// Fix mouse lag
			utils::hook::nop(SELECT_VALUE(0x14043E6CB, 0x140504A2B), 6);
			scheduler::loop([]()
			{
				SetThreadExecutionState(ES_DISPLAY_REQUIRED);
			}, scheduler::pipeline::main);

			if (game::environment::is_sp())
			{
				patch_sp();
			}
			else
			{
				patch_mp();
			}
		}

		static void patch_mp()
		{
			// Bypass Arxan function
			utils::hook::nop(0x1404758C0, 16);
			utils::hook::jump(0x1404758C0, game::engine::SV_GameSendServerCommand, true);

			utils::hook::call(0x140477399, game::engine::SV_SendServerCommand);

			// Register dvars
			com_register_dvars_hook.create(0x140413A90, &com_register_dvars_stub);

			// Use name dvar and add "saved" flags to it
			utils::hook::set<uint8_t>(0x1402C836D, 0x01);
			live_get_local_client_name_hook.create(0x1404FDAA0, &live_get_local_client_name);

			// Patch SV_KickClientNum
			sv_kick_client_num_hook.create(0x14046F730, &sv_kick_client_num);

			// Block changing name in-game
			utils::hook::set<uint8_t>(0x140470300, 0xC3);

			// Unlock all DLC items
			utils::hook::set(0x1404EAC50, 0xC301B0); // Content_DoWeHaveDLCPackByName
			utils::hook::set(0x140599890, 0xC301B0); // Entitlements_IsIDUnlocked

			// Enable DLC items, extra loadouts and map selection in extinction
			dvars::override::register_int("extinction_map_selection_enabled", 1, 0, 1, 0);
			dvars::override::register_int("extendedLoadoutsEnable", 1, 0, 1, 0);
			dvars::override::register_int("igs_announcer", 3, 3, 3, 0);
			dvars::override::register_int("igs_swp", 1, 0, 1, 0);
			dvars::override::register_int("igs_shp", 1, 0, 1, 0);
			dvars::override::register_int("igs_svp", 1, 0, 1, 0);
			dvars::override::register_int("igs_sve", 1, 0, 1, 0);
			dvars::override::register_int("igs_svs", 1, 0, 1, 0);
			dvars::override::register_int("igs_svr", 1, 0, 1, 0);
			dvars::override::register_int("igs_swap", 1, 0, 1, 0);
			dvars::override::register_int("igs_fo", 1, 0, 1, 0);
			dvars::override::register_int("igs_td", 1, 0, 1, 0);
			dvars::override::register_int("igs_sripper", 1, 0, 1, 0);
			dvars::override::register_int("igs_smappacks", 1, 0, 1, 0);
			dvars::override::register_int("igs_sosp", 1, 0, 1, 0);
			dvars::override::register_int("igs_s1", 1, 0, 1, 0);
			dvars::override::register_int("igs_crossgame", 1, 0, 1, 0);

			// Required by UI scripts. Missing when joining a dedi and causes crashes
			game::Dvar_RegisterInt("scr_gun_winlimit", 1, 0, 10, game::DVAR_FLAG_REPLICATED, "Win limit for Gun Game");
			game::Dvar_RegisterInt("scr_gun_scorelimit", 18, 1, 1000, game::DVAR_FLAG_REPLICATED, "Score limit for Gun Game");

			// Patch game chat on resolutions higher than 1080p to use the right font
			utils::hook::call(0x14025C825, get_chat_font_handle);
			utils::hook::call(0x1402BC42F, get_chat_font_handle);
			utils::hook::call(0x1402C3699, get_chat_font_handle);

			dvars::aimassist_enabled = game::Dvar_RegisterBool("aimassist_enabled", true,
			                                                   game::DVAR_FLAG_SAVED,
			                                                   "Enables aim assist for controllers");
			// Client side aim assist dvar
			utils::hook::call(0x14013B9AC, aim_assist_add_to_target_list);

			// Patch "Couldn't find the bsp for this map." error to not be fatal in mp
			utils::hook::call(0x14031E8AB, bsp_sys_error_stub);

			// isProfanity
			utils::hook::set(0x1402F61B0, 0xC3C033);

			// Don't register every replicated dvar as a network dvar
			utils::hook::nop(0x1403E984E, 5); // Dvar_ForEach

			// Prevent clients from ending the game as non host by sending 'end_game' lui notification
			cmd_lui_notify_server_hook.create(0x1403926A0, cmd_lui_notify_server_stub);

			// Remove cheat protection from r_umbraExclusive
			dvars::override::register_bool("r_umbraExclusive", false, game::DVAR_FLAG_NONE);

			// Patch crash caused by the server trying to kick players for 'invalid password'
			utils::hook::jump(0x1403A1A03, utils::hook::assemble(update_last_seen_players_stub), true);
			utils::hook::nop(0x1403A1A0F, 1);
			// ^^
			utils::hook::nop(0x1403A072F, 5); // LiveStorage_RecordMovementInMatchdata

			// Disable Com_Error in NET_SendPacket
			utils::hook::nop(0x140501AE3, 5);
		}

		static void patch_sp()
		{
			// SP doesn't initialize WSA
			WSADATA wsa_data;
			WSAStartup(MAKEWORD(2, 2), &wsa_data);
		}
	};
}

REGISTER_COMPONENT(patches::component)
