import * as filters from "simulation/ai/common-api/filters.js";
import { aiWarn, SquareVectorDistance, VectorDistance } from "simulation/ai/common-api/utils.js";
import { AttackPlan } from "simulation/ai/r28/attackPlan.js";
import { GarrisonManager } from "simulation/ai/r28/garrisonManager.js";
import * as chat from "simulation/ai/r28/chatHelper.js";
import { Config } from "simulation/ai/r28/config.js";
import * as difficulty from "simulation/ai/r28/difficultyLevel.js";
import { allowCapture, getLandAccess } from "simulation/ai/r28/entityExtend.js";
import { Worker } from "simulation/ai/r28/worker.js";

export function AttackManager(config)
{
	this.Config = config;

	this.totalNumber = 0;
	this.attackNumber = 0;
	this.rushNumber = 0;
	this.raidNumber = 0;
	this.lastRangedRetreatTurn = 0; // Last turn when function was called
	this.rangedRetreatInterval = 4; // Interval between calls when enemies are far 
	this.upcomingAttacks = {
		[AttackPlan.TYPE_RUSH]: [],
		[AttackPlan.TYPE_RAID]: [],
		[AttackPlan.TYPE_DEFAULT]: [],
		[AttackPlan.TYPE_HUGE_ATTACK]: []
	};
	this.startedAttacks = {
		[AttackPlan.TYPE_RUSH]: [],
		[AttackPlan.TYPE_RAID]: [],
		[AttackPlan.TYPE_DEFAULT]: [],
		[AttackPlan.TYPE_HUGE_ATTACK]: []
	};
	this.bombingAttacks = new Map();// Temporary attacks for siege units while waiting their current attack to start
	this.debugTime = 0;
	this.maxRushes = 0;
	this.rushSize = [];
	this.currentEnemyPlayer = undefined; // enemy player we are currently targeting
	this.defeated = {};
}

/** More initialisation for stuff that needs the gameState */
AttackManager.prototype.init = function(gameState)
{
	this.outOfPlan = gameState.getOwnUnits().filter(filters.byMetadata(PlayerID, "plan", -1));
	this.outOfPlan.registerUpdates();
};

AttackManager.prototype.setRushes = function(allowed)
{


	this.maxRushes = 5;
	this.rushSize = [ 30, 40, 50, 60, 70];

	// if (this.Config.personality.aggressive > this.Config.personalityCut.strong && allowed > 2)
	// {
	// 	this.maxRushes = 3;
	// 	this.rushSize = [ 16, 20, 24 ];
	// }
	// else if (this.Config.personality.aggressive > this.Config.personalityCut.medium && allowed > 1)
	// {
	// 	this.maxRushes = 2;
	// 	this.rushSize = [ 18, 22 ];
	// }
	// else if (this.Config.personality.aggressive > this.Config.personalityCut.weak && allowed > 0)
	// {
	// 	this.maxRushes = 1;
	// 	this.rushSize = [ 20 ];
	// }
};

AttackManager.prototype.checkEvents = function(gameState, events)
{
	for (const evt of events.PlayerDefeated)
		this.defeated[evt.playerId] = true;

	let answer = "decline";
	let other;
	let targetPlayer;
	for (const evt of events.AttackRequest)
	{
		if (evt.source === PlayerID || !gameState.isPlayerAlly(evt.source) || !gameState.isPlayerEnemy(evt.player))
			continue;
		targetPlayer = evt.player;
		let available = 0;
		for (const attackType in this.upcomingAttacks)
		{
			for (const attack of this.upcomingAttacks[attackType])
			{
				if (attack.state === AttackPlan.STATE_COMPLETING)
				{
					if (attack.targetPlayer === targetPlayer)
						available += attack.unitCollection.length;
					else if (attack.targetPlayer !== undefined && attack.targetPlayer !== targetPlayer)
						other = attack.targetPlayer;
					continue;
				}

				attack.targetPlayer = targetPlayer;

				if (attack.unitCollection.length > 2)
					available += attack.unitCollection.length;
			}
		}

		if (available > 12)	// launch the attack immediately
		{
			for (const attackType in this.upcomingAttacks)
			{
				for (const attack of this.upcomingAttacks[attackType])
				{
					if (attack.state === AttackPlan.STATE_COMPLETING ||
						attack.targetPlayer !== targetPlayer ||
						attack.unitCollection.length < 3)
						continue;
					attack.forceStart();
					attack.requested = true;
				}
			}
			answer = "join";
		}
		else if (other !== undefined)
			answer = "other";
		break;  // take only the first attack request into account
	}
	if (targetPlayer !== undefined)
		chat.answerRequestAttack(gameState, targetPlayer, answer, other);

	for (const evt of events.EntityRenamed)	// take care of packing units in bombing attacks
	{
		for (const [targetId, unitIds] of this.bombingAttacks)
		{
			if (targetId == evt.entity)
			{
				this.bombingAttacks.set(evt.newentity, unitIds);
				this.bombingAttacks.delete(evt.entity);
			}
			else if (unitIds.has(evt.entity))
			{
				unitIds.add(evt.newentity);
				unitIds.delete(evt.entity);
			}
		}
	}
};

/**
 * Check for any structure in range from within our territory, and bomb it
 */
