#include "~/Game Data/gui/include/dialog.as"
#include "~/Game Data/gui/include/gui_skin.as"
#include "~/Game Data/gui/include/empire_image.as"

/* {{{ Constants */
const string@ strStrength = "str";
const string@ strPlanet = "Planet";
/* }}} */
/* {{{ Tracking variables */
Empire@ player;
const Empire@ enemy, neutral,
			remnants, pirates;
System@ homeSys, remnSys, freeSys,
	    neutSys, enemSys;

void setupTracking() {
	// Set up empires
	@player = getActiveEmpire();
	@neutral = getEmpireByID(2);
	@enemy = getEmpireByID(3);
	@remnants = getEmpireByID(-3);
	@pirates = getEmpireByID(-2);

	// Track systems
	@homeSys = getSystem(0);
	@remnSys = getSystem(1);
	@freeSys = getSystem(2);
	@neutSys = getSystem(3);
	@enemSys = getSystem(4);
}

Object@ findShip(System@ sys, Empire@ emp, const HullLayout@ layout) {
	SysObjList list;
	list.prepare(sys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj.getOwner() !is emp)
			continue;
		HulledObj@ hulled = obj;
		if (hulled is null)
			continue;
		if (hulled.getHull() !is layout)
			continue;
		return obj;
	}
	return null;
}

Planet@ findPlanet(System@ sys, Empire@ emp) {
	SysObjList list;
	list.prepare(sys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj.getOwner() !is emp)
			continue;
		Planet@ pl = obj;
		if (pl is null)
			continue;
		return pl;
	}
	return null;
}

bool foundRemnants = false;
bool closedRemnants = false;

void closeRemnantMsg(MessageDialog@ dialog) {
	closedRemnants = true;
}

void checkRemnantScout() {
	if (remnSys.isVisibleTo(player)) {
		foundRemnants = true;
		setCameraFocus(remnSys);
		MessageDialog@ dialog = addMessageDialog(combine(
			"#font:stroked_subtitle#",
			localize("#tut scout title"),
			"#font#\n",
			localize("#tut scout text")
		), closeRemnantMsg);

		pos2di pos = dialog.win.getPosition();
		dialog.win.setPosition(pos2di(pos.x, 106));
	}
}

bool foundEmpty = false;
void checkEmptyScout() {
	if (closedRemnants && freeSys.isVisibleTo(player)) {
		foundEmpty = true;
		setCameraFocus(freeSys);
		MessageDialog@ dialog = addMessageDialog(combine(
			"#font:stroked_subtitle#",
			localize("#tut scout2 title"),
			"#font#\n",
			localize("#tut scout2 text")
		), null);

		pos2di pos = dialog.win.getPosition();
		dialog.win.setPosition(pos2di(pos.x, 106));
	}
}
/* }}} */
/* {{{ Event handling */
void checkReinforcements() {
	if (player is null)
		return;

	if (player.getStat(strStrength) < 0.01f) {
		sendServerMessage("tutorial_events", 0, "", 0.f);
		addMessageDialog(localize("#tut reinforcements"), null);
	}
}

void checkColonizer() {
	if (player.getStat(strPlanet) < 0.01f) {
		const HullLayout@ colo = player.getShipLayout(localize("#SH_Colony Ship"));
		Object@ ship = findShip(homeSys, player, colo);
		if (ship is null)
			SpawnColonizer();
	}
}

void FirstPirates() {
	sendServerMessage("tutorial_events", 1, "", 0.f);
}

void SecondPirates() {
	sendServerMessage("tutorial_events", 2, "", 0.f);
}

void ThirdPirates() {
	sendServerMessage("tutorial_events", 3, "", 0.f);
}

bool zoomToColonizer = false;
bool spawnedColonizer = false;
void SpawnColonizer() {
	sendServerMessage("tutorial_events", 4, "", 0.f);
	if (!spawnedColonizer)
		zoomToColonizer = true;
	spawnedColonizer = true;
}

void ZoomColonizer() {
	const HullLayout@ colo = player.getShipLayout(localize("#SH_Colony Ship"));
	Object@ ship = findShip(homeSys, player, colo);
	if (ship !is null) {
		zoomToColonizer = false;
		cameraZoomTo(ship, true, true);
		cameraSetTopDown(40.f);
		cameraSetDistance(40.f);
	}
}

void EmptyBank() {
	sendServerMessage("tutorial_events", 5, "", 0.f);
	ShowBank();
	CloseSystem();
}

void EmptyMetals() {
	sendServerMessage("tutorial_events", 6, "", 0.f);
	showCivActsButton();
}

void VisionNeutrals() {
	sendServerMessage("tutorial_events", 7, "", 0.f);
	setCameraFocus(neutSys);
}

void VisionEnemies() {
	sendServerMessage("tutorial_events", 8, "", 0.f);
	setCameraFocus(enemSys);
}

void UnlockLasers() {
	sendServerMessage("tutorial_events", 9, "", 0.f);
	showBankButtons();
}

void AchieveCompletion() {
	if(canAchieve)
		achieve(AID_TUTORIAL);
}
/* }}} */
// {{{ Game state checks
quaternion lastRot;
float lastPitch = 0.f, lastYaw = 0.f;
bool checkedRot = false;
bool rotChanged = false;

bool close(float x, float y) {
	return abs(x-y) < 0.02f;
}

bool checkOrbit(Objective@ obj) {
	bool changed = false;
	if (checkedRot) {
		if (getCameraFreeform()) {
			quaternion rot = getCameraRotation();
			changed = !close(rot.x, lastRot.x) || !close(rot.y, lastRot.y)
					|| !close(rot.z, lastRot.z) || !close(rot.w, lastRot.w);
		}
		else {
			changed = !close(getCameraYaw(), lastYaw)
					|| !close(getCameraPitch(), lastPitch);
		}
	}

	checkedRot = true;
	if (getCameraFreeform()) {
		lastRot = getCameraRotation();
	}
	else {
		lastYaw = getCameraYaw();
		lastPitch = getCameraPitch();
	}
	rotChanged = changed;
	return changed && !altKey;
}

bool rollChanged = false;
bool checkRoll(Objective@ obj) {
	if (checkedRot && altKey) {
		rollChanged = rotChanged;
		return rotChanged;
	}
	rollChanged = false;
	return false;
}

float lastZoom = 0.f;
bool checkedZoom = false;
bool zoomChanged = false;
bool checkZoom(Objective@ obj) {
	bool changed = false;
	if (checkedZoom)
		changed = !close(getCameraDistance(), lastZoom);

	checkedZoom = true;
	lastZoom = getCameraDistance();
	zoomChanged = changed;
	return changed;
}

