/*
* 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.agents;

import java.util.List;

import com.tyr.EnemyManager;
import com.tyr.Settings;
import com.tyr.Tyr;

import bwapi.Color;
import bwapi.Game;
import bwapi.Order;
import bwapi.Position;
import bwapi.TechType;
import bwapi.Unit;
import bwapi.UnitType;
import bwapi.WeaponType;


/**
 * An agent which manages a unit.
 * @author Simon
 *
 */
public class Agent
{
	/**
	 * The unit this agent manages.
	 */
	public Unit unit;
	
	/**
	 * The command this agent has been issued.
	 */
	protected Command command;
	
	/**
	 * The frame at which the last order was issued to the unit through this agent.
	 */
	protected int lastOrderFrame;
	
	public boolean mark = false;
	
	/**
	 * An enemy that was the closest enemy on a previous frame.
	 */
	private Unit closeEnemy = null;
	
	/**
	 * An agent which manages a unit.
	 * @param unit The unit this agent manages.
	 */
	public Agent(Unit unit)
	{
		this.unit = unit;
		command = new None(this);
	}
	
	/**
	 * Is the unit managed by this agent already dead?
	 * @return Is the unit managed by this agent already dead?
	 */
	public boolean isDead()
	{
		return unit.getHitPoints() <= 0 || unit.getRemoveTimer() != 0 || !unit.exists() || unit.getPlayer() != Tyr.game.self();
	}
	
	/**
	 * Draw a circle around this agent.
	 * @param color The color for the circle.
	 */
	public void drawCircle(Color color)
	{
		Tyr.drawCircle(unit.getPosition(), color);
	}

	/**
	 * Draw a circle around this agent.
	 * @param color The color of the circle.
	 * @param r The radius of the circle.
	 */
	public void drawCircle(Color color, int r)
	{
		Tyr.drawCircle(unit.getPosition(), color, r);
		
	}
	
	/**
	 * Compute the squared distance from this agent to another agent.
	 * @param agent The agent to which we want to calculate the distance.
	 * @return The squared distance from this agent to the other agent.
	 */
	public int distanceSquared(Agent agent)
	{
		return distanceSquared(agent.unit);
	}
	
	/**
	 * Compute the squared distance from this agent to another unit.
	 * @param unit The unit to which we want to calculate the distance.
	 * @return The squared distance from this agent to the other unit.
	 */
	public int distanceSquared(Unit unit)
	{
		return distanceSquared(unit.getPosition());
	}

	/**
	 * Compute the squared distance from this agent to a position.
	 * @param unit The position to which we want to calculate the distance.
	 * @return The squared distance from this agent to the other unit.
	 */
	public int distanceSquared(Position pos)
	{
		int dx = unit.getPosition().getX() - pos.getX();
		int dy = unit.getPosition().getY() - pos.getY();
		return dx*dx + dy*dy;
	}

	/**
	 * Create an agent for a specific unit.
	 * This will also create the appropriate subtype of the agent class. 
	 * @param unit The unit for which we want to create the agent.
	 * @return An agent wrapping the unit. Can be a subclass of Agent. 
	 */
	public static Agent createAgent(Unit unit) 
	{
		if (unit.getType() == UnitType.Terran_Wraith)
			return new WraithAgent(unit);
		if(unit.getType() == UnitType.Terran_Vulture)
			return new VultureAgent(unit);
		if (unit.getType() == UnitType.Terran_Bunker)
			return new BunkerAgent(unit);
		if(unit.getType().isWorker())
			return new WorkerAgent(unit);
		
		return new Agent(unit);
	}
	
	/**
	 * Determine a retreating position, at the other side of the controlled unit from a given position.
	 * @param other The position from which we want to retreat.
	 * @param dist The distance for which we want to retreat.
	 * @return The position at the other side of the controlled unit from the given position.
	 */
	public Position retreatTarget(Position other, double dist)
	{
		double fleeDX = unit.getPosition().getX() - other.getX();
		double fleeDY = unit.getPosition().getY() - other.getY();
		
		double length = Math.sqrt(fleeDX*fleeDX + fleeDY*fleeDY);
		
		if (length == 0)
		{
			fleeDX = 1;
			fleeDY = 0;
		}
		else
		{
			fleeDX /= length;
			fleeDY /= length;
		}
		
		return new Position(
				(int)(unit.getPosition().getX() + fleeDX * dist),
				(int)(unit.getPosition().getY() + fleeDY * dist));
	}
	
