/**
 * Contains the layout and button settings per selection panel
 *
 * getItems returns a list of basic items used to fill the panel.
 * This method is obligated. If the items list is empty, the panel
 * won't be rendered.
 *
 * Then there's a loop over all items provided. In the loop,
 * the item and some other standard data is added to a data object.
 *
 * The standard data is
 * {
 *   "i":              index
 *   "item":           item coming from the getItems function
 *   "playerState":    playerState
 *   "unitEntStates":  states of the selected entities
 *   "rowLength":      rowLength
 *   "numberOfItems":  number of items that will be processed
 *   "button":         gui Button object
 *   "icon":           gui Icon object
 *   "guiSelection":   gui button Selection overlay
 *   "countDisplay":   gui caption space
 * }
 *
 * Then for every data object, the setupButton function is called which
 * sets the view and handlers of the button.
 */

// Cache some formation info
// Available formations per player
var g_AvailableFormations = new Map();
var g_FormationsInfo = new Map();

var g_SelectionPanels = {};

var g_SelectionPanelBarterButtonManager;

g_SelectionPanels.Alert = {
	"getMaxNumberOfItems": function () {
		return 2;
	},
	"getItems": function (unitEntStates) {
		return unitEntStates.some(state => !!state.alertRaiser) ? ["raise", "end"] : [];
	},
	"setupButton": function (data) {
		data.button.onPress = function () {
			switch (data.item) {
				case "raise":
					raiseAlert();
					return;
				case "end":
					endOfAlert();
					return;
				default:
					break;
			}
		};

		switch (data.item) {
			case "raise":
				data.icon.sprite = "stretched:session/icons/bell_level1.png";
				data.button.tooltip = translate("Raise an alert!");
				break;
			case "end":
				data.button.tooltip = translate("End of alert.");
				data.icon.sprite = "stretched:session/icons/bell_level0.png";
				break;
			default:
				break;
		}
		data.button.enabled = controlsPlayer(data.player);

		setPanelObjectPosition(data.button, this.getMaxNumberOfItems() - data.i, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Barter = {
	"getMaxNumberOfItems": function () {
		return 5;
	},
	"rowLength": 5,
	"conflictsWith": ["Garrison"],
	"getItems": function (unitEntStates) {
		// If more than `rowLength` resources, don't display icons.
		if (unitEntStates.every(state => !state.isBarterMarket) || g_ResourceData.GetBarterableCodes().length > this.rowLength)
			return [];
		return g_ResourceData.GetBarterableCodes();
	},
	"setupButton": function (data) {
		if (g_SelectionPanelBarterButtonManager) {
			g_SelectionPanelBarterButtonManager.setViewedPlayer(data.player);
			g_SelectionPanelBarterButtonManager.update();
		}
		return true;
	}
};

g_SelectionPanels.Command = {
	"getMaxNumberOfItems": function () {
		return 10;
	},
	"rowLength": 10,
	"getItems": function (unitEntStates) {
		const commands = [];

		for (const command in g_EntityCommands) {
			const info = getCommandInfo(command, unitEntStates);
			if (info) {
				info.name = command;
				commands.push(info);
			}
		}
		return commands;
	},
	"setupButton": function (data) {
		data.button.tooltip = data.item.tooltip;

		data.button.onPress = function () {
			if (data.item.callback)
				data.item.callback(data.item);
			else
				performCommand(data.unitEntStates, data.item.name);
		};

		data.countDisplay.caption = data.item.count || "";

		data.button.enabled = data.item.enabled == true;

		data.icon.sprite = `stretched:session/icons/${data.item.icon}`;

		setPanelObjectPosition(data.button, data.i + 40, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Construction = {
	"getMaxNumberOfItems": function () {
		return 27 - getNumberOfRightPanelButtons();
	},
	"rowLength": 9,
	"getItems": function () {
		return getAllBuildableEntitiesFromSelection();
	},
	"setupButton": function (data) {
		const template = GetTemplateData(data.item, data.player);

		if (!template)
			return false;
		const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
			"requirements": template.requirements,
			"player": data.player
		});

		let neededResources;
		if (template.cost)
			neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": multiplyEntityCosts(template, 1),
				"player": data.player
			});

		data.button.onPress = function () { startBuildingPlacement(data.item, data.playerState); };
		const showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
		data.button.onPressRight = showTemplateFunc;
		data.button.onPressRightDisabled = showTemplateFunc;

		const tooltips = [
			getEntityNamesFormatted,
			getVisibleEntityClassesFormatted,
			getAurasTooltip,
			getEntityTooltip
		].map(func => func(template));
		tooltips.push(
			getEntityCostTooltip(template, data.player),
			getResourceDropsiteTooltip(template),
			getGarrisonTooltip(template),
			getTurretsTooltip(template),
			getPopulationBonusTooltip(template),
			showTemplateViewerOnRightClickTooltip(template)
		);

		const limits = getEntityLimitAndCount(data.playerState, data.item);
		tooltips.push(
			formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
			formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
			getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
			getNeededResourcesTooltip(neededResources));

		data.button.tooltip = tooltips.filter(tip => tip).join("\n");

		let modifier = "";
		if (!requirementsMet || limits.canBeAddedCount == 0) {
			data.button.enabled = false;
			modifier += "color:0 0 0 127:grayscale:";
		}
		else if (neededResources) {
			data.button.enabled = false;
			modifier += `${resourcesToAlphaMask(neededResources)}:`;
		}
		else
			data.button.enabled = controlsPlayer(data.player);

		if (template.icon)
			data.icon.sprite = `${modifier}stretched:session/portraits/${template.icon}`;

		setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
		return true;
	}
};

g_SelectionPanels.Formation = {
	"getMaxNumberOfItems": function () {
		return 15;
	},
	"rowLength": 5,
	"conflictsWith": ["Garrison"],
	"getItems": function (unitEntStates) {
		if (unitEntStates.some(state => !hasClass(state, "Unit")))
			return [];

		if (unitEntStates.every(state => !state.unitAI || !state.unitAI.formations.length))
			return [];

		if (!g_AvailableFormations.has(unitEntStates[0].player))
			g_AvailableFormations.set(unitEntStates[0].player, Engine.GuiInterfaceCall("GetAvailableFormations", unitEntStates[0].player));

		return g_AvailableFormations.get(unitEntStates[0].player).filter(formation => unitEntStates.some(state => !!state.unitAI && state.unitAI.formations.includes(formation)));
	},
	"setupButton": function (data) {
		if (!g_FormationsInfo.has(data.item))
			g_FormationsInfo.set(data.item, Engine.GuiInterfaceCall("GetFormationInfoFromTemplate", { "templateName": data.item }));

		const formationOk = canMoveSelectionIntoFormation(data.item);
		const unitIds = data.unitEntStates.map(state => state.id);
		const formationSelected = Engine.GuiInterfaceCall("IsFormationSelected", {
			"ents": unitIds,
			"formationTemplate": data.item
		});

		data.button.onPress = function () {
			performFormation(unitIds, data.item);
		};

		data.button.onMouseRightPress = () => g_AutoFormation.setDefault(data.item);

		const formationInfo = g_FormationsInfo.get(data.item);
		let tooltip = translate(formationInfo.name);

		const isDefaultFormation = g_AutoFormation.isDefault(data.item);
		if (data.item === NULL_FORMATION)
			tooltip += `\n${isDefaultFormation ?
				translate("Default formation is disabled.") :
				translate("Right-click to disable the default formation feature.")}`;
		else
			tooltip += `\n${isDefaultFormation ?
				translate("This is the default formation, used for movement orders.") :
				translate("Right-click to set this as the default formation.")}`;

		if (!formationOk && formationInfo.tooltip)
			tooltip += `\n${coloredText(translate(formationInfo.tooltip), "red")}`;
		data.button.tooltip = tooltip;

		data.button.enabled = formationOk && controlsPlayer(data.player);
		const grayscale = formationOk ? "" : "grayscale:";
		data.guiSelection.hidden = !formationSelected;
		data.countDisplay.hidden = !isDefaultFormation;
		data.icon.sprite = `stretched:${grayscale}session/icons/${formationInfo.icon}`;

		setPanelObjectPosition(data.button, data.i, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Garrison = {
	"getMaxNumberOfItems": function () {
		return 12;
	},
	"rowLength": 4,
	"conflictsWith": ["Barter"],
	"getItems": function (unitEntStates) {
		if (unitEntStates.every(state => !state.garrisonHolder))
			return [];

		const groups = new EntityGroups();

		for (const state of unitEntStates)
			if (state.garrisonHolder)
				groups.add(state.garrisonHolder.entities);

		return groups.getEntsGrouped();
	},
	"setupButton": function (data) {
		const entities = data.item.ents.map(id => GetEntityState(id));
		const template = GetTemplateData(entities[0].template);
		if (!template)
			return false;

		data.button.onPress = function () {
			unloadTemplate(template.selectionGroupName || entities[0].template, entities[0].player);
		};

		data.countDisplay.caption = entities.length || "";

		const canUngarrison = controlsPlayer(data.player) || controlsPlayer(entities[0].player);
		data.button.enabled = canUngarrison;

		// Start with basic tooltip info
		let tooltip = canUngarrison ?
			`${sprintf(translate("Unload %(name)s"), { "name": getEntityNames(template) })}\n${translate("Single-click to unload 1. Shift-click to unload all of this type.")}` :
			getEntityNames(template);

		tooltip += `\n${sprintf(translate("Player: %(playername)s"), {
			"playername": g_Players[entities[0].player].name
		})}`;

		// Process ranks and experience data
		const rankGroups = {};
		const noRankGroup = { count: 0, exp: 0, expReq: 0 }; // For units without rank
		let totalExp = 0;
		let expCount = 0;

		entities.forEach(entState => {
			const rank = (entState.identity && entState.identity.rank);

			if (!rank) {
				noRankGroup.count++;
				if (entState.promotion) {
					noRankGroup.exp += entState.promotion.curr || 0;
					noRankGroup.expReq += entState.promotion.req || 0;
					totalExp += entState.promotion.curr || 0;
					expCount++;
				}
				return;
			}

			if (!rankGroups[rank]) {
				rankGroups[rank] = {
					count: 0,
					exp: 0,
					expReq: 0
				};
			}

			rankGroups[rank].count++;
			if (entState.promotion) {
				rankGroups[rank].exp += entState.promotion.curr || 0;
				rankGroups[rank].expReq += entState.promotion.req || 0;
				totalExp += entState.promotion.curr || 0;
				expCount++;
			}
		});

		// Add rank information with icons (only if we have ranked units)
		if (Object.keys(rankGroups).length > 0 || noRankGroup.count > 0) {

			// Define the desired sort order
			const rankOrder = ["Basic", "Advanced", "Elite"];

			// Add unranked units first if they exist
			if (noRankGroup.count > 0) {
				let line = `\n ${noRankGroup.count} Unit`; // No icon or rank name for unranked units

				// Add experience if available
				if (noRankGroup.exp > 0) {
					const avgExp = noRankGroup.exp / noRankGroup.count;
					const avgExpReq = noRankGroup.expReq / noRankGroup.count;

					if (avgExp < avgExpReq) {
						line += ` (${Math.floor(avgExp)}/${Math.ceil(avgExpReq)})*`;
					} else {
						line += ` (${Math.floor(avgExp)})*`;
					}
				}

				tooltip += line;
			}

			// Convert rankGroups to array and sort
			const sortedRanks = Object.entries(rankGroups).sort((a, b) => {
				const aIndex = rankOrder.indexOf(a[0]);
				const bIndex = rankOrder.indexOf(b[0]);

				// Both are in our ordered list - sort by the predefined order
				if (aIndex !== -1 && bIndex !== -1) return aIndex - bIndex;
				// Only a is in our ordered list - a comes first
				if (aIndex !== -1) return -1;
				// Only b is in our ordered list - b comes first
				if (bIndex !== -1) return 1;
				// Neither are in our ordered list - sort alphabetically
				return a[0].localeCompare(b[0]);
			});

			for (const [rank, data] of sortedRanks) {
				const rankName =
					rank === "Basic" ? translate("Basic") :
						rank === "Advanced" ? translate("Advanced") :
							rank === "Elite" ? translate("Elite") :
								rank;

				let line = `\n [icon="rank_${rank.toLowerCase()}" displace="0 3"] ${data.count} ${rankName}`;

				// Add experience if available for this rank group
				if (data.exp > 0) {
					const avgExp = data.exp / data.count;
					const avgExpReq = data.expReq / data.count;

					if (avgExp < avgExpReq) {
						line += ` (${Math.floor(avgExp)}/${Math.ceil(avgExpReq)})*`;
					} else {
						line += ` (${Math.floor(avgExp)})*`;
					}
				}

				tooltip += line;
			}
		}

		// Add average experience footer if applicable
		if (expCount > 0) {
			const avgExp = totalExp / expCount;
			tooltip += `\n [font="sans-13"]\n${translate("*Average experience")}`;
		}

		data.button.tooltip = tooltip;

		data.guiSelection.sprite = `color:${g_DiplomacyColors.getPlayerColor(entities[0].player, 160)}`;
		data.button.sprite_disabled = data.button.sprite;

		data.icon.sprite =
			`${canUngarrison || g_IsObserver ? "" : "grayscale:"}stretched:session/portraits/${template.icon}`;

		setPanelObjectPosition(data.button, data.i, data.rowLength);

		return true;
	}
};

g_SelectionPanels.Gate = {
	"getMaxNumberOfItems": function () {
		return 27 - getNumberOfRightPanelButtons();
	},
	"rowLength": 9,
	"getItems": function (unitEntStates) {
		const hideLocked = unitEntStates.every(state => !state.gate || !state.gate.locked);
		const hideUnlocked = unitEntStates.every(state => !state.gate || state.gate.locked);

		if (hideLocked && hideUnlocked)
			return [];

		return [
			{
				"hidden": hideLocked,
				"tooltip": translate("Lock Gate"),
				"icon": "session/icons/lock_locked.png",
				"locked": true
			},
			{
				"hidden": hideUnlocked,
				"tooltip": translate("Unlock Gate"),
				"icon": "session/icons/lock_unlocked.png",
				"locked": false
			}
		];
	},
	"setupButton": function (data) {
		data.button.onPress = function () { lockGate(data.item.locked); };
		data.button.tooltip = data.item.tooltip;
		data.button.enabled = controlsPlayer(data.player);
		data.guiSelection.hidden = data.item.hidden;
		data.icon.sprite = `stretched:${data.item.icon}`;

		setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
		return true;
	}
};

g_SelectionPanels.Pack = {
	"getMaxNumberOfItems": function () {
		return 27 - getNumberOfRightPanelButtons();
	},
	"rowLength": 9,
	"getItems": function (unitEntStates) {
		const checks = {};
		for (const state of unitEntStates) {
			if (!state.pack)
				continue;

			if (state.pack.progress == 0) {
				if (state.pack.packed)
					checks.unpackButton = true;
				else
					checks.packButton = true;
			}
			else if (state.pack.packed)
				checks.unpackCancelButton = true;
			else
				checks.packCancelButton = true;
		}

		const items = [];
		if (checks.packButton)
			items.push({
				"packing": false,
				"packed": false,
				"tooltip": translate("Pack"),
				"callback": function () { packUnit(true); }
			});

		if (checks.unpackButton)
			items.push({
				"packing": false,
				"packed": true,
				"tooltip": translate("Unpack"),
				"callback": function () { packUnit(false); }
			});

		if (checks.packCancelButton)
			items.push({
				"packing": true,
				"packed": false,
				"tooltip": translate("Cancel Packing"),
				"callback": function () { cancelPackUnit(true); }
			});

		if (checks.unpackCancelButton)
			items.push({
				"packing": true,
				"packed": true,
				"tooltip": translate("Cancel Unpacking"),
				"callback": function () { cancelPackUnit(false); }
			});

		return items;
	},
	"setupButton": function (data) {
		data.button.onPress = function () { data.item.callback(data.item); };

		data.button.tooltip = data.item.tooltip;

		if (data.item.packing)
			data.icon.sprite = "stretched:session/icons/cancel.png";
		else if (data.item.packed)
			data.icon.sprite = "stretched:session/icons/unpack.png";
		else
			data.icon.sprite = "stretched:session/icons/pack.png";

		data.button.enabled = controlsPlayer(data.player);

		setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
		return true;
	}
};

g_SelectionPanels.Queue = {
	"getMaxNumberOfItems": function () {
		return 16;
	},
	/**
	 * Returns a list of all items in the productionqueue of the selection
	 * The first entry of every entity's production queue will come before
	 * the second entry of every entity's production queue
	 */
	"getItems": function (unitEntStates) {
		const queue = [];
		let foundNew = true;
		for (let i = 0; foundNew; ++i) {
			foundNew = false;
			for (const state of unitEntStates) {
				if (!state.production || !state.production.queue[i])
					continue;
				queue.push({
					"producingEnt": state.id,
					"queuedItem": state.production.queue[i],
					"autoqueue": state.production.autoqueue && state.production.queue[i].unitTemplate
				});
				foundNew = true;
			}
		}
		if (!queue.length)
			return queue;
		// Add 'ghost' items to show autoqueues.
		const repeat = [];
		for (const item of queue)
			if (item.autoqueue) {
				const ghostItem = clone(item);
				ghostItem.ghost = true;
				repeat.push(ghostItem);
			}
		if (repeat.length)
			for (let i = 0; queue.length < g_SelectionPanels.Queue.getMaxNumberOfItems(); ++i)
				queue.push(repeat[i % repeat.length]);
		return queue;
	},
	"resizePanel": function (numberOfItems, rowLength) {
		const numRows = Math.ceil(numberOfItems / rowLength);
		const panel = Engine.GetGUIObjectByName("unitQueuePanel");
		const size = panel.size;
		const buttonSize = Engine.GetGUIObjectByName("unitQueueButton[0]").size.bottom;
		const margin = 4;
		size.top = size.bottom - numRows * buttonSize - (numRows + 2) * margin;
		panel.size = size;
	},
	"setupButton": function (data) {
		const queuedItem = data.item.queuedItem;

		// Differentiate between units and techs
		let template;
		if (queuedItem.unitTemplate)
			template = GetTemplateData(queuedItem.unitTemplate);
		else if (queuedItem.technologyTemplate)
			template = GetTechnologyData(queuedItem.technologyTemplate, GetSimState().players[data.player].civ);
		else {
			warning(`Unknown production queue template ${uneval(queuedItem)}`);
			return false;
		}
		data.button.onPress = function () { removeFromProductionQueue(data.item.producingEnt, queuedItem.id); };

		const tooltips = [getEntityNames(template)];
		if (data.item.ghost)
			tooltips.push(translate("The auto-queue will try to train this item later."));
		if (queuedItem.neededSlots) {
			tooltips.push(coloredText(translate("Insufficient population capacity:"), "red"));
			tooltips.push(sprintf(translate("%(population)s %(neededSlots)s"), {
				"population": resourceIcon("population"),
				"neededSlots": queuedItem.neededSlots
			}));
		}
		tooltips.push(showTemplateViewerOnRightClickTooltip(template));
		data.button.tooltip = tooltips.join("\n");

		data.countDisplay.caption = queuedItem.count > 1 ? queuedItem.count : "";

		const progressSlider = Engine.GetGUIObjectByName(`unitQueueProgressSlider[${data.i}]`);
		if (data.item.ghost) {
			data.button.enabled = false;
			progressSlider.sprite = "color:0 150 250 50";
			const size = progressSlider.size;

			// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
			size.top = size.left;
			progressSlider.size = size;
		}
		else {
			// Show the time remaining to finish the first item
			if (data.i == 0)
				Engine.GetGUIObjectByName("queueTimeRemaining").caption =
					Engine.FormatMillisecondsIntoDateStringGMT(queuedItem.timeRemaining, translateWithContext("countdown format", "m:ss"));

			progressSlider.sprite = "queueProgressSlider";
			const size = progressSlider.size;

			// Buttons are assumed to be square, so left/right offsets can be used for top/bottom.
			size.top = size.left + Math.round(queuedItem.progress * (size.right - size.left));
			progressSlider.size = size;

			data.button.enabled = controlsPlayer(data.player);

			Engine.GetGUIObjectByName(`unitQueuePausedIcon[${data.i}]`).hidden = !queuedItem.paused;
			if (queuedItem.paused)
				// Translation: String displayed when the research is paused. E.g. by being garrisoned or when not the first item in the queue.
				data.button.tooltip += `\n${translate("This item is paused.")}`;
		}

		if (template.icon) {
			let modifier = "stretched:";
			if (queuedItem.paused)
				modifier += "color:0 0 0 127:grayscale:";
			else if (data.item.ghost)
				modifier += "grayscale:";
			data.icon.sprite = `${modifier}session/portraits/${template.icon}`;
		}

		const showTemplateFunc = () => { showTemplateDetails(data.item.queuedItem.unitTemplate || data.item.queuedItem.technologyTemplate, data.playerState.civ); };
		data.button.onPressRight = showTemplateFunc;
		data.button.onPressRightDisabled = showTemplateFunc;

		setPanelObjectPosition(data.button, data.i, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Research = {
	"getMaxNumberOfItems": function () {
		return 9;
	},
	"rowLength": 9,
	"getItems": function (unitEntStates) {
		let ret = [];
		if (unitEntStates.length == 1) {
			const entState = unitEntStates[0];
			if (!entState?.researcher?.technologies)
				return ret;
			if (!entState.production)
				warn(`Researcher without ProductionQueue found: ${entState.id}.`);
			return entState.researcher.technologies.map(tech => ({
				"tech": tech,
				"techCostMultiplier": entState.researcher.techCostMultiplier,
				"researchFacilityId": entState.id,
				"isUpgrading": !!entState.upgrade && entState.upgrade.isUpgrading
			}));
		}

		const sortedEntStates = unitEntStates.sort((a, b) =>
			(!b.upgrade || !b.upgrade.isUpgrading) - (!a.upgrade || !a.upgrade.isUpgrading) ||
			(!a.production ? 0 : a.production.queue.length) - (!b.production ? 0 : b.production.queue.length)
		);

		for (const state of sortedEntStates) {
			if (!state.researcher || !state.researcher.technologies)
				continue;
			if (!state.production)
				warn(`Researcher without ProductionQueue found: ${state.id}.`);

			// Remove the techs we already have in ret (with the same name and techCostMultiplier)
			const filteredTechs = state.researcher.technologies.filter(
				tech => tech != null && !ret.some(
					item =>
						(item.tech == tech ||
							item.tech.pair &&
							tech.pair &&
							item.tech.bottom == tech.bottom &&
							item.tech.top == tech.top) &&
						Object.keys(item.techCostMultiplier).every(
							k => item.techCostMultiplier[k] == state.researcher.techCostMultiplier[k])
				));

			if (filteredTechs.length + ret.length <= this.getMaxNumberOfItems() &&
				getNumberOfRightPanelButtons() <= this.getMaxNumberOfItems() * (filteredTechs.some(tech => !!tech.pair) ? 1 : 2))
				ret = ret.concat(filteredTechs.map(tech => ({
					"tech": tech,
					"techCostMultiplier": state.researcher.techCostMultiplier,
					"researchFacilityId": state.id,
					"isUpgrading": !!state.upgrade && state.upgrade.isUpgrading
				})));
		}
		return ret;
	},
	"hideItem": function (i, rowLength) // Called when no item is found
	{
		Engine.GetGUIObjectByName(`unitResearchButton[${i}]`).hidden = true;
		// We also remove the paired tech and the pair symbol
		Engine.GetGUIObjectByName(`unitResearchButton[${i + rowLength}]`).hidden = true;
		Engine.GetGUIObjectByName(`unitResearchPair[${i}]`).hidden = true;
	},
	"setupButton": function (data) {
		if (!data.item.tech) {
			g_SelectionPanels.Research.hideItem(data.i, data.rowLength);
			return false;
		}

		// Start position (start at the bottom)
		let position = data.i + data.rowLength;

		// Set the top button of the pair
		if (!data.item.tech.pair)
			Engine.GetGUIObjectByName(`unitResearchButton[${data.i}]`).hidden = true;

		// Set up the tech connector
		const pair = Engine.GetGUIObjectByName(`unitResearchPair[${data.i}]`);
		pair.hidden = data.item.tech.pair == null;
		setPanelObjectPosition(pair, data.i, data.rowLength);

		// Handle one or two techs (tech pair)
		const player = data.player;
		const playerState = GetSimState().players[player];
		for (const tech of data.item.tech.pair ? [data.item.tech.bottom, data.item.tech.top] : [data.item.tech]) {
			// Don't change the object returned by GetTechnologyData
			const template = clone(GetTechnologyData(tech, playerState.civ));
			if (!template)
				return false;

			// Not allowed by civ.
			if (!template.reqs) {
				// One of the pair may still be researchable by the current civ,
				// hence don't hide everything.
				Engine.GetGUIObjectByName(`unitResearchButton[${data.i}]`).hidden = true;
				pair.hidden = true;
				continue;
			}

			for (const res in template.cost)
				template.cost[res] *= data.item.techCostMultiplier[res] !== undefined ? data.item.techCostMultiplier[res] : 1;

			const neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": template.cost,
				"player": player
			});

			const requirementsPassed = Engine.GuiInterfaceCall("CheckTechnologyRequirements", {
				"tech": tech,
				"player": player
			});

			const button = Engine.GetGUIObjectByName(`unitResearchButton[${position}]`);
			const icon = Engine.GetGUIObjectByName(`unitResearchIcon[${position}]`);

			const tooltips = [
				getEntityNamesFormatted,
				getEntityTooltip,
				getEntityCostTooltip,
				showTemplateViewerOnRightClickTooltip
			].map(func => func(template));

			if (!requirementsPassed) {
				let tip = template.requirementsTooltip;
				const reqs = template.reqs;
				for (const req of reqs) {
					if (!req.entities)
						continue;

					const entityCounts = [];
					for (const entity of req.entities) {
						let current = 0;
						switch (entity.check) {
							case "count":
								current = playerState.classCounts[entity.class] || 0;
								break;

							case "variants":
								current = playerState.typeCountsByClass[entity.class] ?
									Object.keys(playerState.typeCountsByClass[entity.class]).length : 0;
								break;
							default:
								break;
						}

						const remaining = entity.number - current;
						if (remaining < 1)
							continue;

						entityCounts.push(sprintf(translatePlural("%(number)s entity of class %(class)s", "%(number)s entities of class %(class)s", remaining), {
							"number": remaining,
							"class": translate(entity.class)
						}));
					}

					tip += ` ${sprintf(translate("Remaining: %(entityCounts)s"), {
						"entityCounts": entityCounts.join(translateWithContext("Separator for a list of entity counts", ", "))
					})}`;
				}
				tooltips.push(tip);
			}
			tooltips.push(getNeededResourcesTooltip(neededResources));
			button.tooltip = tooltips.filter(tip => tip).join("\n");

			button.onPress = (t => function () {
				addResearchToQueue(data.item.researchFacilityId, t);
			})(tech);

			const showTemplateFunc = (t => function () {
				showTemplateDetails(
					t,
					GetTemplateData(data.unitEntStates.find(state => state.id == data.item.researchFacilityId).template).nativeCiv);
			});

			button.onPressRight = showTemplateFunc(tech);
			button.onPressRightDisabled = showTemplateFunc(tech);

			if (data.item.tech.pair) {
				// On mouse enter, show a cross over the other icon
				const unchosenIcon = Engine.GetGUIObjectByName(`unitResearchUnchosenIcon[${(position + data.rowLength) % (2 * data.rowLength)}]`);
				button.onMouseEnter = function () {
					unchosenIcon.hidden = false;
				};
				button.onMouseLeave = function () {
					unchosenIcon.hidden = true;
				};
			}

			button.hidden = false;
			let modifier = "";
			if (!requirementsPassed) {
				button.enabled = false;
				modifier += "color:0 0 0 127:grayscale:";
			}
			else if (neededResources) {
				button.enabled = false;
				modifier += `${resourcesToAlphaMask(neededResources)}:`;
			}
			else
				button.enabled = controlsPlayer(data.player);

			if (data.item.isUpgrading) {
				button.enabled = false;
				modifier += "color:0 0 0 127:grayscale:";
				button.tooltip += `\n${coloredText(translate("Cannot research while upgrading."), "red")}`;

			}

			if (template.icon)
				icon.sprite = `${modifier}stretched:session/portraits/${template.icon}`;

			setPanelObjectPosition(button, position + data.rowLength, data.rowLength);

			// Prepare to handle the top button (if any)
			position -= data.rowLength;
		}

		return true;
	}
};

g_SelectionPanels.Selection = {
	"getMaxNumberOfItems": function () {
		return 16;
	},
	"rowLength": 4,
	"getItems": function (unitEntStates) {
		if (unitEntStates.length < 2)
			return [];
		return g_Selection.groups.getEntsGrouped();
	},
	"setupButton": function (data) {
		const entState = GetEntityState(data.item.ents[0]);
		const template = GetTemplateData(entState.template);
		let carriedResources = {};

		if (!template)
			return false;

		// First loop through all entities to accumulate stats
		for (const ent of data.item.ents) {
			const state = GetEntityState(ent);

			// Handle carried resources
			if (state.resourceCarrying && state.resourceCarrying.length !== 0) {
				const carrying = state.resourceCarrying[0];
				if (carriedResources[carrying.type])
					carriedResources[carrying.type] += carrying.amount;
				else
					carriedResources[carrying.type] = carrying.amount;
			}

			if (state.trader && state.trader.goods && state.trader.goods.amount) {
				const amount = state.trader.goods.amount;
				const type = state.trader.goods.type;
				let totalGain = amount.traderGain;
				if (amount.market1Gain)
					totalGain += amount.market1Gain;
				if (amount.market2Gain)
					totalGain += amount.market2Gain;
				if (carriedResources[type])
					carriedResources[type] += totalGain;
				else
					carriedResources[type] = totalGain;
			}
		}

		// Now build the tooltip with all accumulated data
		const unitOwner = GetEntityState(data.item.ents[0]).player;
		let tooltip = getEntityNames(template);

		// Add carried resources if any
		if (Object.keys(carriedResources).length) {
			tooltip += `\n${Object.keys(carriedResources).map(res =>
				resourceIcon(res) + carriedResources[res]
			).join(" ")}`;
		}

		if (g_IsObserver) {
			tooltip += `\n${sprintf(translate("Player: %(playername)s"), {
				"playername": g_Players[unitOwner].name
			})}`;
		}

		data.button.tooltip = tooltip;
		data.guiSelection.sprite = `color:${g_DiplomacyColors.getPlayerColor(unitOwner, 160)}`;
		data.guiSelection.hidden = !g_IsObserver;
		data.countDisplay.caption = data.item.ents.length || "";

		data.button.onPress = function () {
			if (Engine.HotkeyIsPressed("session.deselectgroup"))
				removeFromSelectionGroup(data.item.key);
			else
				makePrimarySelectionGroup(data.item.key);
		};
		data.button.onPressRight = function () { removeFromSelectionGroup(data.item.key); };

		if (template.icon)
			data.icon.sprite = `stretched:session/portraits/${template.icon}`;

		setPanelObjectPosition(data.button, data.i, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Stance = {
	"getMaxNumberOfItems": function () {
		return 5;
	},
	"getItems": function (unitEntStates) {
		if (unitEntStates.some(state => !state.unitAI || !hasClass(state, "Unit") || hasClass(state, "Animal")))
			return [];

		return unitEntStates[0].unitAI.selectableStances;
	},
	"setupButton": function (data) {
		const unitIds = data.unitEntStates.map(state => state.id);
		data.button.onPress = function () { performStance(unitIds, data.item); };

		data.button.tooltip = `${getStanceDisplayName(data.item)}\n` +
			`[font="sans-13"]${getStanceTooltip(data.item)}[/font]`;

		data.guiSelection.hidden = !Engine.GuiInterfaceCall("IsStanceSelected", {
			"ents": unitIds,
			"stance": data.item
		});
		data.icon.sprite = `stretched:session/icons/stances/${data.item}.png`;
		data.button.enabled = controlsPlayer(data.player);

		setPanelObjectPosition(data.button, data.i, data.rowLength);
		return true;
	}
};

g_SelectionPanels.Training = {
	"getMaxNumberOfItems": function () {
		return 27 - getNumberOfRightPanelButtons();
	},
	"rowLength": 9,
	"getItems": function () {
		return getAllTrainableEntitiesFromSelection();
	},
"setupButton": function(data)
	{
		const template = GetTemplateData(data.item, data.player);
		if (!template)
			return false;

		const requirementsMet = Engine.GuiInterfaceCall("AreRequirementsMet", {
			"requirements": template.requirements,
			"player": data.player
		});

		const unitIds = data.unitEntStates.map(status => status.id);
		const [buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch] =
			getTrainingStatus(unitIds, data.item, data.playerState);

		const trainNum = buildingsCountToTrainFullBatch * fullBatchSize + remainderBatch;

		let neededResources;
		if (template.cost)
			neededResources = Engine.GuiInterfaceCall("GetNeededResources", {
				"cost": multiplyEntityCosts(template, trainNum),
				"player": data.player
			});

		data.button.onPress = function() {
			if (!neededResources)
				addTrainingToQueue(unitIds, data.item, data.playerState);
		};

		const showTemplateFunc = () => { showTemplateDetails(data.item, data.playerState.civ); };
		data.button.onPressRight = showTemplateFunc;
		data.button.onPressRightDisabled = showTemplateFunc;

		data.countDisplay.caption = trainNum > 1 ? trainNum : "";

		let tooltips = [
			"[font=\"sans-bold-16\"]" +
				colorizeHotkey("%(hotkey)s", "session.queueunit." + (data.i + 1)) +
				"[/font]" + " " + getEntityNamesFormatted(template),
			getVisibleEntityClassesFormatted(template),
			getAurasTooltip(template),
			getEntityTooltip(template),
			getEntityCostTooltip(template, data.player, unitIds[0], buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch)
		];
		const limits = getEntityLimitAndCount(data.playerState, data.item);
		tooltips.push(formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
			formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type));

		if (Engine.ConfigDB_GetValue("user", "showdetailedtooltips") === "true")
			tooltips = tooltips.concat([
				getHealthTooltip,
				getAttackTooltip,
				getHealerTooltip,
				getResistanceTooltip,
				getGarrisonTooltip,
				getTurretsTooltip,
				getProjectilesTooltip,
				getSpeedTooltip,
				getResourceDropsiteTooltip
			].map(func => func(template)));

		tooltips.push(getTemplateViewerOnRightClickTooltip());
		tooltips.push(
			formatBatchTrainingString(buildingsCountToTrainFullBatch, fullBatchSize, remainderBatch),
			getRequirementsTooltip(requirementsMet, template.requirements, GetSimState().players[data.player].civ),
			getNeededResourcesTooltip(neededResources));

		data.button.tooltip = tooltips.filter(tip => tip).join("\n");

		let modifier = "";
		if (!requirementsMet || limits.canBeAddedCount == 0)
		{
			data.button.enabled = false;
			modifier = "color:0 0 0 127:grayscale:";
		}
		else
		{
			data.button.enabled = controlsPlayer(data.player);
			if (neededResources)
				modifier = resourcesToAlphaMask(neededResources) + ":";
		}

		if (data.unitEntStates.every(state => state.upgrade && state.upgrade.isUpgrading))
		{
			data.button.enabled = false;
			modifier = "color:0 0 0 127:grayscale:";
			data.button.tooltip += "\n" + objectionFont(translate("Cannot train while upgrading."));
		}

		if (template.icon)
			data.icon.sprite = modifier + "stretched:session/portraits/" + template.icon;

		const index = data.i + getNumberOfRightPanelButtons();
		setPanelObjectPosition(data.button, index, data.rowLength);

		return true;
	}
};


g_SelectionPanels.Upgrade = {
	"getMaxNumberOfItems": function () {
		return 27 - getNumberOfRightPanelButtons();
	},
	"rowLength": 9,
	"getItems": function (unitEntStates) {
		// Interface becomes complicated with multiple different units and this is meant per-entity, so prevent it if the selection has multiple different units.
		if (unitEntStates.some(state => state.template != unitEntStates[0].template))
			return false;

		return unitEntStates[0].upgrade && unitEntStates[0].upgrade.upgrades;
	},
	"setupButton": function (data) {
		const template = GetTemplateData(data.item.entity);
		if (!template)
			return false;

		const progressOverlay = Engine.GetGUIObjectByName(`unitUpgradeProgressSlider[${data.i}]`);
		progressOverlay.hidden = true;

		let technologyEnabled = true;

		const limits = getEntityLimitAndCount(data.playerState, data.item.entity);
		const upgradingEntStates = data.unitEntStates.filter(state => state.upgrade.template == data.item.entity);

		const upgradableEntStates = data.unitEntStates.filter(state =>
			!state.upgrade.progress &&
			(!state.production || !state.production.queue || !state.production.queue.length));

		const neededResources = data.item.cost && Engine.GuiInterfaceCall("GetNeededResources", {
			"cost": multiplyEntityCosts(data.item, upgradableEntStates.length),
			"player": data.player
		});

		const requirementsMet = !data.item.requirements ||
			Engine.GuiInterfaceCall("AreRequirementsMet", {
				"requirements": data.item.requirements,
				"player": data.player
			});

		let tooltip;
		let modifier = "";
		if (!upgradingEntStates.length && upgradableEntStates.length) {
			const primaryName = g_SpecificNamesPrimary ? template.name.specific : template.name.generic;
			let secondaryName;
			if (g_ShowSecondaryNames)
				secondaryName = g_SpecificNamesPrimary ? template.name.generic : template.name.specific;

			const tooltips = [];
			if (g_ShowSecondaryNames) {
				if (data.item.tooltip)
					tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s). %(tooltip)s"), {
						"primaryName": primaryName,
						"secondaryName": secondaryName,
						"tooltip": translate(data.item.tooltip)
					}));
				else
					tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s (%(secondaryName)s)."), {
						"primaryName": primaryName,
						"secondaryName": secondaryName
					}));
			}
			else {
				if (data.item.tooltip)
					tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s. %(tooltip)s"), {
						"primaryName": primaryName,
						"tooltip": translate(data.item.tooltip)
					}));
				else
					tooltips.push(sprintf(translate("Upgrade to a %(primaryName)s."), {
						"primaryName": primaryName
					}));
			}

			tooltips.push(
				getEntityCostTooltip(data.item, undefined, undefined, data.unitEntStates.length),
				formatLimitString(limits.entLimit, limits.entCount, limits.entLimitChangers),
				formatMatchLimitString(limits.matchLimit, limits.matchCount, limits.type),
				getRequirementsTooltip(requirementsMet, data.item.requirements, GetSimState().players[data.player].civ),
				getNeededResourcesTooltip(neededResources),
				showTemplateViewerOnRightClickTooltip());

			tooltip = tooltips.filter(tip => tip).join("\n");

			data.button.onPress = function () {
				upgradeEntity(
					data.item.entity,
					upgradableEntStates.map(state => state.id));
			};

			if (!technologyEnabled || limits.canBeAddedCount == 0 &&
				!upgradableEntStates.some(state => hasSameRestrictionCategory(data.item.entity, state.template)) || !requirementsMet) {
				data.button.enabled = false;
				modifier = "color:0 0 0 127:grayscale:";
			}
			else if (neededResources) {
				data.button.enabled = false;
				modifier = `${resourcesToAlphaMask(neededResources)}:`;
			}

			data.countDisplay.caption = upgradableEntStates.length > 1 ? upgradableEntStates.length : "";
		}
		else if (upgradingEntStates.length) {
			tooltip = translate("Cancel Upgrading");
			data.button.onPress = function () { cancelUpgradeEntity(); };
			data.countDisplay.caption = upgradingEntStates.length > 1 ? upgradingEntStates.length : "";

			let progress = 0;
			for (const state of upgradingEntStates)
				progress = Math.max(progress, state.upgrade.progress || 1);
			const progressOverlaySize = progressOverlay.size;
			// TODO This is bad: we assume the progressOverlay is square
			progressOverlaySize.top = progressOverlaySize.bottom + Math.round((1 - progress) * (progressOverlaySize.left - progressOverlaySize.right));
			progressOverlay.size = progressOverlaySize;
			progressOverlay.hidden = false;
		}
		else {
			tooltip = coloredText(translatePlural(
				"Cannot upgrade when the entity is training, researching or already upgrading.",
				"Cannot upgrade when all entities are training, researching or already upgrading.",
				data.unitEntStates.length), "red");

			data.button.onPress = function () { };

			data.button.enabled = false;
			modifier = "color:0 0 0 127:grayscale:";
		}
		data.button.enabled = controlsPlayer(data.player);
		data.button.tooltip = tooltip;

		const showTemplateFunc = () => { showTemplateDetails(data.item.entity, data.playerState.civ); };
		data.button.onPressRight = showTemplateFunc;
		data.button.onPressRightDisabled = showTemplateFunc;

		data.icon.sprite = `${modifier}stretched:session/${data.item.icon || `portraits/${template.icon}`}`;

		setPanelObjectPosition(data.button, data.i + getNumberOfRightPanelButtons(), data.rowLength);
		return true;
	}
};