AttackManager.prototype.assignBombers = function(gameState)
{
	// First some cleaning of current bombing attacks
	for (const [targetId, unitIds] of this.bombingAttacks)
	{
		const target = gameState.getEntityById(targetId);
		if (!target || !gameState.isPlayerEnemy(target.owner()))
			this.bombingAttacks.delete(targetId);
		else
		{
			for (const entId of unitIds.values())
			{
				const ent = gameState.getEntityById(entId);
				if (ent && ent.owner() == PlayerID)
				{
					const plan = ent.getMetadata(PlayerID, "plan");
					const orders = ent.unitAIOrderData();
					const lastOrder = orders && orders.length ? orders[orders.length-1] : null;
					if (lastOrder && lastOrder.target && lastOrder.target == targetId && plan != -2 && plan != -3)
						continue;
				}
				unitIds.delete(entId);
			}
			if (!unitIds.size)
				this.bombingAttacks.delete(targetId);
		}
	}

	const bombers = gameState.updatingCollection("bombers",
		filters.byClasses(["BoltShooter", "StoneThrower"]), gameState.getOwnUnits());
	for (const ent of bombers.values())
	{
		if (!ent.position() || !ent.isIdle() || !ent.attackRange("Ranged"))
			continue;
		if (ent.getMetadata(PlayerID, "plan") == -2 || ent.getMetadata(PlayerID, "plan") == -3)
			continue;
		if (ent.getMetadata(PlayerID, "plan") !== undefined && ent.getMetadata(PlayerID, "plan") != -1)
		{
			const subrole = ent.getMetadata(PlayerID, "subrole");
			if (subrole && (subrole === Worker.SUBROLE_COMPLETING ||
				subrole === Worker.SUBROLE_WALKING || subrole === Worker.SUBROLE_ATTACKING))
				continue;
		}
		let alreadyBombing = false;
		for (const unitIds of this.bombingAttacks.values())
		{
			if (!unitIds.has(ent.id()))
				continue;
			alreadyBombing = true;
			break;
		}
		if (alreadyBombing)
			break;

		const range = ent.attackRange("Ranged").max;
		const entPos = ent.position();
		const access = getLandAccess(gameState, ent);
		for (const struct of gameState.getEnemyStructures().values())
		{
			if (!ent.canAttackTarget(struct, allowCapture(gameState, ent, struct)))
				continue;

			const structPos = struct.position();
			let x;
			let z;
			if (struct.hasClass("Field"))
			{
				if (!struct.resourceSupplyNumGatherers() ||
				    !gameState.isPlayerEnemy(gameState.ai.HQ.territoryMap.getOwner(structPos)))
					continue;
			}
			const dist = VectorDistance(entPos, structPos);
			if (dist > range)
			{
				const safety = struct.footprintRadius() + 30;
				x = structPos[0] + (entPos[0] - structPos[0]) * safety / dist;
				z = structPos[1] + (entPos[1] - structPos[1]) * safety / dist;
				const owner = gameState.ai.HQ.territoryMap.getOwner([x, z]);
				if (owner != 0 && gameState.isPlayerEnemy(owner))
					continue;
				x = structPos[0] + (entPos[0] - structPos[0]) * range / dist;
				z = structPos[1] + (entPos[1] - structPos[1]) * range / dist;
				if (gameState.ai.HQ.territoryMap.getOwner([x, z]) != PlayerID ||
				    gameState.ai.accessibility.getAccessValue([x, z]) != access)
					continue;
			}
			let attackingUnits;
			for (const [targetId, unitIds] of this.bombingAttacks)
			{
				if (targetId != struct.id())
					continue;
				attackingUnits = unitIds;
				break;
			}
			if (attackingUnits && attackingUnits.size > 4)
				continue;	// already enough units against that target
			if (!attackingUnits)
			{
				attackingUnits = new Set();
				this.bombingAttacks.set(struct.id(), attackingUnits);
			}
			attackingUnits.add(ent.id());
			if (dist > range)
				ent.move(x, z);
			ent.attack(struct.id(), false, dist > range);
			break;
		}
	}
};




// AttackManager.prototype.rangedUnitsRetreat = function(gameState) {
//     // Optimize: limit search radius and cache positions
//     const maxSearchRadius = 65;
//     const rangedUnits = gameState.getOwnUnits().filter(filters.byClasses(["Ranged"]));
//     const meleeEnemies = gameState.getEnemyUnits().filter(filters.byClasses(["Melee"]));
    
//     // Pre-filter enemies within reasonable distance to reduce complexity
//     const nearbyEnemies = [];
//     for (let meleeEnemy of meleeEnemies.values()) {
//         if (!meleeEnemy.position())
//             continue;
//         nearbyEnemies.push(meleeEnemy);
//     }

//     for (let rangedUnit of rangedUnits.values()) {
//         if (!rangedUnit.position())
//             continue;

//         let nearestEnemy = null;
//         let nearestDist = Infinity;
//         const unitPos = rangedUnit.position();

//         // Optimize: spatial filtering - only check enemies within search radius
//         for (let meleeEnemy of nearbyEnemies) {
//             if (!meleeEnemy.position())
//                 continue;

//             let dist = VectorDistance(unitPos, meleeEnemy.position());
            
//             // Skip if enemy is too far to matter
//             if (dist > maxSearchRadius)
//                 continue;

//             // Determine retreat distance based on enemy type (20 for infantry, 40 for cavalry)
//             let retreatThreshold = meleeEnemy.hasClass("Cavalry") ? 40 : 20;

//             // Skip if the enemy is not within the retreat distance
//             if (dist > retreatThreshold)
//                 continue;