	/**
	 * Issues a command to this agent.
	 * @param command The command that will be issued to the agent.
	 */
	public void order(Command command)
	{
		if (command.replace(this.command))
			this.command = command;
	}
	
	/**
	 * The command this agent has been issued.
	 * @return The command this agent has been issued.
	 */
	public Command getCommand()
	{
		return command;
	}
	
	/**
	 * Issues an attack order to the unit.
	 * @param target The target position at which we will attack.
	 */
	public void attack(Position target)
	{
		int currentFrame = Tyr.game.getFrameCount();
		
		// Don't issue the attack if a previous attack order has been issued within the last 32 frames (256 for reavers).
		if (currentFrame - lastOrderFrame > (unit.getType() == UnitType.Protoss_Reaver ? 256 : 32))
		{
			unit.attack(target);
			lastOrderFrame = currentFrame;
		}
	}
	
	/**
	 * Issues a move order to the unit.
	 * @param target The target position to which we will move.
	 */
	public void move(Position target)
	{
		int currentFrame = Tyr.game.getFrameCount();
		
		// Don't issue the attack if a previous attack order has been issued within the last 32 frames (256 for reavers).
		if (currentFrame - lastOrderFrame > 32)
		{
			unit.move(target);
			lastOrderFrame = currentFrame;
		}
	}
	
	/**
	 * Issues an attack order to the unit.
	 * @param target The target unit which we will attack.
	 */
	public void attack(Unit target)
	{
		int currentFrame = Tyr.game.getFrameCount();
		
		// Don't issue the attack if a previous attack order has been issued within the last 32 frames.
		if (currentFrame - lastOrderFrame > 32)
		{
			unit.attack(target);
			lastOrderFrame = currentFrame;
			drawCircle(Color.Red, 12);
		}
		else 
			drawCircle(Color.Green, 12);
	}

	/**
	 * Determines if a unit is considered ranged.
	 * @param unit The unit of which we want to know if it is ranged.
	 * @return True if the unit is ranged, false otherwise.
	 */
	public static boolean isRanged(Unit unit)
	{
		if (unit.getType() == UnitType.Terran_Bunker || unit.getType() == UnitType.Protoss_Reaver)
			return true;
		return !unit.getType().isWorker() && unit.getType().groundWeapon().maxRange() >= 32;
	}
	
	/**
	 * The previous frame in which the getYamatoTarget method was called.
	 * This is solely used to draw debug information to the screen.
	 */
	private static int previousFrame = 0;
	
