#include "dedicated.h"
#include "dedicatedlogtoclient.h"
#include "core/tier0.h"
#include "playlist.h"
#include "engine/r2engine.h"
#include "engine/hoststate.h"
#include "server/auth/serverauthentication.h"
#include "masterserver/masterserver.h"
#include "util/printcommands.h"

AUTOHOOK_INIT()

using namespace R2;

bool IsDedicatedServer()
{
	static bool result = strstr(GetCommandLineA(), "-dedicated");
	return result;
}

// CDedidcatedExports defs
struct CDedicatedExports; // forward declare

typedef void (*DedicatedSys_PrintfType)(CDedicatedExports* dedicated, const char* msg);
typedef void (*DedicatedRunServerType)(CDedicatedExports* dedicated);

// would've liked to just do this as a class but have not been able to get it to work
struct CDedicatedExports
{
	void* vtable; // because it's easier, we just set this to &this, since CDedicatedExports has no props we care about other than funcs

	char unused[56];

	DedicatedSys_PrintfType Sys_Printf;
	DedicatedRunServerType RunServer;
};

void Sys_Printf(CDedicatedExports* dedicated, const char* msg)
{
	spdlog::info("[DEDICATED SERVER] {}", msg);
}

void RunServer(CDedicatedExports* dedicated)
{
	spdlog::info("CDedicatedExports::RunServer(): starting");
	spdlog::info(Tier0::CommandLine()->GetCmdLine());

	// initialise engine
	g_pEngine->Frame();

	// add +map if no map loading command is present
	// don't manually execute this from cbuf as users may have it in their startup args anyway, easier just to run from stuffcmds if present
	if (!Tier0::CommandLine()->CheckParm("+map") && !Tier0::CommandLine()->CheckParm("+launchplaylist"))
		Tier0::CommandLine()->AppendParm("+map", g_pCVar->FindVar("match_defaultMap")->GetString());

	// re-run commandline
	Cbuf_AddText(Cbuf_GetCurrentPlayer(), "stuffcmds", cmd_source_t::kCommandSrcCode);
	Cbuf_Execute();

	// main loop
	double frameTitle = 0;
	while (g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING)
	{
		double frameStart = Tier0::Plat_FloatTime();
		g_pEngine->Frame();

		std::this_thread::sleep_for(
			std::chrono::duration<double, std::ratio<1>>(g_pGlobals->m_flTickInterval - fmin(Tier0::Plat_FloatTime() - frameStart, 0.25)));
	}
}

// use server presence to update window title
class DedicatedConsoleServerPresence : public ServerPresenceReporter
{
	void ReportPresence(const ServerPresence* pServerPresence) override
	{
		SetConsoleTitleA(fmt::format(
							 "{} - {} {}/{} players ({})",
							 pServerPresence->m_sServerName,
							 pServerPresence->m_MapName,
							 pServerPresence->m_iPlayerCount,
							 pServerPresence->m_iMaxPlayers,
							 pServerPresence->m_PlaylistName)
							 .c_str());
	}
};

HANDLE consoleInputThreadHandle = NULL;
DWORD WINAPI ConsoleInputThread(PVOID pThreadParameter)
{
	while (!g_pEngine || !g_pHostState || g_pHostState->m_iCurrentState != HostState_t::HS_RUN)
		Sleep(1000);

	// Bind stdin to receive console input.
	FILE* fp = nullptr;
	freopen_s(&fp, "CONIN$", "r", stdin);

	spdlog::info("Ready to receive console commands.");

	{
		// Process console input
		std::string input;
		while (g_pEngine && g_pEngine->m_nQuitting == EngineQuitState::QUIT_NOTQUITTING && std::getline(std::cin, input))
		{
			input += "\n";
			Cbuf_AddText(Cbuf_GetCurrentPlayer(), input.c_str(), cmd_source_t::kCommandSrcCode);
			TryPrintCvarHelpForCommand(input.c_str()); // this needs to be done on main thread, unstable in this one
		}
	}

	return 0;
}

// clang-format off
AUTOHOOK(IsGameActiveWindow, engine.dll + 0x1CDC80,
bool,, ())
// clang-format on
{
	return true;
}