vector lastDest;
bool checkedDest = false;
bool panChanged = false;
bool checkPan(Objective@ obj) {
	bool changed = false;
	if (checkedDest) {
		vector dest = getCameraDestination();
		changed = !close(dest.x, lastDest.x) || !close(dest.y, lastDest.y)
				|| !close(dest.z, lastDest.z);
	}

	checkedDest = true;
	lastDest = getCameraDestination();
	panChanged = changed;
	return changed && !zoomChanged && checkedZoom;
}

bool checkPiratesDead(Objective@ obj) {
	if (obj.completed)
		return false;
	return homeSys.toObject().getStrength(pirates) < 0.001f;
}

bool checkEnemiesDead(Objective@ obj) {
	if (obj.completed)
		return false;
	return enemSys.toObject().getStrength(enemy) < 0.001f;
}

bool checkShiftAlt(Objective@ obj) {
	return (shiftKey || altKey) && (zoomChanged || panChanged || rotChanged) && !rollChanged;
}

bool checkMoveToPoint(Objective@ obj) {
	if (obj.completed)
		return false;
	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (obj.getDestination().getDistanceFromSQ(obj.getPosition()) > 1.f)
			return true;
	}
	return false;
}

bool check3DMove(Objective@ obj) {
	if (obj.completed)
		return false;
	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (abs(obj.getDestination().y) > 1.f)
			return true;
	}
	return false;
}

bool checkMoveObj(Objective@ obj) {
	if (obj.completed)
		return false;
	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (obj.getTarget() !is null)
			return true;
	}
	return false;
}

bool checkAttackOrder(Objective@ obj) {
	if (obj.completed)
		return false;
	SysObjList list;
	list.prepare(homeSys);

	OrderList orders;

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (orders.prepare(obj)) {
			if (orders.getOrderCount() > 0
				&& orders.getOrder(0).getType() == OrdT_Attack)
				return true;
		}
	}
	return false;
}

bool checkDefendOrder(Objective@ obj) {
	if (obj.completed)
		return false;
	SysObjList list;
	list.prepare(homeSys);

	OrderList orders;

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (orders.prepare(obj)) {
			if (orders.hasOrder(OrdT_Defend))
				return true;
		}
	}
	return false;
}

bool hasPlanets(Objective@ obj) {
	if (obj.completed)
		return false;
	return findPlanet(homeSys, player) !is null;
}

bool checkBuild(string@ type, string@ name, uint count) {
	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player || obj.toPlanet() is null)
			continue;

		if (obj.getConstructionQueueSize() >= count) {
			bool found = true;
			for (uint i = 0; i < count; ++i) {
				 if (obj.getConstructionType(i) != type
					 || obj.getConstructionName(i) != name) {
					 found = false;
					 break;
				}
			}

			if (found)
				return true;
		}
	}
	return false;
}

bool buildingShipyard(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("structure", localize("#SS_SHIPYARD_NAME"), 1);
}

bool buildingMining(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("structure", localize("#SS_METALMINE_NAME"), 1);
}

bool buildingScout(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("ship", localize("#SH_Scout"), 1);
}

bool buildingCruiser(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("ship", localize("#SH_Cruiser"), 1);
}

bool buildingCruisers(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("ship", localize("#SH_Cruiser"), 5);
}

bool buildingCapShips(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("ship", localize("#SH_Capital Ship"), 3);
}

bool checkRetrofit(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkBuild("ship", localize("#retrofit") + " " + localize("#SH_Cruiser"), 1);
}

bool checkGovernor(string@ name) {
	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Planet@ obj = list.getChild(i);
		if (obj is null || obj.toObject().getOwner() !is player)
			continue;

		if (obj.usesGovernor() && obj.getGovernorType() == name)
			return true;
	}
	return false;
}

bool checkBalanced(Objective@ obj) {
	if (obj.completed)
		return false;
	return checkGovernor("default");
}

bool checkFull(Objective@ obj) {
	if (obj.completed)
		return false;

	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Planet@ obj = list.getChild(i);
		if (obj is null || obj.toObject().getOwner() !is player)
			continue;

		if (obj.getStructureCount() >= uint(floor(obj.getMaxStructureCount())))
			return true;
	}
	return false;
}

bool hasScouted(Objective@ obj) {
	if (obj.completed)
		return false;
	return foundRemnants && foundEmpty;
}

bool hasFleet(Objective@ obj) {
	if (obj.completed)
		return false;
	return player.getFleetCount() > 0;
}

bool fleetIsEscort(Objective@ obj) {
	if (obj.completed || player.getFleetCount() == 0)
		return false;
	Fleet@ fl = player.getFleet(0);
	return fl.getFormation() == FF_Escort;
}

bool fleetIsHoldPosition(Objective@ obj) {
	if (obj.completed || player.getFleetCount() == 0)
		return false;
	Fleet@ fl = player.getFleet(0);
	return fl.getCommander().getStance() == AIS_HoldPosition;
}

bool fleetIsOrbit(Objective@ obj) {
	if (obj.completed || player.getFleetCount() == 0)
		return false;
	Fleet@ fl = player.getFleet(0);
	return fl.getCommander().isParked();
}

bool checkColonizers(Objective@ obj) {
	if (obj.completed)
		return false;

	SysObjList list;
	list.prepare(homeSys);

	uint amount = 0;
	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Object@ obj = list.getChild(i);
		if (obj is null || obj.getOwner() !is player)
			continue;

		if (obj.toPlanet() !is null) {
			amount += 1;
			continue;
		}

		HulledObj@ hulled = obj;
		if (hulled !is null && hulled.getHull().hasSystemWithTag("Colonizer")) {
			amount += 1;
		}
	}
	return amount >= 3;
}

bool hasHomeSystem(Objective@ obj) {
	if (obj.completed)
		return false;

	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Planet@ obj = list.getChild(i);
		if (obj is null)
			continue;

		if (obj.toObject().getOwner() !is player)
			return false;
	}
	return true;
}

bool correctGovernors(Objective@ obj) {
	if (obj.completed)
		return false;

	SysObjList list;
	list.prepare(homeSys);

	bool balanced = false;
	bool metal = false;
	bool econ = false;

	uint cnt = list.childCount;
	for (uint i = 0; i < cnt; ++i) {
		Planet@ obj = list.getChild(i);
		if (obj is null)
			continue;

		if (obj.toObject().getOwner() !is player)
			return false;

		if (!obj.usesGovernor())
			return false;

		string@ type = obj.getGovernorType();
		if (type == "default") {
			balanced = true;
		}
		else if (type == "metalworld") {
			metal = true;
		}
		else if (type == "economic") {
			econ = true;
		}
	}
	return balanced && metal && econ;
}

bool checkMetalsMandate(Objective@ obj) {
	if (obj.completed)
		return false;

	return uint(round(player.getSetting("eco_mode"))) == 4;
}

