////////////////////////////////////////////////////////////////////////////////////////////////
// Constants
const DEFAULT_NETWORKED_MAP = "Median Oasis";
const DEFAULT_OFFLINE_MAP = "Acropolis II";

// TODO: Move these somewhere like simulation\data\game_types.json, Atlas needs them too
const VICTORY_TEXT = ["Conquest", "None"];
const VICTORY_DATA = ["conquest", "endless"];
const VICTORY_DEFAULTIDX = 0;

// Max number of players for any map
const MAX_PLAYERS = 8;

////////////////////////////////////////////////////////////////////////////////////////////////

// Is this is a networked game, or offline
var g_IsNetworked;

// Is this user in control of game settings (i.e. is a network server, or offline player)
var g_IsController;

// Are we currently updating the GUI in response to network messages instead of user input
// (and therefore shouldn't send further messages to the network)
var g_IsInGuiUpdate;

var g_PlayerAssignments = {};

// Default game setup attributes
var g_DefaultPlayerData = [];
var g_GameAttributes = {
	settings: {}
};

var g_MapSizes = {};

var g_AIs = [];

var g_ChatMessages = [];

// Data caches
var g_MapData = {};
var g_CivData = {};

var g_MapFilters = [];
	
// To prevent the display locking up while we load the map metadata,
// we'll start with a 'loading' message and switch to the main screen in the
// tick handler
var g_LoadingState = 0; // 0 = not started, 1 = loading, 2 = loaded

////////////////////////////////////////////////////////////////////////////////////////////////

function init(attribs)
{
	switch (attribs.type)
	{
	case "offline":
		g_IsNetworked = false;
		g_IsController = true;
		break;
	case "server":
		g_IsNetworked = true;
		g_IsController = true;
		break;
	case "client":
		g_IsNetworked = true;
		g_IsController = false;
		break;
	default:
		error("Unexpected 'type' in gamesetup init: "+attribs.type);
	}
}