//             // Check if distance is decreasing between units
//             let previousDist = rangedUnit.getMetadata(PlayerID, "previousEnemyDist");
//             if (previousDist !== undefined && previousDist > 0) {
//                 // If distance is increasing, enemy is moving away
//                 if (dist > previousDist) {
//                     // Update the distance for next check
//                     rangedUnit.setMetadata(PlayerID, "previousEnemyDist", dist);
//                     continue;
//                 }
//             }
//             // Store current distance for next frame comparison
//             rangedUnit.setMetadata(PlayerID, "previousEnemyDist", dist);

//             // Track the nearest enemy
//             if (dist < nearestDist) {
//                 nearestDist = dist;
//                 nearestEnemy = meleeEnemy;
//             }
//         }

//         // If a nearby enemy is found, retreat
//         if (nearestEnemy) {
//             // Check if currently retreating and distance has increased beyond 15
//             let isRetreating = rangedUnit.getMetadata(PlayerID, "isRetreating");
//             if (isRetreating) {
//                 let currentDist = VectorDistance(unitPos, nearestEnemy.position());
//                 if (currentDist > 15) {
//                     // Stop retreating
//                     rangedUnit.setMetadata(PlayerID, "isRetreating", false);
//                     continue;
//                 }
//             }

//             // Calculate retreat direction
//             let retreatDirection = [
//                 unitPos[0] - nearestEnemy.position()[0],
//                 unitPos[1] - nearestEnemy.position()[1]
//             ];

//             // Normalize the direction
//             let length = Math.sqrt(retreatDirection[0] * retreatDirection[0] + retreatDirection[1] * retreatDirection[1]);
//             retreatDirection[0] /= length;
//             retreatDirection[1] /= length;

//             // Add randomization to the direction
//             let randomAngle = (Math.random() - 0.5) * 0.5; // Randomization within ±0.25 radians
//             let newX = retreatDirection[0] * Math.cos(randomAngle) - retreatDirection[1] * Math.sin(randomAngle);
//             let newZ = retreatDirection[0] * Math.sin(randomAngle) + retreatDirection[1] * Math.cos(randomAngle);

//             // Calculate new retreat position
//             let retreatDistance = 10 + Math.random() * 20; // Distance between 10 and 30
//             let retreatPos = [
//                 unitPos[0] + newX * retreatDistance,
//                 unitPos[1] + newZ * retreatDistance
//             ];
// 			// Stop the unit
// 			rangedUnit.stopMoving();
//             // Command the unit to move to the new position
//             rangedUnit.move(retreatPos[0], retreatPos[1]);
            
//             // Mark as retreating
//             rangedUnit.setMetadata(PlayerID, "isRetreating", true);

//             // Logging
//             if (this.Config.debug > 1) {
//                 aiWarn(`Ranged unit ${rangedUnit.id()} retreating from Melee unit ${nearestEnemy.id()} to [${retreatPos[0]}, ${retreatPos[1]}]`);
//             }
//         } else {
//             // Clear retreat metadata if no enemy nearby
//             rangedUnit.setMetadata(PlayerID, "isRetreating", false);
//             // Clear distance tracking metadata
//             rangedUnit.setMetadata(PlayerID, "previousEnemyDist", undefined);
//         }
//     }
// };