bool checkSufficientMetals(Objective@ obj) {
	if (obj.completed)
		return false;

	return player.getStat("Metals") >= 200.f * 1000.f;
}

bool checkArtyUnlocked(Objective@ obj) {
	if (obj.completed)
		return false;

	return player.subSysUnlocked(getSubSystemDefByName("Artillery"));
}

import int getQueueSize() from "research_win";
bool checkResQueueSize(Objective@ obj) {
	if (obj.completed)
		return false;

	return getQueueSize() >= 3;
}

bool hasArtyDesign(Objective@ obj) {
	if (obj.completed)
		return false;

	uint cnt = player.getShipLayoutCnt();
	for (uint i = 0; i < cnt; ++i) {
		const HullLayout@ lay = player.getShipLayout(i);
		if (lay.hasSystemWithTag("Artillery"))
			return true;
	}
	return false;
}

bool checkArtyDefense(Objective@ obj) {
	if (obj.completed)
		return false;

	uint[] ids;
	uint[] counts;

	ids.resize(3);
	counts.resize(3);

	for (uint i = 0; i < 3; ++i) {
		ids[i] = 0;
		counts[i] = 0;
	}

	SysObjList list;
	list.prepare(homeSys);

	uint cnt = list.childCount;
	uint j = 0;
	for (uint i = 0; i < cnt; ++i) {
		HulledObj@ obj = list.getChild(i);
		if (obj is null)
			continue;

		if (obj.toObject().getOwner() !is player)
			continue;

		if (!obj.getHull().hasSystemWithTag("Artillery"))
			continue;

		Planet@ around = obj.toObject().inOrbitAround();
		if (around is null || around.toObject().getOwner() !is player)
			continue;

		uint ID = around.toObject().uid;
		bool found = false;
		for (uint n = 0; n < j; ++n) {
			if (ids[n] == ID) {
				counts[n] += 1;
				found = true;
				break;
			}
		}

		if (found)
			continue;

		if (j < 3) {
			ids[j] = ID;
			counts[j] = 1;
			++j;
		}
	}

	return counts[0] >= 3 && counts[1] >= 3 && counts[2] >= 3;
}

bool hasExpanded(Objective@ obj) {
	if (obj.completed)
		return false;

	return player.getStat("Planet") > 3.f;
}
// }}}
/* {{{ Interface Hiding */
import void setMessagesVisible(bool) from "gui";
import void setTickerVisible(bool) from "gui";
import void setTickerResearchVisible(bool) from "gui";
import void setObjWinVisible(bool) from "object_info_win";
import void setBPEnabled(bool) from "subsystem_win";
import void toggleMiniviews() from "miniview";
import void setMinimapButtonVisible(bool visible) from "miniview";
import void setPinButtonVisible(bool vis) from "pinned_objects";
import void addPin(Object@) from "pinned_objects";
import void setSPVisible(bool vis) from "star_pedia";
import void setEmpVisible(bool vis) from "empire_win";
import void setPlVisible(bool vis) from "planet_win";
import void setBpVisible(bool vis) from "layout_win";
import void setSysVisible(bool vis) from "sys_win";
import void setResVisible(bool vis) from "research_win";
import void setNotificationsEnabled(bool) from "notifications";
import void setCivActsButtonVisible(bool) from "gui";
import void setBankButtonsVisible(bool) from "gui";

void HideInterface() {
	// Ticker
	setTickerVisible(false);
	setTickerResearchVisible(false);
	setBankButtonsVisible(false);
	setCivActsButtonVisible(false);

	// Messages
	setMessagesVisible(false);
	setNotificationsEnabled(false);

	// Top buttons
	setSPVisible(false);
	setEmpVisible(false);
	setPlVisible(false);
	setBpVisible(false);
	setSysVisible(false);
	setResVisible(false);

	// Minimaps
	toggleMiniviews();
	setMinimapButtonVisible(false);

	// Object Information
	setObjWinVisible(false);

	// Damage panel
	setBPEnabled(false);

	// Pinned objects
	setPinButtonVisible(false);

	// Un-hide stuff that was already visible
	if (objWinVis)
		ShowObjWin();
	if (selVis)
		ShowSelection();
	if (plVis) {
		ShowPlanet();
		ClosePlanet();
	}
	if (pinVis)
		ShowPinned();
	if (sysVis) {
		ShowSystem();
		CloseSystem();
	}
	if (bankVis)
		ShowBank();
	if (bpVis) {
		ShowBlueprints();
		CloseBlueprints();
	}
	if (resVis) {
		ShowResearch();
		CloseResearch();
	}
	if (mmVis)
		ShowMinimaps();
	if (empVis) {
		ShowEmpire();
		CloseEmpire();
	}
	if (spVis)
		ShowInfo();
	if (bankButtonsVis)
		showBankButtons();
	if (civActsButtonVis)
		showCivActsButton();
}

bool bankButtonsVis = false;
void showBankButtons() {
	setBankButtonsVisible(true);
	bankButtonsVis = true;
}

bool civActsButtonVis = false;
void showCivActsButton() {
	setCivActsButtonVisible(true);
	civActsButtonVis = true;
}

bool objWinVis = false;
void ShowObjWin() {
	setObjWinVisible(true);
	objWinVis = true;
}

bool selVis = false;
void ShowSelection() {
	setBPEnabled(true);
	selVis = true;
}

import void togglePlanetWindow(bool) from "planet_win";
bool plVis = false;
void ShowPlanet() {
	setPlVisible(true);
	setMessagesVisible(true);
	togglePlanetWindow(true);
	plVis = true;
}

void ClosePlanet() {
	togglePlanetWindow(false);
}

bool pinVis = false;
void ShowPinned() {
	Planet@ pl = findPlanet(homeSys, player);
	if (pl !is null)
		addPin(pl);
	setPinButtonVisible(true);
	pinVis = true;
}

import void toggleSystemWindow(bool) from "sys_win";
bool sysVis = false;
void ShowSystem() {
	setSysVisible(true);
	toggleSystemWindow(true);
	sysVis = true;
}

void CloseSystem() {
	toggleSystemWindow(false);
}

bool bankVis = false;
void ShowBank() {
	setTickerVisible(true);
	bankVis = true;
}

import void toggleLayoutWindow(bool) from "layout_win";
bool bpVis = false;
void ShowBlueprints() {
	setBpVisible(true);
	toggleLayoutWindow(true);
	bpVis = true;
}

void CloseBlueprints() {
	toggleLayoutWindow(false);
}

import void toggleResearchWindow(bool) from "research_win";
bool resVis = false;
void ShowResearch() {
	CloseBlueprints();
	setNotificationsEnabled(true);
	sendServerMessage("tutorial_events", 10, "", 0.f);
	setResVisible(true);
	setTickerResearchVisible(true);
	toggleResearchWindow(true);
	resVis = true;
}

