Source: session/selection_panels_helpers.js

/**
 * @file Contains all helper functions that are needed only for selection_panels.js
 * and some that are needed for hotkeys, but not for anything inside input.js.
 */

const UPGRADING_NOT_STARTED = -2;
const UPGRADING_CHOSEN_OTHER = -1;

function canMoveSelectionIntoFormation(formationTemplate)
{
	if (formationTemplate == NULL_FORMATION)
		return true;
	if (!(formationTemplate in g_canMoveIntoFormation))
		g_canMoveIntoFormation[formationTemplate] = Engine.GuiInterfaceCall("CanMoveEntsIntoFormation", {
			"ents": g_Selection.toList(),
			"formationTemplate": formationTemplate
		});

	return g_canMoveIntoFormation[formationTemplate];
}

function hasSameRestrictionCategory(templateName1, templateName2)
{
	let template1 = GetTemplateData(templateName1);
	let template2 = GetTemplateData(templateName2);

	if (template1.trainingRestrictions && template2.trainingRestrictions)
		return template1.trainingRestrictions.category == template2.trainingRestrictions.category;

	if (template1.buildRestrictions && template2.buildRestrictions)
		return template1.buildRestrictions.category == template2.buildRestrictions.category;

	return false;
}

/**
 * Returns a "color:255 0 0 Alpha" string based on how many resources are needed.
 */
function resourcesToAlphaMask(neededResources)
{
	let totalCost = 0;
	for (let resource in neededResources)
		totalCost += +neededResources[resource];

	return "color:255 0 0 " + Math.min(125, Math.round(+totalCost / 10) + 50);
}

function getStanceDisplayName(name)
{
	switch (name)
	{
	case "violent":
		return translateWithContext("stance", "Violent");
	case "aggressive":
		return translateWithContext("stance", "Aggressive");
	case "defensive":
		return translateWithContext("stance", "Defensive");
	case "passive":
		return translateWithContext("stance", "Passive");
	case "standground":
		return translateWithContext("stance", "Standground");
	default:
		warn("Internationalization: Unexpected stance found: " + name);
		return name;
	}
}

function getStanceTooltip(name)
{
	switch (name)
	{
	case "violent":
		return translateWithContext("stance", "Attack nearby opponents, focus on attackers and chase while visible");
	case "aggressive":
		return translateWithContext("stance", "Attack nearby opponents");
	case "defensive":
		return translateWithContext("stance", "Attack nearby opponents, chase a short distance and return to the original location");
	case "passive":
		return translateWithContext("stance", "Flee if attacked");
	case "standground":
		return translateWithContext("stance", "Attack opponents in range, but don't move");
	default:
		return "";
	}
}

/**
 * Format entity count/limit message for the tooltip
 */
function formatLimitString(trainEntLimit, trainEntCount, trainEntLimitChangers)
{
	if (trainEntLimit == undefined)
		return "";

	var text = sprintf(translate("Current Count: %(count)s, Limit: %(limit)s."), {
		"count": trainEntCount,
		"limit": trainEntLimit
	});

	if (trainEntCount >= trainEntLimit)
		text = coloredText(text, "red");

	for (var c in trainEntLimitChangers)
	{
		if (!trainEntLimitChangers[c])
			continue;

		let string = trainEntLimitChangers[c] > 0 ?
			translate("%(changer)s enlarges the limit with %(change)s.") :
			translate("%(changer)s lessens the limit with %(change)s.");

		text += "\n" + sprintf(string, {
			"changer": translate(c),
			"change": trainEntLimitChangers[c]
		});
	}
	return text;
}

/**
 * Format template match count/limit message for the tooltip.
 *
 * @param {number} matchEntLimit - The limit of the entity.
 * @param {number} matchEntCount - The count of the entity.
 * @param {string} type - The type of the action (i.e. "build" or "training").
 *
 * @return {string} - The string to show the user with information regarding the limit of this template.
 */
function formatMatchLimitString(matchEntLimit, matchEntCount, type)
{
	if (matchEntLimit == undefined)
		return "";

	let passedLimit = matchEntCount >= matchEntLimit;
	let count = matchEntLimit - matchEntCount;
	let text;
	if (type == "build")
	{
		if (matchEntLimit == 1)
			text = translate("Can be constructed only once.");
		else if (passedLimit)
			text = sprintf(translatePlural(
				"Could only be constructed %(limit)s time.",
				"Could only be constructed %(limit)s times.",
				matchEntLimit),
			{ "limit": matchEntLimit });
		else
			text = sprintf(translatePlural("Can be constructed %(count)s more time.", "Can be constructed %(count)s more times.", count), {
				"count": count
			});
	}
	else if (type == "training")
	{
		if (passedLimit)
			text = sprintf(translatePlural("Could only be trained once.", "Could only be trained %(limit)s times.", matchEntLimit), {
				"limit": matchEntLimit
			});
		else if (matchEntLimit == 1)
			text = translate("Can be trained only once.");
		else
			text = sprintf(translatePlural("Can be trained %(count)s more time.", "Can be trained %(count)s more times.", count), {
				"count": count
			});
	}
	else
	{
		if (passedLimit)
			text = sprintf(translatePlural("Could only be created once.", "Could only be created %(limit)s times.", matchEntLimit), {
				"limit": matchEntLimit
			});
		else if (matchEntLimit == 1)
			text = translate("Can be created only once.");
		else
			text = sprintf(translatePlural("Can be created %(count)s more time.", "Can be created %(count)s more times.", count), {
				"count": count
			});
	}

	return passedLimit ? coloredText(text, "red") : text;
}

