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

import java.util.ArrayList;

import com.tyr.DebugMessages;
import com.tyr.EnemyManager;
import com.tyr.EnemyPosition;
import com.tyr.Settings;
import com.tyr.Tyr;
import com.tyr.agents.Agent;
import com.tyr.agents.KillWorkers;
import com.tyr.agents.None;

import bwapi.Color;
import bwapi.Game;
import bwapi.Player;
import bwapi.Race;
import bwapi.Unit;
import bwapi.UnitType;
import bwta.BaseLocation;


/**
 * This class allows you to send out a worker unit to scout.
 * It also keeps track of what strategy the opponent is likely going for.
 * @author Simon
 *
 */
public class ScoutGroup extends UnitGroup
{
	/**
	 * The next base we intend to scout.
	 */
	int scoutBase = 0;
	
	/**
	 * The strategy the opponent is believed to be going for.
	 */
	public int opponentStrategy = 0;
	
	/*
	 * The different possible strategies.
	 */
	public static int unknown = 0;
	public static int zealotPush = 1;
	public static int cannons = 2;
	public static int tech = 3;
	public static int defensive = 4;
	public static int besiege = 5;
	public static int workerRush = 6;
	
	/**
	 * The number of scouts that will be sent out.
	 * Note that this works only in conjunction with the now deprecated Army.
	 */
	public int scoutCount = 1;
	
	/**
	 * Setting this value will automatically send out a worker scout at the corresponding frame.
	 * If the value is left at -1, no scout will be sent out.
	 * nscouts must also be set for the worker scouts to be sent out.
	 */
	public int workerScoutTiming = -1;
	
	/**
	 * The number of worker scouts that will be sent out automatically.
	 * workerScoutTiming must also be set for the worker scouts to be sent out.
	 */
	public int nscouts = 0;
	
	/**
	 * Have we sent out a scout yet?
	 */
	private boolean scoutsSent = false;
	
	/**
	 * Do we keep sending out units to scout?
	 */
	public boolean keepScouting = false;
	
	/**
	 * Are we clearing the enemy's expands after having destroyed their main?
	 */
	public static boolean clearEnemyExpands = false;
	
	/**
	 * The enemy's race.
	 */
	public static Race enemyRace = Race.Unknown;
	
	/**
	 * Do we treat the opponents race as if it is random?
	 * Only works in debug mode.
	 */
	private static final boolean FAKE_RANDOM = true;
	
	/**
	 * This class allows you to send out a worker unit to scout.
	 * It also keeps track of what strategy the opponent is likely going for. 
	 * @param rejects The OutOfJob unit group to which units are sent that are no longer needed.
	 */
	public ScoutGroup(OutOfJob rejects) 
	{
		super(rejects);
		determineEnemyRace();
	}