void CloseResearch() {
	toggleResearchWindow(false);
}

bool mmVis = false;
void ShowMinimaps() {
	toggleMiniviews();
	setMinimapButtonVisible(true);
	mmVis = true;
}

import void toggleEmpireWindow(bool) from "empire_win";
import void showTreaty(const Treaty@ treaty) from "trade_win";
bool empVis = false;
void ShowEmpire() {
	setEmpVisible(true);
	toggleEmpireWindow(true);

	TreatyList list;
	list.prepare(player);
	Treaty@ proposed = list.getProposedTreaty(neutral);

	if (proposed !is null)
		showTreaty(proposed);
	empVis = true;
}

void CloseEmpire() {
	toggleEmpireWindow(false);
}

bool spVis = false;
void ShowInfo() {
	setSPVisible(true);
	spVis = true;
}

/* }}} */
/* {{{ Tutorial Text */
funcdef void NextCallback();

class PageDesc {
	const Empire@ emp;
	string@ id;
	string@ title;
	string@ text;

	NextCallback@ nextCall;
	string@ nextText;
	string@ nextPrompt;
	bool completed;
	bool autoAdvance;

	Objective@[] objectives;

	PageDesc(string@ ID, string@ Title, string@ Text) {
		@id = ID;
		@title = Title;
		@text = Text;
		@emp = player;
		completed = false;
		autoAdvance = false;

		int64 res;
		if (completedIDs.get(id, res))
			completed = res == 1;
	}

	void callNext() {
		nextCall();
	}
};

class Objective {
	string@ id;
	string@ title;
	string@ text;

	bool completed;

	float checkTimeout;
	ObjectiveCheck@ check;

	Objective(string@ ID, string@ Title, string@ Text,
				ObjectiveCheck@ Check, float timeout) {
		@id = ID;
		@title = Title;
		@text = Text;
		completed = false;

		@check = Check;
		checkTimeout = timeout;

		int64 res;
		if (completedIDs.get(id, res))
			completed = res == 1;
	}

	bool checkCompletion() {
		if (check is null)
			return false;
		if (check(this)) {
			completed = true;
			return true;
		}
		return false;
	}
};

funcdef bool ObjectiveCheck(Objective@ obj);

PageDesc@ addPage(string@ prefix) {
	PageDesc@ desc = PageDesc(
		prefix,
		localize(prefix+" title"),
		localize(prefix+" text"));

	uint n = pages.length();
	pages.resize(n+1);
	@pages[n] = desc;

	return desc;
}

Objective@ addObjective(PageDesc@ desc, string@ prefix) {
	return addObjective(desc, prefix, null, 0);
}

Objective@ addObjective(PageDesc@ desc, string@ prefix, ObjectiveCheck@ check) {
	return addObjective(desc, prefix, check, 0.f);
}

Objective@ addObjective(PageDesc@ desc, string@ prefix, ObjectiveCheck@ check, float timeout) {
	Objective@ obj = Objective(
		prefix,
		localize(prefix+" title"),
		localize(prefix+" text"),
		check, timeout);

	uint n = desc.objectives.length();
	desc.objectives.resize(n + 1);
	@desc.objectives[n] = obj;

	return obj;
}

void addNote(PageDesc@ desc, string@ prefix) {
	desc.text += combine(
		"\n\n#font:frank_11#",
		localize(prefix+" title"),
		"#font#\n",
		localize(prefix+" text"));
}

