/*
* 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.Date;
import java.util.HashMap;

import com.tyr.agents.Agent;
import com.tyr.agents.BuildDefensive;
import com.tyr.agents.VultureAgent;
import com.tyr.agents.WorkerAgent;
import com.tyr.buildingplacement.BuildCommand;
import com.tyr.buildingplacement.DefensiveStructures;
import com.tyr.buildingplacement.SpaceManager;
import com.tyr.buildingplacement.WallOff;
import com.tyr.builds.BuildOrder;
import com.tyr.builds.DefensiveTerran;
import com.tyr.builds.ProtossTech;
import com.tyr.builds.StandardZerg;
import com.tyr.builds.TeamMech;
import com.tyr.builds.TvPGeneric;
import com.tyr.builds.ZealotPush;
import com.tyr.tasks.TaskManager;
import com.tyr.unitgroups.BuilderGroup;
import com.tyr.unitgroups.Bunkers;
import com.tyr.unitgroups.ComsatNetwork;
import com.tyr.unitgroups.DefendingWorkers;
import com.tyr.unitgroups.OutOfJob;
import com.tyr.unitgroups.ProductionStructures;
import com.tyr.unitgroups.ScoutGroup;
import com.tyr.unitgroups.HomeGroup;
import com.tyr.unitgroups.UnderConstruction;
import com.tyr.unitgroups.UnitGroup;
import com.tyr.unitgroups.WorkerGroup;
import com.tyr.unitgroups.WraithSwarm;

import bwapi.Color;
import bwapi.DefaultBWListener;
import bwapi.Game;
import bwapi.Mirror;
import bwapi.Player;
import bwapi.Position;
import bwapi.Race;
import bwapi.TilePosition;
import bwapi.Unit;
import bwapi.UnitType;
import bwapi.WeaponType;
import bwta.BaseLocation;
import bwta.Chokepoint;
import bwta.Region;


/**
 * This is the main class, implementing the basic structure of the bot.
 * @author Simon
 *
 */
public class Tyr extends DefaultBWListener 
{
	/**
	 * The BWAPI Mirror for connecting to the game of Brood War.
	 */
	private Mirror mirror = new Mirror();

	/**
	 * The BWAPI Game, for getting information about the current game state.
	 */
	public static Game game;
	
	/**
	 * The Player object representing this bot in the game.
	 */
	public static Player self;
	
	/**
	 * The amount of minerals currently reserved for buildings.
	 */
	public int reservedMinerals;

	/**
	 * The amount of gas currently reserved for buildings.
	 */
	public int reservedGas;
	
	/**
	 * Are we in testing mode?
	 */
	public static boolean testMode = true;
	
	/**
	 * List of all known BaseLocations that are not starting locations.
	 * 
	 * If we are not sure yet whether a base is a starting location for an enemy, (because we haven't scouted it yet), then the base is not included in this list.
	 * 
	 * While BWTA is still initialising we do not know the base locations, so this list will be empty until initialisation is done.
	 */
	public ArrayList<BaseLocation> expands;
	
	/**
	 * List of all starting location where the enemy may have started.
	 * As we scout these and find out whether or not they are the enemy's starting base, we update the list.
	 */
	public ArrayList<BaseLocation> suspectedEnemy;
	
	/**
	 * List of commands for buildings that are to be built.
	 * As soon as building starts, the command is removed from the list, even though the building has not finished yet.
	 */
	public ArrayList<BuildCommand> buildCommands;
	
	/**
	 * List of locations where we have defensive structures.
	 * The defensive structures class also keeps track of what structures exist at each location.
	*/
	public ArrayList<DefensiveStructures> defensiveStructures;
	
	/*
	 * All the different unit groups.
	 * Each unit group manages a certain group of our units.
	 */
	public HomeGroup homeGroup;
	public ComsatNetwork comsatNetwork;
	public WraithSwarm swarm;
	public Bunkers bunkers;
	public ScoutGroup scout;
	public DefendingWorkers militia;
	public WorkerGroup workForce;
	public BuilderGroup builders;
	public ProductionStructures production;
	public UnderConstruction underConstruction;
	public OutOfJob hobos;
	
