function VisionSharing() {}

VisionSharing.prototype.Schema =
	"<element name='Bribable'>" +
		"<data type='boolean'/>" +
	"</element>" +
	"<optional>" +
		"<element name='Duration' a:help='Duration (in second) of the vision sharing for spies'>" +
			"<ref name='positiveDecimal'/>" +
		"</element>" +
	"</optional>" +
	"<optional>" +
		"<element name='FailureCostRatio' a:help='Fraction of the bribe cost that will be incured if a bribe failed'>" +
			"<ref name='nonNegativeDecimal'/>" +
		"</element>" +
	"</optional>";

VisionSharing.prototype.Init = function()
{
	this.activated = false;
	this.shared = undefined;
	this.spyId = 0;
	this.spies = undefined;
};

/**
 * As entities have not necessarily the VisionSharing component, it has to be activated
 * before use so that the rangeManager can register it
 */
VisionSharing.prototype.Activate = function()
{
	if (this.activated)
		return;
	const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (!cmpOwnership || cmpOwnership.GetOwner() <= 0)
		return;
	this.shared = new Set([cmpOwnership.GetOwner()]);
	Engine.PostMessage(this.entity, MT_VisionSharingChanged,
		{ "entity": this.entity, "player": cmpOwnership.GetOwner(), "add": true });
	this.activated = true;
};

VisionSharing.prototype.CheckVisionSharings = function()
{
	const shared = new Set();

	const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	const owner = cmpOwnership ? cmpOwnership.GetOwner() : INVALID_PLAYER;
	if (owner >= 0)
	{
		// The owner has vision
		if (owner > 0)
			shared.add(owner);

		// Vision sharing due to garrisoned units
		const cmpGarrisonHolder = Engine.QueryInterface(this.entity, IID_GarrisonHolder);
		if (cmpGarrisonHolder)
		{
			for (const ent of cmpGarrisonHolder.GetEntities())
			{
				const cmpEntOwnership = Engine.QueryInterface(ent, IID_Ownership);
				if (!cmpEntOwnership)
					continue;
				const entOwner = cmpEntOwnership.GetOwner();
				if (entOwner > 0 && entOwner != owner)
				{
					shared.add(entOwner);
					// if shared by another player than the owner and not yet activated, do it
					this.Activate();
				}
			}
		}

		// vision sharing due to spies
		if (this.spies)
			for (const spy of this.spies.values())
				if (spy > 0 && spy != owner)
					shared.add(spy);
	}

	if (!this.activated)
		return;

	// compare with previous vision sharing, and update if needed
	for (const player of shared)
		if (!this.shared.has(player))
			Engine.PostMessage(this.entity, MT_VisionSharingChanged,
				{ "entity": this.entity, "player": player, "add": true });
	for (const player of this.shared)
		if (!shared.has(player))
			Engine.PostMessage(this.entity, MT_VisionSharingChanged,
				{ "entity": this.entity, "player": player, "add": false });
	this.shared = shared;
};

VisionSharing.prototype.IsBribable = function()
{
	return this.template.Bribable == "true";
};

VisionSharing.prototype.OnGarrisonedUnitsChanged = function(msg)
{
	this.CheckVisionSharings();
};

VisionSharing.prototype.OnOwnershipChanged = function(msg)
{
	if (this.activated)
		this.CheckVisionSharings();
};

VisionSharing.prototype.AddSpy = function(player, timeLength)
{
	if (!this.IsBribable())
		return 0;

	const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	if (!cmpOwnership || cmpOwnership.GetOwner() == player || player <= 0)
		return 0;

	const cmpTechnologyManager = QueryPlayerIDInterface(player, IID_TechnologyManager);
	if (!cmpTechnologyManager || !cmpTechnologyManager.CanProduce("special/spy"))
		return 0;

	const template = Engine.QueryInterface(SYSTEM_ENTITY, IID_TemplateManager).GetTemplate("special/spy");
	if (!IncurBribeCost(template, player, cmpOwnership.GetOwner(), false))
		return 0;

	// If no duration given, take it from the spy template and scale it with the ent vision
	// When no duration argument nor in spy template, it is a permanent spy
	let duration = timeLength;
	if (!duration && template.VisionSharing && template.VisionSharing.Duration)
	{
		duration = ApplyValueModificationsToTemplate("VisionSharing/Duration", +template.VisionSharing.Duration, player, template);
		const cmpVision = Engine.QueryInterface(this.entity, IID_Vision);
		if (cmpVision)
			duration *= 60 / Math.max(30, cmpVision.GetRange());
	}

	if (!this.spies)
		this.spies = new Map();

	this.spies.set(++this.spyId, player);
	if (duration)
	{
		const cmpTimer = Engine.QueryInterface(SYSTEM_ENTITY, IID_Timer);
		cmpTimer.SetTimeout(this.entity, IID_VisionSharing, "RemoveSpy", duration * 1000, { "id": this.spyId });
	}
	this.Activate();
	this.CheckVisionSharings();

	// update statistics for successful bribes
	const cmpBribesStatisticsTracker = QueryPlayerIDInterface(player, IID_StatisticsTracker);
	if (cmpBribesStatisticsTracker)
		cmpBribesStatisticsTracker.IncreaseSuccessfulBribesCounter();

	return this.spyId;
};

VisionSharing.prototype.RemoveSpy = function(data)
{
	this.spies.delete(data.id);
	this.CheckVisionSharings();
};

/**
 * Returns true if this entity share its vision with player
 */
VisionSharing.prototype.ShareVisionWith = function(player)
{
	if (this.activated)
		return this.shared.has(player);

	const cmpOwnership = Engine.QueryInterface(this.entity, IID_Ownership);
	return cmpOwnership && cmpOwnership.GetOwner() == player;
};

Engine.RegisterComponentType(IID_VisionSharing, "VisionSharing", VisionSharing);
