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

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import com.tyr.buildingplacement.DefensiveStructures;

import bwapi.Color;
import bwapi.Player;
import bwapi.Position;
import bwapi.Race;
import bwapi.Unit;
import bwapi.UnitType;
import bwapi.WeaponType;
import bwta.BaseLocation;

/**
 * This class keeps track of enemy buildings.
 * It can also be used to determine what expands to attack for an harass.
 * @author Simon
 *
 */
public class EnemyManager 
{
	/**
	 * The singleton EnemyManager object.
	 */
	private static EnemyManager manager;
	
	/**
	 * Getter for the singleton EnemyManager object.
	 * @return The singleton EnemyManager object.
	 */
	public static EnemyManager getManager()
	{
		if (manager == null)
			manager = new EnemyManager();
		return manager;
	}
	
	/**
	 * List of all expands, in the order that they are encountered in a sweep around the map.
	 */
	private ArrayList<Position> orderedExpands;
	
	/**
	 * The position of our own base in the orderedExpands list.
	 */
	private int selfPos = -1;
	
	/**
	 * The closest invading enemy ground unit.
	 */
	private Unit invader;
	
	/**
	 * The closest invading enemy flying unit.
	 */
	private Unit flyingInvader;
	
	/**
	 * The closest invading enemy worker that is standing still.
	 */
	private Unit invadingWorker;
	
	/**
	 * The number of enemy units that are considered invaders.
	 */
	private int invaderCount;
	
	/**
	 * The number of enemy flying units that are considered invaders.
	 */
	private int flyingInvaderCount;
	
	/**
	 * The number of enemy workers that are considered invaders.
	 */
	private int invadingWorkerCount;
	
	/**
	 * The frame at which the invader was last updated.
	 */
	private int invaderUpdatedFrame;
    
    /**
     * Set of all known enemy buildings.
     */
    public HashSet<EnemyPosition> enemyBuildingMemory = new HashSet<EnemyPosition>();
    
    /**
     * Set of all known neutral buildings.
     */
    public HashSet<EnemyPosition> neutralBuildingMemory = new HashSet<EnemyPosition>();
    
    /**
     * List of all known enemy defensive structures. This is a subset of the known enemy buildings.
     */
    public ArrayList<EnemyPosition> enemyDefensiveStructures;
    
    /**
     * The frame at which the list of enemy units was last updated.
     */
    private static int enemyUnitsUpdatedFrame = -1;
    
    /**
     * List of all enemy units.
     */
    private static ArrayList<Unit> enemyUnits = new ArrayList<Unit>();
    
    /**
     * Are there only workers currently invading?
     */
    private boolean isInvaderWorker;
    
    /**
     * Keeps track of the last frame during which an enemy attacked.
     * Keys are unit IDs.
     */
    private HashMap<Integer, Integer> lastAttackFrame = new HashMap<Integer, Integer>();
    
    /**
     * Keeps track of the last position where an enemy was seen.
     * Keys are unit IDs.
     */
    private HashMap<Integer, Position> lastKnownPosition = new HashMap<>();
    
    /**
     * List of enemy types for enemies that we have encountered.
     * Dead enemies are removed from the list to give a realistic idea of the enemy's army size.
     */
    private HashMap<Integer, UnitType> enemyTypeMap = new HashMap<Integer, UnitType>();
    
    /**
     * Keeps track of how many of each type of enemy exists, including those outside our vision.
     */
    private HashMap<UnitType, Integer> allEnemyCount = new HashMap<UnitType, Integer>();
	
	/**
	 * List of all expands, in the order that they are encountered in a sweep around the map.
	 * If BWTA has not yet been initialized, or the enemy has not yet been scouted, this will return null.
	 * @return The ordered list of all expands, or null if it cannot yet be initialized.
	 */
	public ArrayList<Position> getOrderedExpands()
	{
		if (orderedExpands == null)
			initializeOrderedExpands();
		return orderedExpands;
	}
	
	/**
	 * Returns the position of our own base in the orderedExpands list, or -1 if the list has not yet been intialized.
	 * @return The position of our own base in the orderedExpands list, or -1 if the list has not yet been intialized.
	 */
	public int getSelfPos()
	{
		if (orderedExpands == null)
			initializeOrderedExpands();
		return selfPos;
	}

