/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 1998-2000, Matthes Bender
 * Copyright (c) 2001-2009, RedWolf Design GmbH, http://www.clonk.de/
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

/* Main class to initialize configuration and execute the game */

#include "C4Include.h"
#include "C4ForbidLibraryCompilation.h"
#include "game/C4Application.h"

#include "C4Version.h"
#include "editor/C4Console.h"
#include "game/C4FullScreen.h"
#include "game/C4GraphicsSystem.h"
#include "graphics/C4Draw.h"
#include "graphics/C4GraphicsResource.h"
#include "graphics/StdPNG.h"
#include "gui/C4GameLobby.h"
#include "gui/C4GfxErrorDlg.h"
#include "gui/C4MessageInput.h"
#ifdef _WIN32
#include "gui/C4UpdateDlg.h"
#endif
#include "gui/C4Startup.h"
#include "landscape/C4Particles.h"
#include "network/C4Network2.h"
#include "network/C4Network2IRC.h"
#include "platform/C4GamePadCon.h"

#include <getopt.h>

static C4Network2IRCClient ApplicationIRCClient;
const std::string C4Application::Revision{ C4REVISION };

C4Application::C4Application():
		IRCClient(ApplicationIRCClient)
{
}

C4Application::~C4Application()
{
	// clear gamepad
	if (pGamePadControl) delete pGamePadControl;
	// Close log
	CloseLog();
}

bool C4Application::DoInit(int argc, char * argv[])
{
	assert(AppState == C4AS_None);
	// Config overwrite by parameter
	StdStrBuf sConfigFilename;
	for (int32_t iPar=0; iPar < argc; iPar++)
		if (SEqual2NoCase(argv[iPar], "--config="))
			sConfigFilename.Copy(argv[iPar] + 9);
	// Config check
	Config.Init();
	Config.Load(sConfigFilename.getData());
	Config.Save();
	// sometimes, the configuration can become corrupted due to loading errors or w/e
	// check this and reset defaults if necessary
	if (Config.IsCorrupted())
	{
		if (sConfigFilename)
		{
			// custom config corrupted: Fail
			Log("ERROR: Custom configuration corrupted - program abort!\n");
			return false;
		}
		else
		{
			// default config corrupted: Restore default
			Log("Warning: Configuration corrupted - restoring default!\n");
			Config.Default();
			Config.Save();
			Config.Load();
		}
	}
	// Open log
	OpenLog();

	// Engine header message
	Log(C4ENGINECAPTION);
	LogF("Version: %s %s (%s - %s)", C4VERSION, C4_OS, GetRevision(), C4REVISION_TS);
	LogF(R"(ExePath: "%s")", Config.General.ExePath.getData());
	LogF(R"(SystemDataPath: "%s")", Config.General.SystemDataPath);
	LogF(R"(UserDataPath: "%s")", Config.General.UserDataPath);

	// Init C4Group
	C4Group_SetProcessCallback(&ProcessCallback);
	C4Group_SetTempPath(Config.General.TempPath.getData());
	C4Group_SetSortList(C4CFN_FLS);

	// Cleanup temp folders left behind
	Config.CleanupTempUpdateFolder();

	// Initialize game data paths
	Reloc.Init();

	// init system group
	if (!Reloc.Open(SystemGroup, C4CFN_System))
	{
		// Error opening system group - no LogFatal, because it needs language table.
		// This will *not* use the FatalErrors stack, but this will cause the game
		// to instantly halt, anyway.
		const char *szMessage = "Error opening system group file (System.ocg)!";
		Log(szMessage);
		// Fatal error, game cannot start - have player notice
		MessageDialog(szMessage);
		return false;
	}
	// Parse command line
	ParseCommandLine(argc, argv);

	// Open additional logs that depend on command line
	OpenExtraLogs();

	// Init external language packs
	Languages.Init();
	// Load language string table
	if (!Languages.LoadLanguage(Config.General.LanguageEx))
		// No language table was loaded - bad luck...
		if (!Languages.HasStringTable())
			Log("WARNING: No language string table loaded!");

#if defined(WIN32) && defined(WITH_AUTOMATIC_UPDATE)
	// Windows: handle incoming updates directly, even before starting up the gui
	//          because updates will be applied in the console anyway.
	if (!Application.IncomingUpdate.empty())
		if (C4UpdateDlg::ApplyUpdate(Application.IncomingUpdate.c_str(), false, nullptr))
			return true;
#endif

	// Fixup resolution
	if (!Config.Graphics.Windowed)
		ApplyResolutionConstraints();

	// activate
	Active=true;

	// Init carrier window
	if (!isEditor)
	{
		if (!(pWindow = FullScreen.Init(this)))
			{ Clear(); ShowGfxErrorDialog(); return false; }
	}
	else
	{
		if (!(pWindow = Console.Init(this)))
			{ Clear(); return false; }
	}

	// init timers (needs window)
	Add(pGameTimer = new C4ApplicationGameTimer());

	// Initialize OpenGL
	bool success = DDrawInit(this, GetConfigWidth(), GetConfigHeight(), Config.Graphics.Monitor);
	if (!success) { LogFatal(LoadResStr("IDS_ERR_DDRAW")); Clear(); ShowGfxErrorDialog(); return false; }

	if (!isEditor)
	{
		if (!SetVideoMode(Application.GetConfigWidth(), Application.GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, !Config.Graphics.Windowed))
			pWindow->SetSize(Config.Graphics.WindowX, Config.Graphics.WindowY);
	}

	// Initialize gamepad
	if (!pGamePadControl && Config.General.GamepadEnabled)
		pGamePadControl = new C4GamePadControl();

	AppState = C4AS_PreInit;

	return true;
}