	@Override
	public void onFrame(Game game, Player self, Tyr bot) 
	{
		determineEnemyStrategy();
		determineEnemyRace();
		
		
		DebugMessages.addMessage("Enemy race: " + raceToString(enemyRace));
		
		// Send out the worker scouts if neccessary.
		if (!scoutsSent && game.getFrameCount() >= workerScoutTiming && workerScoutTiming != -1)
		{
			scoutsSent = true;
			for(int i=0; i<nscouts; i++)
				requestWorkerScout();
		}
		
		// If we do not yet know what base the enemy is located at, see if we can find out.
		if (bot.suspectedEnemy.size() > 1)
		{
			ArrayList<BaseLocation> cleared = new ArrayList<BaseLocation>();
			
			BaseLocation enemyBase = null;
			for(BaseLocation se : bot.suspectedEnemy)
			{
				for(EnemyPosition p : EnemyManager.getManager().enemyBuildingMemory)
				{
					// An enmey building is located here, this must be their main base.
					if (se.getPosition().getDistance(p.pos.getX(), p.pos.getY()) <= 256)
					{
						enemyBase = se;
						break;
					}
				}
				if (enemyBase != null)
					break;
				
				// Nothing to see, this is not their main base.
				if (Tyr.game.isVisible(se.getTilePosition()))
					cleared.add(se);
			}

			if (enemyBase != null && !clearEnemyExpands)
			{
				for(int i=bot.suspectedEnemy.size()-1; i>= 0; i--)
				{
					if (bot.suspectedEnemy.get(i) != enemyBase)
					{
						bot.expands.add(bot.suspectedEnemy.get(i));
						bot.suspectedEnemy.remove(i);
					}
				}
			}
			else
			{
				// The cleared base is now considered a regular expand.
				for(BaseLocation se : cleared)
				{
					bot.expands.add(se);
					bot.suspectedEnemy.remove(se);
				}
			}
		}

		if (keepScouting)
		{
			// Get a unit to scout.
	        if (units.size() < scoutCount && bot.homeGroup.units.size() > 4 && bot.bunkers.bunkersAreManned()
	        		&& (self.getRace() != Race.Zerg || bot.homeGroup.units.size() >= 12))
	        {
	        	Agent agent = bot.homeGroup.pop();
	        	if(agent != null)
	        	{
	        		agent.order(new None(agent));
	        		add(agent);
	        	}
	        }
		}
		
		ArrayList<Agent> done = new ArrayList<Agent>();
		for(Agent scout :units)
		{	
			scout.drawCircle(Color.Purple);
	        
			// If this is scout has been ordered to kill workers, see if it wants to stop doing so and return to base.
			if (scout.getCommand().getClass() == KillWorkers.class)
			{
				if (((KillWorkers)scout.getCommand()).breakOff)
				{
					scout.order(new None(scout));
					done.add(scout);
				}
				
				continue;
			}
			
			if (!scout.unit.isIdle())continue;
			
			ArrayList<BaseLocation> scoutLocations = null;
			
			// If we do not know the enemy location we try to find out.
			// Otherwise, we can scout the expands.
			if (bot.suspectedEnemy.size() > 1 || scout.unit.getType().isWorker())
				scoutLocations = bot.suspectedEnemy;
			else
				scoutLocations = bot.expands;
			
			if (scoutLocations.size() == 0)
				continue;
			int fullcycle = 0;
			// See which base we want to scout.
			while(fullcycle < 2)
			{
				scoutBase++;
				if (scoutBase >= scoutLocations.size())
				{
					scoutBase = 0;
					fullcycle++;
				}
				if (!Tyr.game.isVisible(scoutLocations.get(scoutBase).getTilePosition()))
					break;
			}
			
			BaseLocation b = scoutLocations.get(scoutBase);
			// Move to the base we want to scout.
			if(!scout.unit.getType().isWorker())
				scout.unit.attack(b.getPosition());
			else
				scout.unit.move(b.getPosition());
			
			// Worker scouts are ordered to kill the enemies workers once the enemy main has been located.
			if(scout.unit.getType().isWorker() && game.isVisible(b.getTilePosition()) && bot.suspectedEnemy != null 
					&& bot.suspectedEnemy.size() <= 1 && scout.distanceSquared(b.getPosition()) <= 128*128)
			{
				scout.order(new KillWorkers(scout));
			}
		}
		
		// Remove scouts that are no longer needed.
		for (Agent scout : done)
		{
			rejects.add(scout);
			units.remove(scout);
		}
	}
	
	/**
	 * Determines the race the opponent is playing.
	 */
	private void determineEnemyRace()
	{
		if (enemyRace == Race.Terran || enemyRace == Race.Protoss || enemyRace == Race.Zerg)
			return;
		
		DebugMessages.addMessage("Determining race.");
		
		if ((Tyr.game.enemy().getRace() == Race.Terran
				|| Tyr.game.enemy().getRace() == Race.Protoss
				|| Tyr.game.enemy().getRace() == Race.Zerg)
				&& (!Settings.getDebug() || !FAKE_RANDOM))
			enemyRace = Tyr.game.enemy().getRace();
		
		for (Unit enemy : EnemyManager.getEnemyUnits())
		{
			if (enemy.getType().getRace() == Race.Terran
					|| enemy.getType().getRace() == Race.Protoss
					|| enemy.getType().getRace() == Race.Zerg)
			{
				enemyRace = enemy.getType().getRace();
				break;
			}
		}
	}
	
	public static String raceToString(Race race)
	{
		if (race == Race.Terran)
			return "Terran";
		else if (race == Race.Protoss)
			return "Protoss";
		else if (race == Race.Zerg)
			return "Zerg";
		else if (race == Race.None)
			return "None";
		else if (race == Race.Unknown)
			return "Unknown";
		else if (race == Race.Random)
			return "Random";
		else if (race == null)
			return "null";
		return "Undefined";
		
	}