function initSelectionPanels() {

	const unitBarterPanel = Engine.GetGUIObjectByName("unitBarterPanel");
	if (BarterButtonManager.IsAvailable(unitBarterPanel))
		g_SelectionPanelBarterButtonManager = new BarterButtonManager(unitBarterPanel);
}

/**
 * Pauses game and opens the template details viewer for a selected entity or technology.
 *
 * Technologies don't have a set civ, so we pass along the native civ of
 * the template of the entity that's researching it.
 *
 * @param {string} [civCode] - The template name of the entity that researches the selected technology.
 */
async function showTemplateDetails(templateName, civCode) {
	if (inputState != INPUT_NORMAL)
		return;

	await Engine.PushGuiPage(
		"page_viewer.xml",
		{
			"templateName": templateName,
			"civ": civCode
		},
	);
}

/**
 * If two panels need the same space, so they collide,
 * the one appearing first in the order is rendered.
 *
 * Note that the panel needs to appear in the list to get rendered.
 */
const g_PanelsOrder = [
	// LEFT PANE
	"Barter", // Must always be visible on markets
	"Garrison", // More important than Formation, as you want to see the garrisoned units in ships
	"Alert",
	"Formation",
	"Stance", // Normal together with formation

	// RIGHT PANE
	"Gate", // Must always be shown on gates
	"Pack", // Must always be shown on packable entities
	"Upgrade", // Must always be shown on upgradable entities
	"Training",
	"Construction",
	"Research", // Normal together with training

	// UNIQUE PANES (importance doesn't matter)
	"Command",
	"Queue",
	"Selection"
];