void C4Application::ClearCommandLine()
{
	*Game.PlayerFilenames = 0;
}

void C4Application::ParseCommandLine(int argc, char * argv[])
{

	StdStrBuf CmdLine("Command line:");
	for(int i = 0; i < argc; ++i) {
		CmdLine.Append(" ");
		CmdLine.Append(argv[i]);
	}
	Log(CmdLine.getData());

	ClearCommandLine();
	Game.NetworkActive = false;
	isEditor = 2;
	int c;
	while (true)
	{

		static struct option long_options[] =
		{
			// option, w/ argument?, set directly, set to...
			{"editor", no_argument, &isEditor, 1},
			{"fullscreen", no_argument, &isEditor, 0},
			{"debugwait", no_argument, &Game.DebugWait, 1},
			{"update", no_argument, &CheckForUpdates, 1},
			{"noruntimejoin", no_argument, &Config.Network.NoRuntimeJoin, 1},
			{"runtimejoin", no_argument, &Config.Network.NoRuntimeJoin, 0},
			{"noleague", no_argument, &Config.Network.LeagueServerSignUp, 0},
			{"league", no_argument, &Config.Network.LeagueServerSignUp, 1},
			{"nosignup", no_argument, &Config.Network.MasterServerSignUp, 0},
			{"signup", no_argument, &Config.Network.MasterServerSignUp, 1},
			
			{"debugrecread", required_argument, nullptr, 'K'},
			{"debugrecwrite", required_argument, nullptr, 'w'},

			{"client", required_argument, nullptr, 'c'},
			{"host", no_argument, nullptr, 'h'},
			{"debughost", required_argument, nullptr, 'H'},
			{"debugpass", required_argument, nullptr, 'P'},
			{"debug", required_argument, nullptr, 'D'},
			{"data", required_argument, nullptr, 'd'},
			{"startup", required_argument, nullptr, 's'},
			{"stream", required_argument, nullptr, 'e'},
			{"recdump", required_argument, nullptr, 'R'},
			{"comment", required_argument, nullptr, 'm'},
			{"pass", required_argument, nullptr, 'p'},
			{"udpport", required_argument, nullptr, 'u'},
			{"tcpport", required_argument, nullptr, 't'},
			{"join", required_argument, nullptr, 'j'},
			{"language", required_argument, nullptr, 'L'},
			{"scenpar", required_argument, nullptr, 'S'},

			{"observe", no_argument, nullptr, 'o'},
			{"nonetwork", no_argument, nullptr, 'N'},
			{"network", no_argument, nullptr, 'n'},
			{"record", no_argument, nullptr, 'r'},

			{"lobby", optional_argument, nullptr, 'l'},

			{"debug-opengl", no_argument, &Config.Graphics.DebugOpenGL, 1},
			{"config", required_argument, nullptr, 0},
			{nullptr, 0, nullptr, 0}
		};
		int option_index = 0;
		c = getopt_long (argc, argv, "abc:d:f:",
			long_options, &option_index);
     		// no more options
		if (c == -1)
			break;
		switch (c)
		{
		case 0:
			// Signup
			if (SEqualNoCase(long_options[option_index].name, "signup"))
			{
				Game.NetworkActive = true;
			}
			// League
			if (SEqualNoCase(long_options[option_index].name, "league"))
			{
				Game.NetworkActive = true;
				Config.Network.MasterServerSignUp = true;
			}
			// Config: Already handled earlier.
			break;
		// Lobby
		case 'l':
			Game.fLobby = true;
			// lobby timeout specified? (e.g. --lobby=120)
			if (optarg)
			{
				Game.iLobbyTimeout = atoi(optarg);
				if (Game.iLobbyTimeout < 0) Game.iLobbyTimeout = 0;
			}
			break;
		case 'o': Game.fObserve = true; break;
		// Direct join
		case 'j':
			Game.NetworkActive = true;
			SCopy(optarg, Game.DirectJoinAddress, _MAX_PATH);
			break;
		case 'K':
			if (optarg && optarg[0])
			{
				LogF("Reading from DebugRec file '%s'", optarg);
				SCopy(optarg, Config.General.DebugRecExternalFile, _MAX_PATH);
			}
			else
				Log("Reading DebugRec from CtrlRec file in scenario record");
			Config.General.DebugRec = 1;
			Config.General.DebugRecWrite = 0;
			break;
		case 'w':
			if (optarg && optarg[0])
			{
				LogF("Writing to DebugRec file '%s'", optarg);
				SCopy(optarg, Config.General.DebugRecExternalFile, _MAX_PATH);
			}
			else
				Log("Writing DebugRec to CtrlRec file in scenario record");
			Config.General.DebugRec = 1;
			Config.General.DebugRecWrite = 1;
			break;
		case 'r': Game.Record = true; break;
		case 'n': Game.NetworkActive = true; break;
		case 'N': Game.NetworkActive = false; break;
		// Language override by parameter
		case 'L': SCopy(optarg, Config.General.LanguageEx, CFG_MaxString);
		// port overrides
		case 't': Config.Network.PortTCP = atoi(optarg); break;
		case 'u': Config.Network.PortUDP = atoi(optarg); break;
		// network game password
		case 'p': Network.SetPassword(optarg); break;
		// network game comment
		case 'm': Config.Network.Comment.CopyValidated(optarg); break;
		// record dump
		case 'R': Game.RecordDumpFile.Copy(optarg); break;
		// record stream
		case 'e': Game.RecordStream.Copy(optarg); break;
		// startup start screen
		case 's': C4Startup::SetStartScreen(optarg); break;
		// additional read-only data path
		case 'd': Reloc.AddPath(optarg); break;
		// debug options
		case 'D': Game.DebugPort = atoi(optarg); break;
		case 'P': Game.DebugPassword = optarg; break;
		case 'H': Game.DebugHost = optarg; break;
		// set custom scenario parameter by command line
		case 'S':
			{
			StdStrBuf sopt, soptval; sopt.Copy(optarg);
			int32_t val=1;
			if (sopt.SplitAtChar('=', &soptval)) val=atoi(soptval.getData());
			Game.StartupScenarioParameters.SetValue(sopt.getData(), val, false);
			}
			break;
		// debug configs
		case 'h':
			Game.NetworkActive = true;
			Game.fLobby = true;
			Config.Network.PortTCP = 11112;
			Config.Network.PortUDP = 11113;
			Config.Network.MasterServerSignUp = Config.Network.LeagueServerSignUp = false;
			break;
		case 'c':
			Game.NetworkActive = true;
			SCopy("localhost", Game.DirectJoinAddress, _MAX_PATH);
			Game.fLobby = true;
			Config.Network.PortTCP = 11112 + 2*(atoi(optarg)+1);
			Config.Network.PortUDP = 11113 + 2*(atoi(optarg)+1);
			break;
		case '?': /* getopt_long already printed an error message. */ break;
		default: assert(!"unexpected getopt_long return value");
		}
	}
	if (!Config.Network.MasterServerSignUp)
		Config.Network.LeagueServerSignUp = false;
	if (Game.fObserve || Game.fLobby)
		Game.NetworkActive = true;

	while (optind < argc)
	{
		char * szParameter = argv[optind++];
		{ // Strip trailing / that result from tab-completing unpacked c4groups
			int iLen = SLen(szParameter);
			if (iLen > 5 && szParameter[iLen-1] == '/' && szParameter[iLen-5] == '.' && szParameter[iLen-4] == 'o' && szParameter[iLen-3] == 'c')
			{
				szParameter[iLen-1] = '\0';
			}
		}
		// Scenario file
		if (SEqualNoCase(GetExtension(szParameter),"ocs"))
		{
			if(IsGlobalPath(szParameter))
				Game.SetScenarioFilename(szParameter);
			else
				Game.SetScenarioFilename((std::string(GetWorkingDirectory()) + DirSep + szParameter).c_str());

			continue;
		}
		if (SEqualNoCase(GetFilename(szParameter),"scenario.txt"))
		{
			Game.SetScenarioFilename(szParameter);
			continue;
		}
		// Player file
		if (SEqualNoCase(GetExtension(szParameter),"ocp"))
		{
			if(IsGlobalPath(szParameter))
				SAddModule(Game.PlayerFilenames, szParameter);
			else
				SAddModule(Game.PlayerFilenames, (std::string(GetWorkingDirectory()) + DirSep + szParameter).c_str());

			continue;
		}
		// Definition file
		if (SEqualNoCase(GetExtension(szParameter),"ocd"))
		{
			SAddModule(Game.DefinitionFilenames,szParameter);
			continue;
		}
		// Key file
		if (SEqualNoCase(GetExtension(szParameter),"c4k"))
		{
			Application.IncomingKeyfile = szParameter;
			continue;
		}
		// Update file
		if (SEqualNoCase(GetExtension(szParameter),"ocu"))
		{
			Application.IncomingUpdate = szParameter;
			continue;
		}
		// record stream
		if (SEqualNoCase(GetExtension(szParameter),"c4r"))
		{
			Game.RecordStream.Copy(szParameter);
		}
		// Direct join by URL
		if (SEqual2NoCase(szParameter, "clonk:"))
		{
			// Store address
			SCopy(szParameter + 6, Game.DirectJoinAddress, _MAX_PATH);
			SClearFrontBack(Game.DirectJoinAddress, '/');
			// Special case: if the target address is "update" then this is used for update initiation by url
			if (SEqualNoCase(Game.DirectJoinAddress, "update"))
			{
				Application.CheckForUpdates = true;
				Game.DirectJoinAddress[0] = 0;
				continue;
			}
			// Self-enable network
			Game.NetworkActive = true;
			continue;
		}
	}

#ifdef _WIN32
	// Clean up some forward/backward slach confusion since many internal OC file functions cannot handle both
	SReplaceChar(Game.ScenarioFilename, AltDirectorySeparator, DirectorySeparator);
	SReplaceChar(Game.PlayerFilenames, AltDirectorySeparator, DirectorySeparator);
	SReplaceChar(Game.DefinitionFilenames, AltDirectorySeparator, DirectorySeparator);
	std::replace(begin(IncomingKeyfile), end(IncomingKeyfile), AltDirectorySeparator, DirectorySeparator);
	std::replace(begin(IncomingUpdate), end(IncomingUpdate), AltDirectorySeparator, DirectorySeparator);
	Game.RecordStream.ReplaceChar(AltDirectorySeparator, DirectorySeparator);
#endif

	// Default to editor if scenario given, player mode otherwise
	if (isEditor == 2)
		isEditor = !!*Game.ScenarioFilename && !Config.General.OpenScenarioInGameMode;

	// record?
	Game.Record = Game.Record || (Config.Network.LeagueServerSignUp && Game.NetworkActive);

	// startup dialog required?
	QuitAfterGame = !isEditor && Game.HasScenario();
}