// Called after the map data is loaded and cached
function initMain()
{
	// Load AI list
	g_AIs = Engine.GetAIs();
	
	// Sort AIs by displayed name
	g_AIs.sort(function (a, b) {
		return a.data.name < b.data.name ? -1 : b.data.name < a.data.name ? +1 : 0;
	});
	
	// Get default player data - remove gaia
	g_DefaultPlayerData = initPlayerDefaults();
	g_DefaultPlayerData.shift();
	
	g_MapSizes = initMapSizes();

	// Init civs
	initCivNameList();
	
	// Init map types
	var mapTypes = getGUIObjectByName("mapTypeSelection");
	mapTypes.list = ["Scenario","Random"];
	mapTypes.list_data = ["scenario","random"];
	
	// Setup map filters - will appear in order they are added
	addFilter("Default", function(settings) { return settings && !keywordTestOR(settings.Keywords, ["demo", "hidden"]); });
	addFilter("Demo Maps", function(settings) { return settings && keywordTestAND(settings.Keywords, ["demo"]); });
	addFilter("Old Maps", function(settings) { return !settings; });
	addFilter("All Maps", function(settings) { return true; });
	
	// Populate map filters dropdown
	var mapFilters = getGUIObjectByName("mapFilterSelection");
	mapFilters.list = getFilters();
	g_GameAttributes.mapFilter = "Default";
	
	// Setup controls for host only
	if (g_IsController)
	{
		mapTypes.selected = 0;
		mapFilters.selected = 0;
		
		initMapNameList();
		
		var numPlayersSelect = getGUIObjectByName("numPlayersSelection");
		var players = [];
		for (var i = 1; i <= MAX_PLAYERS; ++i)
			players.push(i);
		numPlayersSelect.list = players;
		numPlayersSelect.list_data = players;
		numPlayersSelect.selected = MAX_PLAYERS - 1;
		
		var victoryConditions = getGUIObjectByName("victoryCondition");
		victoryConditions.list = VICTORY_TEXT;
		victoryConditions.list_data = VICTORY_DATA;
		victoryConditions.onSelectionChange = function()
		{	// Update attributes so other players can see change
			if (this.selected != -1)
			{
				g_GameAttributes.settings.GameType = VICTORY_DATA[this.selected];
			}
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
		victoryConditions.selected = VICTORY_DEFAULTIDX;
		
		var mapSize = getGUIObjectByName("mapSize");
		mapSize.list = g_MapSizes.names;
		mapSize.list_data = g_MapSizes.tiles;
		mapSize.onSelectionChange = function()
		{	// Update attributes so other players can see change
			if (this.selected != -1)
			{
				g_GameAttributes.settings.Size = g_MapSizes.tiles[this.selected];
			}
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
		
		getGUIObjectByName("revealMap").onPress = function()
		{	// Update attributes so other players can see change
			g_GameAttributes.settings.RevealMap = this.checked;
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
		
		getGUIObjectByName("lockTeams").onPress = function()
		{	// Update attributes so other players can see change
			g_GameAttributes.settings.LockTeams = this.checked;
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
	}
	else
	{
		// If we're a network client, disable all the map controls
		// TODO: make them look visually disabled so it's obvious why they don't work
		getGUIObjectByName("mapTypeSelection").hidden = true;
		getGUIObjectByName("mapFilterSelection").hidden = true;
		getGUIObjectByName("mapSelection").enabled = false;
		
		// Disable player and game options controls
		// TODO: Shouldn't players be able to choose their own assignment?
		for (var i = 0; i < MAX_PLAYERS; ++i)
		{
			getGUIObjectByName("playerAssignment["+i+"]").enabled = false;
			getGUIObjectByName("playerCiv["+i+"]").hidden = true;
			getGUIObjectByName("playerTeam["+i+"]").hidden = true;
		}
		
		getGUIObjectByName("numPlayersBox").hidden = true;
		
		// Disable "start game" button
		// TODO: Perhaps replace this with a "ready" button, and require host to wait?
		getGUIObjectByName("startGame").enabled = false;
	}
	
	// Set up offline-only bits:
	if (!g_IsNetworked)
	{
		getGUIObjectByName("chatPanel").hidden = true;
	}
	
	// Settings for all possible player slots
	var boxSpacing = 32;
	for (var i = 0; i < MAX_PLAYERS; ++i)
	{
		// Space player boxes
		var box = getGUIObjectByName("playerBox["+i+"]");
		var boxSize = box.size;
		var h = boxSize.bottom - boxSize.top;
		boxSize.top = i * boxSpacing;
		boxSize.bottom = i * boxSpacing + h;
		box.size = boxSize;
		
		// Populate team dropdowns
		var team = getGUIObjectByName("playerTeam["+i+"]");
		team.list = ["None", "1", "2", "3", "4"];
		team.list_data = [-1, 0, 1, 2, 3];
		team.selected = 0;
		
		let playerSlot = i;	// declare for inner function use
		team.onSelectionChange = function()
		{	// Update team
			if (this.selected != -1)
			{
				g_GameAttributes.settings.PlayerData[playerSlot].Team = this.selected - 1;
			}
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
		
		// Set events
		var civ = getGUIObjectByName("playerCiv["+i+"]");
		civ.onSelectionChange = function()
		{	// Update civ
			if (this.selected != -1)
			{
				g_GameAttributes.settings.PlayerData[playerSlot].Civ = this.list_data[this.selected];
			}
			
			if (!g_IsInGuiUpdate)
			{
				updateGameAttributes();
			}
		};
	}
	
	if (g_IsNetworked)
	{
		// For multiplayer, focus the chat input box by default
		getGUIObjectByName("chatInput").focus();
	}
	else
	{
		// For single-player, focus the map list by default,
		// to allow easy keyboard selection of maps
		getGUIObjectByName("mapSelection").focus();
	}
}

function handleNetMessage(message)
{
	log("Net message: "+uneval(message));

	switch (message.type)
	{
	case "netstatus":
		switch (message.status)
		{
		case "disconnected":
			Engine.DisconnectNetworkGame();
			Engine.PopGuiPage();
			reportDisconnect(message.reason);
			break;
		
		default:
			error("Unrecognised netstatus type "+message.status);
			break;
		}
		break;
	
	case "gamesetup":
		if (message.data) // (the host gets undefined data on first connect, so skip that)
		{
			g_GameAttributes = message.data;
		}

		onGameAttributesChange();
		break;
	
	case "players":
		// Find and report all joinings/leavings
		for (var host in message.hosts)
		{
			if (! g_PlayerAssignments[host])
			{
				addChatMessage({ "type": "connect", "username": message.hosts[host].name });
			}
		}
		for (var host in g_PlayerAssignments)
		{
			if (! message.hosts[host])
			{
				addChatMessage({ "type": "disconnect", "guid": host });
			}
		}
		// Update the player list
		g_PlayerAssignments = message.hosts;
		updatePlayerList();
		break;
	
	case "start":
		Engine.SwitchGuiPage("page_loading.xml", {
			"attribs": g_GameAttributes, 
			"isNetworked" : g_IsNetworked, 
			"playerAssignments": g_PlayerAssignments
		});
		break;
	
	case "chat":
		addChatMessage({ "type": "message", "guid": message.guid, "text": message.text });
		break;

	default:
		error("Unrecognised net message type "+message.type);
	}
}

// Get display name from map data
function getMapDisplayName(map)
{
	var mapData = loadMapData(map);
	
	if (!mapData || !mapData.settings || !mapData.settings.Name)
	{	// Give some msg that map format is unsupported
		log("Map data missing in scenario '"+map+"' - likely unsupported format");
		return map;
	}
	
	return mapData.settings.Name;
}

// Get a setting if it exists or return default
function getSetting(settings, defaults, property)
{
	if (settings && (property in settings))
	{
		return settings[property];
	}
	
	// Use defaults
	if (defaults && (property in defaults))
	{
		return defaults[property];
	}
	
	return undefined;
}

// Initialize the dropdowns containing all the available civs
function initCivNameList()
{
	// Cache civ data
	g_CivData = loadCivData();
	
	// Extract name/code, and skip civs that are explicitly disabled
	// (intended for unusable incomplete civs)
	var civList = [ { "name": civ.Name, "code": civ.Code }
		for each (civ in g_CivData)
		if (civ.SelectableInGameSetup !== false) ];

	// Alphabetically sort the list, ignoring case
	civList.sort(sortNameIgnoreCase);
	
	var civListNames = [ civ.name for each (civ in civList) ];
	var civListCodes = [ civ.code for each (civ in civList) ];

	// Update the dropdowns
	for (var i = 0; i < MAX_PLAYERS; ++i)
	{
		var civ = getGUIObjectByName("playerCiv["+i+"]");
		civ.list = civListNames;
		civ.list_data = civListCodes;
		civ.selected = 0;
	}
}

// Initialise the list control containing all the available maps
function initMapNameList()
{
	// Get a list of map filenames
	// TODO: Should verify these are valid maps before adding to list
	var mapSelectionBox = getGUIObjectByName("mapSelection")
	var mapFiles;
	
	switch (g_GameAttributes.mapType)
	{
	case "scenario":
		mapFiles = getXMLFileList(g_GameAttributes.mapPath);
		break;
	
	case "random":
		mapFiles = getJSONFileList(g_GameAttributes.mapPath);
		break;
		
	default:
		error("initMapNameList: Unexpected map type '"+g_GameAttributes.mapType+"'");
		return;
	}
	
	// Apply map filter, if any defined
	var mapList = [];
	for (var i = 0; i < mapFiles.length; ++i)
	{
		var file = mapFiles[i];
		var mapData = loadMapData(file);
				
		if (g_GameAttributes.mapFilter && mapData && testFilter(g_GameAttributes.mapFilter, mapData.settings))
		{
			mapList.push({ "name": getMapDisplayName(file), "file": file });
		}
	}
	
	// Alphabetically sort the list, ignoring case
	mapList.sort(sortNameIgnoreCase);
	
	var mapListNames = [ map.name for each (map in mapList) ];
	var mapListFiles = [ map.file for each (map in mapList) ];
	
	// Select the default map
	var selected = mapListFiles.indexOf(g_GameAttributes.map);
	// Default to the first element if list is not empty and we can't find the one we searched for
	if (selected == -1 && mapList.length)
	{
		selected = 0;
	}
	
	// Update the list control
	mapSelectionBox.list = mapListNames;
	mapSelectionBox.list_data = mapListFiles;
	mapSelectionBox.selected = selected;
}

function loadMapData(name)
{
	if (!name)
	{
		return undefined;
	}
	
	if (!g_MapData[name])
	{
		switch (g_GameAttributes.mapType)
		{
		case "scenario":
			g_MapData[name] = Engine.LoadMapSettings(g_GameAttributes.mapPath+name);
			break;
			
		case "random":
			g_MapData[name] = parseJSONData(g_GameAttributes.mapPath+name+".json");
			break;
			
		default:
			error("loadMapData: Unexpected map type '"+g_GameAttributes.mapType+"'");
			return undefined;
		}
	}
	
	return g_MapData[name];
}

////////////////////////////////////////////////////////////////////////////////////////////////
// GUI event handlers

function cancelSetup()
{
	Engine.DisconnectNetworkGame();
}

function onTick()
{
	// First tick happens before first render, so don't load yet
	if (g_LoadingState == 0)
	{
		g_LoadingState++;
	}
	else if (g_LoadingState == 1)
	{
		getGUIObjectByName("loadingWindow").hidden = true;
		getGUIObjectByName("setupWindow").hidden = false;
		initMain();
		g_LoadingState++;
	}
	else if (g_LoadingState == 2)
	{
		while (true)
		{
			var message = Engine.PollNetworkClient();
			if (!message)
			{
				break;
			}
			handleNetMessage(message);
		}
	}
}

// Called when user selects number of players
function selectNumPlayers(num)
{
	// Avoid recursion
	if (g_IsInGuiUpdate)
	{
		return;
	}
	
	// Network clients can't change number of players
	if (g_IsNetworked && !g_IsController)
	{
		return;
	}
	
	// Only meaningful for random maps
	if (g_GameAttributes.mapType != "random")
	{
		return;
	}
	
	// Update player data
	var pData = g_GameAttributes.settings.PlayerData;
	if (pData && num < pData.length)
	{
		// Remove extra player data
		g_GameAttributes.settings.PlayerData = pData.slice(0, num);
	}
	else
	{
		// Add player data from defaults
		for (var i = pData.length; i < num; ++i)
		{
			g_GameAttributes.settings.PlayerData.push(g_DefaultPlayerData[i]);
		}
	}
	
	updateGameAttributes();
}

// Called when the user selects a map type from the list
function selectMapType(type)
{
	// Avoid recursion
	if (g_IsInGuiUpdate)
	{
		return;
	}

	// Network clients can't change map type
	if (g_IsNetworked && !g_IsController)
	{
		return;
	}

	// Reset game attributes
	g_GameAttributes.map = "";
	g_GameAttributes.mapType = type;

	// Clear old map data
	g_MapData = {};
	
	// Select correct path
	switch (g_GameAttributes.mapType)
	{
	case "scenario":
		// Set a default map
		// TODO: This should be remembered from the last session
		g_GameAttributes.map = (g_IsNetworked ? DEFAULT_NETWORKED_MAP : DEFAULT_OFFLINE_MAP);
		g_GameAttributes.mapPath = "maps/scenarios/";
		break;
		
	case "random":
		g_GameAttributes.mapPath = "maps/random/";
		g_GameAttributes.settings = {
			PlayerData: g_DefaultPlayerData.slice(0, 4),
			Seed: Math.floor(Math.random() * 65536)
		};
		break;
		
	default:
		error("selectMapType: Unexpected map type '"+g_GameAttributes.mapType+"'");
		return;
	}

	initMapNameList();

	updateGameAttributes();
}

function selectMapFilter(filterName)
{
	// Avoid recursion
	if (g_IsInGuiUpdate)
	{
		return;
	}

	// Network clients can't change map filter
	if (g_IsNetworked && !g_IsController)
	{
		return;
	}
	
	g_GameAttributes.mapFilter = filterName;
	
	initMapNameList();

	updateGameAttributes();
}

// Called when the user selects a map from the list
function selectMap(name)
{
	// Avoid recursion
	if (g_IsInGuiUpdate)
	{
		return;
	}

	// Network clients can't change map
	if (g_IsNetworked && !g_IsController)
	{
		return;
	}
	
	// Return if we have no map
	if (!name)
	{
		return;
	}

	var mapData = loadMapData(name);
	var mapSettings = (mapData && mapData.settings ? deepcopy(mapData.settings) : {});
	
	// Copy any new settings
	g_GameAttributes.map = name;
	g_GameAttributes.script = mapSettings.Script;
	for (var prop in mapSettings)
	{
		g_GameAttributes.settings[prop] = mapSettings[prop];
	}

	// Use default AI if the map doesn't specify any explicitly
	for (var i = 0; i < g_GameAttributes.settings.PlayerData.length; ++i)
	{
		if (!('AI' in g_GameAttributes.settings.PlayerData[i]))
		{
			g_GameAttributes.settings.PlayerData[i].AI = g_DefaultPlayerData[i].AI;
		}
	}

	// Reset player assignments on map change
	if (!g_IsNetworked)
	{	// Slot 1
		g_PlayerAssignments = { "local": { "name": "You", "player": 1, "civ": "", "team": -1} };
	}
	else
	{
		var numPlayers = (mapSettings.PlayerData ? mapSettings.PlayerData.length : g_GameAttributes.settings.PlayerData.length);
		
		for (var guid in g_PlayerAssignments)
		{	// Unassign extra players
			var player = g_PlayerAssignments[guid].player;

			if (player <= MAX_PLAYERS && player > numPlayers)
			{
				Engine.AssignNetworkPlayer(player, "");
			}
		}
	}
	
	updateGameAttributes();
}

function launchGame()
{
	if (g_IsNetworked && !g_IsController)
	{
		error("Only host can start game");
		return;
	}
	
	// Check that we have a map
	if (!g_GameAttributes.map)
	{
		return;
	}
	
	if (g_IsNetworked)
	{
		Engine.SetNetworkGameAttributes(g_GameAttributes);
		Engine.StartNetworkGame();
	}
	else
	{
		// Find the player ID which the user has been assigned to
		var numPlayers = g_GameAttributes.settings.PlayerData.length;
		var playerID = -1;
		for (var i = 0; i < numPlayers; ++i)
		{
			var assignBox = getGUIObjectByName("playerAssignment["+i+"]");
			if (assignBox.list_data[assignBox.selected] == "local")
			{
				playerID = i+1;
			}
		}
		// Remove extra player data
		g_GameAttributes.settings.PlayerData = g_GameAttributes.settings.PlayerData.slice(0, numPlayers);

		Engine.StartGame(g_GameAttributes, playerID);
		Engine.SwitchGuiPage("page_loading.xml", {
			"attribs": g_GameAttributes, 
			"isNetworked" : g_IsNetworked, 
			"playerAssignments": g_PlayerAssignments
		});
	}
}

////////////////////////////////////////////////////////////////////////////////////////////////

function onGameAttributesChange()
{
	g_IsInGuiUpdate = true;
	
	// Don't set any attributes here, just show the changes in GUI
	
	var mapName = g_GameAttributes.map || "";
	var mapSettings = g_GameAttributes.settings;
	var numPlayers = (mapSettings.PlayerData ? mapSettings.PlayerData.length : MAX_PLAYERS);
	
	// Update some controls for clients
	if (!g_IsController)
	{
		var mapFilterHeading = getGUIObjectByName("mapFilterHeading");
		mapFilterHeading.caption = "Map Filter: "+g_GameAttributes.mapFilter;
		var mapTypeSelection = getGUIObjectByName("mapTypeSelection");
		var mapTypeHeading = getGUIObjectByName("mapTypeHeading");
		var idx = mapTypeSelection.list_data.indexOf(g_GameAttributes.mapType);
		mapTypeHeading.caption = "Match Type: "+mapTypeSelection.list[idx];
		var mapSelectionBox = getGUIObjectByName("mapSelection");
		mapSelectionBox.selected = mapSelectionBox.list_data.indexOf(mapName);
		
		initMapNameList();
	}
	
	// Controls common to all map types
	var revealMap = getGUIObjectByName("revealMap");
	var victoryCondition = getGUIObjectByName("victoryCondition");
	var lockTeams = getGUIObjectByName("lockTeams");
	var mapSize = getGUIObjectByName("mapSize");
	var revealMapText = getGUIObjectByName("revealMapText");
	var victoryConditionText = getGUIObjectByName("victoryConditionText");
	var lockTeamsText = getGUIObjectByName("lockTeamsText");
	var mapSizeText = getGUIObjectByName("mapSizeText");
	var numPlayersBox = getGUIObjectByName("numPlayersBox");

	var sizeIdx = (g_MapSizes.tiles.indexOf(mapSettings.Size) != -1 ? g_MapSizes.tiles.indexOf(mapSettings.Size) : g_MapSizes.default);
	var victoryIdx = (VICTORY_DATA.indexOf(mapSettings.GameType) != -1 ? VICTORY_DATA.indexOf(mapSettings.GameType) : VICTORY_DEFAULTIDX);
	
	// Handle map type specific logic
	switch (g_GameAttributes.mapType)
	{
	case "random":
		if (g_IsController)
		{	//Host
			getGUIObjectByName("numPlayersSelection").selected = numPlayers - 1;
			numPlayersBox.hidden = false;
			mapSize.hidden = false;
			revealMap.hidden = false;
			victoryCondition.hidden = false;
			lockTeams.hidden = false;
			
			mapSizeText.caption = "Map size:";
			mapSize.selected = sizeIdx;
			revealMapText.caption = "Reveal map:";
			revealMap.checked = (mapSettings.RevealMap ? true : false);
			
			victoryConditionText.caption = "Victory condition:";
			victoryCondition.selected = victoryIdx;
			lockTeamsText.caption = "Teams locked:";
			lockTeams.checked =  (mapSettings.LockTeams === undefined || mapSettings.LockTeams ? true : false);
		}
		else
		{	// Client
			mapSizeText.caption = "Map size: " + g_MapSizes.names[sizeIdx];
			revealMapText.caption = "Reveal map: " + (mapSettings.RevealMap ? "Yes" : "No");
			victoryConditionText.caption = "Victory condition: " + VICTORY_TEXT[victoryIdx];
			lockTeamsText.caption = "Teams locked: " + (mapSettings.LockTeams === undefined || mapSettings.LockTeams ? "Yes" : "No");
		}
		
		break;
	
	case "scenario":
		// For scenario just reflect settings for the current map
		numPlayersBox.hidden = true;
		mapSize.hidden = true;
		revealMap.hidden = true;
		victoryCondition.hidden = true;
		lockTeams.hidden = true;
		
		mapSizeText.caption = "Map size: Default";
		revealMapText.caption = "Reveal map: " + (mapSettings.RevealMap ? "Yes" : "No");
		victoryConditionText.caption = "Victory condition: " + VICTORY_TEXT[victoryIdx];
		lockTeamsText.caption = "Teams locked: " + (mapSettings.LockTeams === undefined || mapSettings.LockTeams  ? "Yes" : "No");
		
		break;
		
	default:
		error("onGameAttributesChange: Unexpected map type '"+g_GameAttributes.mapType+"'");
		return;
	}
	
	// Display map name
	getGUIObjectByName("mapInfoName").caption = getMapDisplayName(mapName);
	
	// Load the description from the map file, if there is one
	var description = mapSettings.Description || "Sorry, no description available.";
	
	// Describe the number of players
	var playerString = numPlayers + " " + (numPlayers == 1 ? "player" : "players") + ". ";
	
	for (var i = 0; i < MAX_PLAYERS; ++i)
	{
		// Show only needed player slots
		getGUIObjectByName("playerBox["+i+"]").hidden = (i >= numPlayers);
		
		// Show player data or defaults as necessary
		if (i < numPlayers)
		{
			var pName = getGUIObjectByName("playerName["+i+"]");
			var pCiv = getGUIObjectByName("playerCiv["+i+"]");
			var pCivText = getGUIObjectByName("playerCivText["+i+"]");
			var pTeam = getGUIObjectByName("playerTeam["+i+"]");
			var pTeamText = getGUIObjectByName("playerTeamText["+i+"]");
			var pColor = getGUIObjectByName("playerColour["+i+"]");

			// Player data / defaults
			var pData = mapSettings.PlayerData ? mapSettings.PlayerData[i] : {};
			var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[i] : {};
			
			// Common to all game types
			var color = iColorToString(getSetting(pData, pDefs, "Colour"));
			pColor.sprite = "colour:"+color+" 100";
			pName.caption = getSetting(pData, pDefs, "Name");
			
			var team = getSetting(pData, pDefs, "Team");
			var civ = getSetting(pData, pDefs, "Civ");
			
			// For clients or scenarios, hide some player dropdowns
			if (!g_IsController || g_GameAttributes.mapType == "scenario")
			{
				pCivText.hidden = false;
				pCiv.hidden = true;
				pTeamText.hidden = false;
				pTeam.hidden = true;
				
				// Set text values
				pCivText.caption = g_CivData[civ].Name;
				pTeamText.caption = (team !== undefined && team >= 0) ? team+1 : "-";
			}
			else if (g_GameAttributes.mapType == "random")
			{
				pCivText.hidden = true;
				pCiv.hidden = false;
				pTeamText.hidden = true;
				pTeam.hidden = false;
				
				// Set dropdown values
				pCiv.selected = (civ ? pCiv.list_data.indexOf(civ) : 0);
				pTeam.selected = (team !== undefined && team >= 0) ? team+1 : 0;
			}
		}
	}

	getGUIObjectByName("mapInfoDescription").caption = playerString + description;
	
	g_IsInGuiUpdate = false;

	// Game attributes include AI settings, so update the player list
	updatePlayerList();
}

function updateGameAttributes()
{
	if (g_IsNetworked)
	{
		Engine.SetNetworkGameAttributes(g_GameAttributes);
	}
	else 
	{
		onGameAttributesChange();
	}
}

function updatePlayerList()
{
	g_IsInGuiUpdate = true;

	var hostNameList = [];
	var hostGuidList = [];
	var assignments = [];
	var aiAssignments = {};
	var noAssignment;
	
	for (var guid in g_PlayerAssignments)
	{
		var name = g_PlayerAssignments[guid].name;
		var hostID = hostNameList.length;
		var player = g_PlayerAssignments[guid].player;
		
		hostNameList.push(name);
		hostGuidList.push(guid);
		assignments[player] = hostID;
	}

	for each (var ai in g_AIs)
	{	// Give AI a different color so it stands out
		aiAssignments[ai.id] = hostNameList.length;
		hostNameList.push("[color=\"30 90 30 255\"]AI: " + ai.data.name);
		hostGuidList.push("ai:" + ai.id);
	}

	noAssignment = hostNameList.length;
	hostNameList.push("[color=\"90 90 90 255\"]Unassigned");
	hostGuidList.push("");
	
	for (var i = 0; i < MAX_PLAYERS; ++i)
	{
		let playerSlot = i;
		let playerID = i+1; // we don't show Gaia, so first slot is ID 1
		
		var selection = assignments[playerID];
		
		var configButton = getGUIObjectByName("playerConfig["+i+"]");
		configButton.hidden = true;

		// Look for valid player slots
		if (playerSlot < g_GameAttributes.settings.PlayerData.length)
		{
			// If no human is assigned, look for an AI instead
			if (selection === undefined)
			{
				var aiId = g_GameAttributes.settings.PlayerData[playerSlot].AI;
				if (aiId)
				{
					selection = aiAssignments[aiId];
				}
				else
				{
					selection = noAssignment;
				}
				
				// Since no human is assigned, show the AI config button
				if (g_IsController)
				{
					configButton.hidden = false;
					configButton.onpress = function()
					{
						Engine.PushGuiPage("page_aiconfig.xml", {
							ais: g_AIs,
							id: g_GameAttributes.settings.PlayerData[playerSlot].AI,
							callback: function(ai) {
								g_GameAttributes.settings.PlayerData[playerSlot].AI = ai.id;

								if (g_IsNetworked)
								{
									Engine.SetNetworkGameAttributes(g_GameAttributes);
								}
								else
								{
									updatePlayerList();
								}
							}
						});
					};
				}
			}
			else
			{
				// There was a human, so make sure we don't have any AI left
				// over in their slot, if we're in charge of the attributes
				if (g_IsController && g_GameAttributes.settings.PlayerData[playerSlot].AI && g_GameAttributes.settings.PlayerData[playerSlot].AI != "")
				{
					g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
					if (g_IsNetworked)
					{
						Engine.SetNetworkGameAttributes(g_GameAttributes);
					}
				}
			}
			
			var assignBox = getGUIObjectByName("playerAssignment["+i+"]");
			assignBox.list = hostNameList;
			assignBox.list_data = hostGuidList;
			if (assignBox.selected != selection)
			{
				assignBox.selected = selection;
			}

			if (g_IsNetworked && g_IsController)
			{
				assignBox.onselectionchange = function ()
				{
					if (!g_IsInGuiUpdate)
					{
						var guid = hostGuidList[this.selected];
						if (guid == "")
						{
							// Unassign any host from this player slot
							Engine.AssignNetworkPlayer(playerID, "");
							// Remove AI from this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
						}
						else if (guid.substr(0, 3) == "ai:")
						{
							// Unassign any host from this player slot
							Engine.AssignNetworkPlayer(playerID, "");
							// Set the AI for this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
						}
						else
						{
							// Reassign the host to this player slot
							Engine.AssignNetworkPlayer(playerID, guid);
							// Remove AI from this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
						}
						Engine.SetNetworkGameAttributes(g_GameAttributes);
					}
				};
			}
			else if (!g_IsNetworked)
			{
				assignBox.onselectionchange = function ()
				{
					if (!g_IsInGuiUpdate)
					{
						var guid = hostGuidList[this.selected];
						if (guid == "")
						{
							// Remove AI from this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
						}
						else if (guid.substr(0, 3) == "ai:")
						{
							// Set the AI for this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = guid.substr(3);
						}
						else
						{
							// Update the selected host's player ID
							g_PlayerAssignments[guid].player = playerID;
							// Remove AI from this player slot
							g_GameAttributes.settings.PlayerData[playerSlot].AI = "";
						}

						updatePlayerList();
					}
				};
			}
		}
	}

	g_IsInGuiUpdate = false;
}

function submitChatInput()
{
	var input = getGUIObjectByName("chatInput");
	var text = input.caption;
	if (text.length)
	{
		Engine.SendNetworkChat(text);
		input.caption = "";
	}
}

function addChatMessage(msg)
{
	var username = escapeText(msg.username || g_PlayerAssignments[msg.guid].name);
	var message = escapeText(msg.text);
	
	// TODO: Maybe host should have distinct font/color?
	var color = "0 0 0";
	
	if (g_PlayerAssignments[msg.guid] && g_PlayerAssignments[msg.guid].player != 255)
	{	// Valid player who has been assigned - get player colour
		var player = g_PlayerAssignments[msg.guid].player - 1;
		var mapName = g_GameAttributes.map;
		var mapData = loadMapData(mapName);
		var mapSettings = (mapData && mapData.settings ? mapData.settings : {});
		var pData = mapSettings.PlayerData ? mapSettings.PlayerData[player] : {};
		var pDefs = g_DefaultPlayerData ? g_DefaultPlayerData[player] : {};
		
		color = iColorToString(getSetting(pData, pDefs, "Colour"));
	}
	
	var formatted;
	switch (msg.type)
	{
	case "connect":
		formatted = '[font="serif-bold-13"][color="'+ color +'"]' + username + '[/color][/font] [color="64 64 64"]has joined[/color]';
		break;

	case "disconnect":
		formatted = '[font="serif-bold-13"][color="'+ color +'"]' + username + '[/color][/font] [color="64 64 64"]has left[/color]';
		break;

	case "message":
		formatted = '[font="serif-bold-13"]<[color="'+ color +'"]' + username + '[/color]>[/font] ' + message;
		break;

	default:
		error("Invalid chat message '" + uneval(msg) + "'");
		return;
	}

	g_ChatMessages.push(formatted);

	getGUIObjectByName("chatText").caption = g_ChatMessages.join("\n");
}

////////////////////////////////////////////////////////////////////////////////////////////////
// Basic map filters API

// Add a new map list filter
function addFilter(name, filterFunc)
{
	if (filterFunc instanceof Object)
	{	// Basic validity test
		var newFilter = {};
		newFilter.name = name;
		newFilter.filter = filterFunc;
		
		g_MapFilters.push(newFilter);
	}
	else
	{
		error("Invalid map filter: "+name);
	}
}

// Get array of map filter names
function getFilters()
{
	var filters = [];
	for (var i = 0; i < g_MapFilters.length; ++i)
	{
		filters.push(g_MapFilters[i].name);
	}
	
	return filters;
}

// Test map filter on given map settings object
function testFilter(name, mapSettings)
{
	for (var i = 0; i < g_MapFilters.length; ++i)
	{
		if (g_MapFilters[i].name == name)
		{	// Found filter
			return g_MapFilters[i].filter(mapSettings);
		}
	}
	
	error("Invalid map filter: "+name);
	return false;
}

// Test an array of keywords against a match array using AND logic
function keywordTestAND(keywords, matches)
{
	if (!keywords || !matches)
	{
		return false;
	}
	
	for (var m = 0; m < matches.length; ++m)
	{	// Fail on not match
		if (keywords.indexOf(matches[m]) == -1)
		{
			return false;
		}
	}
	return true;
}

// Test an array of keywords against a match array using OR logic
function keywordTestOR(keywords, matches)
{
	if (!keywords || !matches)
	{
		return false;
	}
	
	for (var m = 0; m < matches.length; ++m)
	{	// Success on match
		if (keywords.indexOf(matches[m]) != -1)
		{
			return true;
		}
	}
	return false;
}
