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

import bwapi.Game;
import bwapi.Player;
import bwapi.TilePosition;
import bwapi.UnitType;


/**
 * This class helps find remaining enemy buildings after we have destroyed all the ones we have previously seen.
 * @author Simon
 *
 */
public class Scanner 
{
	/**
	 * The state of the map.
	 * This array registers for each tile of the map during which scan it was last visited. 
	 */
	private int[][] state;
	
	/**
	 * The number of scans of the map have we had.
	 */
	private int scans = 0;
	
	/**
	 * The row we are currently scanning.
	 */
	private int row = 0;
	
	/**
	 * The number of tiles we have not yet seen this scan.
	 */
	private int unseen = 0;
	
	/**
	 * The number of rows we process during a single frame.
	 */
	private final int rowsPerStep = 2;

	public final static int Initializing = 0;
	public final static int Scanning = 1;
	public final static int Eliminating = 2;
	
	/**
	 * The action the scanner is currently performing.
	 */
	public int action = Initializing;
	
	/**
	 * The width of the map.
	 */
	private int width;
	
	/**
	 * The height of the map.
	 */
	private int height;
	
	/**
	 * The ground units the scanner is allowed to use to detect the remaining enemy buildings.
	 */
	private ArrayList<Agent> groundUnits = new ArrayList<Agent>();
	
	/**
	 * The air units the scanner is allowed to use to detect the remaining enemy buildings.
	 */
	private ArrayList<Agent> airUnits = new ArrayList<Agent>();
	
	/**
	 * Has a new game been started without closing Starcraft since the last game?
	 */
	public static boolean restart = false;
	
	/**
	 * This class helps find remaining enemy buildings after we have destroyed all the ones we have previously seen.
	 */
	public Scanner()
	{
		state = new int[Tyr.game.mapWidth()][];
		this.width = Tyr.game.mapWidth();
		this.height = Tyr.game.mapHeight();
	}
	
	/**
	 * This method is assumed to be called each frame.
	 * @param game The current state of the game.
	 * @param self The current player.
	 * @param bot The bot.
	 */
	public void onFrame(Game game, Player self, Tyr bot)
	{
		DebugMessages.addMessage("Scanner units: " + (groundUnits.size() + airUnits.size()));
		
		if (!restart && !BWTAProxy.initialized)
			return;
		for(int i=0; i<rowsPerStep; i++)
		{
			if (action == Initializing)
				initialize(game, self, bot);
			else if (action == Scanning && !restart)
				scan(game, self, bot);
			else if (action == Eliminating || restart)
				eliminate(game, self, bot);
			
			if (incrementRow())
			{
				if (unseen == 0)
					scans++;
				unseen = 0;
			}
		}
			
	}
	
	/**
	 * Move the row counter to the next row.
	 * If we come to the end of the map, we move the counter back to zero.
	 * @return Whether we have moved the counter back to zero.
	 */
	private boolean incrementRow()
	{
		row++;
		if (row >= width)
			row = 0;
		return row == 0;
	}
	
	/**
	 * Perform an initialization step.
	 * @param game The current state of the game.
	 * @param self The current player.
	 * @param bot The bot.
	 */
	private void initialize(Game game, Player self, Tyr bot)
	{
			state[row] = new int[height];
			if (row >= width - 1)
				action = Scanning;
	}
	
	/**
	 * Perform a scan step.
	 * During the scan step we see if there are any newly visible tiles.
	 * @param game The current state of the game.
	 * @param self The current player.
	 * @param bot The bot.
	 */
	private void scan(Game game, Player self, Tyr bot)
	{
		for(int y=0; y < height; y++)
		{
			if (state[row][y] == scans)
				continue;
			if (game.isVisible(new TilePosition(row, y)))
				state[row][y] = scans;
			else
				unseen++;
		}
	}
	