void C4Application::ApplyResolutionConstraints()
{
	// Not changing the resolution always works anyway
	if (Config.Graphics.ResX == -1 && Config.Graphics.ResY == -1)
		return;
	// Enumerate display modes
	int32_t idx = -1, iXRes, iYRes, iBitDepth, iRefreshRate;
	int32_t best_match = -1;
	uint32_t best_delta = ~0;
	while (GetIndexedDisplayMode(++idx, &iXRes, &iYRes, &iBitDepth, &iRefreshRate, Config.Graphics.Monitor))
	{
		if (iBitDepth != C4Draw::COLOR_DEPTH) continue;
		uint32_t delta = std::abs(Config.Graphics.ResX*Config.Graphics.ResY - iXRes*iYRes);
		if (!delta && iBitDepth == C4Draw::COLOR_DEPTH && iRefreshRate == Config.Graphics.RefreshRate)
			return; // Exactly the expected mode
		if (delta < best_delta)
		{
			// Better match than before
			best_match = idx;
			best_delta = delta;
		}
	}
	if (best_match != -1)
	{
		// Apply next-best mode
		GetIndexedDisplayMode(best_match, &iXRes, &iYRes, &iBitDepth, &iRefreshRate, Config.Graphics.Monitor);
		if (iXRes != Config.Graphics.ResX || iYRes != Config.Graphics.ResY)
			// Don't warn if only bit depth changes
			// Also, lang table not loaded yet
			LogF("Warning: The selected resolution %dx%d is not available and has been changed to %dx%d.", Config.Graphics.ResX, Config.Graphics.ResY, iXRes, iYRes);
		Config.Graphics.ResX = iXRes; Config.Graphics.ResY = iYRes;
		Config.Graphics.RefreshRate = iRefreshRate;
	}
}