PageDesc@[] pages;
void loadPages() {
	PageDesc@ desc;

	@desc = addPage("#tut intro");

	@desc = addPage("#tut cam");
	addObjective(desc, "#tut cam act orbit", checkOrbit);
	addObjective(desc, "#tut cam act zoom", checkZoom);
	addObjective(desc, "#tut cam act pan", checkPan);
	addObjective(desc, "#tut cam act mod", checkShiftAlt);
	//addObjective(desc, "#tut cam act roll", checkRoll);
	@desc.nextCall = ShowObjWin;

	@desc = addPage("#tut sel");
	addObjective(desc, "#tut sel act sel");
	addObjective(desc, "#tut sel act box");
	addObjective(desc, "#tut sel act paint");
	addObjective(desc, "#tut sel act toggle");
	addObjective(desc, "#tut sel act cycle");

	@desc.nextCall = ShowSelection;
	@desc = addPage("#tut ord");
	@desc.nextText = localize("#continue_pirates");
	@desc.nextPrompt = localize("#confirm_pirates");
	@desc.nextCall = FirstPirates;

	addObjective(desc, "#tut ord act mov", checkMoveToPoint, 1.f);
	addObjective(desc, "#tut ord act mov3d", check3DMove, 1.f);
	addObjective(desc, "#tut ord act movobj", checkMoveObj, 1.f);
	addObjective(desc, "#tut ord act clear");

	@desc = addPage("#tut pir");
	addObjective(desc, "#tut pir act atk", checkAttackOrder, 1.f);
	addObjective(desc, "#tut pir act auto", checkDefendOrder, 1.f);
	addObjective(desc, "#tut pir act pir", checkPiratesDead, 1.f);
	desc.autoAdvance = true;

	@desc = addPage("#tut vic");

	@desc.nextCall = SpawnColonizer;
	@desc = addPage("#tut colo");
	addObjective(desc, "#tut colo ord", hasPlanets, 1.f);

	@desc.nextCall = ShowPlanet;
	@desc = addPage("#tut pl");

	@desc = addPage("#tut pl2");
	addObjective(desc, "#tut pl2 act build", buildingShipyard);
	addObjective(desc, "#tut pl2 act build2", buildingMining);
	addObjective(desc, "#tut pl2 act gov", checkBalanced, 1.f);
	addObjective(desc, "#tut pl2 act con", checkFull, 1.f);

	@desc = addPage("#tut pl3");
	addObjective(desc, "#tut pl3 act scout", buildingScout);
	addObjective(desc, "#tut pl3 act scout2", hasScouted);

	@desc = addPage("#tut pl4");
	addObjective(desc, "#tut pl4 act cru", buildingCruisers);
	addObjective(desc, "#tut pl4 act cap", buildingCapShips);
	@desc.nextCall = ClosePlanet;

	@desc = addPage("#tut fl");
	@desc.nextText = localize("#continue_pirates");
	@desc.nextPrompt = localize("#confirm_pirates");
	@desc.nextCall = SecondPirates;

	addObjective(desc, "#tut fl act fl", hasFleet);
	addObjective(desc, "#tut fl act formation", fleetIsEscort);
	addObjective(desc, "#tut fl act stance", fleetIsHoldPosition);
	addObjective(desc, "#tut fl act orbit", fleetIsOrbit);

	@desc = addPage("#tut pir2");
	addObjective(desc, "#tut pir2 act pir", checkPiratesDead, 1.f);
	desc.autoAdvance = true;

	@desc.nextCall = ShowPinned;
	@desc = addPage("#tut colo2");
	addObjective(desc, "#tut colo2 act sh", checkColonizers);
	addObjective(desc, "#tut colo2 act sh2", hasHomeSystem);

	@desc.nextCall = ShowSystem;
	@desc = addPage("#tut sys");
	addObjective(desc, "#tut sys act bld", buildingCruiser);
	addObjective(desc, "#tut sys act gov", correctGovernors, 1.f);

	@desc.nextCall = EmptyBank;
	@desc = addPage("#tut res");
	@desc = addPage("#tut res note0");
	@desc = addPage("#tut res note1");
	@desc = addPage("#tut res note2");
	@desc = addPage("#tut res note3");
	@desc = addPage("#tut res note4");
	@desc = addPage("#tut res note5");
	@desc = addPage("#tut eco");
	@desc = addPage("#tut plres");
	@desc = addPage("#tut plres note0");
	@desc = addPage("#tut plres note1");
	@desc = addPage("#tut exp");
	@desc.nextCall = EmptyMetals;
	@desc = addPage("#tut mtl");
	addObjective(desc, "#tut mtl act civ", checkMetalsMandate);
	addObjective(desc, "#tut mtl act goal", checkSufficientMetals);

	@desc.nextCall = UnlockLasers;
	@desc = addPage("#tut bp intro");
	@desc.nextCall = ShowBlueprints;
	@desc = addPage("#tut bp");
	addObjective(desc, "#tut bp act open");
	@desc = addPage("#tut bp2");
	@desc = addPage("#tut bp2 note0");
	@desc = addPage("#tut bp2 note1");
	@desc = addPage("#tut bp2 note2");
	@desc = addPage("#tut bp2 note3");
	@desc = addPage("#tut bp2 note4");
	@desc = addPage("#tut bp2 note5");
	@desc = addPage("#tut bp2 note6");
	@desc = addPage("#tut bp3");
	addObjective(desc, "#tut bp3 act name");
	addObjective(desc, "#tut bp3 act rem");
	addObjective(desc, "#tut bp3 act add");
	@desc = addPage("#tut bp3");
	addObjective(desc, "#tut bp3 act power");
	addObjective(desc, "#tut bp3 act fuel");
	addObjective(desc, "#tut bp3 act save");
	@desc.nextCall = ShowInfo;
	@desc = addPage("#tut bp4");
	@desc = addPage("#tut bp4 note0");
	@desc = addPage("#tut bp4 note1");
	@desc = addPage("#tut bp4 note2");
	@desc = addPage("#tut bp4 note3");
	@desc = addPage("#tut bp4 note4");
	@desc = addPage("#tut bp4 note5");

	@desc.nextCall = ShowResearch;
	@desc = addPage("#tut tech");
	@desc = addPage("#tut tech2");
	@desc = addPage("#tut tech2 note0");
	addObjective(desc, "#tut tech2 act res", checkArtyUnlocked);
	addObjective(desc, "#tut tech2 act queue", checkResQueueSize);
	@desc.nextCall = CloseResearch;

	@desc = addPage("#tut arty");
	addObjective(desc, "#tut arty act cre", hasArtyDesign, 1.f);
	addObjective(desc, "#tut arty act bld", checkArtyDefense, 1.f);

	@desc = addPage("#tut retro");
	addObjective(desc, "#tut retro act upd");
	addObjective(desc, "#tut retro act retro", checkRetrofit);

	@desc.nextCall = ShowMinimaps;
	@desc = addPage("#tut pir3");
	@desc.nextText = localize("#continue_pirates");
	@desc.nextPrompt = localize("#confirm_pirates");
	@desc.nextCall = ThirdPirates;

	@desc = addPage("#tut pir4");
	addObjective(desc, "#tut pir4 act pir", checkPiratesDead, 1.f);
	desc.autoAdvance = true;

	@desc = addPage("#tut pir5");

	@desc.nextCall = VisionNeutrals;
	@desc = addPage("#tut dipl");
	@desc.emp = neutral;
	@desc.nextCall = ShowEmpire;
	@desc = addPage("#tut dipl2");
	addObjective(desc, "#tut dipl2 act hand");
	@desc.emp = neutral;
	@desc = addPage("#tut dipl3");
	addObjective(desc, "#tut dipl3 act create");
	@desc.emp = neutral;
	@desc = addPage("#tut dipl4");
	addObjective(desc, "#tut dipl4 act clause");
	addObjective(desc, "#tut dipl4 act propose");
	@desc.emp = neutral;
	@desc.nextCall = CloseEmpire;

	@desc = addPage("#tut exp2");
	addObjective(desc, "#tut exp2 act exp", hasExpanded);
	@desc = addPage("#tut exp2 note0");

	@desc = addPage("#tut info");

	@desc.nextCall = VisionEnemies;
	@desc = addPage("#tut war");
	addObjective(desc, "#tut war act elim", checkEnemiesDead, 1.f);
	desc.autoAdvance = true;
	@desc.emp = enemy;
	@desc.nextCall = AchieveCompletion;

	@desc = addPage("#tut compl");
}
/* }}} */
/* {{{ Tutorial GUI */
TutorialWindow@ tutWin;
void openTutorialWindow() {
	int width = getScreenWidth();
	recti pos = recti(pos2di(width / 2 - 290, 56), dim2di(580, 240));
	@tutWin = TutorialWindow(pos);
	tutWin.bringToFront();
}

void closeTutorialWindow() {
	tutWin.remove();
	@tutWin = null;
}

void pingObjective(string@ objective) {
	if (tutWin is null)
		return;
	uint cnt = tutWin.script.objDisps.length();
	for (uint i = 0; i < cnt; ++i) {
		ObjectiveDisplay@ disp = tutWin.script.objDisps[i];
		if (disp.obj.id !is null && disp.obj.id == objective) {
			disp.ping();
		}
	}
}

void completeObjective(string@ objective) {
	if (tutWin is null)
		return;
	uint cnt = tutWin.script.objDisps.length();
	for (uint i = 0; i < cnt; ++i) {
		ObjectiveDisplay@ disp = tutWin.script.objDisps[i];
		if (disp.obj.id !is null && !disp.obj.completed && disp.obj.id == objective)
			disp.ping();
	}
}

class TutorialConfirm : ConfirmDialogCallback {
	TutorialWindowScript@ win;

	TutorialConfirm(TutorialWindowScript@ Win) {
		@win = Win;
	}

	void call(ConfirmDialog@ dialog, bool confirmed) {
		if (win is null || win.removed)
			return;
		if (confirmed)
			win.next(true);
	}
}

class FreeformChooser : ScriptedGuiHandler {
	GuiExtText@ text;
	GuiCheckBox@ check;

