/*
* 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.EnemyManager;
import com.tyr.EnemyPosition;
import com.tyr.Settings;
import com.tyr.Tyr;
import com.tyr.agents.Agent;
import com.tyr.agents.WorkerAgent;
import com.tyr.buildingplacement.DefensiveStructures;
import com.tyr.buildingplacement.SpaceManager;
import com.tyr.buildingplacement.TurretBuildSite;

import bwapi.Color;
import bwapi.Game;
import bwapi.Order;
import bwapi.Player;
import bwapi.Position;
import bwapi.Race;
import bwapi.Unit;
import bwapi.UnitType;
import bwapi.WeaponType;


/**
 * This class manages the mining workers for a specific base.
 * @author Simon
 *
 */
public class MineralWorkers extends UnitGroup
{
	/**
	 * The base for which we manage the workers.
	 */
	public Unit resourceDepot;
	
	/**
	 * The minerals close to the base from which we will mine.
	 */
	public ArrayList<Unit> minerals = new ArrayList<Unit>();
	
	/**
	 * The unit groups managing each of the mineral patches.
	 */
	public ArrayList<PatchWorkers> patchWorkers = new ArrayList<PatchWorkers>();
	
	/**
	 * Unit group that manages the workers mining the vespene geyser close to this base.
	 * If no vespene geyser exists close to this base, this will be null.
	 */
	public GasWorkers gasWorkers;
	
	/**
	 * The location where defensive structures can be built for this base, and where the defending units can gather.
	 */
	public Position defensePos;
	
	/**
	 * The defensive structures around this base.
	 */
	public DefensiveStructures defenses = null;
	
	/**
	 * Build site locator for finding placements for turrets defending the mineral line. 
	 */
	public TurretBuildSite turretSite;
	
	/**
	 * Is this base currently under attack?
	 */
	public boolean underAttack;
	
	/**
	 * Do we abandon our base?
	 */
	public boolean abandon;
	
	/**
	 * An invading enemy unit, or null if there is none. 
	 */
	public Unit invader = null;
	
	/**
	 * How many invaders are there?
	 */
	public int invaderCount;
	
	private LocalDefendingWorkers defendingWorkers;
	
	/**
	 * This class manages the mining workers for a specific base. 
	 * @param rejects OutOfJob object to which all units are sent that are no longer used.
	 * @param resourceDepot The base for which we manage the workers.
	 */
	public MineralWorkers(OutOfJob rejects, Unit resourceDepot) 
	{
		super(rejects);
		this.resourceDepot = resourceDepot;
		
		// Find all the mineral patches close to the base. Also see if there is a vespene geyser nearby.
		for (Unit neutralUnit : Tyr.game.neutral().getUnits()) 
		{
            if (neutralUnit.getType().isMineralField() && neutralUnit.getDistance(resourceDepot) <= 270)
            {
                minerals.add(neutralUnit);
                patchWorkers.add(new PatchWorkers(rejects, resourceDepot, neutralUnit));
            }
            else if (neutralUnit.getType() == UnitType.Resource_Vespene_Geyser
            		&& neutralUnit.getDistance(resourceDepot) <= 270)
            	gasWorkers = new GasWorkers(rejects, neutralUnit);
		}
        
		if (resourceDepot == null || resourceDepot.getDistance(Tyr.tileToPosition(Tyr.game.self().getStartLocation())) < 64)
			defensePos = null;
		else
			defensePos = SpaceManager.getDefensePos(resourceDepot.getPosition(), minerals);

		boolean exists = false;
		// See if there already exist a defensive structures object for this base.
		for(DefensiveStructures structures : Tyr.bot.defensiveStructures)
		{
			if (resourceDepot.getDistance(structures.defendedPosition) < 64)
			{
				exists = true;
				defenses = structures;
				defenses.disabled = false;
				break;
			}
		}
		// If no defensive structures object exists, we create a new one.
		if (!exists)
		{
			defenses = new DefensiveStructures(resourceDepot.getPosition(), defensePos);
			Tyr.bot.defensiveStructures.add(defenses);
		}
		
		defendingWorkers = new LocalDefendingWorkers(rejects, this);
	}

