function TechnologyManager() {}

TechnologyManager.prototype.Schema =
	"<a:component type='system'/><empty/>";

TechnologyManager.prototype.Serialize = function()
{
	// The modifications cache will be affected by property reads from the GUI and other places so we shouldn't 
	// serialize it.

	var ret = {};
	for (var i in this)
	{
		if (this.hasOwnProperty(i))
			ret[i] = this[i];
	}
	ret.modificationCache = {};
	return ret;
};

TechnologyManager.prototype.Init = function()
{
	this.researchedTechs = {}; // technologies which have been researched
	this.researchQueued = {};  // technologies which are queued for research
	this.researchStarted = {}; // technologies which are being researched currently (non-queued)

	// This stores the modifications to unit stats from researched technologies
	// Example data: {"ResourceGatherer/Rates/food.grain": [ 
	//                     {"multiply": 1.15, "affects": ["Female", "Infantry Swordsman"]},
	//                     {"add": 2}
	//                 ]}
	this.modifications = {};
	this.modificationCache = {}; // Caches the values after technologies have been applied
	                             // e.g. { "Attack/Melee/Hack" : {5: {"origValue": 8, "newValue": 10}, 7: {"origValue": 9, "newValue": 12}, ...}, ...}
	                             // where 5 and 7 are entity id's

	this.classCounts = {}; // stores the number of entities of each Class
	this.typeCountsByClass = {}; // stores the number of entities of each type for each class i.e.
	                             // {"someClass": {"unit/spearman": 2, "unit/cav": 5} "someOtherClass":...}

	// Some technologies are automatically researched when their conditions are met.  They have no cost and are 
	// researched instantly.  This allows civ bonuses and more complicated technologies.
	this.autoResearchTech = {};
	var allTechs = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager).GetAllTechs();
	for (var key in allTechs)
	{
		if (allTechs[key].autoResearch || allTechs[key].top)
			this.autoResearchTech[key] = allTechs[key];
	}
};

TechnologyManager.prototype.OnUpdate = function() 
{
	this.UpdateAutoResearch();
};


// This function checks if the requirements of any autoresearch techs are met and if they are it researches them
TechnologyManager.prototype.UpdateAutoResearch = function()
{
	var cmpDataTempMan = Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager);
	for (var key in this.autoResearchTech)
	{
		var tech = cmpDataTempMan.GetTechnologyTemplate(key);
		if ((tech.autoResearch && this.CanResearch(key))
			|| (tech.top && (this.IsTechnologyResearched(tech.top) || this.IsTechnologyResearched(tech.bottom))))
		{
			delete this.autoResearchTech[key];
			this.ResearchTechnology(key);
			return; // We will have recursively handled any knock-on effects so can just return
		}
	}
};

TechnologyManager.prototype.GetTechnologyTemplate = function(tech)
{
	return Engine.QueryInterface(SYSTEM_ENTITY, IID_DataTemplateManager).GetTechnologyTemplate(tech);
};

// Checks an entity template to see if its technology requirements have been met
TechnologyManager.prototype.CanProduce = function (templateName)
{
	var cmpTempManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
	var template = cmpTempManager.GetTemplate(templateName);

	if (template.Identity && template.Identity.RequiredTechnology)
		return this.IsTechnologyResearched(template.Identity.RequiredTechnology);
	// If there is no required technology then this entity can be produced
	return true;
};

TechnologyManager.prototype.IsTechnologyResearched = function(tech)
{
	return (this.researchedTechs[tech] !== undefined);
};

// Checks the requirements for a technology to see if it can be researched at the current time
TechnologyManager.prototype.CanResearch = function(tech)
{
	var template = this.GetTechnologyTemplate(tech);
	if (!template)
	{
		warn("Technology \"" + tech + "\" does not exist");
		return false;
	}

	// The technology which this technology supersedes is required
	if (template.supersedes && !this.IsTechnologyResearched(template.supersedes))
		return false;

	if (template.top && this.IsInProgress(template.top) ||
	    template.bottom && this.IsInProgress(template.bottom))
		return false;

	if (template.pair && !this.CanResearch(template.pair))
		return false;

	if (this.IsInProgress(tech))
		return false;

	if (this.IsTechnologyResearched(tech))
		return false;

	return this.CheckTechnologyRequirements(template.requirements || null);
};

/**
 * Private function for checking a set of requirements is met
 * @param reqs Object of technology requirements as given by the technology template
 * @param civonly A boolean set to true if only the civ requirement is checked
 *
 * @return true if the requirements are checked
 * false otherwise
 */