	void init(GuiElement@ ele) {
		const recti absPos = ele.getAbsolutePosition();
		dim2di size = absPos.getSize();

		@text = GuiExtText(recti(4, 4, size.width - 8, 20), ele);
		text.setText(localize("#tut cam freeform"));

		int height = text.getSize().height + 8;

		@check = GuiCheckBox(getCameraFreeform(),
				recti(4, height, size.width - 8, height + 20),
				localize("#tut cam freeform toggle"), ele);


		height += 20;
		if (height % 2 != 0)
			height += 1;
		ele.setSize(dim2di(size.width, height));
	}

	void draw(GuiElement@ ele) {
		ele.toGuiScripted().setAbsoluteClip();
		const recti absPos = ele.getAbsolutePosition();
		pos2di topLeft = absPos.UpperLeftCorner;
		pos2di botRight = absPos.LowerRightCorner;
		dim2di size = absPos.getSize();

		Color bgCol(0xffffffff);
		drawDarkArea(absPos, bgCol);

		clearDrawClip();
		Color lineCol(0xff343434);
		drawRectOutline(absPos, lineCol);
	}

	EventReturn onMouseEvent(GuiElement@ ele, const MouseEvent& evt) {
		return ER_Pass;
	}

	EventReturn onGUIEvent(GuiElement@ ele, const GUIEvent& evt) {
		if (evt.EventType == GEVT_Checkbox_Toggled)
			setCameraFreeform(check.isChecked());
		return ER_Pass;
	}

	EventReturn onKeyEvent(GuiElement@ ele, const KeyEvent& evt) {
		return ER_Pass;
	}
}

class ObjectiveDisplay : ScriptedGuiHandler {
	GuiExtText@ text;
	Objective@ obj;
	float bgPulse;
	float checkTimer;

	ObjectiveDisplay(Objective@ Obj) {
		@obj = Obj;
		bgPulse = 1.f;
		checkTimer = 0.f;
	}

	void init(GuiElement@ ele) {
		const recti absPos = ele.getAbsolutePosition();
		dim2di size = absPos.getSize();

		@text = GuiExtText(recti(4, 4, size.width - 8, 20), ele);
		text.setText(combine(
			"#font:stroked_subtitle#",
			obj.title,
			"#font#\n",
			obj.text
		));

		int height = text.getSize().height + 8;
		if (height % 2 != 0)
			height += 1;
		ele.setSize(dim2di(size.width, height));
	}

	void tick(float time) {
		if (bgPulse > 0) {
			bgPulse -= time;
			return;
		}

		if (obj.check is null)
			return;

		if (checkTimer > 0) {
			checkTimer -= time;
			return;
		}

		checkTimer = obj.checkTimeout;
		if (obj.checkCompletion()) {
			bgPulse = 1.f;
		}
	}

	void ping() {
		obj.completed = true;
		bgPulse = 1.f;
	}

	void draw(GuiElement@ ele) {
		ele.toGuiScripted().setAbsoluteClip();
		const recti absPos = ele.getAbsolutePosition();
		pos2di topLeft = absPos.UpperLeftCorner;
		pos2di botRight = absPos.LowerRightCorner;
		dim2di size = absPos.getSize();

		Color bgCol(0xffffffff);
		drawDarkArea(absPos, bgCol);

		if (bgPulse > 0) {
			Color pulseCol;

			if (obj.completed) {
				pulseCol = Color(0xff006aff);
				pulseCol.A = 48 + (0.5f - abs(0.5f - bgPulse)) * 128;
			}
			else {
				pulseCol = Color(0xffff9600);
				pulseCol.A = (0.5f - abs(0.5f - bgPulse)) * 128;
			}

			drawRect(absPos, pulseCol);
		}
		else if (obj.completed) {
			drawRect(absPos, Color(0x30006aff));
		}

		clearDrawClip();
		Color lineCol(0xff343434);
		drawRectOutline(absPos, lineCol);
	}

	EventReturn onMouseEvent(GuiElement@ ele, const MouseEvent& evt) {
		return ER_Pass;
	}

	EventReturn onGUIEvent(GuiElement@ ele, const GUIEvent& evt) {
		return ER_Pass;
	}

	EventReturn onKeyEvent(GuiElement@ ele, const KeyEvent& evt) {
		return ER_Pass;
	}
}

class TutorialWindow {
	TutorialWindowScript@ script;
	GuiScripted@ ele;

	TutorialWindow(recti Position) {
		@script = TutorialWindowScript();
		@ele = GuiScripted(Position, script, null);

		script.init(ele);
		script.syncPosition(Position.getSize());
	}

	void bringToFront() {
		ele.bringToFront();
		setGuiFocus(ele);

		uint objCount = script.objEles.length();
		for (uint i = 0; i < objCount; ++i) {
			script.objEles[i].bringToFront();
		}
	}

	void setVisible(bool vis) {
		ele.setVisible(vis);
	}

	bool isVisible() {
		return ele.isVisible();
	}

	void update(float time) {
		script.tick(time);
	}

	void remove() {
		clearEscapeEvent(ele);
		ele.remove();
		script.removed = true;
	}
};

import int getPinEndHeight() from "pinned_objects";
class TutorialWindowScript : ScriptedGuiHandler {
	DragResizeInfo drag;
	dim2di size;
	bool removed;
	uint page;

	EmpireImage@ pic;
	GuiPanel@ panel;
	GuiExtText@ text;
	GuiButton@ nextButton;
	GuiButton@ prevButton;
	GuiButton@ shrinkButton;

	GuiScripted@ chooseFreeform;
	FreeformChooser@ freeformScript;

	ConfirmDialog@ confirm;

	ObjectiveDisplay@[] objDisps;
	GuiScripted@[] objEles;
	bool needSkip;
	bool expanded;

	TutorialWindowScript() {
		removed = false;
		needSkip = false;
		expanded = true;
		page = startPage;

		drag.canMaximize = false;
		drag.canResize = false;
	}

	void init(GuiElement@ ele) {
		@pic = EmpireImage(null, recti(7, 20, 140, 233), ele);
		pic.setPortraitMagnify(1.25f);

		@panel = GuiPanel(recti(150, 24, 573, 209), false, SBM_Auto, SBM_Invisible, ele);
		@text = GuiExtText(recti(0, 0, 423, 185), panel);
		panel.fitChildren();

		@prevButton = Button(recti(146, 213, 289, 233), localize("#previous"), ele);
		@nextButton = Button(recti(430, 213, 573, 233), localize("#next"), ele);

		@shrinkButton = GuiButton(getSkinnable("MinButton"), recti(548, 0, 580, 12), null, ele);

		//@freeformScript = FreeformChooser();
		//@chooseFreeform = GuiScripted(recti(0, 0, 204, 20), freeformScript, null);
		//freeformScript.init(chooseFreeform);
		//chooseFreeform.setVisible(false);
	}

