// Network Mode
var g_IsNetworked = false;

// Is this user in control of game settings (i.e. is a network server, or offline player)
var g_IsController;
// Match ID for tracking
var g_MatchID;
// Is this user an observer?
var g_IsObserver = false;

// Cache the basic player data (name, civ, color)
var g_Players = [];
// Cache the useful civ data
var g_CivData = {};

var g_GameSpeeds = {};
var g_CurrentSpeed;

var g_PlayerAssignments = { "local": { "name": translate("You"), "player": 1 } };

// Cache dev-mode settings that are frequently or widely used
var g_DevSettings = {
	controlAll: false
};

// Whether status bars should be shown for all of the player's units.
var g_ShowAllStatusBars = false;

// Indicate when one of the current player's training queues is blocked
// (this is used to support population counter blinking)
var g_IsTrainingBlocked = false;

// Cache simulation state (updated on every simulation update)
var g_SimState;

// Cache EntityStates
var g_EntityStates = {}; // {id:entState}

// Whether the player has lost/won and reached the end of their game
var g_GameEnded = false;

var g_Disconnected = false; // Lost connection to server

// Holds player states from the last tick
var g_CachedLastStates = "";

// Colors to flash when pop limit reached
const DEFAULT_POPULATION_COLOR = "white";
const POPULATION_ALERT_COLOR = "orange";

// List of additional entities to highlight
var g_ShowGuarding = false;
var g_ShowGuarded = false;
var g_AdditionalHighlight = [];

// for saving the hitpoins of the hero (is there a better way to do that?) 
// Should be possible with AttackDetection but might be an overkill because it would have to loop
// always through the list of all ongoing attacks...
var g_previousHeroHitPoints = undefined;

function GetSimState()
{
	if (!g_SimState)
		g_SimState = Engine.GuiInterfaceCall("GetSimulationState");

	return g_SimState;
}

function GetEntityState(entId)
{
	if (!(entId in g_EntityStates))
	{
		var entState = Engine.GuiInterfaceCall("GetEntityState", entId);
		if (entState)
			entState.extended = false;
		g_EntityStates[entId] = entState;
	}

	return g_EntityStates[entId];
}

function GetExtendedEntityState(entId)
{
	if (entId in g_EntityStates)
		var entState = g_EntityStates[entId];
	else
		var entState = Engine.GuiInterfaceCall("GetEntityState", entId);

	if (!entState || entState.extended)
		return entState;

	var extension = Engine.GuiInterfaceCall("GetExtendedEntityState", entId);
	for (var prop in extension)
		entState[prop] = extension[prop];
	entState.extended = true;
	g_EntityStates[entId] = entState;

	return entState;
}

// Cache TemplateData
var g_TemplateData = {}; // {id:template}
var g_TemplateDataWithoutLocalization = {};


function GetTemplateData(templateName)
{
	if (!(templateName in g_TemplateData))
	{
		var template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
		translateObjectKeys(template, ["specific", "generic", "tooltip"]);
		g_TemplateData[templateName] = template;
	}

	return g_TemplateData[templateName];
}

function GetTemplateDataWithoutLocalization(templateName)
{
	if (!(templateName in g_TemplateDataWithoutLocalization))
	{
		var template = Engine.GuiInterfaceCall("GetTemplateData", templateName);
		g_TemplateDataWithoutLocalization[templateName] = template;
	}

	return g_TemplateDataWithoutLocalization[templateName];
}

// Cache TechnologyData
var g_TechnologyData = {}; // {id:template}

function GetTechnologyData(technologyName)
{
	if (!(technologyName in g_TechnologyData))
	{
		var template = Engine.GuiInterfaceCall("GetTechnologyData", technologyName);
		translateObjectKeys(template, ["specific", "generic", "description", "tooltip", "requirementsTooltip"]);
		g_TechnologyData[technologyName] = template;
	}

	return g_TechnologyData[technologyName];
}