AttackManager.prototype.rangedUnitsRetreat = function(gameState) {
	const currentTurn = gameState.ai.playedTurn;
	const currentTime = gameState.ai.elapsedTime;
	
	// Log function call time
	// if (this.Config.debug > 1) {
	// 	const timeSinceLastCall = this.lastRangedRetreatTurn > 0 ? currentTurn - this.lastRangedRetreatTurn : 0;
	// 	aiWarn(`rangedUnitsRetreat called at turn ${currentTurn}, time ${currentTime.toFixed(1)}s, interval since last call: ${timeSinceLastCall} turns`);
	// }
	
	// Early exit: no ranged units
	const rangedUnits = gameState.getOwnUnits().filter(filters.byClasses(["Ranged"]));
	if (!rangedUnits.hasEntities()) {
		if (this.Config.debug > 2) {
			aiWarn("rangedUnitsRetreat: No ranged units found, early exit");
		}
		return;
	}
	
	// Early exit: no enemies
	const meleeEnemies = gameState.getEnemyUnits().filter(filters.byClasses(["Melee"]));
	if (!meleeEnemies.hasEntities()) {
		if (this.Config.debug > 2) {
			aiWarn("rangedUnitsRetreat: No melee enemies found, early exit");
		}
		return;
	}
	
	// Check for enemies in high danger zone (120)
	let enemiesNearby = false;
	let totalRangedUnits = 0;
	let rangedUnitsWithPosition = 0;
	
	for (let rangedUnit of rangedUnits.values()) {
		totalRangedUnits++;
		if (!rangedUnit.position())
			continue;
			
		rangedUnitsWithPosition++;
		
		// Optimization: check only nearest enemies
		for (let meleeEnemy of meleeEnemies.values()) {
			if (!meleeEnemy.position())
				continue;
				
			const dist = VectorDistance(rangedUnit.position(), meleeEnemy.position());
			if (dist < 120) { // High danger zone
				enemiesNearby = true;
				break;
			}
		}
		if (enemiesNearby)
			break;
	}
	
	// Determine if main logic should be executed
	let shouldExecute = false;
	let executionReason = "";
	
	if (enemiesNearby) {
		// Enemies nearby - execute every turn
		shouldExecute = true;
		executionReason = "enemies nearby (<120)";
		this.lastRangedRetreatTurn = currentTurn;
	} else {
		// Enemies far - check interval
		const turnsSinceLastExecution = currentTurn - this.lastRangedRetreatTurn;
		if (turnsSinceLastExecution >= this.rangedRetreatInterval) {
			shouldExecute = true;
			executionReason = `interval reached (${turnsSinceLastExecution} >= ${this.rangedRetreatInterval})`;
			this.lastRangedRetreatTurn = currentTurn;
		}
	}
	
	// Log decision making
	// if (this.Config.debug > 1) {
	// 	aiWarn(`rangedUnitsRetreat decision: ${shouldExecute ? "EXECUTE" : "SKIP"} - ${executionReason}`);
	// 	aiWarn(`Status: ${totalRangedUnits} ranged units, ${rangedUnitsWithPosition} with position, enemies nearby: ${enemiesNearby}`);
	// }
	
	if (!shouldExecute) {
		return;
	}
	
	// Main retreat logic
	let unitsRetreated = 0;
	let totalRetreatAttempts = 0; // Count all retreat attempts
	
    for (let rangedUnit of rangedUnits.values()) {
        if (!rangedUnit.position())
            continue;

        let nearestEnemy = null;
        let nearestDist = Infinity;

        // Find nearest Melee enemy within retreat distance
        for (let meleeEnemy of meleeEnemies.values()) {
            if (!meleeEnemy.position())
                continue;

            let dist = VectorDistance(rangedUnit.position(), meleeEnemy.position());

            // Determine retreat distance based on enemy type (25 for infantry, 40 for cavalry)
            let retreatThreshold = meleeEnemy.hasClass("Cavalry") ? 40 : 25;

            // Skip if enemy is not within retreat distance
            if (dist > retreatThreshold)
                continue;

            // Track the nearest enemy
            if (dist < nearestDist) {
                nearestDist = dist;
                nearestEnemy = meleeEnemy;
            }
        }

        // If a nearby enemy is found, retreat
        if (nearestEnemy) {
            unitsRetreated++;
			totalRetreatAttempts++; // Count all retreat attempts

            // Calculate retreat direction
            let retreatDirection = [
                rangedUnit.position()[0] - nearestEnemy.position()[0],
                rangedUnit.position()[1] - nearestEnemy.position()[1]
            ];

            // Normalize the direction
            let length = Math.sqrt(retreatDirection[0] * retreatDirection[0] + retreatDirection[1] * retreatDirection[1]);
            retreatDirection[0] /= length;
            retreatDirection[1] /= length;

            // Add randomization to the direction
            let randomAngle = (Math.random() - 0.5) * 0.5; // Randomization within ±0.25 radians
            let newX = retreatDirection[0] * Math.cos(randomAngle) - retreatDirection[1] * Math.sin(randomAngle);
            let newZ = retreatDirection[0] * Math.sin(randomAngle) + retreatDirection[1] * Math.cos(randomAngle);

            // Calculate new retreat position
            let retreatDistance = 10 + Math.random() * 20; // Distance between 10 and 30
            let retreatPos = [
                rangedUnit.position()[0] + newX * retreatDistance,
                rangedUnit.position()[1] + newZ * retreatDistance
            ];
			// Stop the unit
			rangedUnit.stopMoving();
            // Command the unit to move to the new position
            rangedUnit.move(retreatPos[0], retreatPos[1]);

            // Logging
            // if (this.Config.debug > 2) {
            //     aiWarn(`Ranged unit ${rangedUnit.id()} retreating from Melee unit ${nearestEnemy.id()} (dist: ${nearestDist.toFixed(1)}) to [${retreatPos[0].toFixed(1)}, ${retreatPos[1].toFixed(1)}]`);
            // }
        }
    }
    
    // Final logging
    // if (this.Config.debug > 1) {
    //     aiWarn(`rangedUnitsRetreat completed: ${unitsRetreated} units retreated out of ${rangedUnitsWithPosition} positioned units`);
    // }
};



AttackManager.prototype.supportUnitsRetreat = function(gameState) {
	//const rangedUnits = gameState.getOwnUnits().filter(filters.byClasses(["Ranged"]));
    const supportUnits = gameState.getOwnUnits().filter(filters.byClasses(["Support"]));
    const meleeEnemies = gameState.getEnemyUnits().filter(filters.byClasses(["Melee"]));
    const rangedEnemies = gameState.getEnemyUnits().filter(filters.byClasses(["Ranged"]));

    for (let supportUnit of supportUnits.values()) {
        if (!supportUnit.position())
            continue;

        let nearestEnemy = null;
        let nearestDist = Infinity;

        // Find the nearest enemy (Melee or Ranged) within the retreat distance
        for (let enemy of [...meleeEnemies.values(), ...rangedEnemies.values()]) {
            if (!enemy.position())
                continue;

            let dist = VectorDistance(supportUnit.position(), enemy.position());

            // Determine retreat distance based on enemy type
            let retreatThreshold = enemy.hasClass("Ranged") ? (enemy.attackRange("Ranged").max + 15) : 40;

            // Skip if the enemy is not within the retreat distance
            if (dist > retreatThreshold)
                continue;

            // Track the nearest enemy
            if (dist < nearestDist) {
                nearestDist = dist;
                nearestEnemy = enemy;
            }
        }

        // If a nearby enemy is found, retreat
        if (nearestEnemy) {
            // Check for nearby garrisonable structures within 50 distance
            let garrisonableStructures = gameState.getAllyStructures().filter(ent => 
                ent.isGarrisonHolder() && 
                supportUnit.hasClasses(ent.garrisonableClasses()) && 
                VectorDistance(supportUnit.position(), ent.position()) <= 70
            ).values();

            let nearestStructure = null;
            let minStructureDist = Infinity;

            for (let structure of garrisonableStructures) {
                let dist = VectorDistance(supportUnit.position(), structure.position());
                if (dist < minStructureDist) {
                    minStructureDist = dist;
                    nearestStructure = structure;
                }
            }

            // If a nearby structure is found, garrison it
			const garrisonManager = gameState.ai.HQ.garrisonManager;
            if (nearestStructure) {
                garrisonManager.garrison(gameState, supportUnit, nearestStructure, GarrisonManager.TYPE_SUPPORT);
                continue; // Skip the rest of the retreat logic
            }

            // Otherwise, retreat randomly as before
            let retreatDirection = [
                supportUnit.position()[0] - nearestEnemy.position()[0],
                supportUnit.position()[1] - nearestEnemy.position()[1]
            ];

            // Normalize the direction
            let length = Math.sqrt(retreatDirection[0] * retreatDirection[0] + retreatDirection[1] * retreatDirection[1]);
            retreatDirection[0] /= length;
            retreatDirection[1] /= length;

            // Add randomization to the direction
            let randomAngle = (Math.random() - 0.5) * 0.5;
            let newX = retreatDirection[0] * Math.cos(randomAngle) - retreatDirection[1] * Math.sin(randomAngle);
            let newZ = retreatDirection[0] * Math.sin(randomAngle) + retreatDirection[1] * Math.cos(randomAngle);

            // Calculate new retreat position
            let retreatDistance = 20 + Math.random() * 10;
            let retreatPos = [
                supportUnit.position()[0] + newX * retreatDistance,
                supportUnit.position()[1] + newZ * retreatDistance
            ];

            // Stop the unit and move to the new position
            supportUnit.stopMoving();
            supportUnit.move(retreatPos[0], retreatPos[1]);
        }
    }
};

