/**
 * This class handles the loading of files.
 */
class TemplateLoader
{
	constructor()
	{
		/**
		 * Raw Data Caches.
		 */
		this.auraData = {};
		this.playerData = {};
		this.technologyData = {};
		this.templateData = {};

		/**
		 * Partly-composed data.
		 */
		this.autoResearchTechList = this.findAllAutoResearchedTechs();
	}

	/**
	 * Loads raw aura template.
	 *
	 * Loads from local cache if available, else from file system.
	 *
	 * @param {string} templateName
	 * @return {Object} Object containing raw template data.
	 */
	loadAuraTemplate(templateName)
	{
		if (!(templateName in this.auraData))
		{
			const data = Engine.ReadJSONFile(this.AuraPath + templateName + ".json");
			translateObjectKeys(data, this.AuraTranslateKeys);

			this.auraData[templateName] = data;
		}

		return this.auraData[templateName];
	}

	/**
	 * Loads raw entity template.
	 *
	 * Loads from local cache if data present, else from file system.
	 *
	 * @param {string} templateName
	 * @param {string} civCode
	 * @return {Object} Object containing raw template data.
	 */
	loadEntityTemplate(templateName, civCode)
	{
		if (!(templateName in this.templateData))
		{
			// We need to clone the template because we want to perform some translations.
			const data = clone(Engine.GetTemplate(templateName));
			translateObjectKeys(data, this.EntityTranslateKeys);

			if (data.Auras)
				for (const auraID of data.Auras._string.split(/\s+/))
					this.loadAuraTemplate(auraID);

			if (data.Identity.Civ != this.DefaultCiv && civCode != this.DefaultCiv && data.Identity.Civ != civCode)
				warn("The \"" + templateName + "\" template has a defined civ of \"" + data.Identity.Civ + "\". " +
					"This does not match the currently selected civ \"" + civCode + "\".");

			this.templateData[templateName] = data;
		}

		return this.templateData[templateName];
	}

	/**
	 * Loads raw player template.
	 *
	 * Loads from local cache if data present, else from file system.
	 *
	 * If a civ doesn't have their own civ-specific template,
	 * then we return the generic template.
	 *
	 * @param {string} civCode
	 * @return {Object} Object containing raw template data.
	 */
	loadPlayerTemplate(civCode)
	{
		if (!(civCode in this.playerData))
		{
			const templateName = this.buildPlayerTemplateName(civCode);
			this.playerData[civCode] = Engine.GetTemplate(templateName);

			// No object keys need to be translated
		}

		return this.playerData[civCode];
	}

	/**
	 * Loads raw technology template.
	 *
	 * Loads from local cache if available, else from file system.
	 *
	 * @param {string} templateName
	 * @return {Object} Object containing raw template data.
	 */
	loadTechnologyTemplate(templateName)
	{
		if (!(templateName in this.technologyData))
		{
			const data = Engine.ReadJSONFile(this.TechnologyPath + templateName + ".json");
			translateObjectKeys(data, this.TechnologyTranslateKeys);

			// Translate specificName as in GetTechnologyData() from gui/session/session.js
			if (typeof (data.specificName) === 'object')
				for (const civ in data.specificName)
					data.specificName[civ] = translate(data.specificName[civ]);
			else if (data.specificName)
				warn("specificName should be an object of civ->name mappings in " + templateName + ".json");

			this.technologyData[templateName] = data;
		}

		return this.technologyData[templateName];
	}

	/**
	 * @param {string} templateName
	 * @param {string} civCode
	 * @return {Object} Contains a list and the requirements of the techs in the pair
	 */
	loadTechnologyPairTemplate(templateName, civCode)
	{
		const template = this.loadTechnologyTemplate(templateName);
		return {
			"techs": [template.top, template.bottom],
			"reqs": DeriveTechnologyRequirements(template, civCode)
		};
	}

	deriveProduction(template, civCode)
	{
		const production = {
			"techs": [],
			"units": []
		};

		if (!template.Researcher && !template.Trainer)
			return production;

		if (template.Trainer?.Entities?._string)
			for (let templateName of template.Trainer.Entities._string.split(" "))
			{
				templateName = templateName.replace(/\{(civ|native)\}/g, civCode);
				if (Engine.TemplateExists(templateName))
					production.units.push(templateName);
			}

		const appendTechnology = (technologyName) => {
			const technology = this.loadTechnologyTemplate(technologyName, civCode);
			if (DeriveTechnologyRequirements(technology, civCode))
				production.techs.push(technologyName);
		};

		if (template.Researcher?.Technologies?._string)
			for (let technologyName of template.Researcher.Technologies._string.split(" "))
			{
				if (technologyName.indexOf("{civ}") != -1)
				{
					const civTechName = technologyName.replace("{civ}", civCode);
					technologyName = TechnologyTemplateExists(civTechName) ? civTechName : technologyName.replace("{civ}", "generic");
				}

				if (this.isPairTech(technologyName))
				{
					const technologyPair = this.loadTechnologyPairTemplate(technologyName, civCode);
					if (technologyPair.reqs)
						for (technologyName of technologyPair.techs)
							appendTechnology(technologyName);
				}
				else
					appendTechnology(technologyName);
			}

		return production;
	}