ON_DLL_LOAD_DEDI_RELIESON("engine.dll", DedicatedServer, ServerPresence, (CModule module))
{
	spdlog::info("InitialiseDedicated");

	AUTOHOOK_DISPATCH_MODULE(engine.dll)

	// Host_Init
	// prevent a particle init that relies on client dll
	module.Offset(0x156799).NOP(5);

	// Host_Init
	// don't call Key_Init to avoid loading some extra rsons from rpak (will be necessary to boot if we ever wanna disable rpaks entirely on
	// dedi)
	module.Offset(0x1565B0).NOP(5);

	{
		// CModAppSystemGroup::Create
		// force the engine into dedicated mode by changing the first comparison to IsServerOnly to an assignment
		MemoryAddress base = module.Offset(0x1C4EBD);

		// cmp => mov
		base.Offset(1).Patch("C6 87");

		// 00 => 01
		base.Offset(7).Patch("01");
	}

	// Some init that i'm not sure of that crashes
	// nop the call to it
	module.Offset(0x156A63).NOP(5);

	// runframeserver
	// nop some access violations
	module.Offset(0x159819).NOP(17);

	module.Offset(0x156B4C).NOP(7);

	// previously patched these, took me a couple weeks to figure out they were the issue
	// removing these will mess up register state when this function is over, so we'll write HS_RUN to the wrong address
	// so uhh, don't do that
	// NSMem::NOP(ea + 0x156B4C + 7, 8);
	module.Offset(0x156B4C).Offset(15).NOP(9);

	// HostState_State_NewGame
	// nop an access violation
	module.Offset(0xB934C).NOP(9);

	// CEngineAPI::Connect
	// remove call to Shader_Connect
	module.Offset(0x1C4D7D).NOP(5);

	// Host_Init
	// remove call to ui loading stuff
	module.Offset(0x156595).NOP(5);

	// some function that gets called from RunFrameServer
	// nop a function that makes requests to stryder, this will eventually access violation if left alone and isn't necessary anyway
	module.Offset(0x15A0BB).NOP(5);

	// RunFrameServer
	// nop a function that access violations
	module.Offset(0x159BF3).NOP(5);

	// func that checks if origin is inited
	// always return 1
	module.Offset(0x183B70).Patch("B0 01 C3"); // mov al,01 ret

	// HostState_State_ChangeLevel
	// nop clientinterface call
	module.Offset(0x1552ED).NOP(16);

	// HostState_State_ChangeLevel
	// nop clientinterface call
	module.Offset(0x155363).NOP(16);

	// IVideoMode::CreateGameWindow
	// nop call to ShowWindow
	module.Offset(0x1CD146).NOP(5);

	CDedicatedExports* dedicatedExports = new CDedicatedExports;
	dedicatedExports->vtable = dedicatedExports;
	dedicatedExports->Sys_Printf = Sys_Printf;
	dedicatedExports->RunServer = RunServer;

	*module.Offset(0x13F0B668).As<CDedicatedExports**>() = dedicatedExports;

	// extra potential patches:
	// nop engine.dll+1c67d1 and +1c67d8 to skip videomode creategamewindow
	// also look into launcher.dll+d381, seems to cause renderthread to get made
	// this crashes HARD if no window which makes sense tbh
	// also look into materialsystem + 5B344 since it seems to be the base of all the renderthread stuff

	// big note: datatable gets registered in window creation
	// make sure it still gets registered

	// add cmdline args that are good for dedi
	Tier0::CommandLine()->AppendParm("-nomenuvid", 0);
	Tier0::CommandLine()->AppendParm("-nosound", 0);
	Tier0::CommandLine()->AppendParm("-windowed", 0);
	Tier0::CommandLine()->AppendParm("-nomessagebox", 0);
	Tier0::CommandLine()->AppendParm("+host_preload_shaders", "0");
	Tier0::CommandLine()->AppendParm("+net_usesocketsforloopback", "1");
	Tier0::CommandLine()->AppendParm("+community_frame_run", "0");

	// use presence reporter for console title
	DedicatedConsoleServerPresence* presenceReporter = new DedicatedConsoleServerPresence;
	g_pServerPresence->AddPresenceReporter(presenceReporter);

	// setup dedicated printing to client
	RegisterCustomSink(std::make_shared<DedicatedServerLogToClientSink>());

	// Disable Quick Edit mode to reduce chance of user unintentionally hanging their server by selecting something.
	if (!Tier0::CommandLine()->CheckParm("-bringbackquickedit"))
	{
		HANDLE stdIn = GetStdHandle(STD_INPUT_HANDLE);
		DWORD mode = 0;

		if (GetConsoleMode(stdIn, &mode))
		{
			if (mode & ENABLE_QUICK_EDIT_MODE)
			{
				mode &= ~ENABLE_QUICK_EDIT_MODE;
				mode &= ~ENABLE_MOUSE_INPUT;

				mode |= ENABLE_PROCESSED_INPUT;

				SetConsoleMode(stdIn, mode);
			}
		}
	}
	else
		spdlog::info("Quick Edit enabled by user request");

	// create console input thread
	if (!Tier0::CommandLine()->CheckParm("-noconsoleinput"))
		consoleInputThreadHandle = CreateThread(0, 0, ConsoleInputThread, 0, 0, NULL);
	else
		spdlog::info("Console input disabled by user request");
}

ON_DLL_LOAD_DEDI("tier0.dll", DedicatedServerOrigin, (CModule module))
{
	// disable origin on dedicated
	// for any big ea lawyers, this can't be used to play the game without origin, game will throw a fit if you try to do anything without
	// an origin id as a client for dedi it's fine though, game doesn't care if origin is disabled as long as there's only a server
	module.GetExport("Tier0_InitOrigin").Patch("C3");
}

// clang-format off
AUTOHOOK(PrintSquirrelError, server.dll + 0x794D0, 
void, __fastcall, (void* sqvm))
// clang-format on
{
	PrintSquirrelError(sqvm);

	// close dedicated server if a fatal error is hit
	// atm, this will crash if not aborted, so this just closes more gracefully
	static ConVar* Cvar_fatal_script_errors = g_pCVar->FindVar("fatal_script_errors");
	if (Cvar_fatal_script_errors->GetBool())
	{
		NS::log::FlushLoggers();
		abort();
	}
}

ON_DLL_LOAD_DEDI("server.dll", DedicatedServerGameDLL, (CModule module))
{
	AUTOHOOK_DISPATCH_MODULE(server.dll)

	if (Tier0::CommandLine()->CheckParm("-nopakdedi"))
	{
		module.Offset(0x6BA350).Patch("C3"); // dont load skins.rson from rpak if we don't have rpaks, as loading it will cause a crash
		module.Offset(0x6BA300).Patch(
			"B8 C8 00 00 00 C3"); // return 200 as the number of skins from server.dll + 6BA300, this is the normal value read from
								  // skins.rson and should be updated when we need it more modular
	}
}