/**
 * Some functions are run every turn
 * Others once in a while
 */
AttackManager.prototype.update = function(gameState, queues, events)
{
	if (this.Config.debug > 2 && gameState.ai.elapsedTime > this.debugTime + 60)
	{
		this.debugTime = gameState.ai.elapsedTime;
		aiWarn(" upcoming attacks =================");
		for (const attackType in this.upcomingAttacks)
		{
			for (const attack of this.upcomingAttacks[attackType])
			{
				aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state +
					" units " + attack.unitCollection.length);
			}
		}
		aiWarn(" started attacks ==================");
		for (const attackType in this.startedAttacks)
		{
			for (const attack of this.startedAttacks[attackType])
			{
				aiWarn(" plan " + attack.name + " type " + attackType + " state " + attack.state +
					" units " + attack.unitCollection.length);
			}
		}
		aiWarn(" ==================================");
	}
	
	this.rangedUnitsRetreat(gameState);
	this.supportUnitsRetreat(gameState);

	this.checkEvents(gameState, events);
	const unexecutedAttacks = {
		[AttackPlan.TYPE_RUSH]: 0,
		[AttackPlan.TYPE_RAID]: 0,
		[AttackPlan.TYPE_DEFAULT]: 0,
		[AttackPlan.TYPE_HUGE_ATTACK]: 0
	};
	for (const attackType in this.upcomingAttacks)
	{
		for (let i = 0; i < this.upcomingAttacks[attackType].length; ++i)
		{
			const attack = this.upcomingAttacks[attackType][i];
			attack.checkEvents(gameState, events);

			if (attack.isStarted())
			{
				aiWarn("R28 problem in attackManager: attack in preparation has already " +
					"started ???");
			}

			const updateStep = attack.updatePreparation(gameState);
			// now we're gonna check if the preparation time is over
			if (updateStep === AttackPlan.PREPARATION_KEEP_GOING || attack.isPaused())
			{
				// just chillin'
				if (attack.state === AttackPlan.STATE_UNEXECUTED)
					++unexecutedAttacks[attackType];
			}
			else if (updateStep === AttackPlan.PREPARATION_FAILED)
			{
				if (this.Config.debug > 1)
				{
					aiWarn("Attack Manager: " + attack.getType() + " plan " + attack.getName() +
						" aborted.");
				}
				attack.Abort(gameState);
				this.upcomingAttacks[attackType].splice(i--, 1);
			}
			else if (updateStep === AttackPlan.PREPARATION_START)
			{
				if (attack.StartAttack(gameState))
				{
					if (this.Config.debug > 1)
					{
						aiWarn("Attack Manager: Starting " + attack.getType() + " plan " +
							attack.getName());
					}
					if (this.Config.chat)
						chat.launchAttack(gameState, attack.targetPlayer, attack.getType());
					this.startedAttacks[attackType].push(attack);
				}
				else
					attack.Abort(gameState);
				this.upcomingAttacks[attackType].splice(i--, 1);
			}
		}
	}

	for (const attackType in this.startedAttacks)
	{
		for (let i = 0; i < this.startedAttacks[attackType].length; ++i)
		{
			const attack = this.startedAttacks[attackType][i];
			attack.checkEvents(gameState, events);
			// okay so then we'll update the attack.
			if (attack.isPaused())
				continue;
			const remaining = attack.update(gameState, events);
			if (!remaining)
			{
				if (this.Config.debug > 1)
				{
					aiWarn("Military Manager: " + attack.getType() + " plan " +
						attack.getName() + " is finished with remaining " + remaining);
				}
				attack.Abort(gameState);
				this.startedAttacks[attackType].splice(i--, 1);
			}
		}
	}

	// creating plans after updating because an aborted plan might be reused in that case.

	const barracksNb = gameState.getOwnEntitiesByClass("Barracks", true).filter(filters.isBuilt()).length;
	if (this.rushNumber < this.maxRushes && barracksNb >= 1)
	{
		if (unexecutedAttacks[AttackPlan.TYPE_RUSH] === 0)
		{
			// we have a barracks and we want to rush, rush.
			const data = { "targetSize": this.rushSize[this.rushNumber] };
			const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber,
				AttackPlan.TYPE_RUSH, data);
			if (!attackPlan.failed)
			{
				if (this.Config.debug > 1)
				{
					aiWarn("Military Manager: Rushing plan " + this.totalNumber +
						" with maxRushes " + this.maxRushes);
				}
				this.totalNumber++;
				attackPlan.init(gameState);
				this.upcomingAttacks[AttackPlan.TYPE_RUSH].push(attackPlan);
			}
			this.rushNumber++;
		}
	}
	else if (this.rushNumber >= 1 && unexecutedAttacks[AttackPlan.TYPE_DEFAULT] == 0 &&
		unexecutedAttacks[AttackPlan.TYPE_HUGE_ATTACK] == 0 &&
		this.startedAttacks[AttackPlan.TYPE_DEFAULT].length +
			this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length <
			5 &&
		(this.startedAttacks[AttackPlan.TYPE_DEFAULT].length +
			this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length == 0 ||
		gameState.getPopulationMax() - gameState.getPopulation() > 12))
	{
		if (barracksNb >= 1 && (gameState.currentPhase() > 1 || gameState.isResearching(gameState.getPhaseName(2))) ||
			!gameState.ai.HQ.hasPotentialBase())	// if we have no base ... nothing else to do than attack
		{
			const type = this.attackNumber < 5 ||
				this.startedAttacks[AttackPlan.TYPE_HUGE_ATTACK].length > 0 ?
				AttackPlan.TYPE_DEFAULT : AttackPlan.TYPE_HUGE_ATTACK;
			const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, type);
			if (attackPlan.failed)
				this.attackPlansEncounteredWater = true; // hack
			else
			{
				if (this.Config.debug > 1)
				{
					aiWarn("Military Manager: Creating the plan " + type + "  " +
						this.totalNumber);
				}
				this.totalNumber++;
				attackPlan.init(gameState);
				this.upcomingAttacks[type].push(attackPlan);
			}
			this.attackNumber++;
		}
	}

	if (unexecutedAttacks[AttackPlan.TYPE_RAID] === 0 &&
		gameState.ai.HQ.defenseManager.targetList.length)
	{
		let target;
		for (const targetId of gameState.ai.HQ.defenseManager.targetList)
		{
			target = gameState.getEntityById(targetId);
			if (!target)
				continue;
			if (gameState.isPlayerEnemy(target.owner()))
				break;
			target = undefined;
		}
		if (target) // prepare a raid against this target
			this.raidTargetEntity(gameState, target);
	}

	// Check if we have some unused ranged siege unit which could do something useful while waiting
	if (this.Config.difficulty > difficulty.VERY_EASY && gameState.ai.playedTurn % 5 == 0)
		this.assignBombers(gameState);
};

