/*
* Tyr is an AI for StarCraft: Broodwar, 
* 
* Please visit https://github.com/SimonPrins/Tyr for further information.
* 
* Copyright 2015 Simon Prins
*
* This file is part of Tyr.
* Tyr is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 of the License, or
* (at your option) any later version.
* Tyr is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
* GNU General Public License for more details.
* You should have received a copy of the GNU General Public License
* along with Tyr.  If not, see http://www.gnu.org/licenses/.
*/


package com.tyr.tasks;

import java.util.ArrayList;

import com.tyr.BWTAProxy;
import com.tyr.DebugMessages;
import com.tyr.EnemyManager;
import com.tyr.EnemyPosition;
import com.tyr.Force;
import com.tyr.ForceTracker;
import com.tyr.Tyr;
import com.tyr.agents.Agent;
import com.tyr.agents.Attack;
import com.tyr.agents.FocusTarget;
import com.tyr.agents.None;
import com.tyr.buildingplacement.SpaceManager;
import com.tyr.unitgroups.AttackGroup;
import com.tyr.unitgroups.UnitGroup;

import bwapi.Color;
import bwapi.Game;
import bwapi.Player;
import bwapi.Position;
import bwapi.TechType;
import bwapi.Unit;
import bwapi.UnitType;
import bwapi.WeaponType;
import bwta.Region;

/**
 * This class implements a solution for attacking the opponent.
 * During the attack it will periodically regroup to make sure attacking units stay together.
 */
public class AttackRegroupSolution extends SolutionItem implements Force
{
	/**
	 *  The group of attacking units.
	 */
	private AttackGroup attackGroup;
	
	/**
	 *  The target of the attack.
	 */
	private Position attackTarget;
	
	/**
	 * The position at which we want to regroup.
	 */
	private Position regroupTarget;
	
	/**
	 * The position of the enemy structure we want to focus-fire.
	 */
	private EnemyPosition focusTarget;
	
	/**
	 * Time at which regrouping started.
	 */
	private int regroupTime;
	
	/**
	 * The attacking state.
	 */
	private final int ATTACK = 1;

	/**
	 * The regrouping state.
	 */
	private final int REGROUP = 2;

	/**
	 * The focus fire state.
	 */
	private final int FOCUS = 3;
	
	/**
	 * The state the object is currently in.
	 */
	private int phase = ATTACK;
	
	/**
	 * The average position of the attacking units.
	 */
	private Position averagePos;
	
	/**
	 * Do we focus workers etc. when no enemy combat units are around?
	 */
	public static boolean focusWorkers = true;
	
	
	/**
	 * This class implements a solution for attacking the opponent.
	 * During the attack it will periodically regroup to make sure attacking units stay together.
	 * @param task The task that started this solution.
	 * @param attackTarget The position which we should attack.
	 */
	public AttackRegroupSolution(Task task, Position attackTarget) 
	{
		super(task);
		
		ForceTracker.register(this);
		
		this.attackTarget = attackTarget;
		
		this.attackGroup = new AttackGroup(attackTarget);
	}
	
	/**
	 * Sets the target which the units will attack.
	 * @param attackTarget
	 */
	public void setTarget(Position attackTarget)
	{
		this.attackTarget = attackTarget;
	}

	@Override
	public void onFrame(Game game, Player self, Tyr bot)
	{
		// Determine whether we are in the attacking or regrouping phase.
		determinePhase();
		
		// Draw some debug info.
		Tyr.drawCircle(phase == ATTACK? attackTarget:regroupTarget, phase == ATTACK?Color.Orange:Color.Green, 64);

		// Manage the attackGroup.
		attackGroup.cleanup();
		
		if (phase != FOCUS)
		{
			int count = 0;
			for (Unit unit : EnemyManager.getEnemyUnits())
			{
				if (unit.getType().isBuilding())
					continue;
				if (unit.getType().groundWeapon() == WeaponType.None)
					continue;
				if (unit.isCloaked())
					continue;
				if (unit.getType().isWorker())
					continue;
				if (unit.getDistance(averagePos) >= 600)
					continue;
				count++;
				if(count >= 10)
					break;
			}
			
			if (count >= 10)
			{
				for(Agent agent : attackGroup.units)
				{
					if (agent.unit.getType() == UnitType.Terran_Marine
							&& agent.unit.getHitPoints() > agent.unit.getType().maxHitPoints() - 10
							&& !agent.unit.isStimmed()
							&& self.hasResearched(TechType.Stim_Packs))
						agent.unit.useTech(TechType.Stim_Packs);
				}
			}
			
			int prio = 100;
			int distance = Integer.MAX_VALUE;
			Unit killTarget = null;
			
			for (Unit unit : EnemyManager.getEnemyUnits())
			{
				if (unit.getType() == UnitType.Zerg_Larva || unit.isMorphing() || !unit.isVisible())
					continue;
				
				int newPrio = 100;
				
				if (unit.getType().groundWeapon() == WeaponType.None && unit.getType() != UnitType.Terran_Bunker)
					newPrio = 10;
				else if (unit.getType().isWorker())
					newPrio = 9;
				else
					newPrio = 8;
				
				if (newPrio > prio)
					continue;
				
				int newDistance = unit.getDistance(this.getCenter());
				if (newDistance >= 800 || (newPrio > 8 && newDistance >= 500))
					continue;
				
				if (newPrio < prio || newDistance < distance)
				{
					distance = newDistance;
					prio = newPrio;
					killTarget = unit;
				}
			}

			if (prio == 8 || !focusWorkers)
				attackGroup.setKillTarget(null);
			else
				attackGroup.setKillTarget(killTarget);
			attackGroup.setTarget(phase == ATTACK?attackTarget:regroupTarget);
			attackGroup.onFrame(game, self, bot);
		}

		// If we have no more attacking units, the medics can return home.
		boolean attackerRemaining = false;
		for(Agent agent : attackGroup.units){
			if (agent.unit.getType() == UnitType.Terran_Medic)
				continue;
			attackerRemaining = true;
			break;
		}
		
		if (!attackerRemaining)
			attackGroup.clear();
	}
	