	/**
	 * List of all the unit groups.
	 */
	public ArrayList<UnitGroup> groups;
	
	/**
	 * Keeps track of building placements and allows you to find new positions for buildings.
	 * Also helps in finding positions for defending Siege Tanks to siege up.
	 */
	public SpaceManager spaceManager;
	
	/**
	 * Keeps track of which parts of the map have been seen at some point.
	 * When the enemy's base is destroyed but he still has buildings somewhere on the map, this class takes over control of units to try to find them.
	 */
	public Scanner scanner;
	
	/**
	 * Helps finding a walloff placement and helps managing it.
	 * WARNING: this system has not been used in any recent build and may no longer work.
	 */
	public WallOff wallOff;
	
	/**
	 * Manages the tasks, such as defending the main base and attack or harassing the enemy.
	 */
	public TaskManager taskManager;
	
	/**
	 * The buildorder that is being executed.
	 */
	public BuildOrder build;
	
	/**
	 * Static reference to the Singleton Tyr object.
	 */
	public static Tyr bot;
	
	/**
	 * Stopwatch for measuring the amount of time various parts of the program take.
	 */
	private StopWatch stopWatch = new StopWatch();
	
	/**
	 * The amount of wins as recorded in the /read/ folder.
	 */
	public int wins = 0;

	/**
	 * The amount of losses as recorded in the /read/ folder.
	 */
	public int losses = 0;
	
	/**
	 * Is this the second time we are performing the startup for a game since the program was launched?
	 */
	public boolean restart = false;
	
	/**
	 * Have we already left the game?
	 */
	private boolean hasleft = false;
	
	/**
	 * Class that tries to determine what strategy the opponent is using.
	 * This is only used in the recording in the /read/ folder.
	 * For in game purposes, the Scout class is used.
	 */
	StrategyDetector strategyDetector = new StrategyDetector();

	public void run() {
		
		mirror.getModule().setEventListener(this);
		mirror.startGame();
	}
	
	/**
	 * The mapping that maps UnitIDs to agents.
	 */
	public HashMap<Integer, Agent> agentMap = new HashMap<Integer, Agent>();
	
