import { Class } from "simulation/ai/common-api/class.js";
import { VectorDistance } from "simulation/ai/common-api/utils.js";

// defines a template.
export const Template = Class({

	"_init": function(sharedAI, templateName, template)
	{
		this._templateName = templateName;
		this._template = template;
		// save a reference to the template tech modifications
		if (!sharedAI._templatesModifications[this._templateName])
			sharedAI._templatesModifications[this._templateName] = {};
		this._templateModif = sharedAI._templatesModifications[this._templateName];
		this._tpCache = new Map();
	},

	// Helper function to return a template value, adjusting for tech.
	"get": function(string)
	{
		if (this._entityModif && this._entityModif.has(string))
			return this._entityModif.get(string);
		else if (this._templateModif)
		{
			const owner = this._entity ? this._entity.owner : PlayerID;
			if (this._templateModif[owner] && this._templateModif[owner].has(string))
				return this._templateModif[owner].get(string);
		}

		if (!this._tpCache.has(string))
		{
			let value = this._template;
			const args = string.split("/");
			for (const arg of args)
			{
				value = value[arg];
				if (value == undefined)
					break;
			}
			this._tpCache.set(string, value);
		}
		return this._tpCache.get(string);
	},

	"templateName": function() { return this._templateName; },

	"genericName": function() { return this.get("Identity/GenericName"); },

	"civ": function() { return this.get("Identity/Civ"); },

	"matchLimit": function() {
		if (!this.get("TrainingRestrictions"))
			return undefined;
		return this.get("TrainingRestrictions/MatchLimit");
	},

	"classes": function() {
		const template = this.get("Identity");
		if (!template)
			return undefined;
		return GetIdentityClasses(template);
	},

	"hasClass": function(name) {
		if (!this._classes)
			this._classes = this.classes();
		return this._classes && this._classes.indexOf(name) != -1;
	},

	"hasClasses": function(array) {
		if (!this._classes)
			this._classes = this.classes();
		return this._classes && MatchesClassList(this._classes, array);
	},

	"requirements": function() {
		return this.get("Identity/Requirements");
	},

	"available": function(gameState) {
		const requirements = this.requirements();
		return !requirements || Sim.RequirementsHelper.AreRequirementsMet(requirements, PlayerID);
	},

	"cost": function(productionQueue) {
		if (!this.get("Cost"))
			return {};

		const ret = {};
		for (const type in this.get("Cost/Resources"))
			ret[type] = +this.get("Cost/Resources/" + type);
		return ret;
	},

	"costSum": function(productionQueue) {
		const cost = this.cost(productionQueue);
		if (!cost)
			return 0;
		let ret = 0;
		for (const type in cost)
			ret += cost[type];
		return ret;
	},

	"techCostMultiplier": function(type) {
		return +(this.get("Researcher/TechCostMultiplier/"+type) || 1);
	},

	/**
	 * Returns { "max": max, "min": min } or undefined if no obstruction.
	 * max: radius of the outer circle surrounding this entity's obstruction shape
	 * min: radius of the inner circle
	 */
	"obstructionRadius": function() {
		if (!this.get("Obstruction"))
			return undefined;

		if (this.get("Obstruction/Static"))
		{
			const w = +this.get("Obstruction/Static/@width");
			const h = +this.get("Obstruction/Static/@depth");
			return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
		}

		if (this.get("Obstruction/Unit"))
		{
			const r = +this.get("Obstruction/Unit/@radius");
			return { "max": r, "min": r };
		}

		const right = this.get("Obstruction/Obstructions/Right");
		const left = this.get("Obstruction/Obstructions/Left");
		if (left && right)
		{
			const w = +right["@x"] + right["@width"] / 2 - left["@x"] + left["@width"] / 2;
			const h = Math.max(+right["@z"] + right["@depth"] / 2, +left["@z"] + left["@depth"] / 2) -
			        Math.min(+right["@z"] - right["@depth"] / 2, +left["@z"] - left["@depth"] / 2);
			return { "max": Math.sqrt(w * w + h * h) / 2, "min": Math.min(h, w) / 2 };
		}

		return { "max": 0, "min": 0 }; // Units have currently no obstructions
	},

	/**
	 * Returns the radius of a circle surrounding this entity's footprint.
	 */
	"footprintRadius": function() {
		if (!this.get("Footprint"))
			return undefined;

		if (this.get("Footprint/Square"))
		{
			const w = +this.get("Footprint/Square/@width");
			const h = +this.get("Footprint/Square/@depth");
			return Math.sqrt(w * w + h * h) / 2;
		}

		if (this.get("Footprint/Circle"))
			return +this.get("Footprint/Circle/@radius");

		return 0; // this should never happen
	},

	"maxHitpoints": function() { return +(this.get("Health/Max") || 0); },

	"isHealable": function()
	{
		if (this.get("Health") !== undefined)
			return this.get("Health/Unhealable") !== "true";
		return false;
	},

	"isRepairable": function() { return this.get("Repairable") !== undefined; },

	"getPopulationBonus": function() {
		if (!this.get("Population"))
			return 0;

		return +this.get("Population/Bonus");
	},

	"resistanceStrengths": function() {
		const resistanceTypes = this.get("Resistance");
		if (!resistanceTypes || !resistanceTypes.Entity)
			return undefined;

		const resistance = {};
		if (resistanceTypes.Entity.Capture)
			resistance.Capture = +this.get("Resistance/Entity/Capture");

		if (resistanceTypes.Entity.Damage)
		{
			resistance.Damage = {};
			for (const damageType in resistanceTypes.Entity.Damage)
				resistance.Damage[damageType] = +this.get("Resistance/Entity/Damage/" + damageType);
		}

		// ToDo: Resistance to StatusEffects.

		return resistance;
	},

	"attackTypes": function() {
		const attack = this.get("Attack");
		if (!attack)
			return undefined;

		const ret = [];
		for (const type in attack)
			ret.push(type);
		return ret;
	},

	"attackRange": function(type) {
		if (!this.get("Attack/" + type))
			return undefined;

		return {
			"max": +this.get("Attack/" + type +"/MaxRange"),
			"min": +(this.get("Attack/" + type +"/MinRange") || 0)
		};
	},

	"attackStrengths": function(type) {
		const attackDamageTypes = this.get("Attack/" + type + "/Damage");
		if (!attackDamageTypes)
			return undefined;

		const damage = {};
		for (const damageType in attackDamageTypes)
			damage[damageType] = +attackDamageTypes[damageType];

		return damage;
	},

	"captureStrength": function() {
		if (!this.get("Attack/Capture"))
			return undefined;

		return +this.get("Attack/Capture/Capture") || 0;
	},

	"attackTimes": function(type) {
		if (!this.get("Attack/" + type))
			return undefined;

		return {
			"prepare": +(this.get("Attack/" + type + "/PrepareTime") || 0),
			"repeat": +(this.get("Attack/" + type + "/RepeatTime") || 1000)
		};
	},

	// returns the classes this templates counters:
	// Return type is [ [-neededClasses- , multiplier], … ].
	"getCounteredClasses": function() {
		const attack = this.get("Attack");
		if (!attack)
			return undefined;

		const Classes = [];
		for (const type in attack)
		{
			const bonuses = this.get("Attack/" + type + "/Bonuses");
			if (!bonuses)
				continue;
			for (const b in bonuses)
			{
				const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
				if (bonusClasses)
					Classes.push([bonusClasses.split(" "), +this.get("Attack/" + type +"/Bonuses/" + b +"/Multiplier")]);
			}
		}
		return Classes;
	},

	// returns true if the entity counters the target entity.
	// TODO: refine using the multiplier
	"counters": function(target) {
		const attack = this.get("Attack");
		if (!attack)
			return false;
		const mcounter = [];
		for (const type in attack)
		{
			const bonuses = this.get("Attack/" + type + "/Bonuses");
			if (!bonuses)
				continue;
			for (const b in bonuses)
			{
				const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
				if (bonusClasses)
					mcounter.concat(bonusClasses.split(" "));
			}
		}
		return target.hasClasses(mcounter);
	},

	// returns, if it exists, the multiplier from each attack against a given class
	"getMultiplierAgainst": function(type, againstClass) {
		if (!this.get("Attack/" + type +""))
			return undefined;

		const bonuses = this.get("Attack/" + type + "/Bonuses");
		if (bonuses)
		{
			for (const b in bonuses)
			{
				const bonusClasses = this.get("Attack/" + type + "/Bonuses/" + b + "/Classes");
				if (!bonusClasses)
					continue;
				for (const bcl of bonusClasses.split(" "))
					if (bcl == againstClass)
						return +this.get("Attack/" + type + "/Bonuses/" + b + "/Multiplier");
			}
		}
		return 1;
	},

	"buildableEntities": function(civ) {
		const templates = this.get("Builder/Entities/_string");
		if (!templates)
			return [];
		return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
	},

	"trainableEntities": function(civ) {
		const templates = this.get("Trainer/Entities/_string");
		if (!templates)
			return undefined;
		return templates.replace(/\{native\}/g, this.civ()).replace(/\{civ\}/g, civ).split(/\s+/);
	},

	"researchableTechs": function(gameState, civ) {
		const templates = this.get("Researcher/Technologies/_string");
		if (!templates)
			return undefined;
		const techs = templates.split(/\s+/);
		for (let i = 0; i < techs.length; ++i)
		{
			const tech = techs[i];
			if (tech.indexOf("{civ}") == -1)
				continue;
			const civTech = tech.replace("{civ}", civ);
			techs[i] = TechnologyTemplates.Has(civTech) ?
				civTech : tech.replace("{civ}", "generic");
		}
		return techs;
	},

	"resourceSupplyType": function() {
		if (!this.get("ResourceSupply"))
			return undefined;
		const [type, subtype] = this.get("ResourceSupply/Type").split('.');
		return { "generic": type, "specific": subtype };
	},

	"getResourceType": function() {
		if (!this.get("ResourceSupply"))
			return undefined;
		return this.get("ResourceSupply/Type").split('.')[0];
	},

	"getDiminishingReturns": function() { return +(this.get("ResourceSupply/DiminishingReturns") || 1); },

	"resourceSupplyMax": function() { return +this.get("ResourceSupply/Max"); },

	"maxGatherers": function() { return +(this.get("ResourceSupply/MaxGatherers") || 0); },

	"resourceGatherRates": function() {
		if (!this.get("ResourceGatherer"))
			return undefined;
		const ret = {};
		const baseSpeed = +this.get("ResourceGatherer/BaseSpeed");
		for (const r in this.get("ResourceGatherer/Rates"))
			ret[r] = +this.get("ResourceGatherer/Rates/" + r) * baseSpeed;
		return ret;
	},

	"resourceDropsiteTypes": function() {
		if (!this.get("ResourceDropsite"))
			return undefined;

		const types = this.get("ResourceDropsite/Types");
		return types ? types.split(/\s+/) : [];
	},

	"isResourceDropsite": function(resourceType) {
		const types = this.resourceDropsiteTypes();
		return types && (!resourceType || types.indexOf(resourceType) !== -1);
	},

	"isTreasure": function() { return this.get("Treasure") !== undefined; },

	"treasureResources": function() {
		if (!this.get("Treasure"))
			return undefined;
		const ret = {};
		for (const r in this.get("Treasure/Resources"))
			ret[r] = +this.get("Treasure/Resources/" + r);
		return ret;
	},


	"garrisonableClasses": function() { return this.get("GarrisonHolder/List/_string"); },

	"garrisonMax": function() { return this.get("GarrisonHolder/Max"); },

	"garrisonSize": function() { return this.get("Garrisonable/Size"); },

	"garrisonEjectHealth": function() { return +this.get("GarrisonHolder/EjectHealth"); },

	"getDefaultArrow": function() { return +this.get("BuildingAI/DefaultArrowCount"); },

	"getArrowMultiplier": function() { return +this.get("BuildingAI/GarrisonArrowMultiplier"); },

	"getGarrisonArrowClasses": function() {
		if (!this.get("BuildingAI"))
			return undefined;
		return this.get("BuildingAI/GarrisonArrowClasses").split(/\s+/);
	},

	"buffHeal": function() { return +this.get("GarrisonHolder/BuffHeal"); },

	"promotion": function() { return this.get("Promotion/Entity"); },

	"isPackable": function() { return this.get("Pack") != undefined; },

	"isHuntable": function() {
		// Do not hunt retaliating animals (dead animals can be used).
		// Assume entities which can attack, will attack.
		return this.get("ResourceSupply/KillBeforeGather") &&
			(!this.get("Health") || !this.get("Attack"));
	},

	"walkSpeed": function() { return +this.get("UnitMotion/WalkSpeed"); },

	"trainingCategory": function() { return this.get("TrainingRestrictions/Category"); },

	"buildTime": function(researcher) {
		let time = +this.get("Cost/BuildTime");
		if (researcher)
			time *= researcher.techCostMultiplier("time");
		return time;
	},

	"buildCategory": function() { return this.get("BuildRestrictions/Category"); },

	"buildDistance": function() {
		const distance = this.get("BuildRestrictions/Distance");
		if (!distance)
			return undefined;
		const ret = {};
		for (const key in distance)
			ret[key] = this.get("BuildRestrictions/Distance/" + key);
		return ret;
	},

	"buildPlacementType": function() { return this.get("BuildRestrictions/PlacementType"); },

	"buildTerritories": function() {
		if (!this.get("BuildRestrictions"))
			return undefined;
		const territory = this.get("BuildRestrictions/Territory");
		return !territory ? undefined : territory.split(/\s+/);
	},

	"hasBuildTerritory": function(territory) {
		const territories = this.buildTerritories();
		return territories && territories.indexOf(territory) != -1;
	},

	"hasTerritoryInfluence": function() {
		return this.get("TerritoryInfluence") !== undefined;
	},

	"hasDefensiveFire": function() {
		if (!this.get("Attack/Ranged"))
			return false;
		return this.getDefaultArrow() || this.getArrowMultiplier();
	},

	"territoryInfluenceRadius": function() {
		if (this.get("TerritoryInfluence") !== undefined)
			return +this.get("TerritoryInfluence/Radius");
		return -1;
	},

	"territoryInfluenceWeight": function() {
		if (this.get("TerritoryInfluence") !== undefined)
			return +this.get("TerritoryInfluence/Weight");
		return -1;
	},

	"territoryDecayRate": function() {
		return +(this.get("TerritoryDecay/DecayRate") || 0);
	},

	"defaultRegenRate": function() {
		return +(this.get("Capturable/RegenRate") || 0);
	},

	"garrisonRegenRate": function() {
		return +(this.get("Capturable/GarrisonRegenRate") || 0);
	},

	"visionRange": function() { return +this.get("Vision/Range"); },

	"gainMultiplier": function() { return +this.get("Trader/GainMultiplier"); },

	"isBuilder": function() { return this.get("Builder") !== undefined; },

	"isGatherer": function() { return this.get("ResourceGatherer") !== undefined; },

	"canGather": function(type) {
		const gatherRates = this.get("ResourceGatherer/Rates");
		if (!gatherRates)
			return false;
		for (const r in gatherRates)
			if (r.split('.')[0] === type)
				return true;
		return false;
	},

	"isGarrisonHolder": function() { return this.get("GarrisonHolder") !== undefined; },

	"isTurretHolder": function() { return this.get("TurretHolder") !== undefined; },

	/**
	 * returns true if the tempalte can capture the given target entity
	 * if no target is given, returns true if the template has the Capture attack
	 */
	"canCapture": function(target)
	{
		if (!this.get("Attack/Capture"))
			return false;
		if (!target)
			return true;
		if (!target.get("Capturable"))
			return false;
		const restrictedClasses = this.get("Attack/Capture/RestrictedClasses/_string");
		return !restrictedClasses || !target.hasClasses(restrictedClasses);
	},

	"isCapturable": function() { return this.get("Capturable") !== undefined; },

	"canGuard": function() { return this.get("UnitAI/CanGuard") === "true"; },

	"canGarrison": function() { return "Garrisonable" in this._template; },

	"canOccupyTurret": function() { return "Turretable" in this._template; },

	"isTreasureCollector": function() { return this.get("TreasureCollector") !== undefined; },

	"hasUnitAI": function() { return this.get("UnitAI") !== undefined; },
});