	/**
	 * This method is called at the start of each frame.
	 * It determines when the phase changes.
	 */
	private void determinePhase()
	{
		if(phase == FOCUS)
		{
			if (Tyr.game.isVisible(Tyr.positionToTile(focusTarget.pos)))
			{
				boolean found = false;
				for(Unit unit : EnemyManager.getEnemyUnits())
				{
					if (unit.getType() != focusTarget.type)
						continue;
					if(Math.abs(unit.getX() - focusTarget.pos.getX()) > 10)
						continue;
					if(Math.abs(unit.getY() - focusTarget.pos.getY()) > 10)
						continue;
					
					found = true;
					break;
				}
				
				if (!found)
				{
					for (Agent agent : attackGroup.units)
					{
						agent.order(new None(agent));
						agent.unit.stop();
					}
					phase = ATTACK;
				}
			}

			if(phase == FOCUS)
			{
				DebugMessages.addMessage("Focus firing " 
						+ this.size());
				
				return;
			}
			
		}
		
		getAveragePos();
		
		Tyr.drawCircle(averagePos, Color.Orange, 8);
		
		// We now compute a score for how much our army is spread out.
		double armySpread = 0;

		for(Agent agent : attackGroup.units)
			armySpread += agent.distanceSquared(averagePos);
		
		armySpread /= (double)attackGroup.units.size() + (double)attackGroup.units.size() / 60.0;
		
		DebugMessages.addMessage((phase == ATTACK? "Attacking" : "Regrouping") + " " 
				+ this.size() + " " 
				+ ((int)armySpread+500)/1000*1000);
		
		for(EnemyPosition enemyPos : EnemyManager.getManager().enemyDefensiveStructures)
		{
			boolean inRange = false;
			boolean allInRange = true;
			
			for(Agent agent : attackGroup.units)
			{
				if (agent.distanceSquared(enemyPos.pos) <= 400 * 400)
					inRange = true;
				else
					allInRange = false;
				if (inRange && !allInRange)
					break;
			}
			
			if (!inRange)
				continue;
			
			if(allInRange)
			{
				focusTarget = enemyPos;
				
				phase = FOCUS;
				
				for(Agent agent : attackGroup.units)
				{
					if (agent.unit.getType() == UnitType.Terran_Siege_Tank_Siege_Mode 
							|| agent.unit.getType() == UnitType.Terran_Siege_Tank_Tank_Mode)
						agent.order(new Attack(agent, enemyPos.pos));
					else
						agent.order(new FocusTarget(agent, enemyPos));
				}
				
				break;
			}
			
			if (armySpread >= 40000)
			{
				if (phase != REGROUP)
				{
					phase = REGROUP;
					regroupTime = Tyr.game.getFrameCount();
				}
				
				double distanceSq = 2000000000;
				
				Position baseCenter = BWTAProxy.getRegion(Tyr.self.getStartLocation()).getCenter();
				Position mainExit = SpaceManager.getMainExit();

				// The regroupTarget is where we will regroup.
				// We regroup at the unit closest to the attack target.
				for (Agent agent : attackGroup.units)
				{
					// After an attack is defeated the medics are sent home.
					// They can be added back into an attack regroup solution.
					// If we then set a regroup point, we do not want to regroup at the medics' position.
					if (agent.unit.getType() == UnitType.Terran_Medic)
						continue;

					Region r = BWTAProxy.getRegion(agent.unit.getPosition());
					Position containingRegionCenter = r!=null?r.getCenter():agent.unit.getPosition();
					double myDistSq;
					if (containingRegionCenter.getX() == baseCenter.getX() && containingRegionCenter.getY() == baseCenter.getY())
						myDistSq = agent.distanceSquared(mainExit) + Tyr.game.mapHeight()*32*Tyr.game.mapHeight()*32 + Tyr.game.mapWidth()*32*Tyr.game.mapWidth()*32;
					else
						myDistSq = agent.distanceSquared(enemyPos.pos);
					if (myDistSq < 400 * 400)
						continue;
					if (myDistSq < distanceSq)
					{
						distanceSq = myDistSq;
						regroupTarget = agent.unit.getPosition();
					}
				}

			}
		}
		
		
		if (phase == ATTACK)
		{
			if (armySpread >= 75000)
			{
				// If the army is too spread out, we will start our attack again.
				phase = REGROUP;
				regroupTime = Tyr.game.getFrameCount();
				
				double distanceSq = 2000000000;
				
				Position baseCenter = BWTAProxy.getRegion(Tyr.self.getStartLocation()).getCenter();
				Position mainExit = SpaceManager.getMainExit();

				// The regroupTarget is where we will regroup.
				// We regroup at the unit closest to the attack target.
				for (Agent agent : attackGroup.units)
				{
					// After an attack is defeated the medics are sent home.
					// They can be added back into an attack regroup solution.
					// If we then set a regroup point, we do not want to regroup at the medics' position.
					if (agent.unit.getType() == UnitType.Terran_Medic)
						continue;
					
					Region r = BWTAProxy.getRegion(agent.unit.getPosition());
					Position containingRegionCenter = r!=null?r.getCenter():agent.unit.getPosition();
					double myDistSq;
					if (containingRegionCenter.getX() == baseCenter.getX() && containingRegionCenter.getY() == baseCenter.getY())
						myDistSq = agent.distanceSquared(mainExit) + Tyr.game.mapHeight()*32*Tyr.game.mapHeight()*32 + Tyr.game.mapWidth()*32*Tyr.game.mapWidth()*32;
					else
						myDistSq = agent.distanceSquared(attackTarget);
					if (myDistSq < 400 * 400)
						continue;
					if (myDistSq < distanceSq)
					{
						distanceSq = myDistSq;
						regroupTarget = agent.unit.getPosition();
					}
				}
			}
		}
		else if (phase == REGROUP)
		{
			int elapsedTime = Tyr.game.getFrameCount() - regroupTime;
			int min = 20000;
			if (elapsedTime > 200)
				min += (elapsedTime - 200) * 55; 
			// If the army is close enough together again we continue the attack.
			if (armySpread <= min)
				phase = ATTACK;
			else if (elapsedTime >= 1200)
			{
				phase = ATTACK;
				for(int i = attackGroup.units.size() - 1; i >= 0; i--)
				{
					// Remove distant units to allow the attack to continue;
					if (attackGroup.units.get(i).distanceSquared(averagePos) >= 512*512)
					{
						Agent agent = attackGroup.units.get(i);
						
						agent.order(new None(agent));
						agent.unit.stop();
						
						Tyr.bot.hobos.add(agent);
						attackGroup.units.remove(i);
					}
				}
			}
			
			
		}
	}
	