bool C4Application::PreInit()
{
	// startup dialog: Only use if no next mission has been provided
	bool fUseStartupDialog = !Game.HasScenario();

	// Load graphics early, before we draw anything, since we need shaders
	// loaded to draw.
	Game.SetInitProgress(0.0f);
	Log(LoadResStr("IDS_PRC_GFXRES"));
	if (!GraphicsResource.Init()) return false;
	Game.SetInitProgress(fUseStartupDialog ? 10.0f : 1.0f);

	// Startup message board
	if (!isEditor)
		if (Config.Graphics.ShowStartupMessages || Game.NetworkActive)
		{
			C4Facet cgo; cgo.Set(FullScreen.pSurface,0,0,C4GUI::GetScreenWdt(), C4GUI::GetScreenHgt());
			GraphicsSystem.MessageBoard->Init(cgo,true);
		}

	// init loader: Black screen for first start if a video is to be shown; otherwise default spec
	if (fUseStartupDialog && !isEditor)
	{
		if (!::GraphicsSystem.InitLoaderScreen(C4CFN_StartupBackgroundMain))
			{ LogFatal(LoadResStr("IDS_PRC_ERRLOADER")); return false; }
	}
	Game.SetInitProgress(fUseStartupDialog ? 20.0f : 2.0f);

	if (!Game.PreInit()) return false;

	// Music
	if (!MusicSystem.Init("frontend"))
		Log(LoadResStr("IDS_PRC_NOMUSIC"));

	Game.SetInitProgress(fUseStartupDialog ? 34.0f : 2.0f);

	// Sound
	if (!SoundSystem.Init())
		Log(LoadResStr("IDS_PRC_NOSND"));

	// Play some music! - after sound init because sound system might be needed by music system
	if (fUseStartupDialog && !isEditor && Config.Sound.FEMusic)
		MusicSystem.Play();

	Game.SetInitProgress(fUseStartupDialog ? 35.0f : 3.0f);

	if (fUseStartupDialog)
	{
		AppState = C4AS_Startup;
		// default record?
		Game.Record = Game.Record || Config.General.DefRec;
		// if no scenario or direct join has been specified, get game startup parameters by startup dialog
		if (!isEditor)
			C4Startup::InitStartup();
	}
	// directly launch scenario / network game
	else
	{
		AppState = C4AS_StartGame;
	}

	return true;
}