	/**
	 * Initializes the orderedExpands list.
	 */
	private void initializeOrderedExpands() 
	{
		if (!BWTAProxy.initialized)
			return;
	
		Tyr bot = Tyr.bot;
		
		// We need to know where the enemy is to make the projection.
		if (bot.suspectedEnemy.size() != 1)
			return;
		
		orderedExpands = new ArrayList<Position>();
		
		ArrayList<Position> positions = new ArrayList<Position>();
		
		// A projection that maps all positions to a double.
		// The double determines how far along the edge of the map the base lies if projected onto the map border.
		HashMap<Position, Double> projection = new HashMap<Position, Double>();
		
		for(BaseLocation b : bot.expands)
		{
			// We skip our main base.
			if(b.getPosition().getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) <= 100)
				continue;
			
			// Fill the list of positions and the projection.
			positions.add(b.getPosition());
			projection.put(b.getPosition(), projectToBorder(b.getPosition()));
		}

		// Our own, and our enemy's position along the border.
		double selfProjection = projectToBorder(Tyr.tileToPosition(Tyr.self.getStartLocation()));
		double enemyProjection = projectToBorder(bot.suspectedEnemy.get(0).getPosition());
		
		positions.add(bot.suspectedEnemy.get(0).getPosition());
		projection.put(bot.suspectedEnemy.get(0).getPosition(), enemyProjection);
		
		positions.add(Tyr.tileToPosition(Tyr.self.getStartLocation()));
		projection.put(Tyr.tileToPosition(Tyr.self.getStartLocation()), selfProjection);

		// Bubblesort on list of positions according to how far along the map border they are.
		for(boolean changes = true; changes;)
		{
			changes = false;
			for(int i=0; i<positions.size()-1; i++)
			{
				double p1 = projection.get(positions.get(i));
				double p2 = projection.get(positions.get(i+1));
				if (p1 > selfProjection && p2 < selfProjection)
					continue;
				
				if ((p2 > selfProjection && p1 < selfProjection) || projection.get(positions.get(i)) > projection.get(positions.get(i+1)))
				{
					Position temp = positions.get(i);
					positions.set(i, positions.get(i+1));
					positions.set(i+1, temp);
					changes = true;
				}
			}
		}

		// Fill the orderedExpands list, in such a way that the first and last expand on the list both border the enemy position.
		int pos;
		for(pos = 0; pos<positions.size() && projection.get(positions.get(pos)) != enemyProjection; pos++);
		
		int enemyPos = pos;
		
		for(pos++; pos < positions.size(); pos++)
			orderedExpands.add(positions.get(pos));
		for(pos = 0; pos < enemyPos; pos++)
			orderedExpands.add(positions.get(pos));
		
