/*
* 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.Tyr;
import com.tyr.agents.Agent;

import bwapi.Color;
import bwapi.Game;
import bwapi.Player;
import bwapi.Position;
import bwapi.Unit;


/**
 * This group manages all workers.
 * @author Simon
 *
 */
public class WorkerGroup extends UnitGroup
{
	public static boolean evacuateThreatenedBases = true;
	
	/**
	 * This group manages all workers.
	 * @param rejects The OutOfJob object to which all units will be sent that are no longer needed.
	 */
	public WorkerGroup(OutOfJob rejects) 
	{
		super(rejects);
	}
	
	/**
	 * List of all MineralWorker groups.
	 * There is one such group per mining base.
	 */
	public ArrayList<MineralWorkers> mineralWorkers = new ArrayList<MineralWorkers>();
	
	/**
	 * OutOfJob where units will be sent when they are no longer needed.
	 */
	private OutOfJob outOfJob = new OutOfJob();

	@Override
	public void onFrame(Game game, Player self, Tyr bot)
	{
		if (mineralWorkers.size() > 0)
		{
			int size = units.size();
			
			// See which bases are mined out.
			ArrayList<MineralWorkers> minedOut = new ArrayList<MineralWorkers>();
			for(MineralWorkers mw : mineralWorkers)
				if(mw.minedOut())
					minedOut.add(mw);
			
			for(MineralWorkers mw : minedOut)
			{
				// Remove the mined out bases from the list.
				mineralWorkers.remove(mw);
				
				mw.clear(outOfJob);
			}
			
			// We distribute workers over our bases.
			// First we see how many working bases there actually are.
			int totalPatches = 0;
			int totalWorkers = outOfJob.units.size();

			boolean safeExists = false;
			for (MineralWorkers mw : mineralWorkers)
				if (!mw.underAttack)
					safeExists = true;
			
			if (!evacuateThreatenedBases)
				safeExists = false;
			
			for (MineralWorkers mw : mineralWorkers)
			{
				if (mw.underAttack && safeExists)
					continue;
				
				if (mw.resourceDepot.isCompleted())
					totalPatches += mw.minerals.size();
				for (PatchWorkers patch : mw.patchWorkers)
					totalWorkers += patch.units.size();
			}
			
			if (totalPatches > 0)
			{
				// If a base has too many workers, we remove workers from it.
				for(MineralWorkers mw : mineralWorkers)
				{
					if (mw.underAttack && safeExists)
						continue;
					
					while(mw.size() > totalWorkers * mw.minerals.size() / totalPatches + 1 || mw.minerals.size() == 0)
					{
						Agent worker = mw.pop();
						if (worker != null)
							outOfJob.add(worker);
						else
							break;
					}
				}
			}
			
			if (units.size() > size)
				DebugMessages.addMessage("WorkerGroup incorrect size 1: " + (units.size() - size));
			
			// We add those workers back to bases which do not have enough.
			int filled = 0;
			while(outOfJob.units.size() > 0)
			{
				Agent worker = outOfJob.pop();
				boolean done = false;
				while (filled < mineralWorkers.size())
				{
					if ((!mineralWorkers.get(filled).underAttack || !safeExists)
							&& mineralWorkers.get(filled).resourceDepot.isCompleted() 
							&& (totalPatches == 0 || mineralWorkers.get(filled).size() < totalWorkers * mineralWorkers.get(filled).minerals.size() / totalPatches + 1) 
							&& mineralWorkers.get(filled).minerals.size() > 0)
					{
						mineralWorkers.get(filled).add(worker);
						done = true;
						break;
					}
					else
						filled++;
				}
				if (!done)
				{
					outOfJob.add(worker);
					break;
				}
			}
			
			if (units.size() > size)
				DebugMessages.addMessage("WorkerGroup incorrect size 2: " + (units.size() - size));
			
			// We let all mineral workers perform their own logic.
			for(MineralWorkers mw : mineralWorkers)
				mw.onFrame(game, self, bot);
			
			if (units.size() > size)
				DebugMessages.addMessage("WorkerGroup incorrect size 3: " + (units.size() - size));
		}
		
		if (outOfJob.units.size() > 0)
			DebugMessages.addMessage("OutOfJob workers: " + outOfJob.units.size());
		for(Agent agent : outOfJob.units)
			agent.drawCircle(Color.Orange);
	}
	
	@Override
	public Agent pop()
	{
		Agent result = null;
		
		if (!outOfJob.units.isEmpty())
		{
			result = outOfJob.units.get(outOfJob.units.size() - 1);
			outOfJob.remove(outOfJob.units.size() - 1);
			return result;
		}
		
		for(MineralWorkers mw : mineralWorkers)
		{
			result = mw.pop();
			if (result != null)
			{
				units.remove(result);
				return result;
			}
		}
		
		return null;
	}
	
	@Override
	public void cleanup()
	{
		super.cleanup();
		for(MineralWorkers base : mineralWorkers)
			base.cleanup();
	}
	
	/**
	 * Returns the closest available worker to the position, or null if no worker is available.
	 * @param pos The position to which we want to find the closest worker.
	 * @return The worker, or null if none is available.
	 */
	public Agent pop(Position pos)
	{
		return pop(pos, false);
	}
	
	/**
	 * Returns the closest available worker to the position, or null if no worker is available.
	 * @param pos The position to which we want to find the closest worker.
	 * @param healthyOnly Only use workers with at least 20 hp.
	 * @return The worker, or null if none is available.
	 */
	public Agent pop(Position pos, boolean healthyOnly)
	{
		if (pos == null)
		{
			Agent worker = pop();
			if (worker == null)
				worker = outOfJob.pop();
			return pop();
		}
		
		
		Agent result = null;
		MineralWorkers mwResult = null;
		double distance = Double.MAX_VALUE;
		
		// Loop over all mineral workers objects to see which has the closest worker.
		for(MineralWorkers mw : mineralWorkers)
		{
			Agent newWorker = mw.pop(pos, healthyOnly);
			if (newWorker == null)
				continue;
			newWorker.drawCircle(Color.Green, 6);
			double newDist = newWorker.distanceSquared(pos);
			if (newDist < distance)
			{
				if (mwResult != null)
					mwResult.add(result);
				result = newWorker;
				mwResult = mw;
				distance = newDist;
			}
			else mw.add(newWorker);
		}

		if (result != null)
		{
			units.remove(result);
			mwResult.remove(result);
		}
		else
			result = outOfJob.pop();
		
		units.remove(result);
		
		return result;
	}

	@Override
	public void add(Agent agent)
	{
		super.add(agent);
		outOfJob.add(agent);
	}
	
	/**
	 * Register a new base, so that a mineral workers group can be added. 
	 * @param base The resource depot that is to be added.
	 */
	public void newBase(Unit base)
	{
		mineralWorkers.add(new MineralWorkers(outOfJob, base));
	}
	
	@Override
	public int takeAgent(Agent agent)
	{
		if (agent.unit.getType().isWorker())
			return 1;
		else
			return -1;
	}
}