	@SuppressWarnings("deprecation")
	@Override
	public void onStart() 
	{
		try
		{

			if (testMode)
				DebugMessages.testGameStart();
			
		BWTAProxy.initialized = false;
		expands = new ArrayList<BaseLocation>();
		scanner = null;
		spaceManager = null;
		suspectedEnemy = new ArrayList<BaseLocation>();
		buildCommands = new ArrayList<BuildCommand>();
		defensiveStructures = new ArrayList<DefensiveStructures>();
		reservedGas = 0;
		reservedMinerals = 0;
		
		groups = new ArrayList<UnitGroup>();
		EnemyManager.getManager().reset();
		
		// Get the classes to retrieve information from BWAPI.
		game = mirror.getGame();
		self = game.self();
		Date now = new Date();
		DebugMessages.log((now.getYear() + 1900) + "-" + (now.getMonth()+1) + "-" + now.getDate() + " " + (now.getHours()>9?"":"0") + now.getHours() + ":" + (now.getMinutes()>9?"":"0") + now.getMinutes() + ":" + (now.getSeconds()>9?"":"0") + now.getSeconds()
				+ " Game started against " + DebugMessages.getEnemyName() + "(" + game.enemy().getRace() + ")");

		Strategy.initialize();
		
		if (!restart)
		{
			BWTAProxy.readMap();
			
			// Problem with reading the map asynchronously, now map is read synchronously.
			new BWTAProxy().run();
			
			// Start the thread that will asynchronously initialize BWTA.
			//Thread t = new Thread(new BWTAProxy());
			//t.start();
		}
		else
			Scanner.restart = true;
		
		game.setLocalSpeed(10);
		
		try
		{
			spaceManager = new SpaceManager();
			scanner = new Scanner();
			wallOff = null;
			taskManager = new TaskManager();
			
			// Determine the correct build to use.
			// This build may later be overriden by player profiles.
			if (game.enemies().size() > 1 && self.getRace() == Race.Terran)
				build = new TeamMech();
			else if (self.getRace() == Race.Terran)
			{
				if (game.enemy().getRace() == Race.Zerg)
					build = new DefensiveTerran();
				else if (game.enemy().getRace() == Race.Protoss)
					build = new DefensiveTerran();
				else if (game.enemy().getRace() == Race.Terran)
					build = new DefensiveTerran();
				else
					build = new DefensiveTerran();
			}
			else if (self.getRace() == Race.Protoss)
				build = new ZealotPush(true, false);
			else
				build = new StandardZerg();
			
			// Determine the correct army size.
			int requiredArmySize = -1;
			if (self.getRace() == Race.Zerg)
				requiredArmySize = 5;
			else if (self.getRace() == Race.Protoss)
				requiredArmySize = 15;
			else if(game.enemy().getRace() == Race.Zerg)
				requiredArmySize = 15;
			else if(game.enemy().getRace() == Race.Protoss)
				requiredArmySize = 40;
			else
				requiredArmySize = 30;
			
			Settings.setRequiredSize(requiredArmySize);
			
			int maxArmySize = -1;
			if (self.getRace() == Race.Zerg)
				maxArmySize = 5;
			else if (self.getRace() == Race.Protoss)
				maxArmySize = 30;
			else if(game.enemy().getRace() == Race.Terran)
				maxArmySize = 60;
			else if (game.enemy().getRace() == Race.Zerg)
				maxArmySize = 30;
			else
				maxArmySize = 60;
			
			Settings.setMaximumSize(maxArmySize);
			
			// Initialize unit groups.
			hobos = new OutOfJob();
			bunkers = new Bunkers(hobos);
			underConstruction = new UnderConstruction(hobos);
			production = new ProductionStructures(hobos);
			builders = new BuilderGroup(hobos);
			militia = new DefendingWorkers(hobos);
			workForce = new WorkerGroup(hobos);
			homeGroup = new HomeGroup(hobos);
			comsatNetwork = new ComsatNetwork(hobos);
			scout = new ScoutGroup(hobos);
			swarm = new WraithSwarm(hobos);
			
			
			groups.add(hobos);
			groups.add(bunkers);
			groups.add(underConstruction);
			groups.add(production);
			groups.add(builders);
			groups.add(militia);
			groups.add(workForce);
			groups.add(comsatNetwork);
			groups.add(scout);
			groups.add(swarm);
			groups.add(homeGroup);
			
			ArrayList<String> records = DebugMessages.readFile();
			for(String s : records)
			{
				if (s.startsWith("win"))
					wins++;
				else
					losses++;
			}
			
			boolean match = false;
			// Allow player profiles to override the build order against specific opponenets.
			if (game.enemies().size() == 1)
			{
				ArrayList<PlayerProfile> profiles = PlayerProfile.getProfiles();
				for(PlayerProfile pp : profiles)
					if (pp.match(game, this))
					{
						match = true;
						break;
					}
			}
			
			if (!match)
				Strategy.determine().set();
			
			build.initialize(game, self, bot);
			
			game.sendText("Good luck, have fun!!! :D");
		}
		catch(Exception e)
		{
			DebugMessages.addMessagePermanent("Error starting up: " + e.toString());
			e.printStackTrace();
			quickStartup();
			throw e;
		}
		restart = true;
		
		}catch (Throwable t)
		{
			DebugMessages.log(t.getMessage());
			for (StackTraceElement elem : t.getStackTrace())
				DebugMessages.log(elem.toString());
		}
	}
	