bool C4Application::ProcessCallback(const char *szMessage, int iProcess)
{
	Console.Out(szMessage);
	return true;
}

void C4Application::Clear()
{
	Game.Clear();
	NextMission.clear();
	// stop timer
	if (pGameTimer)
	{
		Remove(pGameTimer);
		delete pGameTimer; pGameTimer = nullptr;
	}
	// quit irc
	IRCClient.Close();
	// close system group (System.ocg)
	SystemGroup.Close();
	// Log
	if (::Languages.HasStringTable()) // Avoid (double and undefined) message on (second?) shutdown...
		Log(LoadResStr("IDS_PRC_DEINIT"));
	// Clear external language packs and string table
	Languages.Clear();
	Languages.ClearLanguage();
	// gamepad clear
	if (pGamePadControl) { delete pGamePadControl; pGamePadControl=nullptr; }
	// music system clear
	MusicSystem.Clear();
	SoundSystem.Clear();
	RestoreVideoMode();
	// clear editcursor holding graphics before clearing draw
	::Console.EditCursor.Clear();
	// Clear direct draw (late, because it's needed for e.g. Log)
	if (pDraw) { delete pDraw; pDraw=nullptr; }
	// Close window
	FullScreen.Clear();
	Console.Clear();
	// There might be pending saves - do them after the fullscreen windows got closed
	// so the app just remains as a lingering process until saving is done
	CPNGFile::WaitForSaves();
	// The very final stuff
	C4AbstractApp::Clear();
}