// Init
function init(initData, hotloadData)
{
	if (initData)
	{
		g_IsNetworked = initData.isNetworked; // Set network mode
		g_IsController = initData.isController; // Set controller mode
		g_PlayerAssignments = initData.playerAssignments;
		g_MatchID = initData.attribs.matchID;

		// Cache the player data
		// (This may be updated at runtime by handleNetMessage)
		g_Players = getPlayerData(g_PlayerAssignments);

		if (initData.savedGUIData)
			restoreSavedGameData(initData.savedGUIData);

		Engine.GetGUIObjectByName("gameSpeedButton").hidden = g_IsNetworked;
	}
	else // Needed for autostart loading option
	{
		g_Players = getPlayerData(null);
	}

	// Cache civ data
	g_CivData = loadCivData();
	g_CivData["gaia"] = { "Code": "gaia", "Name": translate("Gaia") };

	if (Engine.GetPlayerID() <= 0)
	{
		g_IsObserver = true;
		// Hide stuff observers don't use.
		Engine.GetGUIObjectByName("food").hidden = true;
		Engine.GetGUIObjectByName("wood").hidden = true;
		Engine.GetGUIObjectByName("stone").hidden = true;
		Engine.GetGUIObjectByName("metal").hidden = true;
		Engine.GetGUIObjectByName("population").hidden = true;
		Engine.GetGUIObjectByName("diplomacyButton1").hidden = true;
		Engine.GetGUIObjectByName("tradeButton1").hidden = true;
		Engine.GetGUIObjectByName("menuResignButton").enabled = false;
		Engine.GetGUIObjectByName("pauseButton").enabled = false;
		Engine.GetGUIObjectByName("observerText").hidden = false;
	}
	else
	{
		// TODO: Get a civ icon for gaia/observers.
		Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[Engine.GetPlayerID()].civ].Emblem;
		Engine.GetGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[Engine.GetPlayerID()].civ].Name;
	}

	g_GameSpeeds = initGameSpeeds();
	g_CurrentSpeed = Engine.GetSimRate();
	var gameSpeed = Engine.GetGUIObjectByName("gameSpeed");
	gameSpeed.list = g_GameSpeeds.names;
	gameSpeed.list_data = g_GameSpeeds.speeds;
	var idx = g_GameSpeeds.speeds.indexOf(g_CurrentSpeed);
	gameSpeed.selected = idx != -1 ? idx : g_GameSpeeds["default"];
	gameSpeed.onSelectionChange = function() { changeGameSpeed(+this.list_data[this.selected]); }
	initMenuPosition(); // set initial position

	// Populate player selection dropdown
	var playerNames = [];
	var playerIDs = [];
	for (var player in g_Players)
	{
		playerNames.push(g_Players[player].name);
		playerIDs.push(player);
	}

	var viewPlayerDropdown = Engine.GetGUIObjectByName("viewPlayer");
	viewPlayerDropdown.list = playerNames;
	viewPlayerDropdown.list_data = playerIDs;
	viewPlayerDropdown.selected = Engine.GetPlayerID();

	// If in Atlas editor, disable the exit button
	if (Engine.IsAtlasRunning())
		Engine.GetGUIObjectByName("menuExitButton").enabled = false;

	if (hotloadData)
	{
		g_Selection.selected = hotloadData.selection;
	}
	// Starting for the first time:
	initMusic();
	if (!g_IsObserver){
		var civMusic = g_CivData[g_Players[Engine.GetPlayerID()].civ].Music;
		global.music.storeTracks(civMusic);
	}
	global.music.setState(global.music.states.PEACE);
	playRandomAmbient("temperate");
	onSimulationUpdate();

	// Report the performance after 5 seconds (when we're still near
	// the initial camera view) and a minute (when the profiler will
	// have settled down if framerates as very low), to give some
	// extremely rough indications of performance
	//
	// DISABLED: this information isn't currently useful for anything much,
	// and it generates a massive amount of data to transmit and store
	//setTimeout(function() { reportPerformance(5); }, 5000);
	//setTimeout(function() { reportPerformance(60); }, 60000);
}

function selectViewPlayer(playerID)
{
	Engine.SetPlayerID(playerID);
	if (playerID > 0) {
		Engine.GetGUIObjectByName("civIcon").sprite = "stretched:" + g_CivData[g_Players[playerID].civ].Emblem;
		Engine.GetGUIObjectByName("civIcon").tooltip = g_CivData[g_Players[playerID].civ].Name;
	}
}

function reportPerformance(time)
{
	var settings = Engine.GetMapSettings();
	var data = {
		time: time,
		map: settings.Name,
		seed: settings.Seed, // only defined for random maps
		size: settings.Size, // only defined for random maps
		profiler: Engine.GetProfilerState()
	};

	Engine.SubmitUserReport("profile", 3, JSON.stringify(data));
}

/**
 * Resign a player.
 * @param leaveGameAfterResign If player is quitting after resignation.
 */