/**
 * Format batch training string for the tooltip
 * Examples:
 * buildingsCountToTrainFullBatch = 1, fullBatchSize = 5, remainderBatch = 0:
 * "Shift-click to train 5"
 * buildingsCountToTrainFullBatch = 2, fullBatchSize = 5, remainderBatch = 0:
 * "Shift-click to train 10 (2*5)"
 * buildingsCountToTrainFullBatch = 1, fullBatchSize = 15, remainderBatch = 12:
 * "Shift-click to train 27 (15 + 12)"
 */
function formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
{
	var totalBatchTrainingCount = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;

	// Don't show the batch training tooltip if either units of this type can't be trained at all
	// or only one unit can be trained
	if (totalBatchTrainingCount < 2)
		return "";

	let fullBatchesString = "";
	if (buildingsCountToTrainFullBatch > 1)
		fullBatchesString = sprintf(translate("%(buildings)s*%(batchSize)s"), {
			"buildings": buildingsCountToTrainFullBatch,
			"batchSize": fullBatchSize
		});
	else if (buildingsCountToTrainFullBatch == 1)
		fullBatchesString = fullBatchSize;

	// We need to display the batch details part if there is either more than
	// one structure with full batch or one structure with the full batch and
	// another with a partial batch
	let batchString;
	if (buildingsCountToTrainFullBatch > 1 ||
	    buildingsCountToTrainFullBatch == 1 && remainderBatch > 0)
		if (remainderBatch > 0)
			batchString = translate("%(action)s to train %(number)s (%(fullBatch)s + %(remainderBatch)s).");
		else
			batchString = translate("%(action)s to train %(number)s (%(fullBatch)s).");
	else
		batchString = translate("%(action)s to train %(number)s.");

	return "[font=\"sans-13\"]" +
		setStringTags(
			sprintf(batchString, {
				"action": "[font=\"sans-bold-13\"]" + translate("Shift-click") + "[/font]",
				"number": totalBatchTrainingCount,
				"fullBatch": fullBatchesString,
				"remainderBatch": remainderBatch
			}),
			g_HotkeyTags) +
		"[/font]";
}

/**
 * Camera jumping: when the user presses a hotkey the current camera location is marked.
 * When pressing another camera jump hotkey the camera jumps back to that position.
 * When the camera is already roughly at that location, jump back to where it was previously.
 */
var g_JumpCameraPositions = [];
var g_JumpCameraLast;

function jumpCamera(index)
{
	let position = g_JumpCameraPositions[index];
	if (!position)
		return;

	let threshold = Engine.ConfigDB_GetValue("user", "gui.session.camerajump.threshold");
	let cameraPivot = Engine.GetCameraPivot();
	if (g_JumpCameraLast &&
	    Math.abs(cameraPivot.x - position.x) < threshold &&
	    Math.abs(cameraPivot.z - position.z) < threshold)
	{
		Engine.CameraMoveTo(g_JumpCameraLast.x, g_JumpCameraLast.z);
	}
	else
	{
		g_JumpCameraLast = cameraPivot;
		Engine.CameraMoveTo(position.x, position.z);
	}
}

function setJumpCamera(index)
{
	g_JumpCameraPositions[index] = Engine.GetCameraPivot();
}

/**
 * Called by GUI when user clicks a research button.
 */
function addResearchToQueue(entity, researchType)
{
	Engine.PostNetworkCommand({
		"type": "research",
		"entity": entity,
		"template": researchType,
		"pushFront": Engine.HotkeyIsPressed("session.pushorderfront")
	});
}

/**
 * Called by GUI when user clicks a production queue item.
 */
function removeFromProductionQueue(entity, id)
{
	Engine.PostNetworkCommand({
		"type": "stop-production",
		"entity": entity,
		"id": id
	});
}

/**
 * Called by unit selection buttons.
 */
function makePrimarySelectionGroup(templateName)
{
	g_Selection.makePrimarySelection(templateName);
}

function removeFromSelectionGroup(templateName)
{
	g_Selection.removeGroupFromSelection(templateName);
}

function performCommand(entStates, commandName)
{
	if (!entStates.length)
		return;

	if (getCommandInfo(commandName, entStates))
		g_EntityCommands[commandName].execute(entStates);
}

function performFormation(entities, formationTemplate)
{
	if (!entities)
		return;

	Engine.PostNetworkCommand({
		"type": "formation",
		"entities": entities,
		"formation": formationTemplate
	});
}

function performStance(entities, stanceName)
{
	if (!entities)
		return;

	Engine.PostNetworkCommand({
		"type": "stance",
		"entities": entities,
		"name": stanceName
	});
}