	/**
	 * Perform an elimination step.
	 * During the elimination step, we order units to move to not yet visible tiles to see if we can find the remaining enemy buildings.
	 * @param game The current state of the game.
	 * @param self The current player.
	 * @param bot The bot.
	 */
	private void eliminate(Game game, Player self, Tyr bot)
	{
		scan(game, self, bot);
		
		if (EnemyManager.getManager().getInvader() != null || EnemyManager.getManager().enemyBuildingMemory.size() > 0)
			return;
		
		// Siege tanks should always unsiege. 
		for(Agent agent : groundUnits)
			if (agent.unit.getType() == UnitType.Terran_Siege_Tank_Siege_Mode)
				agent.unit.unsiege();
		
		int groundPos = nextGroundPos(-1);
		int airPos = nextAirPos(-1);
		for(int y=0; y < height; y++)
		{
			if (state[row][y] == scans)
				continue;
			
			boolean walkable = false;
			
			for(int dx = 0; !walkable && dx<4; dx++)
				for(int dy=0; !walkable && dy<4; dy++)
					if(game.isWalkable(row*4+dx, y*4+dy))
					{
						walkable = true;
						break;
					}
			
			// Ground units are sent to walkable tiles, air units to unwalkable ones.
			if (walkable)
			{
				if (groundPos < groundUnits.size())
				{
					Agent agent = groundUnits.get(groundPos);
					agent.order(new None(agent));
					agent.unit.attack(Tyr.tileToPosition(new TilePosition(row, Math.min(y+2, game.mapHeight()))));
					groundPos = nextGroundPos(groundPos);
					y+=5;
				}
			}
			else
			{
				if (airPos < airUnits.size())
				{
					Agent agent = airUnits.get(airPos);
					agent.order(new None(agent));
					agent.unit.attack(Tyr.tileToPosition(new TilePosition(row, Math.min(y+2, game.mapHeight()))));
					airPos = nextAirPos(airPos);
					y+=5;
				}
			}
		}
	}
	
	/**
	 * Get the next ground unit that is free to be ordered to scout a new tile.
	 * @param i The position in the list we are at now.
	 * @return The position in the list of the next ground unit we are allowed to give an order.
	 */
	private int nextGroundPos(int i) 
	{
		for(i++; i<groundUnits.size() && !groundUnits.get(i).unit.isIdle(); i++);
		return i;
	}
	
	/**
	 * Get the next air unit that is free to be ordered to scout a new tile.
	 * @param i The position in the list we are at now.
	 * @return The position in the list of the next air unit we are allowed to give an order.
	 */
	private int nextAirPos(int i) 
	{
		for(i++; i<airUnits.size() && !airUnits.get(i).unit.isIdle(); i++);
		return i;
	}
	
	/**
	 * This method is called when we go from scanning mode to elimination mode.
	 * This allows units to be added to the scanner.
	 */
	public void startElimination() 
	{
		if (action != Eliminating)
		{
			action = Eliminating;
			
			Settings.scannerEliminating();
			
			if (UnitTracker.count(UnitType.Terran_Factory) == 0)
				Tyr.bot.spaceManager.build(UnitType.Terran_Factory);
			if(UnitTracker.count(UnitType.Terran_Refinery) == 0)
				Tyr.bot.spaceManager.build(UnitType.Terran_Refinery);
			if(UnitTracker.count(UnitType.Terran_Starport) == 0)
				Tyr.bot.spaceManager.build(UnitType.Terran_Starport);
		}
	}
	
	/**
	 * Add an agent as an air unit.
	 * @param agent An air unit to be added.
	 */
	public void addAir(Agent agent) 
	{
		if (agent != null)
		{
			airUnits.add(agent);
			agent.order(new None(agent));
		}
	}
	
	/**
	 * Add an agent as a ground unit.
	 * @param agent A ground unit to be added.
	 */
	public void addGround(Agent agent) 
	{
		if (agent != null)
		{
			groundUnits.add(agent);
			agent.order(new None(agent));
		}
	}
	
	/**
	 * Add an agent.
	 * @param agent An agent to be added.
	 */
	public void add(Agent agent) 
	{
		if (agent.unit.getType().isFlyer())
			addAir(agent);
		else
			addGround(agent);
		
	}

	/**
	 * Clears the scanner, releasing all units to the rest of the program.
	 * @param rejects The OutOfJob to which the units will be released.
	 */
	public void clear(OutOfJob rejects) 
	{
		for(Agent agent : this.groundUnits)
			rejects.add(agent);
		this.groundUnits = new ArrayList<Agent>();
		
		for(Agent agent : this.airUnits)
			rejects.add(agent);
		this.airUnits = new ArrayList<Agent>();
	}
	
	/**
	 * The number of ground units registered with the scanner.
	 * @return The number of ground units registered with the scanner.
	 */
	public int getGroundUnitCount() 
	{
		return groundUnits.size();
	}
}