// defines an entity, with a super Template.
// also redefines several of the template functions where the only change is applying aura and tech modifications.
export const Entity = Class({
	"_super": Template,

	"_init": function(sharedAI, entity)
	{
		this._super.call(this, sharedAI, entity.template, sharedAI.GetTemplate(entity.template));

		this._entity = entity;
		this._ai = sharedAI;
		// save a reference to the template tech modifications
		if (!sharedAI._templatesModifications[this._templateName])
			sharedAI._templatesModifications[this._templateName] = {};
		this._templateModif = sharedAI._templatesModifications[this._templateName];
		// save a reference to the entity tech/aura modifications
		if (!sharedAI._entitiesModifications.has(entity.id))
			sharedAI._entitiesModifications.set(entity.id, new Map());
		this._entityModif = sharedAI._entitiesModifications.get(entity.id);
	},

	"queryInterface": function(iid) { return SimEngine.QueryInterface(this.id(), iid); },

	"toString": function() { return "[Entity " + this.id() + " " + this.templateName() + "]"; },

	"id": function() { return this._entity.id; },

	/**
	 * Returns extra data that the AI scripts have associated with this entity,
	 * for arbitrary local annotations.
	 * (This data should not be shared with any other AI scripts.)
	 */
	"getMetadata": function(player, key) { return this._ai.getMetadata(player, this, key); },

	/**
	 * Sets extra data to be associated with this entity.
	 */
	"setMetadata": function(player, key, value) { this._ai.setMetadata(player, this, key, value); },

	"deleteAllMetadata": function(player) { delete this._ai._entityMetadata[player][this.id()]; },

	"deleteMetadata": function(player, key) { this._ai.deleteMetadata(player, this, key); },

	"position": function() { return this._entity.position; },
	"angle": function() { return this._entity.angle; },

	"isIdle": function() { return this._entity.idle; },

	"getStance": function() { return this._entity.stance; },
	"unitAIState": function() { return this._entity.unitAIState; },
	"unitAIOrderData": function() { return this._entity.unitAIOrderData; },

	"hitpoints": function() { return this._entity.hitpoints; },
	"isHurt": function() { return this.hitpoints() < this.maxHitpoints(); },
	"healthLevel": function() { return this.hitpoints() / this.maxHitpoints(); },
	"needsHeal": function() { return this.isHurt() && this.isHealable(); },
	"needsRepair": function() { return this.isHurt() && this.isRepairable(); },
	"decaying": function() { return this._entity.decaying; },
	"capturePoints": function() {return this._entity.capturePoints; },
	"isInvulnerable": function() { return this._entity.invulnerability || false; },

	"isSharedDropsite": function() { return this._entity.sharedDropsite === true; },

	/**
	 * Returns the current training queue state, of the form
	 * [ { "id": 0, "template": "...", "count": 1, "progress": 0.5, "metadata": ... }, ... ]
	 */
	"trainingQueue": function() {
		return this._entity.trainingQueue;
	},

	"trainingQueueTime": function() {
		const queue = this._entity.trainingQueue;
		if (!queue)
			return undefined;
		let time = 0;
		for (const item of queue)
			time += item.timeRemaining;
		return time / 1000;
	},

	"foundationProgress": function() {
		return this._entity.foundationProgress;
	},

	"getBuilders": function() {
		if (this._entity.foundationProgress === undefined)
			return undefined;
		if (this._entity.foundationBuilders === undefined)
			return [];
		return this._entity.foundationBuilders;
	},

	"getBuildersNb": function() {
		if (this._entity.foundationProgress === undefined)
			return undefined;
		if (this._entity.foundationBuilders === undefined)
			return 0;
		return this._entity.foundationBuilders.length;
	},

	"owner": function() {
		return this._entity.owner;
	},

	"isOwn": function(player) {
		if (typeof this._entity.owner === "undefined")
			return false;
		return this._entity.owner === player;
	},

	"resourceSupplyAmount": function() {
		return this.queryInterface(Sim.IID_ResourceSupply)?.GetCurrentAmount();
	},

	"resourceSupplyNumGatherers": function()
	{
		return this.queryInterface(Sim.IID_ResourceSupply)?.GetNumGatherers();
	},

	"isFull": function()
	{
		const numGatherers = this.resourceSupplyNumGatherers();
		if (numGatherers)
			return this.maxGatherers() === numGatherers;

		return undefined;
	},

	"resourceCarrying": function() {
		return this.queryInterface(Sim.IID_ResourceGatherer)?.GetCarryingStatus();
	},

	"currentGatherRate": function() {
		// returns the gather rate for the current target if applicable.
		if (!this.get("ResourceGatherer"))
			return undefined;

		if (this.unitAIOrderData().length &&
			this.unitAIState().split(".")[1] == "GATHER")
		{
			let res;
			// this is an abuse of "_ai" but it works.
			if (this.unitAIState().split(".")[1] == "GATHER" && this.unitAIOrderData()[0].target !== undefined)
				res = this._ai._entities.get(this.unitAIOrderData()[0].target);
			else if (this.unitAIOrderData()[1] !== undefined && this.unitAIOrderData()[1].target !== undefined)
				res = this._ai._entities.get(this.unitAIOrderData()[1].target);
			if (!res)
				return 0;
			const type = res.resourceSupplyType();
			if (!type)
				return 0;

			const tstring = type.generic + "." + type.specific;
			let rate = +this.get("ResourceGatherer/BaseSpeed");
			rate *= +this.get("ResourceGatherer/Rates/" +tstring);
			if (rate)
				return rate;
			return 0;
		}
		return undefined;
	},

	"garrisonHolderID": function() {
		return this._entity.garrisonHolderID;
	},

	"garrisoned": function() { return this._entity.garrisoned; },

	"garrisonedSlots": function() {
		let count = 0;

		if (this._entity.garrisoned)
			for (const ent of this._entity.garrisoned)
				count += +this._ai._entities.get(ent).garrisonSize();

		return count;
	},

	"canGarrisonInside": function()
	{
		return this.garrisonedSlots() < this.garrisonMax();
	},

	/**
	 * returns true if the entity can attack (including capture) the given class.
	 */
	"canAttackClass": function(aClass)
	{
		const attack = this.get("Attack");
		if (!attack)
			return false;

		for (const type in attack)
		{
			if (type == "Slaughter")
				continue;
			const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
			if (!restrictedClasses || !MatchesClassList([aClass], restrictedClasses))
				return true;
		}
		return false;
	},

	/**
	 * Derived from Attack.js' similary named function.
	 * @return {boolean} - Whether an entity can attack a given target.
	 */
	"canAttackTarget": function(target, allowCapture)
	{
		const attackTypes = this.get("Attack");
		if (!attackTypes)
			return false;

		const canCapture = allowCapture && this.canCapture(target);
		const health = target.get("Health");
		if (!health)
			return canCapture;

		for (const type in attackTypes)
		{
			if (type == "Capture" ? !canCapture : target.isInvulnerable())
				continue;

			const restrictedClasses = this.get("Attack/" + type + "/RestrictedClasses/_string");
			if (!restrictedClasses || !target.hasClasses(restrictedClasses))
				return true;
		}

		return false;
	},

	"move": function(x, z, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": x, "z": z, "queued": queued, "pushFront": pushFront });
		return this;
	},

	"moveToRange": function(x, z, min, max, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "walk-to-range", "entities": [this.id()], "x": x, "z": z, "min": min, "max": max, "queued": queued, "pushFront": pushFront });
		return this;
	},

	"attackMove": function(x, z, targetClasses, allowCapture = true, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "attack-walk", "entities": [this.id()], "x": x, "z": z, "targetClasses": targetClasses, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
		return this;
	},

	// violent, aggressive, defensive, passive, standground
	"setStance": function(stance) {
		if (this.getStance() === undefined)
			return undefined;
		Engine.PostCommand(PlayerID, { "type": "stance", "entities": [this.id()], "name": stance });
		return this;
	},

	"stopMoving": function() {
		Engine.PostCommand(PlayerID, { "type": "stop", "entities": [this.id()], "queued": false, "pushFront": false });
	},

	"unload": function(id) {
		if (!this.get("GarrisonHolder"))
			return undefined;
		Engine.PostCommand(PlayerID, { "type": "unload", "garrisonHolder": this.id(), "entities": [id] });
		return this;
	},

	// Unloads all owned units, don't unload allies
	"unloadAll": function() {
		if (!this.get("GarrisonHolder"))
			return undefined;
		Engine.PostCommand(PlayerID, { "type": "unload-all-by-owner", "garrisonHolders": [this.id()] });
		return this;
	},

	"garrison": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "garrison", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
		return this;
	},

	"occupy-turret": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "occupy-turret", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
		return this;
	},

	"attack": function(unitId, allowCapture = true, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "attack", "entities": [this.id()], "target": unitId, "allowCapture": allowCapture, "queued": queued, "pushFront": pushFront });
		return this;
	},

	"collectTreasure": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, {
			"type": "collect-treasure",
			"entities": [this.id()],
			"target": target.id(),
			"queued": queued,
			"pushFront": pushFront
		});
		return this;
	},

	// moveApart from a point in the opposite direction with a distance dist
	"moveApart": function(point, dist) {
		if (this.position() !== undefined)
		{
			let direction = [this.position()[0] - point[0], this.position()[1] - point[1]];
			const norm = VectorDistance(point, this.position());
			if (norm === 0)
				direction = [1, 0];
			else
			{
				direction[0] /= norm;
				direction[1] /= norm;
			}
			Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + direction[0]*dist, "z": this.position()[1] + direction[1]*dist, "queued": false, "pushFront": false });
		}
		return this;
	},

	// Flees from a unit in the opposite direction.
	"flee": function(unitToFleeFrom) {
		if (this.position() !== undefined && unitToFleeFrom.position() !== undefined)
		{
			const FleeDirection = [this.position()[0] - unitToFleeFrom.position()[0],
				this.position()[1] - unitToFleeFrom.position()[1]];
			const dist = m.VectorDistance(unitToFleeFrom.position(), this.position());
			FleeDirection[0] = 40 * FleeDirection[0] / dist;
			FleeDirection[1] = 40 * FleeDirection[1] / dist;

			Engine.PostCommand(PlayerID, { "type": "walk", "entities": [this.id()], "x": this.position()[0] + FleeDirection[0], "z": this.position()[1] + FleeDirection[1], "queued": false, "pushFront": false });
		}
		return this;
	},

	"gather": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "gather", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
		return this;
	},

	"repair": function(target, autocontinue = false, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "repair", "entities": [this.id()], "target": target.id(), "autocontinue": autocontinue, "queued": queued, "pushFront": pushFront });
		return this;
	},

	"returnResources": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "returnresource", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
		return this;
	},

	"destroy": function() {
		Engine.PostCommand(PlayerID, { "type": "delete-entities", "entities": [this.id()] });
		return this;
	},

	"barter": function(buyType, sellType, amount) {
		Engine.PostCommand(PlayerID, { "type": "barter", "sell": sellType, "buy": buyType, "amount": amount });
		return this;
	},

	"tradeRoute": function(target, source) {
		Engine.PostCommand(PlayerID, { "type": "setup-trade-route", "entities": [this.id()], "target": target.id(), "source": source.id(), "route": undefined, "queued": false, "pushFront": false });
		return this;
	},

	"setRallyPoint": function(target, command) {
		const data = { "command": command, "target": target.id() };
		Engine.PostCommand(PlayerID, { "type": "set-rallypoint", "entities": [this.id()], "x": target.position()[0], "z": target.position()[1], "data": data });
		return this;
	},

	"unsetRallyPoint": function() {
		Engine.PostCommand(PlayerID, { "type": "unset-rallypoint", "entities": [this.id()] });
		return this;
	},

	"train": function(civ, type, count, metadata, pushFront = false)
	{
		const trainable = this.trainableEntities(civ);
		if (!trainable)
		{
			error("Called train("+type+", "+count+") on non-training entity "+this);
			return this;
		}
		if (trainable.indexOf(type) == -1)
		{
			error("Called train("+type+", "+count+") on entity "+this+" which can't train that");
			return this;
		}

		Engine.PostCommand(PlayerID, {
			"type": "train",
			"entities": [this.id()],
			"template": type,
			"count": count,
			"metadata": metadata,
			"pushFront": pushFront
		});
		return this;
	},

	"construct": function(template, x, z, angle, metadata) {
		// TODO: verify this unit can construct this, just for internal
		// sanity-checking and error reporting

		Engine.PostCommand(PlayerID, {
			"type": "construct",
			"entities": [this.id()],
			"template": template,
			"x": x,
			"z": z,
			"angle": angle,
			"autorepair": false,
			"autocontinue": false,
			"queued": false,
			"pushFront": false,
			"metadata": metadata	// can be undefined
		});
		return this;
	},

	"research": function(template, pushFront = false) {
		Engine.PostCommand(PlayerID, {
			"type": "research",
			"entity": this.id(),
			"template": template,
			"pushFront": pushFront
		});
		return this;
	},

	"stopProduction": function(id) {
		Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": id });
		return this;
	},

	"stopAllProduction": function(percentToStopAt) {
		const queue = this._entity.trainingQueue;
		if (!queue)
			return true;	// no queue, so technically we stopped all production.
		for (const item of queue)
			if (item.progress < percentToStopAt)
				Engine.PostCommand(PlayerID, { "type": "stop-production", "entity": this.id(), "id": item.id });
		return this;
	},

	"guard": function(target, queued = false, pushFront = false) {
		Engine.PostCommand(PlayerID, { "type": "guard", "entities": [this.id()], "target": target.id(), "queued": queued, "pushFront": pushFront });
		return this;
	},

	"removeGuard": function() {
		Engine.PostCommand(PlayerID, { "type": "remove-guard", "entities": [this.id()] });
		return this;
	}
});
