// Emacs style mode select   -*- C++ -*-
//-----------------------------------------------------------------------------
//
// $Id: c85d1cae60681a3d64ac5c93b4a18d5672a9c90e $
//
// Copyright (C) 1993-1996 by id Software, Inc.
// Copyright (C) 2006-2026 by The Odamex Team.
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; either version 2
// of the License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// DESCRIPTION:
//	Main program, simply calls D_DoomMain high level loop.
//
//-----------------------------------------------------------------------------

#include "odamex.h"

#include <stack>
#include <iostream>

#include "win32inc.h"
#ifdef _WIN32
    #include "resource.h"
	#include "mmsystem.h"
#endif

#ifdef UNIX
#include <unistd.h>
#include <signal.h>
#endif

#include <stdlib.h>

#include "fmt/color.h"

#include "i_crash.h"
#include "m_argv.h"
#include "d_main.h"
#include "i_system.h"
#include "c_console.h"
#include "z_zone.h"
#include "i_net.h"
#include "m_fileio.h"
#include "m_consolecommandstream.h"

void AddCommandString(std::string cmd);

#ifdef _WIN32
extern UINT TimerPeriod;
#endif

// functions to be called at shutdown are stored in this stack
using term_func_t = void (*)();
std::stack< std::pair<term_func_t, std::string> > TermFuncs;

void addterm (void (*func) (), const char *name)
{
	TermFuncs.emplace(func, name);
}

void call_terms()
{
	while (!TermFuncs.empty())
		TermFuncs.top().first(), TermFuncs.pop();
}

EXTERN_CVAR(log_color)
static constexpr auto ERROR_STYLE = fmt::emphasis::bold | fg(fmt::color::red);

#ifdef _WIN32
static HANDLE hEvent;

int ShutdownNow()
{
	return (WaitForSingleObject(hEvent, 0) == WAIT_OBJECT_0);
}

BOOL WINAPI ConsoleHandlerRoutine(DWORD dwCtrlType)
{
	SetEvent(hEvent);
	return true;
}

class PowerThrottleController
{
	public:
		void Disable(ULONG flag)
		{
			PROCESS_POWER_THROTTLING_STATE throttleCommand{};
			throttleCommand.Version     = PROCESS_POWER_THROTTLING_CURRENT_VERSION;
			throttleCommand.ControlMask = m_toggledFlags | flag;
			throttleCommand.StateMask   = 0;

			if (SetProcessInformation(GetCurrentProcess(),
			                          ProcessPowerThrottling,
			                          & throttleCommand,
			                          sizeof(throttleCommand)))
			{
				m_toggledFlags |= flag;
			}
		}

	protected:
		// Record the successfully-toggled flags so that we can support successive calls
		// to SetProcessInformation with a potential mixture of successes and failures,
		// which is to be expected on Windows versions prior to 11.
		ULONG m_toggledFlags = 0;
};

static void DisablePowerThrottling()
{
	// Sometime during 2022, Windows 11 was updated to throttle processes' access to
	// the high-resolution timer, even if processes explicitly requested it.  Throttling
	// occurs when the process is no longer in the foreground, including when being
	// obscured by other windows.  When this happens, the server spends excessive time
	// sleeping in various system API calls, such as kbhit() in the console code and/or
	// sendto() in the network code.
	//
	// Fortunately we can disable this behavior via the SetProcessInformation API.

	PowerThrottleController throttler;

	throttler.Disable(PROCESS_POWER_THROTTLING_EXECUTION_SPEED);
	throttler.Disable(PROCESS_POWER_THROTTLING_IGNORE_TIMER_RESOLUTION);
}

int __cdecl main(int argc, char *argv[])
{
	// [AM] Set crash callbacks, so we get something useful from crashes.
#ifdef NDEBUG
	I_SetCrashCallbacks();
#endif

	DisablePowerThrottling();

	try
	{
		// Handle close box, shutdown and logoff events
		if (!(hEvent = CreateEvent(NULL, false, false, NULL)))
			throw CDoomError("Could not create console control event!\n");

		if (!SetConsoleCtrlHandler(ConsoleHandlerRoutine, true))
			throw CDoomError("Could not set console control handler!\n");

		// Disable QuickEdit mode as any text selection will cause all functions
		// that use stdout (printf etc) to block
		// but only try to do so if stdin is actually a terminal
		// otherwise this will prevent being able to pipe output
		const auto hIn = GetStdHandle(STD_INPUT_HANDLE);
		if (GetFileType(hIn) == FILE_TYPE_CHAR)
		{
			DWORD consoleMode = 0;

			if (!GetConsoleMode(hIn, &consoleMode))
				throw CDoomError("GetConsoleMode (input) failed!\n");

			consoleMode &= ~ENABLE_QUICK_EDIT_MODE;
			consoleMode |= ENABLE_EXTENDED_FLAGS;

			if (!SetConsoleMode(hIn, consoleMode))
				throw CDoomError("SetConsoleMode (input) failed!\n");
		}

		// Fixes icon not showing in titlebar and alt-tab menu under windows 7
		HANDLE hIcon;

		hIcon = LoadIcon(GetModuleHandle(NULL), MAKEINTRESOURCE(IDI_ICON1));

		if(hIcon)
		{
			SendMessage(GetConsoleWindow(), WM_SETICON, ICON_SMALL, (LPARAM)hIcon);
			SendMessage(GetConsoleWindow(), WM_SETICON, ICON_BIG, (LPARAM)hIcon);
		}

		// [ML] 2007/9/3: From Eternity (originally chocolate Doom) Thanks SoM & fraggle!
		::Args.SetArgs(argc, argv);

		D_CheckInfoDumps();

		const char* crashdir = ::Args.CheckValue("-crashdir");
		if (crashdir)
		{
			I_SetCrashDir(crashdir);
		}
		else
		{
			std::string writedir = M_GetWriteDir();
			I_SetCrashDir(writedir.c_str());
		}

		M_InitConsoleInputFile(Args.CheckValue("-confile"));

		// Set the timer to be as accurate as possible
		TIMECAPS tc;
		if (timeGetDevCaps (&tc, sizeof(tc) != TIMERR_NOERROR))
			TimerPeriod = 1;    // Assume minimum resolution of 1 ms
		else
			TimerPeriod = tc.wPeriodMin;

		timeBeginPeriod (TimerPeriod);

		// Don't call this on windows!
		//atexit (call_terms);

		Z_Init();

		atterm (I_Quit);
		atterm (DObject::StaticShutdown);

		D_DoomMain();
	}
	catch (CDoomError& error)
	{
		// It's possible that the fatal error was encountered before
		// loading the config file or setting cvars set from the command line
		// so we should default to no color unless we know that we have already loaded that
		if (LOG.is_open())
		{
			if (DefaultsLoaded && log_color == 2)
				LOG << fmt::format(ERROR_STYLE, "=== ERROR: {} ===\n\n", error.GetMsg());
			else
				LOG << "=== ERROR: " << error.GetMsg() << " ===\n\n";
		}

		fmt::text_style style;
		try
		{
			// I_ConsoleUseColor can throw on Windows
			// but we've already gotten another fatal error,
			// possibly even from another call to it
			// so lets just ignore that exception since
			// we're about to exit and log another one anyway
			if (DefaultsLoaded && log_color && I_ConsoleUseColor())
				style = ERROR_STYLE;
		}
		catch (CDoomError&)
		{}

		fmt::print(stderr, style, "=== ERROR: {} ===\n\n", error.GetMsg());

		call_terms();
		exit(EXIT_FAILURE);
	}
	catch (...)
	{
		call_terms();
		throw;
	}
	return 0;
}
#else