function resignGame(leaveGameAfterResign)
{
	var simState = GetSimState();

	// Players can't resign if they've already won or lost.
	if (simState.players[Engine.GetPlayerID()].state != "active" || g_Disconnected)
		return;

	// Tell other players that we have given up and been defeated
	Engine.PostNetworkCommand({
		"type": "defeat-player",
		"playerId": Engine.GetPlayerID()
	});

	global.music.setState(global.music.states.DEFEAT);
	
	// Resume the game if not resigning.
	if (!leaveGameAfterResign)
		resumeGame();
}

/**
 * Leave the game
 * @param willRejoin If player is going to be rejoining a networked game.
 */
function leaveGame(willRejoin)
{
	var extendedSimState = Engine.GuiInterfaceCall("GetExtendedSimulationState");
	var mapSettings = Engine.GetMapSettings();
	var gameResult;

	if (g_IsObserver)
	{
		// Observers don't win/lose.
		gameResult = translate("You have left the game.");
		global.music.setState(global.music.states.VICTORY);
	}
	else
	{
		var playerState = extendedSimState.players[Engine.GetPlayerID()];
		if (g_Disconnected)
			gameResult = translate("You have been disconnected.");
		else if (playerState.state == "won")
			gameResult = translate("You have won the battle!");
		else if (playerState.state == "defeated")
			gameResult = translate("You have been defeated...");
		else // "active"
		{
			global.music.setState(global.music.states.DEFEAT);
			if (willRejoin)
				gameResult = translate("You have left the game.");
			else
			{
				gameResult = translate("You have abandoned the game.");
				resignGame(true);
			}
		}
	}

	stopAmbient();
	Engine.EndGame();

	if (g_IsController && Engine.HasXmppClient())
		Engine.SendUnregisterGame();

	Engine.SwitchGuiPage("page_summary.xml", {
							"gameResult"  : gameResult,
							"timeElapsed" : extendedSimState.timeElapsed,
							"playerStates": extendedSimState.players,
							"players": g_Players,
							"mapSettings": mapSettings
						 });
}

// Return some data that we'll use when hotloading this file after changes
function getHotloadData()
{
	return { selection: g_Selection.selected };
}

// Return some data that will be stored in saved game files
function getSavedGameData()
{
	var data = {};
	data.playerAssignments = g_PlayerAssignments;
	data.groups = g_Groups.groups;
	// TODO: any other gui state?
	return data;
}

function restoreSavedGameData(data)
{
	// Restore camera if any
	if (data.camera)
		Engine.SetCameraData(data.camera.PosX, data.camera.PosY, data.camera.PosZ,
			data.camera.RotX, data.camera.RotY, data.camera.Zoom);

	// Clear selection when loading a game
	g_Selection.reset();

	// Restore control groups
	for (var groupNumber in data.groups)
	{
		g_Groups.groups[groupNumber].groups = data.groups[groupNumber].groups;
		g_Groups.groups[groupNumber].ents = data.groups[groupNumber].ents;
	}
	updateGroups();
}

var lastTickTime = new Date;
var lastXmppClientPoll = Date.now();

/**
 * Called every frame.
 */
function onTick()
{
	var now = new Date;
	var tickLength = new Date - lastTickTime;
	lastTickTime = now;

	checkPlayerState();

	while (true)
	{
		var message = Engine.PollNetworkClient();
		if (!message)
			break;
		handleNetMessage(message);
	}

	updateCursorAndTooltip();

	// If the selection changed, we need to regenerate the sim display (the display depends on both the
	// simulation state and the current selection).
	if (g_Selection.dirty)
	{
		g_Selection.dirty = false;

		onSimulationUpdate();

		// Display rally points for selected buildings
		if (!g_IsObserver)
			Engine.GuiInterfaceCall("DisplayRallyPoint", { "entities": g_Selection.toList() });
	}

	// Run timers
	updateTimers();

	// Animate menu
	updateMenuPosition(tickLength);

	// When training is blocked, flash population (alternates colour every 500msec)
	if (g_IsTrainingBlocked && (Date.now() % 1000) < 500)
		Engine.GetGUIObjectByName("resourcePop").textcolor = POPULATION_ALERT_COLOR;
	else
		Engine.GetGUIObjectByName("resourcePop").textcolor = DEFAULT_POPULATION_COLOR;

	// Clear renamed entities list
	Engine.GuiInterfaceCall("ClearRenamedEntities");
}