void C4Application::Quit()
{
	// Participants should not be cleared for usual startup dialog

	// Save config if there was no loading error
	if (Config.fConfigLoaded) Config.Save();
	// make sure startup data is unloaded
	C4Startup::Unload();
	// fonts are loaded at start and never unloaded
	::GraphicsResource.ClearFonts();
	// quit app
	C4AbstractApp::Quit();
	AppState = C4AS_Quit;
}

void C4Application::OpenGame(const char * scenario)
{
	if (AppState == C4AS_Startup)
	{
		if (scenario) Game.SetScenarioFilename(scenario);
		AppState = C4AS_StartGame;
	}
	else
	{
		SetNextMission(scenario);
		AppState = C4AS_AfterGame;
	}

}

void C4Application::QuitGame()
{
	// reinit desired? Do restart
	if (!QuitAfterGame || !NextMission.empty())
	{
		AppState = C4AS_AfterGame;
	}
	else
	{
		Quit();
	}
}

void C4Application::GameTick()
{
	// Exec depending on game state
	switch (AppState)
	{
	case C4AS_None:
		assert(AppState != C4AS_None);
		break;
	case C4AS_Quit:
		// Do nothing, the main loop will exit soon
		break;
	case C4AS_PreInit:
		if (!PreInit()) Quit();
		break;
	case C4AS_Startup:
		SoundSystem.Execute();
		MusicSystem.Execute();
		if (pGamePadControl) pGamePadControl->Execute();
		// wait for the user to start a game
		break;
	case C4AS_StartGame:
		// immediate progress to next state; OpenGame will enter HandleMessage-loops in startup and lobby!
		C4Startup::CloseStartup();
		AppState = C4AS_Game;
#ifdef WITH_QT_EDITOR
		// Notify console
		if (isEditor) ::Console.OnStartGame();
#endif
		// first-time game initialization
		if (!Game.Init())
		{
			// set error flag (unless this was a lobby user abort)
			if (!C4GameLobby::UserAbort)
				Game.fQuitWithError = true;
			// no start: Regular QuitGame; this may reset the engine to startup mode if desired
			QuitGame();
			break;
		}
		if(Config.Graphics.Windowed == 2 && FullScreenMode())
			Application.SetVideoMode(GetConfigWidth(), GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, true);
		if (!isEditor)
			pWindow->GrabMouse(true);
		// Gamepad events have to be polled here so that the controller
		// connection state is always up-to-date before players are
		// joining.
		if (pGamePadControl) pGamePadControl->Execute();
		break;
	case C4AS_AfterGame:
		// stop game
		Game.Clear();
		if(Config.Graphics.Windowed == 2 && NextMission.empty() && !isEditor)
			Application.SetVideoMode(GetConfigWidth(), GetConfigHeight(), Config.Graphics.RefreshRate, Config.Graphics.Monitor, false);
		if (!isEditor)
			pWindow->GrabMouse(false);
		AppState = C4AS_PreInit;
		// if a next mission is desired, set to start it
		if (!NextMission.empty())
		{
			Game.SetScenarioFilename(NextMission.c_str());
			Game.fLobby = Game.NetworkActive;
			Game.fObserve = false;
			NextMission.clear();
		}
		break;
	case C4AS_Game:
		// Game
		if (Game.IsRunning)
			Game.Execute();
		// Sound
		SoundSystem.Execute();
		MusicSystem.Execute();
		// Gamepad
		if (pGamePadControl) pGamePadControl->Execute();
		break;
	}
}