TechnologyManager.prototype.CheckTechnologyRequirements = function(reqs, civonly)
{
	// If there are no requirements then all requirements are met
	if (!reqs)
		return true;

	if (reqs.all)
		return reqs.all.every(r => this.CheckTechnologyRequirements(r, civonly));
	if (reqs.any)
		return reqs.any.some(r => this.CheckTechnologyRequirements(r, civonly));
	if (reqs.civ)
	{
		let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
		return cmpPlayer && cmpPlayer.GetCiv() == reqs.civ;
	}
	if (reqs.notciv)
	{
		let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
		return cmpPlayer && cmpPlayer.GetCiv() != reqs.notciv;
	}
	if (civonly)
		return true;
	if (reqs.tech)
		return this.IsTechnologyResearched(reqs.tech);
	if (reqs.class && reqs.numberOfTypes)
		return this.typeCountsByClass[reqs.class] &&
			Object.keys(this.typeCountsByClass[reqs.class]).length >= reqs.numberOfTypes;
	if (reqs.class && reqs.number)
		return this.classCounts[reqs.class] &&
			this.classCounts[reqs.class] >= reqs.number;
	// The technologies requirements are not a recognised format
	error("Bad requirements " + uneval(reqs));
	return false;
};

TechnologyManager.prototype.OnGlobalOwnershipChanged = function(msg)
{
	// This automatically updates classCounts and typeCountsByClass
	var playerID = (Engine.QueryInterface(this.entity, IID_Player)).GetPlayerID();
	if (msg.to == playerID)
	{
		var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
		var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);

		var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
		if (!cmpIdentity)
			return;

		var classes = cmpIdentity.GetClassesList();
		// don't use foundations for the class counts but check if techs apply (e.g. health increase)
		if (!Engine.QueryInterface(msg.entity, IID_Foundation))
		{
			for (let cls of classes)
			{
				this.classCounts[cls] = this.classCounts[cls] || 0;
				this.classCounts[cls] += 1;

				this.typeCountsByClass[cls] = this.typeCountsByClass[cls] || {};
				this.typeCountsByClass[cls][template] = this.typeCountsByClass[cls][template] || 0;
				this.typeCountsByClass[cls][template] += 1;
			}
		}

		// Newly created entity, check if any researched techs might apply
		// (only do this for new entities because even if an entity is converted or captured,
		//	we want it to maintain whatever technologies previously applied)
		if (msg.from == -1)
		{
			var modifiedComponents = {};
			for (var name in this.modifications)
			{
				// We only need to find one one tech per component for a match
				var modifications = this.modifications[name];
				var component = name.split("/")[0];
				for (let modif of modifications)
 					if (DoesModificationApply(modif, classes))
					{
						if (!modifiedComponents[component])
							modifiedComponents[component] = [];
						modifiedComponents[component].push(name);
					}
			}

			// Send mesage(s) to the entity so it knows about researched techs
			for (var component in modifiedComponents)
				Engine.PostMessage(msg.entity, MT_ValueModification, { "entities": [msg.entity], "component": component, "valueNames": modifiedComponents[component] });
		}
	}
	if (msg.from == playerID)
	{
		var cmpTemplateManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager);
		var template = cmpTemplateManager.GetCurrentTemplateName(msg.entity);

		// don't use foundations for the class counts
		if (!Engine.QueryInterface(msg.entity, IID_Foundation))
		{
			var cmpIdentity = Engine.QueryInterface(msg.entity, IID_Identity);
			if (cmpIdentity)
			{
				var classes = cmpIdentity.GetClassesList();
				for (let cls of classes)
				{
					this.classCounts[cls] -= 1;
					if (this.classCounts[cls] <= 0)
						delete this.classCounts[cls];

					this.typeCountsByClass[cls][template] -= 1;
					if (this.typeCountsByClass[cls][template] <= 0)
						delete this.typeCountsByClass[cls][template];
				}
			}
		}

		this.clearModificationCache(msg.entity);
	}
};