	@Override
	public void onFrame(Game game, Player self, Tyr bot)
	{
		Tyr.drawCircle(resourceDepot.getPosition(), Color.Orange, 16);
		Tyr.game.drawTextMap(resourceDepot.getX(), resourceDepot.getY(), "" + size());
		
		// Initialize the turret build site.
		if (turretSite == null && self.getRace() == Race.Terran)
			turretSite = new TurretBuildSite(this);
		
		// Find the nearby minerals and possible vespene geyser.
		// Due to fog of war, we may not have found all minerals when we first built the base.
		for (Unit neutralUnit : Tyr.game.neutral().getUnits()) 
		{
            if (neutralUnit.getType().isMineralField() && neutralUnit.getDistance(resourceDepot) <= 270)
            {
                if (!minerals.contains(neutralUnit))
                	minerals.add(neutralUnit);
                
                boolean alreadyAdded = false;
                for(PatchWorkers patch : patchWorkers)
                	if (patch.mineral.getID() == neutralUnit.getID())
                	{
                		alreadyAdded = true;
                		break;
                	}
                if(!alreadyAdded)
                	patchWorkers.add(new PatchWorkers(rejects, resourceDepot, neutralUnit));
            }
            else if (gasWorkers == null && neutralUnit.getType() == UnitType.Resource_Vespene_Geyser
            		&& neutralUnit.getDistance(resourceDepot) <= 270)
            	gasWorkers = new GasWorkers(rejects, neutralUnit);
		}
		
		if (defensePos != null)
			Tyr.drawCircle(defensePos, Color.Green, 64);
		
		// Remove mineral patches from the list when they have been mined out.
		for(int i=minerals.size()-1; i>=0; i--)
		{
			if (!minerals.get(i).exists())
				minerals.remove(i);
		}
		
		// When there are no more minerals, or the base has been destroyed, we no longer need to manage it.
		// Defenses are disabled.
		if (minedOut()
				|| resourceDepot == null || !resourceDepot.exists() 
				|| resourceDepot.getHitPoints() <= 0 || resourceDepot.getRemoveTimer() != 0 )
		{
			if(defenses != null && defenses.defendedPosition.getDistance(Tyr.tileToPosition(self.getStartLocation())) >= 200)
			{
				defenses.disable();
				defenses = null;
			}
			return;
		}
		
		determineThreat();
		
		if (abandon)
		{
			for (Agent agent : units)
				rejects.add(agent);
			units = new ArrayList<Agent>();
			for (PatchWorkers patch : patchWorkers)
			{
				for (Agent agent : patch.units)
					rejects.add(agent);
				patch.units = new ArrayList<Agent>();
			}
		}
		else
		{
			defendingWorkers.cleanup();
			defendingWorkers.onFrame(game, self, bot);
		}
		
		// Assign workers to the right mineral patch.
		for (int i=0; units.size() > 0; i++)
		{
			for (PatchWorkers patch : patchWorkers)
			{
				if (patch.units.size() <= i)
				{
					patch.add(units.get(units.size()-1));
					units.remove(units.size()-1);
				}
				if (units.size() == 0)
					break;
			}
		}
		
		for (PatchWorkers patch : patchWorkers)
			patch.onFrame(game, self, bot);
		
		// When the geyser is mined out, take all workers out of gas.
		if(gasWorkers != null && gasWorkers.geyser.getResources() == 0
				&& gasWorkers.geyser.getType().isRefinery())
		{
			for(Agent unit : gasWorkers.units)
				add(unit);
			
			gasWorkers = null;
		}
		else if(gasWorkers != null)
		{
			// Add workers to the gas workers unit group if there are not enough.
			while (gasWorkers.geyser.isCompleted() && gasWorkers.units.size() < Settings.getWorkersPerGas()
					&& gasWorkers.geyser.getType().isRefinery()
					&& gasWorkers.geyser.getPlayer() == self)
			{
				Agent gasWorker = pop(gasWorkers.geyser.getPosition());
				if(gasWorker == null)
					break;
				remove(gasWorker);
				gasWorkers.add(gasWorker);
			}
			gasWorkers.onFrame(game, self, bot);
		}
	}
	
	/**
	 * Determine whether the enemy is threatening our base.
	 */
	private void determineThreat()
	{
		underAttack = false;
		abandon = false;
		
		invaderCount = 0;
		
		boolean lurkers = false;
		
		for (Unit enemy : EnemyManager.getEnemyUnits())
		{
			if (enemy.getType().groundWeapon() == WeaponType.None)
				continue;
			
			if (resourceDepot == null || resourceDepot.getDistance(enemy) >= 300)
				continue;
			
			invaderCount++;
			underAttack = true;
			invader = enemy;
			
			if (enemy.getType() == UnitType.Zerg_Lurker)
				lurkers = true;
			
			if (!enemy.getType().isWorker())
				abandon = true;
		}
		if (abandon
				&& !lurkers
				&& resourceDepot != null 
				&& resourceDepot.getDistance(Tyr.tileToPosition(Tyr.self.getStartLocation())) <= Settings.getDefendExpandDist())
			abandon = false;
		
		if (Tyr.bot.defensiveStructures.size() <= 1)
			abandon = false;
		
		if (underAttack
				&& invaderCount == 1
				&& invader.getType().isWorker())
			underAttack = false;
			
		
		if (abandon)
			Tyr.drawCircle(resourceDepot.getPosition(), Color.Red, 16);
		else if (underAttack)
			Tyr.drawCircle(resourceDepot.getPosition(), Color.Orange, 16);
	}