function checkPlayerState()
{
	// Once the game ends, we're done here.
	if (g_GameEnded || g_IsObserver)
		return;

	// Send a game report for each player in this game.
	var m_simState = GetSimState();
	var playerState = m_simState.players[Engine.GetPlayerID()];
	var tempStates = "";
	for each (var player in m_simState.players) {tempStates += player.state + ",";}

	if (g_CachedLastStates != tempStates)
	{
		g_CachedLastStates = tempStates;
		reportGame(Engine.GuiInterfaceCall("GetExtendedSimulationState"));
	}

	// If the local player hasn't finished playing, we return here to avoid the victory/defeat messages.
	if (playerState.state == "active")
		return;

	// We can't resign once the game is over.
	Engine.GetGUIObjectByName("menuResignButton").enabled = false;

	// Make sure nothing is open to avoid stacking.
	closeMenu();
	closeOpenDialogs();

	// Make sure this doesn't run again.
	g_GameEnded = true;

	if (Engine.IsAtlasRunning())
	{
		// If we're in Atlas, we can't leave the game
		var btCaptions = [translate("OK")];
		var btCode = [null];
		var message = translate("Press OK to continue");
	}
	else
	{
		var btCaptions = [translate("No"), translate("Yes")];
		var btCode = [null, leaveGame];
		var message = translate("Do you want to quit?");
	}

	if (playerState.state == "defeated")
	{
		global.music.setState(global.music.states.DEFEAT);
		messageBox(400, 200, message, translate("DEFEATED!"), 0, btCaptions, btCode);
	}
	else if (playerState.state == "won")
	{
		global.music.setState(global.music.states.VICTORY);
		// TODO: Reveal map directly instead of this silly proxy.
		if (!Engine.GetGUIObjectByName("devCommandsRevealMap").checked)
			Engine.GetGUIObjectByName("devCommandsRevealMap").checked = true;
		messageBox(400, 200, message, translate("VICTORIOUS!"), 0, btCaptions, btCode);
	}
}

function changeGameSpeed(speed)
{
	// For non-networked games only
	if (!g_IsNetworked)
	{
		Engine.SetSimRate(speed);
		g_CurrentSpeed = speed;
	}
}

/**
 * Recomputes GUI state that depends on simulation state or selection state. Called directly every simulation
 * update (see session.xml), or from onTick when the selection has changed.
 */
function onSimulationUpdate()
{
	g_EntityStates = {};
	g_TemplateData = {};
	g_TechnologyData = {};

	g_SimState = Engine.GuiInterfaceCall("GetSimulationState");

	// If we're called during init when the game is first loading, there will be no simulation yet, so do nothing
	if (!g_SimState)
		return;

	handleNotifications();

	if (g_ShowAllStatusBars)
		recalculateStatusBarDisplay();

	if (g_ShowGuarding || g_ShowGuarded)
		updateAdditionalHighlight();

	updateHero();
	updateGroups();
	updateDebug();
	updatePlayerDisplay();
	updateSelectionDetails();
	updateBuildingPlacementPreview();
	updateTimeElapsedCounter();
	updateTimeNotifications();
	if (!g_IsObserver)
		updateResearchDisplay();

	if (!g_IsObserver && !g_GameEnded)
	{
		// Update music state on basis of battle state.
		var battleState = Engine.GuiInterfaceCall("GetBattleState", Engine.GetPlayerID());
		if (battleState)
			global.music.setState(global.music.states[battleState]);
	}
}

/**
* updates a status bar on the GUI
* nameOfBar: name of the bar
* points: points to show
* maxPoints: max points 
* direction: gets less from (right to left) 0; (top to bottom) 1; (left to right) 2; (bottom to top) 3;
*/
function updateGUIStatusBar(nameOfBar, points, maxPoints, direction) 
{
	// check, if optional direction parameter is valid.
	if (!direction || !(direction >= 0 && direction < 4))
		direction = 0;

	// get the bar and update it
	var statusBar = Engine.GetGUIObjectByName(nameOfBar);
	if (!statusBar) 
		return;
		
	var healthSize = statusBar.size;
	var value = 100*Math.max(0, Math.min(1, points / maxPoints));
	
	// inverse bar
	if(direction == 2 || direction == 3) 
		value = 100 - value;

	if(direction == 0)
		healthSize.rright = value;
	else if(direction == 1)
		healthSize.rbottom = value;
	else if(direction == 2)
		healthSize.rleft = value;
	else if(direction == 3)
		healthSize.rtop = value;
	
	// update bar
	statusBar.size = healthSize;
}