	/**
	 * A shortened startup method.
	 * This only starts the barest necessary systems.
	 * This is called when the regular startup fails, in hope of recovering enough to still be able to play.
	 */
	public void quickStartup()
	{
		DebugMessages.log("performing quickStartup()");
		DebugMessages.addMessagePermanent("Error starting, doing quick startup.");
		System.out.println("Error starting, doing quick startup.");
		
		groups = new ArrayList<UnitGroup>();
		
		spaceManager = new SpaceManager();
		scanner = new Scanner();
		wallOff = null;
		taskManager = new TaskManager();
		
		// Initialize basic build order.
       	if (self.getRace() == Race.Terran)
        	build = new TvPGeneric();
       	else if (self.getRace() == Race.Protoss)
       		build = new ProtossTech();
       	else
       		build = new StandardZerg();
       	
       	Settings.setRequiredSize(20);
       	Settings.setMaximumSize(40);
       	
       	// Initializes unit groups.
       	hobos = new OutOfJob();
       	bunkers = new Bunkers(hobos);
       	underConstruction = new UnderConstruction(hobos);
       	production = new ProductionStructures(hobos);
       	builders = new BuilderGroup(hobos);
       	militia = new DefendingWorkers(hobos);
       	workForce = new WorkerGroup(hobos);
       	homeGroup = new HomeGroup(hobos);
       	comsatNetwork = new ComsatNetwork(hobos);
       	scout = new ScoutGroup(hobos);
       	swarm = new WraithSwarm(hobos);
       	
       	
       	groups.add(hobos);
       	groups.add(bunkers);
       	groups.add(underConstruction);
       	groups.add(production);
       	groups.add(builders);
       	groups.add(militia);
       	groups.add(workForce);
       	groups.add(comsatNetwork);
       	groups.add(scout);
       	groups.add(swarm);
       	groups.add(homeGroup);
       	
       	build.initialize(game, self, bot);
	}
	
	/**
	 * Returns the available minerals.
	 * This is the amount of minerals the player currently has, minus the amount he has reserved for building buildings.
	 * @return The amount of available minerals.
	 */
	public int getAvailableMinerals()
	{
		return self.minerals() - reservedMinerals;
	}
	
	/**
	 * Returns the available gas.
	 * This is the amount of gas the player currently has, minus the amount he has reserved for building buildings.
	 * @return The amount of available gas.
	 */
	public int getAvailableGas()
	{
		return self.gas() - reservedGas;
	}
	
	long initTime;
	long buildTime;
	long unitGroupsTime;
	long taskManagerTime;
	long agentTime;
	
	Position natural;
	Region natRegion = null;
	