void C4Application::Draw()
{
	// Graphics

	// Fullscreen mode
	if (!isEditor)
		FullScreen.Execute();
	// Console mode
	else
		Console.Execute();
}

void C4Application::SetGameTickDelay(int iDelay)
{
	if (!pGameTimer) return;
	pGameTimer->SetGameTickDelay(iDelay);
}

void C4Application::OnResolutionChanged(unsigned int iXRes, unsigned int iYRes)
{
	// notify game
	if (pDraw)
	{
		Game.OnResolutionChanged(iXRes, iYRes);
		pDraw->OnResolutionChanged(iXRes, iYRes);
	}
	if (pWindow)
	{
		if (pWindow->pSurface)
			pWindow->pSurface->UpdateSize(iXRes, iYRes);
		if (!FullScreenMode())
		{
			C4Rect r;
			pWindow->GetSize(&r);
			Config.Graphics.WindowX = r.Wdt;
			Config.Graphics.WindowY = r.Hgt;
		}
	}
}

void C4Application::OnKeyboardLayoutChanged()
{
	// re-resolve all keys
	Game.OnKeyboardLayoutChanged();
	if (AppState == C4AS_Startup) C4Startup::Get()->OnKeyboardLayoutChanged();
}

bool C4Application::SetGameFont(const char *szFontFace, int32_t iFontSize)
{
#ifndef USE_CONSOLE
	// safety
	if (!szFontFace || !*szFontFace || iFontSize<1 || SLen(szFontFace)>=static_cast<int>(sizeof Config.General.RXFontName)) return false;
	// first, check if the selected font can be created at all
	// check regular font only - there's no reason why the other fonts couldn't be created
	CStdFont TestFont;
	if (!::FontLoader.InitFont(&TestFont, szFontFace, C4FontLoader::C4FT_Main, iFontSize, &::GraphicsResource.Files))
		return false;
	// OK; reinit all fonts
	StdStrBuf sOldFont; sOldFont.Copy(Config.General.RXFontName);
	int32_t iOldFontSize = Config.General.RXFontSize;
	SCopy(szFontFace, Config.General.RXFontName);
	Config.General.RXFontSize = iFontSize;
	if (!::GraphicsResource.InitFonts() || !C4Startup::Get()->Graphics.InitFonts())
	{
		// failed :o
		// shouldn't happen. Better restore config.
		SCopy(sOldFont.getData(), Config.General.RXFontName);
		Config.General.RXFontSize = iOldFontSize;
		return false;
	}
#endif
	// save changes
	return true;
}