function lockGate(lock)
{
	Engine.PostNetworkCommand({
		"type": "lock-gate",
		"entities": g_Selection.toList(),
		"lock": lock
	});
}

function packUnit(pack)
{
	Engine.PostNetworkCommand({
		"type": "pack",
		"entities": g_Selection.toList(),
		"pack": pack,
		"queued": false
	});
}

function cancelPackUnit(pack)
{
	Engine.PostNetworkCommand({
		"type": "cancel-pack",
		"entities": g_Selection.toList(),
		"pack": pack,
		"queued": false
	});
}

function upgradeEntity(Template, selection)
{
	Engine.PostNetworkCommand({
		"type": "upgrade",
		"entities": selection,
		"template": Template,
		"queued": false
	});
}

function cancelUpgradeEntity()
{
	Engine.PostNetworkCommand({
		"type": "cancel-upgrade",
		"entities": g_Selection.toList(),
		"queued": false
	});
}

/**
 * Set the camera to follow the given entity if it's a unit.
 * Otherwise stop following.
 */
function setCameraFollow(entity)
{
	let entState = entity && GetEntityState(entity);
	if (entState && hasClass(entState, "Unit"))
		Engine.CameraFollow(entity);
	else
		Engine.CameraFollow(0);
}

function stopUnits(entities)
{
	Engine.PostNetworkCommand({
		"type": "stop",
		"entities": entities,
		"queued": false
	});
}

function unloadTemplate(template, owner)
{
	Engine.PostNetworkCommand({
		"type": "unload-template",
		"all": Engine.HotkeyIsPressed("session.unloadtype"),
		"template": template,
		"owner": owner,
		// Filter out all entities that aren't garrisonable.
		"garrisonHolders": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return state && !!state.garrisonHolder;
		})
	});
}

function unloadAll()
{
	const garrisonHolders = g_Selection.filter(e => {
		let state = GetEntityState(e);
		return state && !!state.garrisonHolder;
	});

	if (!garrisonHolders.length)
		return;

	let ownEnts = [];
	let otherEnts = [];

	for (let ent of garrisonHolders)
	{
		if (controlsPlayer(GetEntityState(ent).player))
			ownEnts.push(ent);
		else
			otherEnts.push(ent);
	}

	if (ownEnts.length)
		Engine.PostNetworkCommand({
			"type": "unload-all",
			"garrisonHolders": ownEnts
		});

	if (otherEnts.length)
		Engine.PostNetworkCommand({
			"type": "unload-all-by-owner",
			"garrisonHolders": otherEnts
		});
}

function unloadAllTurrets()
{
	const turretHolders = g_Selection.filter(e => {
		let state = GetEntityState(e);
		return state && !!state.turretHolder;
	});

	if (!turretHolders.length)
		return;

	let ownedHolders = [];
	let ejectables = [];
	for (let ent of turretHolders)
	{
		let turretHolderState = GetEntityState(ent);
		if (controlsPlayer(turretHolderState.player))
			ownedHolders.push(ent);
		else
		{
			for (let turret of turretHolderState.turretHolder.turretPoints.map(tp => tp.entity))
				if (turret && controlsPlayer(GetEntityState(turret).player))
					ejectables.push(turret);
		}
	}

	if (ejectables.length)
		Engine.PostNetworkCommand({
			"type": "leave-turret",
			"entities": ejectables
		});

	if (ownedHolders.length)
		Engine.PostNetworkCommand({
			"type": "unload-turrets",
			"entities": ownedHolders
		});
}

function leaveTurretPoints()
{
	const entities = g_Selection.filter(entity => {
		let entState = GetEntityState(entity);
		return entState && entState.turretable &&
			entState.turretable.holder != INVALID_ENTITY;
	});

	Engine.PostNetworkCommand({
		"type": "leave-turret",
		"entities": entities
	});
}

function backToWork()
{
	Engine.PostNetworkCommand({
		"type": "back-to-work",
		// Filter out all entities that can't go back to work.
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return state && state.unitAI && state.unitAI.hasWorkOrders;
		})
	});
}

function removeGuard()
{
	Engine.PostNetworkCommand({
		"type": "remove-guard",
		// Filter out all entities that are currently guarding/escorting.
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return state && state.unitAI && state.unitAI.isGuarding;
		})
	});
}

function raiseAlert()
{
	Engine.PostNetworkCommand({
		"type": "alert-raise",
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return state && !!state.alertRaiser;
		})
	});
}

function endOfAlert()
{
	Engine.PostNetworkCommand({
		"type": "alert-end",
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return state && !!state.alertRaiser;
		})
	});
}

function turnAutoQueueOn()
{
	Engine.PostNetworkCommand({
		"type": "autoqueue-on",
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return !!state?.trainer?.entities?.length &&
				!state.production.autoqueue;
		})
	});
}

function turnAutoQueueOff()
{
	Engine.PostNetworkCommand({
		"type": "autoqueue-off",
		"entities": g_Selection.filter(ent => {
			let state = GetEntityState(ent);
			return !!state?.trainer?.entities?.length &&
				state.production.autoqueue;
		})
	});
}