// Marks a technology as researched.  Note that this does not verify that the requirements are met.
TechnologyManager.prototype.ResearchTechnology = function(tech)
{
	this.StoppedResearch(tech); // The tech is no longer being currently researched

	var template = this.GetTechnologyTemplate(tech);

	if (!template)
	{
		error("Tried to research invalid technology: " + uneval(tech));
		return;
	}

	var modifiedComponents = {};
	this.researchedTechs[tech] = template;
	// store the modifications in an easy to access structure
	if (template.modifications)
	{
		var affects = [];
		if (template.affects && template.affects.length > 0)
		{
			for (let affect of template.affects)
			{
				// Put the list of classes into an array for convenient access
				affects.push(affect.split(/\s+/));
			}
		}
		else
		{
			affects.push([]);
		}

		// We add an item to this.modifications for every modification in the template.modifications array
		for (var i in template.modifications)
		{
			var modification = template.modifications[i];
			if (!this.modifications[modification.value])
				this.modifications[modification.value] = [];
			
			var modAffects = affects.slice();
			if (modification.affects)
			{
				var extraAffects = modification.affects.split(/\s+/);
				for (var a in modAffects)
					modAffects[a] = modAffects[a].concat(extraAffects);
			}

			var mod = {"affects": modAffects};

			// copy the modification data into our new data structure
			for (var j in modification)
				if (j !== "value" && j !== "affects")
					mod[j] = modification[j];

			this.modifications[modification.value].push(mod);
			var component = modification.value.split("/")[0];
			if (!modifiedComponents[component])
				modifiedComponents[component] = [];
			modifiedComponents[component].push(modification.value);
			this.modificationCache[modification.value] = {};
		}
	}

	if (template.replaces && template.replaces.length > 0)
	{
		for (var i of template.replaces) 
		{
			if (!i || this.IsTechnologyResearched(i))
				continue;

			var template = this.GetTechnologyTemplate(i);
			this.researchedTechs[i] = template;

			// Change the EntityLimit if any
			let cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
			if (cmpPlayer && cmpPlayer.GetPlayerID() !== undefined)
			{
				let playerID = cmpPlayer.GetPlayerID();
				let cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
				if (cmpPlayerEntityLimits)
					cmpPlayerEntityLimits.UpdateLimitsFromTech(i);
			}
		}
	}

	this.UpdateAutoResearch();

	var cmpPlayer = Engine.QueryInterface(this.entity, IID_Player);
	if (!cmpPlayer || cmpPlayer.GetPlayerID() === undefined)
		return;
	var playerID = cmpPlayer.GetPlayerID();
	var cmpRangeManager = Engine.QueryInterface(SYSTEM_ENTITY, IID_RangeManager);
	var ents = cmpRangeManager.GetEntitiesByPlayer(playerID);

	// Change the EntityLimit if any
	var cmpPlayerEntityLimits = QueryPlayerIDInterface(playerID, IID_EntityLimits);
	if (cmpPlayerEntityLimits)
		cmpPlayerEntityLimits.UpdateLimitsFromTech(tech);

	// always send research finished message 
	Engine.PostMessage(this.entity, MT_ResearchFinished, {"player": playerID, "tech": tech});

	for (var component in modifiedComponents)
	{
		Engine.PostMessage(SYSTEM_ENTITY, MT_TemplateModification, { "player": playerID, "component": component, "valueNames": modifiedComponents[component]});
		Engine.BroadcastMessage(MT_ValueModification, { "entities": ents, "component": component, "valueNames": modifiedComponents[component]});
	}
};

// Clears the cached data for an entity from the modifications cache
TechnologyManager.prototype.clearModificationCache = function(ent)
{
	for (var valueName in this.modificationCache)
		delete this.modificationCache[valueName][ent];
};

// Caching layer in front of ApplyModificationsWorker
//	Note: be careful with the type of curValue, if it should be a numerical
//		value and is derived from template data, you must convert the string
//		from the template to a number using the + operator, before calling
//		this function!
TechnologyManager.prototype.ApplyModifications = function(valueName, curValue, ent)
{
	if (!this.modificationCache[valueName])
		this.modificationCache[valueName] = {};

	if (!this.modificationCache[valueName][ent] || this.modificationCache[valueName][ent].origValue != curValue)
	{
		let cmpIdentity = Engine.QueryInterface(ent, IID_Identity);
		if (!cmpIdentity)
			return curValue;
		this.modificationCache[valueName][ent] = {
			"origValue": curValue,
			"newValue": GetTechModifiedProperty(this.modifications, cmpIdentity.GetClassesList(), valueName, curValue)
		};
	}

	return this.modificationCache[valueName][ent].newValue;
};

// Alternative version of ApplyModifications, applies to templates instead of entities
TechnologyManager.prototype.ApplyModificationsTemplate = function(valueName, curValue, template)
{
	if (!template || !template.Identity)
		return curValue;
	return GetTechModifiedProperty(this.modifications, GetIdentityClasses(template.Identity), valueName, curValue);
};

// Marks a technology as being queued for research
TechnologyManager.prototype.QueuedResearch = function(tech, researcher)
{
	this.researchQueued[tech] = researcher;
};

// Marks a technology as actively being researched
TechnologyManager.prototype.StartedResearch = function(tech)
{
	this.researchStarted[tech] = true;
};

// Marks a technology as not being currently researched
TechnologyManager.prototype.StoppedResearch = function(tech)
{
	delete this.researchQueued[tech];
	delete this.researchStarted[tech];
};

// Checks whether a technology is set to be researched
TechnologyManager.prototype.IsInProgress = function(tech)
{
	if (this.researchQueued[tech])
		return true;
	else
		return false;
};

// Get all techs that are currently being researched
TechnologyManager.prototype.GetTechsStarted = function()
{
	return this.researchStarted;
};

// Gets the entity currently researching a technology
TechnologyManager.prototype.GetResearcher = function(tech)
{
	if (this.researchQueued[tech])
		return this.researchQueued[tech];
	return undefined;
};

// Get helper data for tech modifications
TechnologyManager.prototype.GetTechModifications = function()
{
	return this.modifications;
};

// called by GUIInterface for PlayerData. AI use.
TechnologyManager.prototype.GetQueuedResearch = function()
{
	return this.researchQueued;
};
TechnologyManager.prototype.GetStartedResearch = function()
{
	return this.researchStarted;
};
TechnologyManager.prototype.GetResearchedTechs = function()
{
	return this.researchedTechs;
};
TechnologyManager.prototype.GetClassCounts = function()
{
	return this.classCounts;
};
TechnologyManager.prototype.GetTypeCountsByClass = function()
{
	return this.typeCountsByClass;
};

Engine.RegisterComponentType(IID_TechnologyManager, "TechnologyManager", TechnologyManager);