	/**
	 * Add an agent to the attack group with the attacking agents.
	 * @param unit The agent to be added.
	 */
	public void add(Agent agent)
	{
		attackGroup.add(agent);
	}
	
	/**
	 * This method is called when this solution is no longer needed.
	 */
	public void done(UnitGroup unitGroup) 
	{
		for(Agent unit : attackGroup.units)
		{
			unitGroup.add(unit);
			unit.order(new None(unit));
		}
		attackGroup.units = new ArrayList<Agent>();
	}

	/**
	 * Adds the attacking units to the scanner to start finding the remaining enemy buildings.
	 */
	public void addToScanner() 
	{
		for(Agent agent : attackGroup.units)
			Tyr.bot.scanner.add(agent);
		attackGroup.units = new ArrayList<Agent>();
	}
	
	/**
	 * Clears the solution so that it can be removed.
	 */
	public void clear() 
	{
		attackGroup.clear();
	}

	/**
	 * Returns the number if agents sent out by this solution.
	 * @return Returns the number if agents sent out by this solution.
	 */
	public int size() 
	{
		return attackGroup.units.size();
	}
	
	/**
	 * Computes the average position of the attacking units and stores it in averagePos.
	 */
	private void getAveragePos()
	{
		// We compute the average position of all attacking units.
		double averageX = 0, averageY = 0;
		
		for(Agent agent : attackGroup.units)
		{
			Position pos = agent.unit.getPosition();
			averageX += pos.getX();
			averageY += pos.getY();
		}
		
		averageX /= attackGroup.units.size();
		averageY /= attackGroup.units.size();
		
		averagePos = new Position((int)averageX, (int) averageY);
	}

	@Override
	public Position getCenter() 
	{
		if (averagePos == null)
			getAveragePos();
		return averagePos;
	}

	@Override
	public boolean done() 
	{
		return size() == 0;
	}

	@Override
	public boolean disabled()
	{
		return false;
	}
}