AttackManager.prototype.getPlan = function(planName)
{
	for (const attackType in this.upcomingAttacks)
	{
		for (const attack of this.upcomingAttacks[attackType])
			if (attack.getName() == planName)
				return attack;
	}
	for (const attackType in this.startedAttacks)
	{
		for (const attack of this.startedAttacks[attackType])
			if (attack.getName() == planName)
				return attack;
	}
	return undefined;
};

AttackManager.prototype.pausePlan = function(planName)
{
	const attack = this.getPlan(planName);
	if (attack)
		attack.setPaused(true);
};

AttackManager.prototype.unpausePlan = function(planName)
{
	const attack = this.getPlan(planName);
	if (attack)
		attack.setPaused(false);
};

AttackManager.prototype.pauseAllPlans = function()
{
	for (const attackType in this.upcomingAttacks)
		for (const attack of this.upcomingAttacks[attackType])
			attack.setPaused(true);

	for (const attackType in this.startedAttacks)
		for (const attack of this.startedAttacks[attackType])
			attack.setPaused(true);
};

AttackManager.prototype.unpauseAllPlans = function()
{
	for (const attackType in this.upcomingAttacks)
		for (const attack of this.upcomingAttacks[attackType])
			attack.setPaused(false);

	for (const attackType in this.startedAttacks)
		for (const attack of this.startedAttacks[attackType])
			attack.setPaused(false);
};

AttackManager.prototype.getAttackInPreparation = function(type)
{
	return this.upcomingAttacks[type].length ? this.upcomingAttacks[type][0] : undefined;
};

/**
 * Determine which player should be attacked: when called when starting the attack,
 * attack.targetPlayer is undefined and in that case, we keep track of the chosen target
 * for future attacks.
 */