	@Override
	public void onFrame() 
	{
		try
		{
		if (game.isReplay())
			return;

		if (game.getFrameCount() >= 1000)
		{
			if (natural == null)
				natural = SpaceManager.getNatural();
			drawCircle(natural, Color.Purple, 100);
		}
		if (game.getFrameCount() >= 1000)
		{
			if (natRegion == null)
			{
				final Position nat = SpaceManager.getNatural();
				natRegion = nat == null ? null : BWTAProxy.getRegion(nat);
			}
		}
		
		if (natRegion != null)
			for (Chokepoint choke : natRegion.getChokepoints())
				drawCircle(choke.getCenter(), Color.Blue, 64);
		
		DebugMessages.addMessage("Map name: " + game.mapFileName());
		
		boolean canAttackGround = false;
		boolean haveWorker = false;
		boolean haveCC = false;
		for (Unit unit : self.getUnits())
		{
			if (unit.getType().groundWeapon() != WeaponType.None && !unit.getType().isBuilding())
				canAttackGround = true;
			if (unit.getType().isWorker())
				haveWorker = true;
			if (unit.getType().isResourceDepot())
				haveCC = true;
		}
		
		if (!hasleft && !canAttackGround && !haveWorker && (!haveCC || self.minerals() < 50))
		{
			hasleft = true;
			game.sendText("Good game, well played!");
			game.leaveGame();
		}
		
		stopWatch.start();
		
		if (BWTAProxy.initialized && game.getFrameCount() >= 10)
			drawCircle(SpaceManager.getMainExit(), Color.Green, 128);
		
		// Let all unit groups perform their cleanup.
		for(UnitGroup group : groups)
			group.cleanup();
		
		// Defensive structures also have to be cleaned.
		for(int i=0; i<defensiveStructures.size(); i++)
		{
			if (defensiveStructures.get(i).disabled)
			{
				defensiveStructures.remove(i);
				i--;
			}
		}
		
		// Divide the new units over the various unit groups.
		for(int i = hobos.units.size() - 1; i >= 0; i--)
		{
			Agent agent = hobos.units.get(i);
			if (!agent.unit.isCompleted() && !agent.unit.getType().isBuilding())
				continue;
			
			// find the unit group that wants to take the unit with the highest priority.
			int topPrio = -1;
			UnitGroup taker = null;
			for(UnitGroup group : groups)
			{
				int myPrio = group.takeAgent(agent); 
				if (myPrio > topPrio)
				{
					taker = group;
					topPrio = myPrio;
				}
			}
			
			// Assign the agent to the group with the highest priority.
			if (taker != null)
			{
				taker.add(agent);
				hobos.units.remove(i);
				continue;
			}
		}

		
		EnemyManager.getManager().update();
		
		spaceManager.onFrame(game, self, this);
		scanner.onFrame(game, self, this);
		strategyDetector.onFrame(game, self, bot);
		
		
		initTime = stopWatch.time();
		
		build.onFrame(game, self, this);
		
		buildTime = stopWatch.time();
		
		
		for(DefensiveStructures structures : defensiveStructures)
			structures.onFrame(game, self, this);

		for(UnitGroup group : groups)
			group.onFrame(game, self, this);
		
		unitGroupsTime = stopWatch.time();
		
		taskManager.onFrame(game, self, bot);
		
		taskManagerTime = stopWatch.time();
		
		// Let the agents perform their onFrame methods.
		for(Unit unit : self.getUnits())
		{
			Agent agent = agentMap.get(unit.getID());
			agent.getCommand().execute(game, self, bot);
			
			if (agent.unit.getType().isWorker())
				((WorkerAgent)agent).onFrame(game, self, bot);
			else if (agent instanceof VultureAgent)
				((VultureAgent)agent).onFrame(game, self, bot);
		}
		
		agentTime = stopWatch.time();
		
		// Display some debug messages.
		DebugMessages.addMessage("Available Resources: " + this.getAvailableMinerals() + " - " + this.getAvailableGas());
		DebugMessages.addMessage("Workers: " + (workForce.units.size() + builders.units.size()));
		if (suspectedEnemy.size() != 1)
			DebugMessages.addMessage("Suspected enemy bases: " + suspectedEnemy.size());
		DebugMessages.addMessage("Bases: " + (workForce.mineralWorkers.size()));
		DebugMessages.addMessage("Frame count: " + game.getFrameCount());
		
		DebugMessages.addMessage("Neutral structures: " + EnemyManager.getManager().neutralBuildingMemory.size());
		
		if (!BWTAProxy.initialized)
			DebugMessages.addMessage("Initializing BWTA.");
		if (initTime >= 55)
			DebugMessages.addMessage("Initialization Time: " + initTime);
		if (buildTime >= 55)
			DebugMessages.addMessage("Build Time: " + buildTime);
		if (unitGroupsTime >= 55)
			DebugMessages.addMessage("Unit Groups Time: " + unitGroupsTime);
		if (taskManagerTime >= 55)
			DebugMessages.addMessage("Task Manager Time: " + taskManagerTime);
		if (agentTime >= 55)
			DebugMessages.addMessage("Agent Time: " + agentTime);
		
		for(Unit u : self.getUnits())
			if (agentMap.get(u.getID()).mark)
				agentMap.get(u.getID()).drawCircle(Color.Red, 4);
		
		for (BaseLocation loc : suspectedEnemy)
			drawCircle(loc.getPosition(), Color.Red, 16);
		
		DebugMessages.toScreen();
		
		if(game.getFrameCount() <= 300)
			game.drawTextScreen(230, 210, "Good luck, have fun!!! :D");


		}catch(Throwable t)
		{
			logThrowable(t);
			System.out.print(t.toString());
			t.printStackTrace();

			game.drawTextScreen(10, 85, t.getMessage());
			throw t;
		}
	}
	
	public void logThrowable(Throwable t)
	{
		DebugMessages.log(t.toString());
		for (StackTraceElement elem : t.getStackTrace())
			DebugMessages.log(elem.toString());
		if (t.getCause() != null)
			logThrowable(t);
	}
	