function updateHero()
{
	var simState = GetSimState();
	var playerState = simState.players[Engine.GetPlayerID()];
	var unitHeroPanel = Engine.GetGUIObjectByName("unitHeroPanel");
	var heroButton = Engine.GetGUIObjectByName("unitHeroButton");

	if (!playerState || playerState.heroes.length <= 0)
	{
		g_previousHeroHitPoints = undefined;
		unitHeroPanel.hidden = true;
		return;
	}

	var heroImage = Engine.GetGUIObjectByName("unitHeroImage");
	var heroState = GetExtendedEntityState(playerState.heroes[0]);
	var template = GetTemplateData(heroState.template);
	heroImage.sprite = "stretched:session/portraits/" + template.icon;
	var hero = playerState.heroes[0];

	heroButton.onpress = function()
	{
		if (!Engine.HotkeyIsPressed("selection.add"))
			g_Selection.reset();
		g_Selection.addList([hero]);
	};
	heroButton.ondoublepress = function() { selectAndMoveTo(getEntityOrHolder(hero)); };
	unitHeroPanel.hidden = false;

	// Setup tooltip
	var tooltip = "[font=\"sans-bold-16\"]" + template.name.specific + "[/font]";
	var healthLabel = "[font=\"sans-bold-13\"]" + translate("Health:") + "[/font]";
	tooltip += "\n" + sprintf(translate("%(label)s %(current)s / %(max)s"), { label: healthLabel, current: heroState.hitpoints, max: heroState.maxHitpoints });
	if (heroState.attack)
		tooltip += "\n" + getAttackTooltip(heroState);

	tooltip += "\n" + getArmorTooltip(heroState.armour);
	tooltip += "\n" + template.tooltip;

	heroButton.tooltip = tooltip;
	
	// update heros health bar
	updateGUIStatusBar("heroHealthBar", heroState.hitpoints, heroState.maxHitpoints);
	
	// define the hit points if not defined
	if (!g_previousHeroHitPoints)
		g_previousHeroHitPoints = heroState.hitpoints;
	
	// if the health of the hero changed since the last update, trigger the animation
	if (heroState.hitpoints < g_previousHeroHitPoints)
		startColorFade("heroHitOverlay", 100, 0, colorFade_attackUnit, true, smoothColorFadeRestart_attackUnit);

	g_previousHeroHitPoints = heroState.hitpoints;
}


function updateGroups()
{
	var guiName = "Group";
	g_Groups.update();
	for (var i = 0; i < 10; i++)
	{
		var button = Engine.GetGUIObjectByName("unit"+guiName+"Button["+i+"]");
		var label = Engine.GetGUIObjectByName("unit"+guiName+"Label["+i+"]").caption = i;
		if (g_Groups.groups[i].getTotalCount() == 0)
			button.hidden = true;
		else
			button.hidden = false;
		button.onpress = (function(i) { return function() { performGroup((Engine.HotkeyIsPressed("selection.add") ? "add" : "select"), i); } })(i);
		button.ondoublepress = (function(i) { return function() { performGroup("snap", i); } })(i);
		button.onpressright = (function(i) { return function() { performGroup("breakUp", i); } })(i);
		setPanelObjectPosition(button, i, 1);
	}
}