AttackManager.prototype.getEnemyPlayer = function(gameState, attack)
{
	let enemyPlayer;

	// First check if there is a preferred enemy based on our victory conditions.
	// If both wonder and relic, choose randomly between them TODO should combine decisions

	if (gameState.getVictoryConditions().has("wonder"))
		enemyPlayer = this.getWonderEnemyPlayer(gameState, attack);

	if (gameState.getVictoryConditions().has("capture_the_relic"))
		if (!enemyPlayer || randBool())
			enemyPlayer = this.getRelicEnemyPlayer(gameState, attack) || enemyPlayer;

	if (enemyPlayer)
		return enemyPlayer;

	const veto = {};
	for (const i in this.defeated)
		veto[i] = true;
	// No rush if enemy too well defended (i.e. iberians)
	if (attack.type === AttackPlan.TYPE_RUSH)
	{
		for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
		{
			if (!gameState.isPlayerEnemy(i) || veto[i])
				continue;
			if (this.defeated[i])
				continue;
			let enemyDefense = 0;
			for (const ent of gameState.getEnemyStructures(i).values())
				if (ent.hasClasses(["Tower", "WallTower", "Fortress"]))
					enemyDefense++;
			if (enemyDefense > 160)
				veto[i] = true;
		}
	}

	// then if not a huge attack, continue attacking our previous target as long as it has some entities,
	// otherwise target the most accessible one
	if (attack.type !== AttackPlan.TYPE_HUGE_ATTACK)
	{
		if (attack.targetPlayer === undefined && this.currentEnemyPlayer !== undefined &&
			!this.defeated[this.currentEnemyPlayer] &&
			gameState.isPlayerEnemy(this.currentEnemyPlayer) &&
			gameState.getEntities(this.currentEnemyPlayer).hasEntities())
			return this.currentEnemyPlayer;

		let distmin;
		let ccmin;
		const ccEnts = gameState.updatingGlobalCollection("allCCs", filters.byClass("CivCentre"));
		for (const ourcc of ccEnts.values())
		{
			if (ourcc.owner() != PlayerID)
				continue;
			const ourPos = ourcc.position();
			const access = getLandAccess(gameState, ourcc);
			for (const enemycc of ccEnts.values())
			{
				if (veto[enemycc.owner()])
					continue;
				if (!gameState.isPlayerEnemy(enemycc.owner()))
					continue;
				if (access !== getLandAccess(gameState, enemycc))
					continue;
				const dist = SquareVectorDistance(ourPos, enemycc.position());
				if (distmin && dist > distmin)
					continue;
				ccmin = enemycc;
				distmin = dist;
			}
		}
		if (ccmin)
		{
			enemyPlayer = ccmin.owner();
			if (attack.targetPlayer === undefined)
				this.currentEnemyPlayer = enemyPlayer;
			return enemyPlayer;
		}
	}

	// then let's target our strongest enemy (basically counting enemies units)
	// with priority to enemies with civ center
	let max = 0;
	for (let i = 1; i < gameState.sharedScript.playersData.length; ++i)
	{
		if (veto[i])
			continue;
		if (!gameState.isPlayerEnemy(i))
			continue;
		let enemyCount = 0;
		let enemyCivCentre = false;
		for (const ent of gameState.getEntities(i).values())
		{
			enemyCount++;
			if (ent.hasClass("CivCentre"))
				enemyCivCentre = true;
		}
		if (enemyCivCentre)
			enemyCount += 500;
		if (!enemyCount || enemyCount < max)
			continue;
		max = enemyCount;
		enemyPlayer = i;
	}
	if (attack.targetPlayer === undefined)
		this.currentEnemyPlayer = enemyPlayer;
	return enemyPlayer;
};

/**
 * Target the player with the most advanced wonder.
 * TODO currently the first built wonder is kept, should chek on the minimum wonderDuration left instead.
 */
AttackManager.prototype.getWonderEnemyPlayer = function(gameState, attack)
{
	let enemyPlayer;
	let enemyWonder;
	let moreAdvanced;
	for (const wonder of gameState.getEnemyStructures().filter(filters.byClass("Wonder")).values())
	{
		if (wonder.owner() == 0)
			continue;
		const progress = wonder.foundationProgress();
		if (progress === undefined)
		{
			enemyWonder = wonder;
			break;
		}
		if (enemyWonder && moreAdvanced > progress)
			continue;
		enemyWonder = wonder;
		moreAdvanced = progress;
	}
	if (enemyWonder)
	{
		enemyPlayer = enemyWonder.owner();
		if (attack.targetPlayer === undefined)
			this.currentEnemyPlayer = enemyPlayer;
	}
	return enemyPlayer;
};

/**
 * Target the player with the most relics (including gaia).
 */
AttackManager.prototype.getRelicEnemyPlayer = function(gameState, attack)
{
	let enemyPlayer;
	const allRelics = gameState.updatingGlobalCollection("allRelics", filters.byClass("Relic"));
	let maxRelicsOwned = 0;
	for (let i = 0; i < gameState.sharedScript.playersData.length; ++i)
	{
		if (!gameState.isPlayerEnemy(i) || this.defeated[i] ||
		    i == 0 && !gameState.ai.HQ.victoryManager.tryCaptureGaiaRelic)
			continue;

		const relicsCount = allRelics.filter(relic => relic.owner() == i).length;
		if (relicsCount <= maxRelicsOwned)
			continue;
		maxRelicsOwned = relicsCount;
		enemyPlayer = i;
	}
	if (enemyPlayer !== undefined)
	{
		if (attack.targetPlayer === undefined)
			this.currentEnemyPlayer = enemyPlayer;
		if (enemyPlayer == 0)
			gameState.ai.HQ.victoryManager.resetCaptureGaiaRelic(gameState);
	}
	return enemyPlayer;
};

/** f.e. if we have changed diplomacy with another player. */
AttackManager.prototype.cancelAttacksAgainstPlayer = function(gameState, player)
{
	for (const attackType in this.upcomingAttacks)
		for (const attack of this.upcomingAttacks[attackType])
			if (attack.targetPlayer === player)
				attack.targetPlayer = undefined;

	for (const attackType in this.startedAttacks)
		for (let i = 0; i < this.startedAttacks[attackType].length; ++i)
		{
			const attack = this.startedAttacks[attackType][i];
			if (attack.targetPlayer === player)
			{
				attack.Abort(gameState);
				this.startedAttacks[attackType].splice(i--, 1);
			}
		}
};