		for(pos = 0; pos < orderedExpands.size(); pos++)
		{
			if (orderedExpands.get(pos).getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) <= 100)
			{
				selfPos = pos;
				break;
			}
		}
	}
	
	/**
	 * Projects the position to the border of the map.
	 * @param p The position to be projected.
	 * @return A value indicating how far along the border the position is.
	 */
	private double projectToBorder(Position p)
	{
		int width = Tyr.game.mapWidth()*32;
		int height = Tyr.game.mapHeight()*32;
		int xd1 = p.getX();
		int xd2 = width - p.getX();
		int yd1 = p.getY();
		int yd2 = height - p.getY();
		if (Math.min(xd1, xd2) < Math.min(yd1, yd2))
		{
			if (xd1 < xd2)
				return 0.5 + (double)yd1/4.0/(double)height;
			else
				return (double)yd2/4.0/(double)height;
		}
		else
		{
			if (yd1 < yd2)
				return 0.25 + (double)xd2/4.0/(double)width;
			else
				return 0.75 + (double)xd1/4.0/(double)width;
		}
	}
	
	/**
	 * The closest invading enemy ground unit.
	 * @return The closest invading enemy ground unit.
	 */
	public Unit getInvader()
	{
		updateInvader();
		return invader;
	}
	
	/**
	 * The closest invading enemy worker.
	 * @return The closest invading enemy worker.
	 */
	public Unit getInvadingWorker()
	{
		updateInvader();
		return invadingWorker;
	}
	
	/**
	 * The closest invading enemy flying unit.
	 * @return The closest invading enemy flying unit.
	 */
	public Unit getFlyingInvader()
	{
		updateInvader();
		return flyingInvader;
	}
	
	/**
	 * The number of invading enemy units.
	 * @return The number of invading enemy units.
	 */
	public int getInvaderCount()
	{
		updateInvader();
		return this.invaderCount;
	}
	
	/**
	 * The number of flying invading enemy units.
	 * @return The number of flying invading enemy units.
	 */
	public int getFlyingInvaderCount()
	{
		updateInvader();
		return this.flyingInvaderCount;
	}
	
	/**
	 * The number of enemy workers that are considered invaders.
	 * @return The number of enemy workers that are considered invaders.
	 */
	public int getInvadingWorkerCount()
	{
		updateInvader();
		return this.invadingWorkerCount;
	}
	
	/**
	 * Are there only workers currently invading?
	 * @return Are there only workers currently invading?
	 */
	public boolean isInvaderWorker()
	{
		updateInvader();
		return this.isInvaderWorker;
	}
	
	public int getLastAttackFrame(Unit unit)
	{
		if (!lastAttackFrame.containsKey(unit.getID()))
			return -1000000;
		return lastAttackFrame.get(unit.getID());
	}
	
	public Position getLastPosition(Unit unit)
	{
		if (!lastKnownPosition.containsKey(unit.getID()))
			return null;
		return lastKnownPosition.get(unit.getID());
	}
	
	public int getAllCount(UnitType type)
	{
		if (allEnemyCount.containsKey(type))
			return allEnemyCount.get(type);
		else
			return 0;
	}
	
	/**
	 * Update the invader and invaderCount, if they have not already been updated this frame.
	 */
	private void updateInvader()
	{
		int frame = Tyr.game.getFrameCount();
		if(invaderUpdatedFrame == frame)
			return;
		invaderUpdatedFrame = frame;
		
		this.isInvaderWorker = true;

		invaderCount = 0;
		flyingInvaderCount = 0;
		invadingWorkerCount = 0;
		invader = null;
		invadingWorker = null;
		flyingInvader = null;
		
		// Our start location.
		Position start = Tyr.tileToPosition(Tyr.self.getStartLocation());

		Tyr.drawCircle(start, Color.Cyan, Settings.getLargeInvasionDist());
		Tyr.drawCircle(start, Color.Cyan, Settings.getDefendExpandDist());
		
		final HashSet<Unit> invadingEnemies = new HashSet<Unit>();
		
		// Loop over all currently visible enemy units to see if any of them is considered an invader.
		for (Unit u : EnemyManager.getEnemyUnits())
		{
			// We are not worried about observers.
			// They will not attack us and trying to 'defend' against them will only result in problems.
			// This is especially true if we are unable to detect them!
			if (u.getType() == UnitType.Protoss_Observer)
				continue;
			
			if (u.isCloaked() && !u.isDetected())
				continue;

			if (Tyr.self.getRace() == Race.Protoss 
					&& !u.isDetected() 
					&& (u.getType() == UnitType.Protoss_Dark_Templar || u.isBurrowed()))
				continue;
			
			// How far away is the enemy unit?
			double uDist = u.getDistance(start.getX(), start.getY()); 
			
			// If he is too close, we have an invader!
			if (Settings.getDefendStart() && uDist <= Settings.getLargeInvasionDist())
			{
				if (!u.getType().isWorker() && u.getType() != UnitType.Zerg_Overlord && u.getType() != UnitType.Protoss_Observer)
					this.isInvaderWorker = false;
				if (u.getType().isFlyer() || u.isLifted())
				{
					// If the current invader is null, or if this unit is closer than the current invader, then we have a new closest invader.
					if (flyingInvader == null || uDist < flyingInvader.getDistance(start.getX(), start.getY()))
						flyingInvader = u;
					
					flyingInvaderCount++;
				}
				
				// Drop capable units are considered ground units as well as air units.
				// This allows us to more easily clean up drops.
				// Overlords are not considered ground units as scouting overlords would otherwise mess up our defenses.
				if (u.getType() == UnitType.Protoss_Shuttle || u.getType() == UnitType.Terran_Dropship || (!u.getType().isFlyer() && !u.isLifted()))
				{
					// If the current invader is null, or if it is a worker, or if this unit is closer than the current invader, then we have a new closest invader.
					if (invader == null)
						invader = u;
					else if (invader.getType().isWorker() && !u.getType().isWorker())
						invader = u;
					else if ((!u.getType().isWorker() || invader.getType().isWorker())
							&& uDist < invader.getDistance(start.getX(), start.getY()))
						invader = u;
				}
				if (u.getType().isWorker())
					invadingWorkerCount++;
				if (invadingWorker == null || uDist < invadingWorker.getDistance(start.getX(), start.getY()))
					invadingWorker = u;
				invaderCount++;
				invadingEnemies.add(u);
			}
		}
		
		if (invaderCount == 0 || true)
		{
			// A list of all the location that we need to defend.
			ArrayList<Position> needsDefending = new ArrayList<Position>();
			
			for(DefensiveStructures structures : Tyr.bot.defensiveStructures)
			{
				// If the defenses for this expand are disabled we do not need to defend it.
				if (structures.disabled)
					continue;
				
				if (!Settings.getDefendStart() && structures.defendedPosition.getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) <= 100)
					continue;
				
				// The distance from this expand to our main base.
				double distance = structures.defendedPosition.getDistance(start);
				
				// See if this expand needs defending.
				// Note that expands that are close enough to our main are already defended.
				if ( distance < Settings.getDefendExpandDist()
						&& distance > Settings.getLargeInvasionDist() - Settings.getSmallInvasionDist())
				{
					needsDefending.add(structures.defendedPosition);
					Tyr.drawCircle(structures.defendedPosition, Color.Cyan, Settings.getSmallInvasionDist());
				}
			}
			
			for (Unit u : EnemyManager.getEnemyUnits())
			{
				if (invadingEnemies.contains(u))
					continue;
				
				// We are not worried about observers.
				// They will not attack us and trying to 'defend' against them will only result in problems.
				// This is especially true if we are unable to detect them!
				if (u.getType() == UnitType.Protoss_Observer)
					continue;

				if (u.isCloaked() && !u.isDetected())
					continue;

				if (Tyr.self.getRace() == Race.Protoss 
						&& !u.isDetected() 
						&& (u.getType() == UnitType.Protoss_Dark_Templar || u.isBurrowed()))
					continue;
				
				for(Position defendedPos : needsDefending)
				{
					// How far away is the enemy unit from the defended expansion?
					double uDist = u.getDistance(defendedPos.getX(), defendedPos.getY()); 
					
					// If he is too close, we have an invader!
					if (uDist <= Settings.getSmallInvasionDist())
					{
						// The distance from the unit to our main base.
						double mainDist = u.getDistance(start);
						

						if (u.getType().isFlyer())
						{
							// If the current invader is null, or if this unit is closer than the current invader, then we have a new closest invader.
							if (flyingInvader == null || mainDist < flyingInvader.getDistance(start))
								flyingInvader = u;
							flyingInvaderCount++;
						}
						else
						{
							// If the current invader is null, or if this unit is closer than the current invader, then we have a new closest invader.
							if (invader == null || mainDist < invader.getDistance(start))
								invader = u;
						}
						invaderCount++;
					}
				}
			}
		}

        if(invader != null)
        	Tyr.drawCircle(invader.getPosition(), Color.Red);
        if(flyingInvader != null)
        	Tyr.drawCircle(flyingInvader.getPosition(), Color.Blue);
	}
	
	/**
	 * This method is called each frame and updates all information on the enemy.
	 */
	public void update()
	{
		updateBuildings();
		updateEnemyInfo();
	}
	
	/**
	 * Updates enemyBuildingMemory, neutralBuildingMemory and enemyDefensiveStructures.
	 */
	private void updateBuildings()
	{
        final Object[] enemyBuildings = enemyBuildingMemory.toArray();
        for(int i = enemyBuildings.length -1; i >= 0; i--)
        {
        	final EnemyPosition p = (EnemyPosition) enemyBuildings[i];
    		final Unit unit = Tyr.game.getUnit(p.id);
    		final Position lastKnown = unit == null ? null : getLastPosition(unit);
    		if (lastKnown != null)
    			p.pos = lastKnown;
    		
        	if (Tyr.game.isVisible(p.pos.getX()/32, p.pos.getY()/32))
        	{
        		Tyr.drawCircle(p.pos, Color.Blue, 4);
        		enemyBuildingMemory.remove(p);
        	}
        	else
        		Tyr.drawCircle(p.pos, Color.Orange);
        }
        
        enemyDefensiveStructures = new ArrayList<EnemyPosition>();
        
        
        // Find the enemy's buildings.
        for (Unit u : EnemyManager.getEnemyUnits()) 
        {
        	// If this unit is in fact a building...
        	if (u.getType().isBuilding() && !u.isLifted()) 
        	{
        		EnemyPosition enemyPos = new EnemyPosition(u.getID(), u.getType(), u.getPosition(), u.isCompleted());
        		// Check if we have it's position in memory and add it if we do not...
        		if (!enemyBuildingMemory.contains(enemyPos))
        		{
        			Tyr.drawCircle(enemyPos.pos, Color.White, 6);
        			enemyBuildingMemory.add(enemyPos);
        		}
        	}
        }
        
        for (EnemyPosition pos : enemyBuildingMemory)
        	if (pos.type == UnitType.Protoss_Photon_Cannon || pos.type == UnitType.Zerg_Sunken_Colony || pos.type == UnitType.Terran_Bunker)
        	{
        		enemyDefensiveStructures.add(pos);
        		Tyr.drawCircle(pos.pos, Color.Red, WeaponType.Phase_Disruptor_Cannon.maxRange());
        	}
        
        for(EnemyPosition enemyPos : enemyBuildingMemory)
        	Tyr.drawCircle(enemyPos.pos, Color.Red);

        List<EnemyPosition> removePositions = new ArrayList<EnemyPosition>();
        
        for(EnemyPosition p : neutralBuildingMemory)
        {
        	if (Tyr.game.isVisible(p.pos.getX()/32, p.pos.getY()/32))
        		removePositions.add(p);
        	else
        		Tyr.drawCircle(p.pos, Color.Teal);
        }
        
        for(EnemyPosition p : removePositions)
        	neutralBuildingMemory.remove(p);
        
        // Find the enemy's buildings.
        for (Unit u : Tyr.game.getNeutralUnits()) 
        {
        	// If this unit is in fact a building...
        	if (!u.getType().canMove()) 
        	{
        		EnemyPosition enemyPos = new EnemyPosition(u.getID(), u.getType(), u.getPosition(), u.isCompleted());
        		// Check if we have it's position in memory and add it if we do not...
        		if (!neutralBuildingMemory.contains(enemyPos))
        		{
        			neutralBuildingMemory.add(enemyPos);
        		}
        	}
        }
        
        for(EnemyPosition enemyPos : neutralBuildingMemory)
        	Tyr.drawCircle(enemyPos.pos, Color.Teal);
	}
	
	/**
	 * Updates the map that keeps track of enemy units, including those no longer visible.
	 */
	private void updateEnemyInfo()
	{
		for (int unitID : lastKnownPosition.keySet())
			if (lastKnownPosition.get(unitID) != null && Tyr.game.isVisible(Tyr.positionToTile(lastKnownPosition.get(unitID))))
				lastKnownPosition.put(unitID, null);
		
		for (Unit unit : getEnemyUnits())
		{
			if (unit.isAttacking() || unit.isAttackFrame())
				lastAttackFrame.put(unit.getID(), Tyr.game.getFrameCount());
			
			lastKnownPosition.put(unit.getID(), unit.getPosition());
			
			enemyTypeMap.put(unit.getID(), unit.getType());
		}
		
		allEnemyCount = new HashMap<UnitType, Integer>();
		
		for (UnitType type : enemyTypeMap.values())
		{
			if (!allEnemyCount.containsKey(type))
				allEnemyCount.put(type, 1);
			else
				allEnemyCount.put(type, allEnemyCount.get(type) + 1);
		}
	}
	
	/**
	 * Get a list of all enemy units.
	 * @return Get a list of all enemy units.
	 */
	public static ArrayList<Unit> getEnemyUnits()
	{
		int frame = Tyr.game.getFrameCount(); 
		if (frame > enemyUnitsUpdatedFrame)
		{
			enemyUnitsUpdatedFrame = frame;
			enemyUnits = new ArrayList<Unit>();
			for (Player enemy : Tyr.game.enemies())
				for (Unit unit : enemy.getUnits())
					if (unit.getType() != UnitType.Unknown)
						enemyUnits.add(unit);
		}
		return enemyUnits;
	}

	/**
	 * Resets the enemy manager when a new game starts.
	 */
	public void reset() 
	{
		orderedExpands = null;
		selfPos = -1;
		enemyBuildingMemory = new HashSet<EnemyPosition>();
		enemyDefensiveStructures = new ArrayList<EnemyPosition>();
		invaderUpdatedFrame = 0;
	}

	/**
	 * This method gets called whenever a unit dies.
	 * @param unit The dying unit.
	 */
	public void died(Unit unit)
	{
		if (enemyTypeMap.containsKey(unit.getID()))
		{
			UnitType type = enemyTypeMap.get(unit.getID());
			if (allEnemyCount.containsKey(type))
				allEnemyCount.put(type, allEnemyCount.get(type) - 1);
			enemyTypeMap.remove(unit.getID());
		}
		
	}

	/**
	 * Get the set of known enemy unit types, that have been constructed sometime during the game.
	 * @return The set of known enemy unit types, that have been constructed sometime during the game.
	 */
	public Set<UnitType> getEnemyTypes()
	{
		return (Set<UnitType>)allEnemyCount.keySet();
	}
}