function updateDebug()
{
	let simState = GetSimState();
	let debug = Engine.GetGUIObjectByName("debug");

	if (!Engine.GetGUIObjectByName("devDisplayState").checked)
	{
		debug.hidden = true;
		return;
	}

	debug.hidden = false;

	let conciseSimState = deepcopy(simState);
	conciseSimState.players = "<<<omitted>>>";
	let text = "simulation: " + uneval(conciseSimState);

	let selection = g_Selection.toList();
	if (selection.length)
	{
		let entState = GetExtendedEntityState(selection[0]);
		if (entState)
		{
			let template = GetTemplateData(entState.template);
			text += "\n\nentity: {\n";
			for (let k in entState)
				text += "  "+k+":"+uneval(entState[k])+"\n";
			text += "}\n\ntemplate: " + uneval(template);
		}
	}

	debug.caption = text.replace(/\[/g, "\\[");
}

function updatePlayerDisplay()
{
	var simState = GetSimState();
	var playerState = simState.players[Engine.GetPlayerID()];
	if (!playerState)
		return;

	Engine.GetGUIObjectByName("resourceFood").caption = Math.floor(playerState.resourceCounts.food);
	Engine.GetGUIObjectByName("resourceWood").caption = Math.floor(playerState.resourceCounts.wood);
	Engine.GetGUIObjectByName("resourceStone").caption = Math.floor(playerState.resourceCounts.stone);
	Engine.GetGUIObjectByName("resourceMetal").caption = Math.floor(playerState.resourceCounts.metal);
	Engine.GetGUIObjectByName("resourcePop").caption = playerState.popCount + "/" + playerState.popLimit;

	g_IsTrainingBlocked = playerState.trainingBlocked;
}

function selectAndMoveTo(ent)
{
	var entState = GetEntityState(ent);
	if (!entState || !entState.position)
		return;

	g_Selection.reset();
	g_Selection.addList([ent]);

	var position = entState.position;
	Engine.CameraMoveTo(position.x, position.z);
}

function updateResearchDisplay()
{
	var researchStarted = Engine.GuiInterfaceCall("GetStartedResearch", Engine.GetPlayerID());
	if (!researchStarted)
		return;

	// Set up initial positioning.
	var buttonSideLength = Engine.GetGUIObjectByName("researchStartedButton[0]").size.right;
	for (var i = 0; i < 10; ++i)
	{
		var button = Engine.GetGUIObjectByName("researchStartedButton[" + i + "]");
		var size = button.size;
		size.top = (4 + buttonSideLength) * i;
		size.bottom = size.top + buttonSideLength;
		button.size = size;
	}

	var numButtons = 0;
	for (var tech in researchStarted)
	{
		// Show at most 10 in-progress techs.
		if (numButtons >= 10)
			break;

		var template = GetTechnologyData(tech);
		var button = Engine.GetGUIObjectByName("researchStartedButton[" + numButtons + "]");
		button.hidden = false;
		button.tooltip = getEntityNames(template);
		button.onpress = (function(e) { return function() { selectAndMoveTo(e) } })(researchStarted[tech].researcher);

		var icon = "stretched:session/portraits/" + template.icon;
		Engine.GetGUIObjectByName("researchStartedIcon[" + numButtons + "]").sprite = icon;

		// Scale the progress indicator.
		var size = Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size;

		// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
		size.top = size.left + Math.round(researchStarted[tech].progress * (size.right - size.left));
		Engine.GetGUIObjectByName("researchStartedProgressSlider[" + numButtons + "]").size = size;

		++numButtons;
	}

	// Hide unused buttons.
	for (var i = numButtons; i < 10; ++i)
		Engine.GetGUIObjectByName("researchStartedButton[" + i + "]").hidden = true;
}

function updateTimeElapsedCounter()
{
	var simState = GetSimState();
	var timeElapsedCounter = Engine.GetGUIObjectByName("timeElapsedCounter");
	if (g_CurrentSpeed != 1.0)
		timeElapsedCounter.caption = sprintf(translate("%(time)s (%(speed)sx)"), { time: timeToString(simState.timeElapsed), speed: Engine.FormatDecimalNumberIntoString(g_CurrentSpeed) });
	else
		timeElapsedCounter.caption = timeToString(simState.timeElapsed);
}

// Toggles the display of status bars for all of the player's entities.
function recalculateStatusBarDisplay()
{
	if (g_ShowAllStatusBars)
		var entities = Engine.PickFriendlyEntitiesOnScreen(Engine.GetPlayerID());
	else
	{
		var selected = g_Selection.toList();
		for each (var ent in g_Selection.highlighted)
			selected.push(ent);

		// Remove selected entities from the 'all entities' array, to avoid disabling their status bars.
		var entities = Engine.GuiInterfaceCall("GetPlayerEntities").filter(
				function(idx) { return (selected.indexOf(idx) == -1); }
		);
	}

	Engine.GuiInterfaceCall("SetStatusBars", { "entities": entities, "enabled": g_ShowAllStatusBars });
}

// Update the additional list of entities to be highlighted.
function updateAdditionalHighlight()
{
	var entsAdd = [];    // list of entities units to be highlighted
	var entsRemove = [];
	var highlighted = g_Selection.toList();
	for each (var ent in g_Selection.highlighted)
		highlighted.push(ent);

	if (g_ShowGuarding)
	{
		// flag the guarding entities to add in this additional highlight
		for each (var sel in g_Selection.selected)
		{
			var state = GetEntityState(sel);
			if (!state.guard || !state.guard.entities.length)
				continue;
			for each (var ent in state.guard.entities)
				if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
					entsAdd.push(ent);
		}
	}

	if (g_ShowGuarded)
	{
		// flag the guarded entities to add in this additional highlight
		for each (var sel in g_Selection.selected)
		{
			var state = GetEntityState(sel);
			if (!state.unitAI || !state.unitAI.isGuarding)
				continue;
			var ent = state.unitAI.isGuarding;
			if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1)
				entsAdd.push(ent);
		}
	}

	// flag the entities to remove (from the previously added) from this additional highlight
	for each (var ent in g_AdditionalHighlight)
	    if (highlighted.indexOf(ent) == -1 && entsAdd.indexOf(ent) == -1 && entsRemove.indexOf(ent) == -1)
			entsRemove.push(ent);

	_setHighlight(entsAdd   , HIGHLIGHTED_ALPHA, true );
	_setHighlight(entsRemove, 0                , false);
	g_AdditionalHighlight = entsAdd;
}