	/**
	 * Find an appropriate target for a yamato cannon.
	 * @return A Unit which we can attack using the yamato cannon.
	 */
	public Unit getYamatoTarget()
	{	
		Game game = Tyr.game;
		if (!game.self().hasResearched(TechType.Yamato_Gun))
		{
			Tyr.drawCircle(unit.getPosition(), Color.Yellow, 6);
			return null;
		}
		
		if (unit.getEnergy() < TechType.Yamato_Gun.energyCost())
		{
			Tyr.drawCircle(unit.getPosition(), Color.Blue, 6);
			return null;
		}


		// We only draw debug information for one unit each frame.
		boolean first = false;
		if (previousFrame != game.getFrameCount())
		{
			previousFrame = game.getFrameCount();
			first = true;
			Tyr.drawCircle(unit.getPosition(), Color.Red, WeaponType.Yamato_Gun.maxRange());
		}

		Unit target = null;
		int preference = 0;
		
		// Find the unit in range with the highest preference.
		List<Unit> inRange = game.getUnitsInRadius(unit.getPosition(), WeaponType.Yamato_Gun.maxRange() + 32);
		for(Unit unit : inRange)
		{
			if (!unit.getPlayer().isEnemy(game.self()))
				continue;
			
			// See if the unit is of an appropriate type for us to target.
			// We prefer targeting units with high preference.
			UnitType enemyType = unit.getType();
			int newPreference = 0;
			if (enemyType == UnitType.Protoss_Carrier || enemyType == UnitType.Terran_Battlecruiser)
				newPreference = 5;
			else if (enemyType == UnitType.Protoss_Photon_Cannon)
				newPreference = 4;
			else if (enemyType == UnitType.Protoss_High_Templar || enemyType == UnitType.Protoss_Dark_Archon
					 || enemyType == UnitType.Terran_Goliath)
				newPreference = 3;
			else if (enemyType == UnitType.Protoss_Archon
					 || enemyType == UnitType.Terran_Missile_Turret)
				newPreference = 2;
			else if (enemyType == UnitType.Protoss_Dragoon || enemyType == UnitType.Terran_Bunker)
				newPreference = 1;
			
			if (first)
			{
				if(preference == 0)
					Tyr.drawCircle(unit.getPosition(), Color.Red, 6);
				else
					Tyr.drawCircle(unit.getPosition(), Color.Green, 6);
			}
			
			if (newPreference > preference)
			{
				preference = newPreference;
				target = unit;
			}
		}
		
		if (target == null)
		{
			Tyr.drawCircle(unit.getPosition(), Color.Red, 6);
			return null;
		}

		Tyr.drawCircle(unit.getPosition(), Color.Green, 6);
		Tyr.drawCircle(target.getPosition(), Color.Yellow);
		game.drawLineMap(unit.getPosition().getX(), unit.getPosition().getY(), target.getPosition().getX(), target.getPosition().getY(), Color.Yellow);
		return target;
	}

	/**
	 * Is this unit a siege tank?
	 * @return Is this unit a siege tank?
	 */
	public boolean isTank()
	{
		return unit.getType() == UnitType.Terran_Siege_Tank_Siege_Mode || unit.getType() == UnitType.Terran_Siege_Tank_Tank_Mode;
	}

	public static void clean(List<Agent> agents)
	{
		for (int i = agents.size() - 1; i >= 0; i--)
			if (agents.get(i) == null || agents.get(i).isDead())
				agents.remove(i);
	}

	public static boolean isDead(Unit unit)
	{
		return unit.getHitPoints() <= 0 || unit.getRemoveTimer() != 0 || !unit.exists() || unit.getPlayer() != Tyr.game.self();
	}

	public static boolean isTank(Unit unit)
	{
		return unit.getType() == UnitType.Terran_Siege_Tank_Siege_Mode
				|| unit.getType() == UnitType.Terran_Siege_Tank_Tank_Mode;
	}
	
	/**
	 * Performs the stutterstepping toward the enemy.
	 * @param target The location we want to stutterstep toward.
	 * @param moveCloser Do we move closer when the enemy is far away?
	 */
	public void stutterstep(Position target, boolean moveCloser)
	{
		boolean enemyInRange = false;
		if (closeEnemy != null 
				&& closeEnemy.getHitPoints() > 0 
				&& closeEnemy.getRemoveTimer() == 0 
				&& closeEnemy.exists() 
				&& closeEnemy.getPlayer() != Tyr.game.self())
		{
			int distSq = distanceSquared(closeEnemy);
			if ( distSq < 40000
					|| (closeEnemy.getType() != UnitType.Zerg_Overlord && distSq < 1000000))
				enemyInRange = true;
		}
		if (!enemyInRange)
		{
			for(Unit unit : EnemyManager.getEnemyUnits())
			{
				int distSq = distanceSquared(unit);
				if ( distSq < 40000
						|| (unit.getType() != UnitType.Zerg_Overlord && distSq < 1000000))
				{
					enemyInRange = true;
					closeEnemy = unit;
					break;
				}
			}
		}
		// If there is no enemy near, see if there is a neutral structure we want to destroy.
		if (Settings.attackNeutralStructures() && !enemyInRange)
		{
			for (Unit unit : Tyr.game.neutral().getUnits())
			{
				if (distanceSquared(unit) < 40000 
						&& unit.getType().isBuilding() 
						&& !unit.getType().isResourceContainer() 
						&& unit.getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) < 1536)
				{
					Tyr.drawCircle(unit.getPosition(), Color.Blue);
					Tyr.game.drawLineMap(unit.getX(), unit.getY(), unit.getX(), unit.getY(), Color.Red);
					
					attack(unit);
					return;
				}
			}
		}
		