	void toggleExpand(GuiElement@ ele) {
		if (expanded) {
			expanded = false;

			recti oldPos = ele.getAbsolutePosition();
			pos2di topLeft = oldPos.UpperLeftCorner;
			pos2di botRight = oldPos.LowerRightCorner;
			dim2di oldSize = oldPos.getSize();
			pos2di origin = pos2di(topLeft.x + oldSize.width / 2, topLeft.y + oldSize.height / 2);

			int screenWidth = getScreenWidth();
			int screenHeight = getScreenWidth();

			dim2di newSize = dim2di(300, 47);
			pos2di newPos;

			if (origin.x < screenWidth / 2) {
				if (origin.y < screenHeight / 2)
					newPos = topLeft;
				else
					newPos = pos2di(topLeft.x, botRight.y - newSize.height);
			}
			else {
				if (origin.y < screenHeight / 2)
					newPos = pos2di(botRight.x - newSize.width, topLeft.y);
				else
					newPos = pos2di(botRight.x - newSize.width, botRight.y - newSize.height);
			}

			panel.setVisible(false);
			pic.setVisible(false);

			prevButton.setPosition(pos2di(7, 20));
			nextButton.setPosition(pos2di(150, 20));

			shrinkButton.setPosition(pos2di(newSize.width - 32, 0));
			shrinkButton.setSkinnable(getSkinnable("RestoreButton"));

			ele.setPosition(newPos);
			ele.setSize(newSize);
		}
		else {
			expanded = true;

			recti oldPos = ele.getAbsolutePosition();
			pos2di topLeft = oldPos.UpperLeftCorner;
			pos2di botRight = oldPos.LowerRightCorner;
			dim2di oldSize = oldPos.getSize();
			pos2di origin = pos2di(topLeft.x + oldSize.width / 2, topLeft.y + oldSize.height / 2);

			int screenWidth = getScreenWidth();
			int screenHeight = getScreenWidth();

			dim2di newSize = dim2di(580, 240);
			pos2di newPos;

			if (origin.x < screenWidth / 2) {
				if (origin.y < screenHeight / 2)
					newPos = topLeft;
				else
					newPos = pos2di(topLeft.x, botRight.y - newSize.height);
			}
			else {
				if (origin.y < screenHeight / 2)
					newPos = pos2di(botRight.x - newSize.width, topLeft.y);
				else
					newPos = pos2di(botRight.x - newSize.width, botRight.y - newSize.height);
			}

			ele.setPosition(newPos);
			ele.setSize(newSize);

			panel.setVisible(true);
			pic.setVisible(true);

			prevButton.setPosition(pos2di(146, 213));
			nextButton.setPosition(pos2di(430, 213));

			shrinkButton.setPosition(pos2di(newSize.width - 32, 0));
			shrinkButton.setSkinnable(getSkinnable("MinButton"));
		}
	}

	void tick(float time) {
		uint cnt = objDisps.length();
		bool completed = true;
		for (uint i = 0; i < cnt; ++i) {
			objDisps[i].tick(time);

			if (!objDisps[i].obj.completed || objDisps[i].bgPulse > 0.f)
				completed = false;
		}

		if (completed) {
			if (pages[page].autoAdvance && !pages[page].completed) {
				next(true);
			}
			else if (needSkip) {
				needSkip = false;
				if (pages[page].nextText is null)
					nextButton.setText(localize("#next"));
			}
		}
	}

	void update() {
		PageDesc@ desc = pages[page];

		// Window contents
		pic.setEmpire(desc.emp);
		text.setText(combine(
			"#font:stroked_subtitle#",
			desc.title,
			"#font#\n",
			desc.text
		));
		panel.fitChildren();

		// Next and previous button
		prevButton.setVisible(page > 0 && page != pages.length() - 1);
		nextButton.setVisible(page < pages.length());

		needSkip = false;

		// Remove old objectives
		uint objCnt = objEles.length();
		for (uint i = 0; i < objCnt; ++i)
			objEles[i].remove();
		objEles.resize(0);
		objDisps.resize(0);

		// Add new objectives
		objCnt = desc.objectives.length();
		objEles.resize(objCnt);
		objDisps.resize(objCnt);
		uint y = getPinEndHeight() + 4;
		for (uint i = 0; i < objCnt; ++i) {
			@objDisps[i] = ObjectiveDisplay(desc.objectives[i]);
			@objEles[i] = GuiScripted(recti(4, y, 204, y+20), objDisps[i], null);
			objDisps[i].init(objEles[i]);
			y += objEles[i].getSize().height - 1;

			if (!objDisps[i].obj.completed)
				needSkip = true;
		}

		//if (desc.id !is null && desc.id == "#tut cam") {
			//chooseFreeform.setPosition(pos2di(4, y+20));
			//chooseFreeform.setVisible(true);
		//}
		//else {
			//chooseFreeform.setVisible(false);
		//}

		if (page == pages.length() - 1)
			nextButton.setText(localize("#close"));
		else if (desc.nextText !is null && !desc.completed)
			nextButton.setText(desc.nextText);
		else if (!desc.completed && needSkip)
			nextButton.setText(localize("#skip"));
		else
			nextButton.setText(localize("#next"));
	}

	void next(bool force) {
		if (page < pages.length() - 1) {
			PageDesc@ desc = pages[page];
			if (!force && desc.nextPrompt !is null && !desc.completed) {
				if (confirm !is null)
					removeDialog(confirm);
				@confirm = addConfirmDialog(desc.nextPrompt, localize("#yes"),
							localize("#no"), TutorialConfirm(this));
			}
			else {
				@confirm = null;
				if (!desc.completed && desc.nextCall !is null) {
					desc.callNext();
					tutWin.bringToFront();
				}
				desc.completed = true;

				page += 1;
				update();
			}
		}
		else {
			closeTutorialWindow();
		}
	}

	void previous() {
		if (page > 0) {
			page -= 1;
			update();
		}
	}

	void syncPosition(dim2di Size) {
		size = Size;
		update();
	}

	void draw(GuiElement@ ele) {
		const recti absPos = ele.getAbsolutePosition();
		pos2di topLeft = absPos.UpperLeftCorner;
		pos2di botRight = absPos.LowerRightCorner;
		dim2di size = absPos.getSize();

		ele.toGuiScripted().setAbsoluteClip();

		drawDarkArea(absPos);
		drawWindowFrame(absPos);

		if (expanded)
			drawVSep(recti(pos2di(topLeft.x + 139, topLeft.y + 19), dim2di(7, size.height - 25)));

		clearDrawClip();
	}