	deriveBuildQueue(template, civCode)
	{
		const buildQueue = [];

		if (!template.Builder || !template.Builder.Entities._string)
			return buildQueue;

		for (let build of template.Builder.Entities._string.split(" "))
		{
			build = build.replace(/\{(civ|native)\}/g, civCode);
			if (Engine.TemplateExists(build))
				buildQueue.push(build);
		}

		return buildQueue;
	}

	deriveModifications(civCode, auraList)
	{
		const modificationData = [];
		for (const techName of this.autoResearchTechList)
			modificationData.push(GetTechnologyBasicDataHelper(this.loadTechnologyTemplate(techName), civCode));

		for (const auraName of auraList)
			modificationData.push(this.loadAuraTemplate(auraName));

		return DeriveModificationsFromTechnologies(modificationData);
	}

	/**
	 * If a civ doesn't have its own civ-specific player template,
	 * this returns the name of the generic player template.
	 *
	 * @see simulation/helpers/Player.js GetPlayerTemplateName()
	 *      (Which can't be combined with this due to different Engine contexts)
	 */
	buildPlayerTemplateName(civCode)
	{
		const templateName = this.PlayerPath + civCode;
		if (Engine.TemplateExists(templateName))
			return templateName;

		warn("No template found for civ " + civCode + ".");

		return this.PlayerPath + this.DefaultCiv;
	}

	/**
	 * Crudely iterates through every tech JSON file and identifies those
	 * that are auto-researched.
	 *
	 * @return {array} List of techs that are researched automatically
	 */
	findAllAutoResearchedTechs()
	{
		const techList = [];
		for (const templateName of listFiles(this.TechnologyPath, ".json", true))
		{
			const data = this.loadTechnologyTemplate(templateName);
			if (data && data.autoResearch)
				techList.push(templateName);
		}
		return techList;
	}

	/**
	 * A template may be a variant of another template,
	 * eg. `*_house`, `*_trireme`, or a promotion.
	 *
	 * This method returns an array containing:
	 * [0] - The template's basename
	 * [1] - The variant type
	 * [2] - Further information (if available)
	 *
	 * e.g.:
	 * units/athen/infantry_swordsman_e
	 *    -> ["units/athen/infantry_swordsman_b", TemplateVariant.promotion, "elite"]
	 *
	 * units/brit/support_female_citizen_house
	 *    -> ["units/brit/support_female_citizen", TemplateVariant.unlockedByTechnology, "unlock_female_house"]
	 */
	getVariantBaseAndType(templateName, civCode)
	{
		if (!templateName || !Engine.TemplateExists(templateName))
			return undefined;

		templateName = removeFiltersFromTemplateName(templateName);
		const template = this.loadEntityTemplate(templateName, civCode);

		if (!dirname(templateName) || dirname(template["@parent"]) != dirname(templateName))
			return [templateName, TemplateVariant.base];

		const parentTemplate = this.loadEntityTemplate(template["@parent"], civCode);
		const inheritedVariance = this.getVariantBaseAndType(template["@parent"], civCode);

		if (parentTemplate.Identity)
		{
			if (parentTemplate.Identity.Civ && parentTemplate.Identity.Civ != template.Identity.Civ)
				return [templateName, TemplateVariant.base];

			if (parentTemplate.Identity.Rank && parentTemplate.Identity.Rank != template.Identity.Rank)
				return [inheritedVariance[0], TemplateVariant.promotion, template.Identity.Rank.toLowerCase()];
		}

		if (parentTemplate.Upgrade)
			for (const upgrade in parentTemplate.Upgrade)
				if (parentTemplate.Upgrade[upgrade].Entity)
					return [inheritedVariance[0], TemplateVariant.upgrade, upgrade.toLowerCase()];

		if (template.Identity.Requirements?.Techs)
			return [inheritedVariance[0], TemplateVariant.unlockedByTechnology, template.Identity.Requirements?.Techs];

		if (parentTemplate.Cost)
			for (const res in parentTemplate.Cost.Resources)
				if (+parentTemplate.Cost.Resources[res])
					return [inheritedVariance[0], TemplateVariant.trainable];

		warn("Template variance unknown: " + templateName);
		return [templateName, TemplateVariant.unknown];
	}

	isPairTech(technologyCode)
	{
		return !!this.loadTechnologyTemplate(technologyCode).top;
	}

	isPhaseTech(technologyCode)
	{
		return basename(technologyCode).startsWith("phase");
	}

}

/**
 * Paths to certain files.
 *
 * It might be nice if we could get these from somewhere, instead of having them hardcoded here.
 */
TemplateLoader.prototype.AuraPath = "simulation/data/auras/";
TemplateLoader.prototype.PlayerPath = "special/players/";
TemplateLoader.prototype.TechnologyPath = "simulation/data/technologies/";

TemplateLoader.prototype.DefaultCiv = "gaia";

/**
 * Keys of template values that are to be translated on load.
 */
TemplateLoader.prototype.AuraTranslateKeys = ["auraName", "auraDescription"];
TemplateLoader.prototype.EntityTranslateKeys = ["GenericName", "SpecificName", "Tooltip", "History"];
TemplateLoader.prototype.TechnologyTranslateKeys = ["genericName", "tooltip", "description"];