		if (distanceSquared(target) <= 128*128)
			return;
		
		if (unit.getGroundWeaponCooldown() > 1 && unit.getAirWeaponCooldown() > 1 && isRanged(unit))
		{
			int minRadius = Math.max(32, unit.getType().groundWeapon().maxRange() - 64);
			int maxRadius = unit.getType().groundWeapon().maxRange();
			int level = 2;
			
			if (!moveCloser)
				level = 1;
			
			int distSq;
			if (closeEnemy != null 
					&& closeEnemy.getHitPoints() > 0 
					&& closeEnemy.getRemoveTimer() == 0 
					&& closeEnemy.exists() 
					&& closeEnemy.getPlayer() != Tyr.game.self())
			{
				distSq = distanceSquared(closeEnemy);
				if (!closeEnemy.getType().isBuilding() && !Agent.isRanged(closeEnemy) && distSq <= maxRadius * maxRadius)
					level = 0;
				else if (!closeEnemy.getType().isBuilding() && distSq <= minRadius * minRadius)
					level = 0;
				else if (distSq <= maxRadius)
					level = 1;
			}
			
			if (level > 0)
			{
				for(Unit unit : EnemyManager.getEnemyUnits())
				{
					// Against melee units we can always stutterstep back.
					distSq = distanceSquared(unit);
					if (!unit.getType().isBuilding() && !Agent.isRanged(unit) && distSq <= maxRadius * maxRadius)
					{
						closeEnemy = unit;
						level = 0;
						break;
					}
					if (!unit.getType().isBuilding() && distSq <= minRadius * minRadius)
					{
						closeEnemy = unit;
						level = 0;
						break;
					}
					if (distSq <= maxRadius)
					{
						closeEnemy = unit;
						level = 1;
					}
				}
			}
			
			if (level == 0)
			{
				// If the enemy is close, stutterstep backward.
				unit.move(Tyr.tileToPosition(Tyr.game.self().getStartLocation()));
				drawCircle(Color.Green, 4);
			}
			else if (level == 1)
			{
				drawCircle(Color.Yellow, 4);
			}
			else if (level == 2)
			{
				// If the enemy is far away stutterstep forward.
				unit.move(target);
				drawCircle(Color.Red, 4);
			}
		}
		else if (unit.isHoldingPosition()
				|| unit.isIdle()
				|| unit.getGroundWeaponCooldown() == 1 
				|| unit.getAirWeaponCooldown() == 1)
		{
			Tyr.drawCircle(unit.getPosition(), Color.Green, 6);
			attack(target);
		}
		else
			orderAttack(target);
	}
	
	/**
	 * Orders the agent to attack a certain position.
	 * @param target The position where the agent will attack.
	 */
	public void orderAttack(Position target)
	{
		Order order = unit.getOrder();
		Position orderTarget = unit.getOrderTargetPosition();
		double dist = target.getDistance(orderTarget);
		if ((order == Order.AttackMove || order == Order.Move || unit.isIdle())
				&& unit.getTarget()== null
				&& (Agent.isRanged(unit) || unit.getGroundWeaponCooldown() == 0)
				&&
			(Math.abs(target.getX() - orderTarget.getX()) >= 10 
				|| Math.abs(target.getY() - orderTarget.getY()) >= 10
				|| dist >= 10
				|| order == Order.Move))
		{
			// Attack the target position.
			Tyr.drawCircle(unit.getPosition(), Color.White, 6);
			attack(target);
		}
		
	}
}