	@Override
	public Agent pop()
	{
		for(int i=units.size()-1; i>= 0; i--)
		{
			if (!((WorkerAgent)units.get(i)).isBlocked
					&& !units.get(i).unit.isConstructing()
					&& units.get(i).distanceSquared(resourceDepot.getPosition()) <= 1152*1152
					&& units.get(i).unit.getOrder() != Order.ConstructingBuilding)
			{
				Agent result = units.get(i);
				remove(i);
				return result;
			}
		}
		
		for (PatchWorkers patch : patchWorkers)
		{
			Agent result = patch.pop();
			if (result != null)
				return result;
		}
		return null;
	}
	
	/**
	 * Removes an agent from this unit group and returns it.
	 * @param pos The worker closest to this position will be returned.
	 * @return The removed agent. This may be null if no agent is available.
	 *		   This does not necessarily mean that this unit group contains no more agents.
	 */
	public Agent pop(Position pos) 
	{
		return pop(pos, false);
	}
	
	/**
	 * Removes an agent from this unit group and returns it.
	 * @param pos The worker closest to this position will be returned.
	 * @param healthyOnly Only use workers with at least 20 hp.
	 * @return The removed agent. This may be null if no agent is available.
	 *		   This does not necessarily mean that this unit group contains no more agents.
	 */
	public Agent pop(Position pos, boolean healthyOnly) 
	{
		Agent result = null;
		double distance = Double.MAX_VALUE;
		for(Agent worker : units)
		{
			if (((WorkerAgent)worker).isBlocked 
					|| worker.unit.isConstructing()
					|| worker.unit.getBuildUnit() != null
					|| worker.unit.getOrder().equals(Order.ConstructingBuilding)
					|| (healthyOnly && worker.unit.getHitPoints() < 20))
				continue;
			
			int newDist = worker.distanceSquared(pos);
			if(newDist < distance)
			{
				distance = newDist;
				result = worker;
			}
		}
		
		if (result != null)
			units.remove(result);
		
		PatchWorkers last = null;

		for (PatchWorkers patch : patchWorkers)
		{
			for (Agent agent : patch.units)
			{
				if (((WorkerAgent)agent).isBlocked 
						|| agent.unit.isConstructing()
						|| agent.unit.getBuildUnit() != null
						|| agent.unit.getOrder().equals(Order.ConstructingBuilding)
						|| (healthyOnly && agent.unit.getHitPoints() < 20))
					continue;
				
				int newDist = agent.distanceSquared(pos);
				
				if (newDist < distance)
				{
					if (result != null && last == null)
						units.add(result);
					
					last = patch;
					distance = newDist;
					result = agent;
				}
			}
		}
		
		if (last != null && result != null)
			last.units.remove(result);
		
		return result;
	}

	@Override
	public void add(Agent agent)
	{
		if (agent == null)
			return;
		if(agent.unit == null)
			return;
		super.add(agent);
	}
	
	@Override
	public void cleanup()
	{
		super.cleanup();

		for(int i=patchWorkers.size()-1; i >= 0; i--)
		{
			patchWorkers.get(i).cleanup();
			if (patchWorkers.get(i).mineral == null)
				patchWorkers.remove(i);
		}
		
		
		for(int i=minerals.size()-1; i >= 0; i--)
		{
			if (!minerals.get(i).exists() || minerals.get(i).getResources() <= 0)
			{
				minerals.remove(i);
			}
		}
		
		if (minedOut())
		{
			for(Agent unit : units)
				rejects.add(unit);
			units = new ArrayList<Agent>();
		}
		
		if (gasWorkers != null)
			gasWorkers.cleanup();
		
		defendingWorkers.cleanup();
	}
	
	/**
	 * Removes all units from this unit group and adds them to the receiving group.
	 * @param receivingGroup The group to which all units are sent.
	 */
	public void clear(UnitGroup receivingGroup) 
	{
		for (PatchWorkers patch : patchWorkers)
		{
			for (Agent worker : patch.units)
				receivingGroup.add(worker);
			patch.units = new ArrayList<Agent>();
		}
	}

	/**
	 * The total number of units managed by this group.
	 * @return The total number of units managed by this group.
	 */
	public int size() 
	{
		int result = 0;
		for (PatchWorkers patch : patchWorkers)
			result += patch.units.size();
		return result;
	}

	/**
	 * Is this base mined out?
	 * @return Is this base mined out?
	 */
	public boolean minedOut()
	{
		if (minerals.size() > 0)
			return false;

		for (EnemyPosition neutralUnit : EnemyManager.getManager().neutralBuildingMemory)
            if (neutralUnit.type.isMineralField() && neutralUnit.distanceSq(resourceDepot) <= 270 * 270)
            	return false;
		return true;
	}
}