	@Override
	public void onUnitCreate(Unit unit) 
	{
		if (unit.getPlayer() != self)
			return;
		
		// When a unit is created, we create an Agent wrapper.
		Agent agent = Agent.createAgent(unit);
		agentMap.put(unit.getID(), agent);
		
		// The Agent is added to the OutOfJob unit group.
		// It will eventually be added to its own unit group in the next onFrame().
		hobos.add(agent);
		if (unit.getType() == UnitType.Terran_Bunker
				|| unit.getType() == UnitType.Terran_Missile_Turret)
		{
			// Defensive structures need to be added to the correct defensive structures unit group.
			DefensiveStructures structures = null;
			for (Unit builder : self.getUnits())
			{
				if (!builder.getType().isWorker())
					continue;
				if (builder.getBuildUnit() != unit)
					continue;
				Agent builderAgent = agentMap.get(builder.getID());
				if (builderAgent.getCommand() == null)
					continue;
				
				if (!builderAgent.getCommand().getClass().equals(BuildDefensive.class))
					continue;
				
				
				
				structures = ((BuildDefensive)builderAgent.getCommand()).defensePos;
				break;
			}
			if (structures != null)
				structures.add(unit);
			else
				System.out.println("No matching BuildDefensive command found.");
		}
		else if(unit.getType().isResourceDepot())
			workForce.newBase(unit);
	}
	
	@Override
	public void onUnitMorph(Unit unit) 
	{
		// Refineries are not created, they are morphed.
		if (unit.getType() == UnitType.Terran_Refinery 
				|| unit.getType() == UnitType.Protoss_Assimilator
				|| unit.getType() == UnitType.Zerg_Extractor)
		{
			Agent agent = Agent.createAgent(unit);
			agentMap.put(unit.getID(), agent);
			
			hobos.add(agent);
		}
	}
	
	@Override
	public void onUnitDestroy(Unit unit)
	{
		EnemyManager.getManager().died(unit);
	}
	
	@Override
	public void onUnitRenegade(Unit unit)
	{
		EnemyManager.getManager().died(unit);
	}
	
	@Override
	public void onEnd(boolean win)
	{
		// Save the result of the game.
		DebugMessages.saveMessage((win?"win":"loss") + " " + strategyDetector.opponentStrategy + (Strategy.chosenStrategy == null?"":" "
				+ Strategy.chosenStrategy.code));
		
		Strategy.writeFlags();
		
		if (testMode)
		{
			DebugMessages.testGameEnd(win);
			System.exit(0);
		}
	}
	
	/**
	 * Draws a circle on the map.
	 * @param position The position where the circle should be drawn.
	 * @param color The color for the circle.
	 * @param r The radius of the circle.
	 */
	public static void drawCircle(Position position, Color color, int r)
	{
		if (position != null)
			game.drawCircleMap(position.getX(),
					position.getY(),
					r, color);
	}

	/**
	 * Draws a circle on the map.
	 * @param position The position where the circle should be drawn.
	 * @param color The color for the circle.
	 */
	public static void drawCircle(Position position, Color color)
	{
		drawCircle(position, color, 10);
	}
	
	/**
	 * Convert a TilePosition to a Position.
	 * @param pos The TilePosition to convert.
	 * @return The converted position.
	 */
	public static Position tileToPosition(TilePosition pos)
	{
		return new Position(pos.getX()*32+16, pos.getY()*32+16);
	}
	
	/**
	 * Convert a Position to a TilePosition.
	 * @param pos The position to convert.
	 * @return The converted TilePosition.
	 */
	public static TilePosition positionToTile(Position pos)
	{
		return new TilePosition(pos.getX()/32, pos.getY()/32);
	}
	
	/**
	 * The static main entrypoint for the program.
	 * @param args
	 */
	public static void main(String[] args)
	{
		DebugMessages.log("\nSTARTING NEW GAME.");
		bot = new Tyr();
		bot.run();
	}

	public static Position getStartLocation()
	{
		return tileToPosition(self.getStartLocation());
	}
}