void C4Application::OnCommand(const char *szCmd)
{
	if (AppState == C4AS_Game)
		::MessageInput.ProcessInput(szCmd);
	else if (AppState == C4AS_Startup)
	{
		AppState = C4AS_PreInit;
		Game.SetScenarioFilename(szCmd);
	}
}

void C4Application::Activate()
{
#ifdef USE_WIN32_WINDOWS
	BringWindowToTop(FullScreen.hWindow);
	ShowWindow(FullScreen.hWindow, SW_SHOW);
#endif
}

void C4Application::SetNextMission(const char *szMissionFilename)
{
	// set next mission if any is desired
	if (szMissionFilename)
	{
		NextMission = szMissionFilename;
		// scenarios tend to use the wrong slash
		std::replace(begin(NextMission), end(NextMission), AltDirectorySeparator, DirectorySeparator);
	}
	else
		NextMission.clear();
}

void C4Application::NextTick()
{
	if (!pGameTimer) return;
	pGameTimer->Set();
}

bool C4Application::FullScreenMode()
{
	if(isEditor)
		return false;
	if(!Config.Graphics.Windowed)
		return true;
	if(Config.Graphics.Windowed == 2 && Game.IsRunning)
		return true;
	return false;
}

// *** C4ApplicationGameTimer

C4ApplicationGameTimer::C4ApplicationGameTimer()
		: CStdMultimediaTimerProc(26),
		tLastGameTick(C4TimeMilliseconds::NegativeInfinity)
{
}

void C4ApplicationGameTimer::SetGameTickDelay(uint32_t iDelay)
{
	// Remember delay
	iGameTickDelay = iDelay;
	// Smaller than minimum refresh delay?
	if (iDelay < uint32_t(Config.Graphics.MaxRefreshDelay))
	{
		// Set critical timer
		SetDelay(iDelay);
		// No additional breaking needed
		iExtraGameTickDelay = 0;
	}
	else
	{
		// Set critical timer
		SetDelay(Config.Graphics.MaxRefreshDelay);
		// Slow down game tick
		iExtraGameTickDelay = iDelay;
	}
}

bool C4ApplicationGameTimer::Execute(int iTimeout, pollfd *)
{
	// Check timer and reset
	if (!CheckAndReset()) return true;
	C4TimeMilliseconds tNow = C4TimeMilliseconds::Now();
	// Execute
	if (tNow >= tLastGameTick + iExtraGameTickDelay || Game.GameGo)
	{
		if (iGameTickDelay)
			tLastGameTick += iGameTickDelay;
		else
			tLastGameTick = tNow;

		// Compensate if things get too slow
		if (tNow > tLastGameTick + iGameTickDelay)
			tLastGameTick += (tNow - tLastGameTick) / 2;

		Application.GameTick();
	}
	// Draw
	if (!Game.DoSkipFrame)
	{
		C4TimeMilliseconds tPreGfxTime = C4TimeMilliseconds::Now();

		Application.Draw();

		// Automatic frame skip if graphics are slowing down the game (skip max. every 2nd frame)
		Game.DoSkipFrame = Game.Parameters.AutoFrameSkip && (tPreGfxTime + iGameTickDelay < C4TimeMilliseconds::Now());
	} else {
		Game.DoSkipFrame=false;
	}
	return true;
}

bool  C4ApplicationGameTimer::IsLowPriority() { return true; }