	EventReturn onMouseEvent(GuiElement@ ele, const MouseEvent& evt) {
		DragResizeEvent re = handleDragResize(ele, evt, drag, 0, 0);
		if (re != RE_None) {
			if (re == RE_Resized)
				syncPosition(ele.getSize());
			return ER_Absorb;
		}
		return ER_Pass;
	}

	EventReturn onGUIEvent(GuiElement@ ele, const GUIEvent& evt) {
		if (evt.EventType == GEVT_Focus_Gained && evt.Caller.isAncestor(ele)) {
			ele.bringToFront();

			uint objCount = objEles.length();
			for (uint i = 0; i < objCount; ++i) {
				objEles[i].bringToFront();
			}
		}
		else if (evt.EventType == GEVT_Closed) {
			return ER_Absorb;
		}

		switch (evt.EventType) {
			case GEVT_Clicked:
				if (evt.Caller is nextButton) {
					if (!expanded)
						toggleExpand(ele);
					next(ctrlKey);
				}
				else if (evt.Caller is prevButton) {
					if (!expanded)
						toggleExpand(ele);
					previous();
				}
				else if (evt.Caller is shrinkButton) {
					toggleExpand(ele);
				}
				return ER_Pass;
		}
		return ER_Pass;
	}

	EventReturn onKeyEvent(GuiElement@ ele, const KeyEvent& evt) {
		return ER_Pass;
	}
};
/* }}} */
/* {{{ Ticking */
dictionary completedIDs;
int startPage = 0;
bool windowOpen = true;

void init() {
	initSkin();
	HideInterface();
}

void tickInit() {
	loadPages();
	if (windowOpen)
		openTutorialWindow();

	if (gameTime < 1.f) {
		const HullLayout@ capShip = player.getShipLayout(localize("#SH_Capital Ship"));
		Object@ cap = findShip(homeSys, player, capShip);
		cameraZoomTo(cap, true, true);
		cameraSetTopDown(40.f);
		cameraSetDistance(40.f);

		const HullLayout@ cruiser = player.getShipLayout(localize("#SH_Cruiser"));
		cruiser.updateThreshold = 0.f;
	}
}

float reinforceTimer = 0.f;
float trackTimer = 0.f;
void tick(float time) {
	// Update tutorial window
	if (tutWin !is null && tutWin.isVisible())
		tutWin.update(time);

	// Make sure we initialize the tracking soon enough
	if (trackTimer > 0.5f) {
		if (trackTimer < 100.f) {
			setupTracking();
			tickInit();
			trackTimer = 250.f;
		}
	}
	else {
		trackTimer += time;
		return;
	}

	// Try to zoom to stuff
	if (zoomToColonizer)
		ZoomColonizer();

	// Check if we should send a new colonizer
	if (spawnedColonizer)
		checkColonizer();

	// Check scouting
	if (!foundRemnants)
		checkRemnantScout();

	if (!foundEmpty)
		checkEmptyScout();

	// Check if we should send reinforcements
	if (reinforceTimer > 1.f) {
		checkReinforcements();
		reinforceTimer = 0.f;
	}
	else
		reinforceTimer += time;
}
/* }}} */
// {{{ Saving and Loading
void onSaveFileWrite(XMLWriter@ xml) {
	xml.createHeader();

	if (tutWin !is null) {
		xml.addElement("progress", true, "page", i_to_s(tutWin.script.page));
		xml.addElement("spawnedColonizer", true, "v", spawnedColonizer ? "true" : "false");
		xml.addElement("foundRemnants", true, "v", foundRemnants ? "true" : "false");
		xml.addElement("closedRemnants", true, "v", closedRemnants ? "true" : "false");
		xml.addElement("foundEmpty", true, "v", foundEmpty ? "true" : "false");

		xml.addElement("spVis", true, "v", spVis ? "true" : "false");
		xml.addElement("empVis", true, "v", empVis ? "true" : "false");
		xml.addElement("mmVis", true, "v", mmVis ? "true" : "false");
		xml.addElement("resVis", true, "v", resVis ? "true" : "false");
		xml.addElement("bpVis", true, "v", bpVis ? "true" : "false");
		xml.addElement("bankVis", true, "v", bankVis ? "true" : "false");
		xml.addElement("sysVis", true, "v", sysVis ? "true" : "false");
		xml.addElement("pinVis", true, "v", pinVis ? "true" : "false");
		xml.addElement("plVis", true, "v", plVis ? "true" : "false");
		xml.addElement("selVis", true, "v", selVis ? "true" : "false");
		xml.addElement("objWinVis", true, "v", objWinVis ? "true" : "false");
		xml.addElement("bankButtonsVis", true, "v", bankButtonsVis ? "true" : "false");
		xml.addElement("civActsButtonVis", true, "v", civActsButtonVis ? "true" : "false");

		uint pageCnt = pages.length();
		for (uint i = 0; i < pageCnt; ++i) {
			PageDesc@ desc = pages[i];
			if (desc.completed && desc.id !is null) {
				xml.addElement("complete", true, "id", desc.id);
			}

			uint objCnt = desc.objectives.length();
			for (uint j = 0; j < objCnt; ++j) {
				Objective@ obj = desc.objectives[j];
				if (obj.completed && obj.id !is null) {
					xml.addElement("complete", true, "id", obj.id);
				}
			}
		}
	}
}

void onSaveFileLoad(XMLReader@ xml) {
	windowOpen = false;
	while(xml.advance()) {
		if(xml.getNodeType() != XN_Element)
			continue;

		string@ name = xml.getNodeName();
		if (name == "complete") {
			string@ id = xml.getAttributeValue("id");
			completedIDs.set(id, 1);
		}
		else if (name == "progress") {
			startPage = s_to_i(xml.getAttributeValue("page"));
			windowOpen = true;
		}
		else if (name == "spawnedColonizer") {
			spawnedColonizer = xml.getAttributeValue("v") == "true";
		}
		else if (name == "foundRemnants") {
			foundRemnants = xml.getAttributeValue("v") == "true";
		}
		else if (name == "foundEmpty") {
			foundEmpty = xml.getAttributeValue("v") == "true";
		}
		else if (name == "closedRemnants") {
			closedRemnants = xml.getAttributeValue("v") == "true";
		}
		else if (name == "spVis") {
			spVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "empVis") {
			empVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "mmVis") {
			mmVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "resVis") {
			resVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "bpVis") {
			bpVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "bankVis") {
			bankVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "sysVis") {
			sysVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "pinVis") {
			pinVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "plVis") {
			plVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "selVis") {
			selVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "objWinVis") {
			objWinVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "bankButtonsVis") {
			bankButtonsVis = xml.getAttributeValue("v") == "true";
		}
		else if (name == "civActsButtonVis") {
			civActsButtonVis = xml.getAttributeValue("v") == "true";
		}
	}
}
// }}}