//
// daemon_init
//
void daemon_init(void)
{
    PrintFmt(PRINT_HIGH, "Launched into the background\n");

	int pid;
    if ((pid = fork()) != 0)
    {
    	call_terms();
    	exit(EXIT_SUCCESS);
    }

    std::string pidfile;
	const char *forkargs = Args.CheckValue("-fork");
	if (forkargs)
		pidfile = std::string(forkargs);

    if(!pidfile.size() || pidfile[0] == '-')
    	pidfile = "doomsv.pid";

    pid = getpid();
    FILE* fpid = fopen(pidfile.c_str(), "w");
    fmt::print(fpid, "{}\n", pid);
    fclose(fpid);
}

static void set_window_title()
{
	if (!isatty(STDOUT_FILENO))
		return;

	const char* term = getenv("TERM");
	if (!term || strcmp(term, "dumb") == 0)
		return;

	fmt::print("\033]2;Odamex Server {}\033\\", NiceVersion());
}

int main(int argc, char **argv)
{
	// [AM] Set crash callbacks, so we get something useful from crashes.
#ifdef NDEBUG
	I_SetCrashCallbacks();
#endif

    try
    {
		::Args.SetArgs(argc, argv);

		D_CheckInfoDumps();

		if(!getuid() || !geteuid())
			I_FatalError("root user detected, quitting odamex immediately");

		int r_euid = seteuid (getuid ());

		if(r_euid < 0)
			perror(NULL);

		const char* crashdir = ::Args.CheckValue("-crashdir");
		if (crashdir)
		{
			I_SetCrashDir(crashdir);
		}
		else
		{
			std::string writedir = M_GetWriteDir();
			I_SetCrashDir(writedir.c_str());
		}

		M_InitConsoleInputFile(Args.CheckValue("-confile"));

		set_window_title();

		/*
		  killough 1/98:

		  This fixes some problems with exit handling
		  during abnormal situations.

		  The old code called I_Quit() to end program,
		  while now I_Quit() is installed as an exit
		  handler and exit() is called to exit, either
		  normally or abnormally. Seg faults are caught
		  and the error handler is used, to prevent
		  being left in graphics mode or having very
		  loud SFX noise because the sound card is
		  left in an unstable state.
		*/

        // Don't use this on other platforms either
		//atexit (call_terms);
		Z_Init();					// 1/18/98 killough: start up memory stuff first

		atterm (I_Quit);
		atterm (DObject::StaticShutdown);

		// [AM] There used to be a signal handler here that attempted to
		//      shut the server off gracefully.  I'm not sure masking the
		//      signal is a good idea, and it stomped over the crashlog handler
		//      I set earlier.

		D_DoomMain();
	}
	catch (CDoomError& error)
	{
		// It's possible that the fatal error was encountered before
		// loading the config file or setting cvars set from the command line
		// so we should default to no color unless we know that we have already loaded that
		if (LOG.is_open())
		{
			if (DefaultsLoaded && log_color == 2)
				LOG << fmt::format(ERROR_STYLE, "=== ERROR: {} ===\n\n", error.GetMsg());
			else
				LOG << "=== ERROR: " << error.GetMsg() << " ===\n\n";
		}

		if (DefaultsLoaded && log_color && I_ConsoleUseColor())
			fmt::print(stderr, ERROR_STYLE, "=== ERROR: {} ===\n\n", error.GetMsg());
		else
			fmt::print(stderr, "=== ERROR: {} ===\n\n", error.GetMsg());

		call_terms();
		exit(EXIT_FAILURE);
	}
	catch (...)
	{
		call_terms();
		throw;
	}
	return 0;
}

#endif

VERSION_CONTROL (i_main_cpp, "$Id: c85d1cae60681a3d64ac5c93b4a18d5672a9c90e $")