	/**
	 * Determines the strategy the opponent is using.
	 */
	private void determineEnemyStrategy()
	{
		if(Tyr.game.enemy().getRace() == Race.Protoss)
		{
			if(opponentStrategy != tech && opponentStrategy != cannons)
			{
				int nexusCount = 0;
				for(EnemyPosition enemy : EnemyManager.getManager().enemyBuildingMemory)
				{
					if (enemy.type == UnitType.Protoss_Nexus)
						nexusCount++;
				}
				// An opponent who has taken an expansion must be going for late game.
				if(nexusCount >= 2)
					opponentStrategy = tech;
				for(Unit enemy : EnemyManager.getEnemyUnits())
				{
					// An opponent who has taken gas must be doing a tech focussed build.
					if(enemy.getType().gasPrice() > 0 
							|| enemy.getType() == UnitType.Protoss_Assimilator 
							|| enemy.getType() == UnitType.Protoss_Cybernetics_Core)
						opponentStrategy = tech;
				}
			}
			if (Tyr.game.getFrameCount() <= 5200 && opponentStrategy != tech && opponentStrategy != cannons)
			{
				int gatewayCount = 0;
				for(EnemyPosition enemy : EnemyManager.getManager().enemyBuildingMemory)
				{
					// If the enemy has cannons or a forge, he must be using a cannon strategy.
					if (enemy.type == UnitType.Protoss_Photon_Cannon || enemy.type == UnitType.Protoss_Forge)
						opponentStrategy = cannons;
					if (enemy.type == UnitType.Protoss_Gateway)
						gatewayCount++;
				}
				// Two gateways early on imply a zealot push.
				if(gatewayCount >= 2)
					opponentStrategy = zealotPush;
			}
		}
		if(Tyr.game.enemy().getRace() == Race.Terran)
		{
			if (opponentStrategy != defensive)
			{
				// A bunker implies a defensive strategy.
				for(EnemyPosition enemy : EnemyManager.getManager().enemyBuildingMemory)
					if (enemy.type == UnitType.Terran_Bunker)
						opponentStrategy = defensive;
				
				int tankCount = 0;
				for(Unit enemy : EnemyManager.getEnemyUnits())
				{
					if(enemy.getType() == UnitType.Terran_Siege_Tank_Siege_Mode
							|| enemy.getType() == UnitType.Terran_Siege_Tank_Tank_Mode)
						tankCount++;
				}
				// A high tank count implies a defensive strategy.
				if(tankCount >= 3)
					opponentStrategy = defensive;
			}
			
			if (opponentStrategy == unknown)
			{
				int enemyInvadingWorkers = 0;
				for(Unit enemy : EnemyManager.getEnemyUnits())
				{
					if (!enemy.getType().isWorker())
						continue;
					if (enemy.getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) < Settings.getLargeInvasionDist())
					{
						enemyInvadingWorkers++;
						// Lots of workers in my base imply a worker rush strategy.
						if (enemyInvadingWorkers > 2)
						{
							opponentStrategy = workerRush;
							break;
						}
					}
				
				}
			}
		}
	
	}

	/**
	 * Requests a single worker scout.
	 */
	public void requestWorkerScout()
	{
		Agent scout = Tyr.bot.workForce.pop();
		if(scout != null)
		{
			units.add(scout);
			scout.unit.stop();
			scoutsSent = true;
		}
	}
	
	/**
	 * Requests a single worker scout.
	 * If this method is called multiple times, still only one scout will be sent out.
	 */
	public void requestWorkerScoutOnce()
	{
		if (scoutsSent)
			return;
		Agent scout = Tyr.bot.workForce.pop();
		if(scout != null)
		{
			units.add(scout);
			scout.unit.stop();
			scoutsSent = true;
		}
	}

	/**
	 * Returns the name of the strategy the opponent is going for.
	 * Note: this method is outdated and does not display all strategies correctly.
	 * @return A string description of the strategy the opponent is going for.
	 */
	public String getEnemyStrategy() 
	{
		if(opponentStrategy == zealotPush)
			return "Zealot push";
		if(opponentStrategy == cannons)
			return "Cannons";
		return "Unknown";
	}

	public static boolean enemyRaceKnown()
	{
		return enemyRace == Race.Terran || enemyRace == Race.Protoss || enemyRace == Race.Zerg;
	}
}