AttackManager.prototype.raidTargetEntity = function(gameState, ent)
{
	const data = { "target": ent };
	const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber,
		AttackPlan.TYPE_RAID, data);
	if (attackPlan.failed)
		return null;
	if (this.Config.debug > 1)
		aiWarn("Military Manager: Raiding plan " + this.totalNumber);
	this.raidNumber++;
	this.totalNumber++;
	attackPlan.init(gameState);
	this.upcomingAttacks[AttackPlan.TYPE_RAID].push(attackPlan);
	return attackPlan;
};

/**
 * Return the number of units from any of our attacking armies around this position
 */
AttackManager.prototype.numAttackingUnitsAround = function(pos, dist)
{
	let num = 0;
	for (const attackType in this.startedAttacks)
		for (const attack of this.startedAttacks[attackType])
		{
			if (!attack.position)	// this attack may be inside a transport
				continue;
			if (SquareVectorDistance(pos, attack.position) < dist*dist)
				num += attack.unitCollection.length;
		}
	return num;
};

/**
 * Switch defense armies into an attack one against the given target
 * data.range: transform all defense armies inside range of the target into a new attack
 * data.armyID: transform only the defense army ID into a new attack
 * data.uniqueTarget: the attack will stop when the target is destroyed or captured
 */
AttackManager.prototype.switchDefenseToAttack = function(gameState, target, data)
{
	if (!target || !target.position())
		return false;
	if (!data.range && !data.armyID)
	{
		aiWarn(" attackManager.switchDefenseToAttack inconsistent data " + uneval(data));
		return false;
	}
	const attackData = data.uniqueTarget ? { "uniqueTargetId": target.id() } : undefined;
	const pos = target.position();
	const attackType = AttackPlan.TYPE_DEFAULT;
	const attackPlan = new AttackPlan(gameState, this.Config, this.totalNumber, attackType, attackData);
	if (attackPlan.failed)
		return false;
	this.totalNumber++;
	attackPlan.init(gameState);
	this.startedAttacks[attackType].push(attackPlan);

	const targetAccess = getLandAccess(gameState, target);
	for (const army of gameState.ai.HQ.defenseManager.armies)
	{
		if (data.range)
		{
			army.recalculatePosition(gameState);
			if (SquareVectorDistance(pos, army.foePosition) > data.range * data.range)
				continue;
		}
		else if (army.ID != +data.armyID)
			continue;

		while (army.foeEntities.length > 0)
			army.removeFoe(gameState, army.foeEntities[0]);
		while (army.ownEntities.length > 0)
		{
			const unitId = army.ownEntities[0];
			army.removeOwn(gameState, unitId);
			const unit = gameState.getEntityById(unitId);
			const accessOk = unit.getMetadata(PlayerID, "transport") !== undefined ||
				unit.position() && getLandAccess(gameState, unit) == targetAccess;
			if (unit && accessOk && attackPlan.isAvailableUnit(gameState, unit))
			{
				unit.setMetadata(PlayerID, "plan", attackPlan.name);
				unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK);
				attackPlan.unitCollection.updateEnt(unit);
			}
		}
	}
	if (!attackPlan.unitCollection.hasEntities())
	{
		attackPlan.Abort(gameState);
		return false;
	}
	for (const unit of attackPlan.unitCollection.values())
		unit.setMetadata(PlayerID, "role", Worker.ROLE_ATTACK);
	attackPlan.targetPlayer = target.owner();
	attackPlan.targetPos = pos;
	attackPlan.target = target;
	attackPlan.state = AttackPlan.STATE_ARRIVED;
	return true;
};

AttackManager.prototype.Serialize = function()
{
	const properties = {
		"totalNumber": this.totalNumber,
		"attackNumber": this.attackNumber,
		"rushNumber": this.rushNumber,
		"raidNumber": this.raidNumber,
		"debugTime": this.debugTime,
		"maxRushes": this.maxRushes,
		"rushSize": this.rushSize,
		"currentEnemyPlayer": this.currentEnemyPlayer,
		"defeated": this.defeated
	};

	const upcomingAttacks = {};
	for (const key in this.upcomingAttacks)
	{
		upcomingAttacks[key] = [];
		for (const attack of this.upcomingAttacks[key])
			upcomingAttacks[key].push(attack.Serialize());
	}

	const startedAttacks = {};
	for (const key in this.startedAttacks)
	{
		startedAttacks[key] = [];
		for (const attack of this.startedAttacks[key])
			startedAttacks[key].push(attack.Serialize());
	}

	return { "properties": properties, "upcomingAttacks": upcomingAttacks, "startedAttacks": startedAttacks };
};

AttackManager.prototype.Deserialize = function(gameState, data)
{
	for (const key in data.properties)
		this[key] = data.properties[key];

	this.upcomingAttacks = {};
	for (const key in data.upcomingAttacks)
	{
		this.upcomingAttacks[key] = [];
		for (const dataAttack of data.upcomingAttacks[key])
		{
			const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name);
			attack.Deserialize(gameState, dataAttack);
			attack.init(gameState);
			this.upcomingAttacks[key].push(attack);
		}
	}

	this.startedAttacks = {};
	for (const key in data.startedAttacks)
	{
		this.startedAttacks[key] = [];
		for (const dataAttack of data.startedAttacks[key])
		{
			const attack = new AttackPlan(gameState, this.Config, dataAttack.properties.name);
			attack.Deserialize(gameState, dataAttack);
			attack.init(gameState);
			this.startedAttacks[key].push(attack);
		}
	}
};