// Temporarily adding this here
const AMBIENT_TEMPERATE = "temperate";
var currentAmbient;
function playRandomAmbient(type)
{
	switch (type)
	{
		case AMBIENT_TEMPERATE:
			const AMBIENT = "audio/ambient/dayscape/day_temperate_gen_03.ogg";
			Engine.PlayAmbientSound(AMBIENT, true);
			break;

		default:
			error("Unrecognized ambient type: '" + type + "'");
			break;
	}
}

// Temporarily adding this here
function stopAmbient()
{
	if (currentAmbient)
	{
		currentAmbient.free();
		currentAmbient = null;
	}
}

function getBuildString()
{
	return sprintf(translate("Build: %(buildDate)s (%(revision)s)"), { buildDate: Engine.GetBuildTimestamp(0), revision: Engine.GetBuildTimestamp(2) });
}

function showTimeWarpMessageBox()
{
	messageBox(500, 250, translate("Note: time warp mode is a developer option, and not intended for use over long periods of time. Using it incorrectly may cause the game to run out of memory or crash."), translate("Time warp mode"), 2);
}

// Send a report on the game status to the lobby
function reportGame(extendedSimState)
{
	if (!Engine.HasXmppClient() || !Engine.IsRankedGame())
		return;
	// units
	var unitsClasses = [
		"total",
		"Infantry",
		"Worker",
		"Female",
		"Cavalry",
		"Champion",
		"Hero",
		"Ship"
	];
	var unitsCountersTypes = [
		"unitsTrained",
		"unitsLost",
		"enemyUnitsKilled"
	];
	// buildings
	var buildingsClasses = [
		"total",
		"CivCentre",
		"House",
		"Economic",
		"Outpost",
		"Military",
		"Fortress",
		"Wonder"
	];
	var buildingsCountersTypes = [
		"buildingsConstructed",
		"buildingsLost",
		"enemyBuildingsDestroyed"
	];
	// resources
	var resourcesTypes = [
		"wood",
		"food",
		"stone",
		"metal"
	];
	var resourcesCounterTypes = [
		"resourcesGathered",
		"resourcesUsed",
		"resourcesSold",
		"resourcesBought"
	];

	var playerStatistics = { };

	// Unit Stats
	for each (var unitCounterType in unitsCountersTypes)
	{
		if (!playerStatistics[unitCounterType])
			playerStatistics[unitCounterType] = { };
		for each (var unitsClass in unitsClasses)
			playerStatistics[unitCounterType][unitsClass] = "";
	}

	playerStatistics.unitsLostValue = "";
	playerStatistics.unitsKilledValue = "";
	// Building stats
	for each (var buildingCounterType in buildingsCountersTypes)
	{
		if (!playerStatistics[buildingCounterType])
			playerStatistics[buildingCounterType] = { };
		for each (var buildingsClass in buildingsClasses)
			playerStatistics[buildingCounterType][buildingsClass] = "";
	}

	playerStatistics.buildingsLostValue = "";
	playerStatistics.enemyBuildingsDestroyedValue = "";
	// Resources
	for each (var resourcesCounterType in resourcesCounterTypes)
	{
		if (!playerStatistics[resourcesCounterType])
			playerStatistics[resourcesCounterType] = { };
		for each (var resourcesType in resourcesTypes)
			playerStatistics[resourcesCounterType][resourcesType] = "";
	}
	playerStatistics.resourcesGathered.vegetarianFood = "";

	playerStatistics.tradeIncome = "";
	// Tribute
	playerStatistics.tributesSent = "";
	playerStatistics.tributesReceived = "";
	// Total
	playerStatistics.economyScore = "";
	playerStatistics.militaryScore = "";
	playerStatistics.totalScore = "";
	// Various
	playerStatistics.treasuresCollected = "";
	playerStatistics.feminisation = "";
	playerStatistics.percentMapExplored = "";
	var mapName = Engine.GetMapSettings().Name;
	var playerStates = "";
	var playerCivs = "";
	var teams = "";
	var teamsLocked = true;

	// Serialize the statistics for each player into a comma-separated list.
	// Ignore gaia
	for (let i = 1; i < extendedSimState.players.length; ++i)
	{
		let player = extendedSimState.players[i];

		playerStates += player.state + ",";
		playerCivs += player.civ + ",";
		teams += player.team + ",";
		teamsLocked = teamsLocked && player.teamsLocked;
		for each (var resourcesCounterType in resourcesCounterTypes)
			for each (var resourcesType in resourcesTypes)
				playerStatistics[resourcesCounterType][resourcesType] += player.statistics[resourcesCounterType][resourcesType] + ",";
		playerStatistics.resourcesGathered.vegetarianFood += player.statistics.resourcesGathered.vegetarianFood + ",";

		for each (var unitCounterType in unitsCountersTypes)
			for each (var unitsClass in unitsClasses)
				playerStatistics[unitCounterType][unitsClass] += player.statistics[unitCounterType][unitsClass] + ",";

		for each (var buildingCounterType in buildingsCountersTypes)
			for each (var buildingsClass in buildingsClasses)
				playerStatistics[buildingCounterType][buildingsClass] += player.statistics[buildingCounterType][buildingsClass] + ",";
		var total = 0;
		for each (var res in player.statistics.resourcesGathered)
			total += res;
		playerStatistics.economyScore += total + ",";
		playerStatistics.militaryScore += Math.round((player.statistics.enemyUnitsKilledValue +
			player.statistics.enemyBuildingsDestroyedValue) / 10)  + ",";
		playerStatistics.totalScore += (total + Math.round((player.statistics.enemyUnitsKilledValue +
			player.statistics.enemyBuildingsDestroyedValue) / 10)) + ",";
		playerStatistics.tradeIncome += player.statistics.tradeIncome + ",";
		playerStatistics.tributesSent += player.statistics.tributesSent + ",";
		playerStatistics.tributesReceived += player.statistics.tributesReceived + ",";
		playerStatistics.percentMapExplored += player.statistics.percentMapExplored + ",";
		playerStatistics.treasuresCollected += player.statistics.treasuresCollected + ",";
	}

	// Send the report with serialized data
	var reportObject = { };
	reportObject.timeElapsed = extendedSimState.timeElapsed;
	reportObject.playerStates = playerStates;
	reportObject.playerID = Engine.GetPlayerID();
	reportObject.matchID = g_MatchID;
	reportObject.civs = playerCivs;
	reportObject.teams = teams;
	reportObject.teamsLocked = String(teamsLocked);
	reportObject.mapName = mapName;
	reportObject.economyScore = playerStatistics.economyScore;
	reportObject.militaryScore = playerStatistics.militaryScore;
	reportObject.totalScore = playerStatistics.totalScore;
	for each (var rct in resourcesCounterTypes)
	{
		for each (var rt in resourcesTypes)
			reportObject[rt+rct.substr(9)] = playerStatistics[rct][rt];
			// eg. rt = food rct.substr = Gathered rct = resourcesGathered
	}
	reportObject.vegetarianFoodGathered = playerStatistics.resourcesGathered.vegetarianFood;
	for each (var type in unitsClasses)
	{
		// eg. type = Infantry (type.substr(0,1)).toLowerCase()+type.substr(1) = infantry
		reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsTrained"] = playerStatistics.unitsTrained[type];
		reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"UnitsLost"] = playerStatistics.unitsLost[type];
		reportObject["enemy"+type+"UnitsKilled"] = playerStatistics.enemyUnitsKilled[type];
	}
	for each (var type in buildingsClasses)
	{
		reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsConstructed"] = playerStatistics.buildingsConstructed[type];
		reportObject[(type.substr(0,1)).toLowerCase()+type.substr(1)+"BuildingsLost"] = playerStatistics.buildingsLost[type];
		reportObject["enemy"+type+"BuildingsDestroyed"] = playerStatistics.enemyBuildingsDestroyed[type];
	}
	reportObject.tributesSent = playerStatistics.tributesSent;
	reportObject.tributesReceived = playerStatistics.tributesReceived;
	reportObject.percentMapExplored = playerStatistics.percentMapExplored;
	reportObject.treasuresCollected = playerStatistics.treasuresCollected;
	reportObject.tradeIncome = playerStatistics.tradeIncome;

	Engine.SendGameReport(reportObject);
}
