/*
 * Created on 23.11.2004
 *
 */
package de.farafin.snEADy;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.util.Random;

import de.farafin.snEADy.communication.D_GameInfo;
import de.farafin.snEADy.communication.D_PlayerData;
import de.farafin.snEADy.communication.D_RecoverData;
import de.farafin.snEADy.communication.I_Constants;
import de.farafin.snEADy.player.Player;
import de.farafin.snEADy.player.*;
import de.farafin.snEADy.inOut.C_LogFileWriter;

/**
 * M_PlayerHandler manages the player that should play the game. it is necessary to controll the threads
 * and who is next to calculate.
 * 
 * @author lars, roland
 *  
 * @version $Revision: 1.54 $
 */
public final class M_PlayerHandler implements I_Constants
{
	//-------------------------------------------------------------------------------------
	//- internal Classes ------------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	
	/** informations for the player
	 * 
	 * @author roland, lars 
	 */
	private class PlayerInfo
	{
		/** */
		public GameInfo playerGameInfo = null;
		/** */
		public LevelInfo playerLevelInfo = null;
		/** */
		public SnakeInfo[] playerSnakeInfo = null;
		/** */
		public OwnSnakeInfo playerOwnSnakeInfo = null;
	}
	
	
	/** realize the player handling
	 * TODO roland: make it more effective: dont need to make a new insrtance every time!!
	 * @author lars, roland
	 */
	private class PlayerRun implements Runnable
	{
		/** the player that should calculate its move */
		public Player player;
		
		/** the information the player needs for his moove */
		public final PlayerInfo playerInfo; 
		
		/** is set true after the player has finished his calculation */
		public boolean calculationFinished = false;
		
		/** if the player threw an exception, its noticed and the player is deleted */
		public boolean threwException = false;
		
		/** if the player threw an exception, it is stored here fore logging. */
		public Throwable thrownException;
		
		/** create Constructor
		 * @param player
		 * @param playerInfo
		 */
		public PlayerRun(Player player, PlayerInfo playerInfo)
		{
			super();
			this.player = player;
			this.playerInfo = playerInfo;
			this.calculationFinished = false;
			this.threwException = false;
		}
		
		/* (non-Javadoc)
		 * @see java.lang.Runnable#run()
		 */
		public void run()
		{
			this.calculationFinished = false;
			this.threwException = false;
			playerCalculationtime = -System.currentTimeMillis();
			try
			{
				this.player.startCalculation(this.playerInfo.playerGameInfo, this.playerInfo.playerLevelInfo, this.playerInfo.playerSnakeInfo, this.playerInfo.playerOwnSnakeInfo);
			}
			catch(ThreadDeath e)
			{throw e;}
			catch(Throwable e)
			{
				e.printStackTrace();
				this.threwException = true;
				this.thrownException = e;
			}
			
			playerCalculationtime += System.currentTimeMillis();
						
			this.calculationFinished = true;
		}
	}
	
	//-------------------------------------------------------------------------------------
	//- attributes ------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	// player attributes
	/** Instances of the Player-Classes. Its a reference copy from GameEngine. */
	private Player[] playerInstances;
			
	/** Index of the last player who was calculated. */
	private int lastPlayer;

	/** the thread of the player who is actually calculated */
	private PlayerRun playerRun;
	
	/** the thread of the player who is actually calculated */
	private Thread playerThread;
	
	// others
	/** Comment for <code>inst</code> */
	private static M_PlayerHandler instance;
	
	/** flag if game was imergency paused */
	private boolean imergencyPaused = false;
	
	/** a prototype of playerInfo, so it doesnt need to be a new instance constructed every time. */
	private final PlayerInfo playerInfo;
		
	/** mesures the time a player needs to calculate its moove. */
	private long playerCalculationtime = 0;
	
	/** a logfile to safe the players moves */
	private C_LogFileWriter playerLog;

	/** random number generator for controll time */
	private Random randNumber = null;
	
	/** the gameTime when the next time the size of player should be checked */
	private long nextSizeControllTime = 0;

	/** for storing the information of which player should be controlled the space nextMemCheck */
	private int nextMemCheck = 0;

	/** parameter set of GameParameter */
	private final GameParameter parameter;

	//-------------------------------------------------------------------------------------
	//- constructor -----------------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	/** default constructor
	 */
	private M_PlayerHandler()
	{
		this.playerInstances = null;
		this.parameter = M_Main.getInstance().getParameter();
		this.imergencyPaused = false;
		this.playerThread = null;
		this.lastPlayer = -1;
		this.playerInfo = new PlayerInfo();
		this.playerLog = null;
	}

	//-------------------------------------------------------------------------------------
	//- private methods -------------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	
	/** regenerate Player Info. it is no new instance build, but if the length some
	 * arrays doesnt fit, it will be renewed.
	 * 
	 * @param playerIndex
	 * @param gameInfo
	 */
	private void reGenPlayerInfo(int playerIndex, D_GameInfo gameInfo)
	{
		int j = 0;
		
		if(this.playerInfo.playerSnakeInfo == null || this.playerInfo.playerSnakeInfo.length != gameInfo.playerData.length-1)
		{
			this.playerInfo.playerSnakeInfo = new SnakeInfo[this.playerInstances.length-1];
			for(int i=0; i < this.playerInfo.playerSnakeInfo.length; i++)
			{
				this.playerInfo.playerSnakeInfo[i] = new SnakeInfo();
			}
		}
		for(int i = 0; i < this.playerInfo.playerSnakeInfo.length+1; i++)
		{
			if(i==playerIndex)
			{
				j = 1;
				continue; 
			}
			//if(DEBUG) System.out.println("DEBUG M_PlayerHandler: \n\ti:" + i + "\n\tj:" + j + "\n\tp:" + this.playerInfo.otherSnakes[i-j] + "\n\tg:" + gameInfo.playerData[i]);
			this.playerInfo.playerSnakeInfo[i-j].playerName = gameInfo.playerData[i].name;
			this.playerInfo.playerSnakeInfo[i-j].snakeChar = gameInfo.playerData[i].ownChar;
			this.playerInfo.playerSnakeInfo[i-j].headPosLine = gameInfo.playerData[i].headPos.y;
			this.playerInfo.playerSnakeInfo[i-j].headPosRow = gameInfo.playerData[i].headPos.x;
			this.playerInfo.playerSnakeInfo[i-j].points = gameInfo.playerData[i].killPoints;
			this.playerInfo.playerSnakeInfo[i-j].snakeLength = gameInfo.playerData[i].length;
			this.playerInfo.playerSnakeInfo[i-j].snakeStatus = gameInfo.playerData[i].snakeStatus;
			this.playerInfo.playerSnakeInfo[i-j].waitCycles = gameInfo.playerData[i].waitCycles;
		}
		
		if(this.playerInfo.playerOwnSnakeInfo == null) this.playerInfo.playerOwnSnakeInfo = new OwnSnakeInfo();  
		this.playerInfo.playerOwnSnakeInfo.playerName = gameInfo.playerData[playerIndex].name;
		this.playerInfo.playerOwnSnakeInfo.snakeChar = gameInfo.playerData[playerIndex].ownChar;
		this.playerInfo.playerOwnSnakeInfo.headPosLine = gameInfo.playerData[playerIndex].headPos.y;
		this.playerInfo.playerOwnSnakeInfo.headPosRow = gameInfo.playerData[playerIndex].headPos.x;
		this.playerInfo.playerOwnSnakeInfo.points = gameInfo.playerData[playerIndex].killPoints;
		this.playerInfo.playerOwnSnakeInfo.snakeLength = gameInfo.playerData[playerIndex].length;
		this.playerInfo.playerOwnSnakeInfo.waitCycles = gameInfo.playerData[playerIndex].waitCycles;
		this.playerInfo.playerOwnSnakeInfo.nextMoveTime = gameInfo.playerData[playerIndex].nextUpdateTime;
		this.playerInfo.playerOwnSnakeInfo.lastCalculatedMemUsage = gameInfo.playerData[lastPlayer].lastCalculatedMemUsage ;
		switch(gameInfo.playerData[playerIndex].snakeStatus)
		{
			case IN_ACTION:
			case IN_HEAVEN:
			case IN_EXIT:
				this.playerInfo.playerOwnSnakeInfo.snakeStatus = gameInfo.playerData[playerIndex].snakeStatus;
				break;
			default:
				this.playerInfo.playerOwnSnakeInfo.snakeStatus = IN_HEAVEN;
		}
		this.playerInfo.playerOwnSnakeInfo.headDirection = gameInfo.playerData[playerIndex].watchDirection;
		
		this.playerInfo.playerLevelInfo.levelName = gameInfo.level.name;
		this.playerInfo.playerLevelInfo.height = gameInfo.level.height;
		this.playerInfo.playerLevelInfo.width = gameInfo.level.width;
		this.playerInfo.playerLevelInfo.playField = gameInfo.level.playField;
		
		this.playerInfo.playerGameInfo.gameTime = parameter.getGameTime();
		this.playerInfo.playerGameInfo.exitTime = parameter.getExit_time();
		this.playerInfo.playerGameInfo.suddenDeathTime = parameter.getSuddend_time();
		this.playerInfo.playerGameInfo.thinkingMS = parameter.getMax_thinking_ms();
		this.playerInfo.playerGameInfo.analyseMS = parameter.getAnalyse_ms();
		this.playerInfo.playerGameInfo.maxMem = parameter.getMax_player_mem();
		this.playerInfo.playerGameInfo.damage_points_radius = parameter.getDamage_points_radius();
		this.playerInfo.playerGameInfo.kill_points_radius = parameter.getKill_points_radius();
		this.playerInfo.playerGameInfo.kill_point_goodies = parameter.getKill_point_goodies();
		this.playerInfo.playerGameInfo.damage_length_grow = parameter.getDamage_length_grow();
		this.playerInfo.playerGameInfo.min_move_delay = parameter.getMin_move_delay();
		this.playerInfo.playerGameInfo.max_move_delay = parameter.getMax_move_delay();
		this.playerInfo.playerGameInfo.auto_grow_delay = parameter.getAuto_grow_delay();
		this.playerInfo.playerGameInfo.auto_slowdown_delay = parameter.getAuto_slowdown_delay();
		this.playerInfo.playerGameInfo.max_goody_occ_delay = parameter.getMax_goody_occ_delay();
		this.playerInfo.playerGameInfo.goody_speed_occ = parameter.getGoody_speed_occ();
		this.playerInfo.playerGameInfo.goody_slowdown_occ = parameter.getGoody_slowdown_occ();
		this.playerInfo.playerGameInfo.goody_length_occ = parameter.getGoody_length_occ();
		this.playerInfo.playerGameInfo.goody_points_occ = parameter.getGoody_points_occ();
		this.playerInfo.playerGameInfo.goody_shorter_occ = parameter.getGoody_shorter_occ();
		this.playerInfo.playerGameInfo.goody_length_value = parameter.getGoody_length_value();
		this.playerInfo.playerGameInfo.goody_points_value = parameter.getGoody_points_value();
		this.playerInfo.playerGameInfo.goody_shorter_value = parameter.getGoody_shorter_value();
		this.playerInfo.playerGameInfo.survival_points = parameter.getSurvival_points();
		this.playerInfo.playerGameInfo.easy_points = parameter.getEasy_points();	
	}
	
	//-------------------------------------------------------------------------------------
	//- public methods --------------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	/**
	 * @param players
	 */
	protected void initGame(Player[] players)
	{
		this.playerInstances = players;
		this.playerLog = new C_LogFileWriter("logs", "player.log");
		this.playerLog.storeLine( "start Game\n" +
								"Player:");

		this.playerInfo.playerGameInfo = new GameInfo();
		this.playerInfo.playerLevelInfo = new LevelInfo();
		this.playerInfo.playerSnakeInfo = new SnakeInfo[players.length];
		this.playerInfo.playerOwnSnakeInfo = new OwnSnakeInfo();
		
		for(int i= 0; i<players.length; i++)
		{
			if(players[i] != null) this.playerLog.storeLine(i+": " + players[i].getName());
			this.playerInfo.playerSnakeInfo[i] = new SnakeInfo();
		}
		this.playerLog.storeLine("-----------------------------------------------------------------------\n");

		this.randNumber = new Random();
		this.nextSizeControllTime = (long)(randNumber.nextDouble()*parameter.getMax_mem_check_delay());
		if(DEBUG) System.out.println("DEBUG M_GameEngine.M_GameEngine: nextSizeControllTime = " + this.nextSizeControllTime);
		

	}
	
	/**
	 * @param recoverData
	 */
	protected void recover(D_RecoverData recoverData)
	{
	// TODO Auto-generated method stub
	}

	/** This methods organizes the player calculation.
	 * @param gameInfo
	 * @param playerData
	 */
	synchronized protected void runPlayer(D_GameInfo gameInfo, D_PlayerData[] playerData)
	{

		//if(this.ownGameTime >= 100) System.out.println("WARNING: 100 holdPlayer!" + lastPlayer);
		long time = 0;
		ByteArrayOutputStream baos = null;
		String logString1 = "";
		String logString2 = "";
		
		ObjectOutputStream oos = null;
		
		
		if(gameInfo.playerData.length <= 0) return;
		this.lastPlayer = (this.lastPlayer+1) % gameInfo.playerData.length;
		// if the snake is still in action on the playfield
		if(parameter.getNo_thread_calc() == 1 && gameInfo.playerData[this.lastPlayer].snakeStatus == IN_ACTION)
		{
			this.reGenPlayerInfo(lastPlayer, gameInfo);
			try
			{
				this.playerInstances[this.lastPlayer].startCalculation(this.playerInfo.playerGameInfo, this.playerInfo.playerLevelInfo, this.playerInfo.playerSnakeInfo, this.playerInfo.playerOwnSnakeInfo);
			}
			catch(ThreadDeath e)
			{throw e;}
			catch(Throwable e)
			{
				e.printStackTrace();
			}
			playerData[this.lastPlayer].turnDirection = this.playerInstances[this.lastPlayer].getTurnDirection();
			if(parameter.getGameTime() == 0 || playerData[this.lastPlayer].nextUpdateTime <= parameter.getGameTime()) playerData[this.lastPlayer].move = true;
			return;
		}
		if(gameInfo.playerData[this.lastPlayer].snakeStatus == IN_ACTION)
		{
			logString1 += "gameTime:  " + this.parameter.getGameTime() + "\tPlayer " + this.lastPlayer;
			//System.out.println("calculate!!");
			this.reGenPlayerInfo(lastPlayer, gameInfo);
			//if(DEBUG) System.out.println("DEBUG M_PlayerHandler: overHere!! ("+ gameInfo.gameTime +")");
			this.playerRun = new PlayerRun(this.playerInstances[this.lastPlayer], this.playerInfo);
			this.playerThread = new Thread(this.playerRun);
			
			//if(this.ownGameTime >= 100) System.out.println("WARNING: run " + lastPlayer);
			this.playerThread.start();
			//if(this.ownGameTime >= 100) System.out.println("WARNING: run finished" + lastPlayer);
		}
		else return;

		
		
		// check the time!
		if(parameter.getTimeout_ms() <= 0)	// its waited for the player without deadline
		{
			if(!this.playerRun.calculationFinished)
			{
				try
				{			
					this.playerThread.join();
				}
				catch(InterruptedException e)
				{
					e.printStackTrace();
				}
			}
		}
		else	// there is a deadline
		{
			// wait for the players calculation
			try
			{
				this.playerThread.join(parameter.getMax_thinking_ms() + ((parameter.getGameTime()==0)? parameter.getAnalyse_ms() : 0));
			}
			catch(InterruptedException e)
			{
				e.printStackTrace();
			}
			
			// if the player hasnt finsished his moove
			if(!this.playerRun.calculationFinished)
			{
				// if there is a real difference between thinkingTime and Timeout.
				// that must be asked because join(0) waits for ever, but in this case, it meens
				// it should not wait at all.
				if(this.parameter.getTimeout_ms() > this.parameter.getMax_thinking_ms())
				{
					try
					{
						time = -System.currentTimeMillis();
						// wait at most the differemce between max_thinkingMS and timeoutMS.
						this.playerThread.join(parameter.getTimeout_ms() - this.parameter.getMax_thinking_ms());
						//playerCalculationtime += System.currentTimeMillis();
						time += System.currentTimeMillis();
						
						//if(DEBUG) System.out.println("DEBUG M_PlayerHandler.holdPlayer: additional needed MS: " + time);

						logString2 += "\t- Wait for player to timeout! (deadline: " + (this.parameter.getTimeout_ms() - this.parameter.getMax_thinking_ms()) + " ms) ..." + time + "\n";
					}
					catch(InterruptedException e)
					{
						e.printStackTrace();
					}
				}
				// the player needed too long, he will be deleted.
				if(!this.playerRun.calculationFinished)
				{
					// sais that the player guilted an error
					playerData[this.lastPlayer].snakeStatus = IN_ERROR_TIME;
					// kills the Thread. (unfortunatelly its depricated)
					this.playerThread.stop();
					// set the player instance to null.. a case for the garbage collector!
					this.playerInstances[lastPlayer] = null;
					System.out.println("WARNING: gameTime " + parameter.getGameTime() + "Player " + this.lastPlayer + " needed too much time and was deleted!!");
					
					logString2 += "\t- Player needed too much time and was deleted!!\n";
					this.playerLog.storeLine(logString1);
					this.playerLog.storeLine(logString2);
					return;
				}
			}
		}
		
		// if player threw an exception
		if(this.playerRun.threwException)
		{
			playerData[this.lastPlayer].snakeStatus = IN_ERROR_EXC;
			System.out.println("WARNING: gameTime " + parameter.getGameTime() + "Player " + this.lastPlayer + " threw an exception. he was deleted!!");
			
			logString2 += "\t- Player threw an exception. he was deleted!!\n";
			logString2 += "\t- " + this.playerRun.thrownException.toString() + "\n";	
			
			this.playerInstances[this.lastPlayer] = null;
			this.playerThread = null;
			this.playerRun = null;
			
			this.playerLog.storeLine(logString1);
			this.playerLog.storeLine(logString2);
			return;
		}
		// check players Space
		if(parameter.getMax_player_mem() > 0 && (parameter.getGameTime() >= this.nextSizeControllTime && lastPlayer == nextMemCheck))
		{
			this.nextMemCheck = (this.nextMemCheck+1)%this.playerInstances.length;
			baos = new ByteArrayOutputStream();

			if(playerData[lastPlayer].snakeStatus == IN_ACTION && !playerInstances[lastPlayer].getClass().getName().endsWith("C_Human"))
			{
				//time = -System.currentTimeMillis();
				this.nextSizeControllTime = parameter.getGameTime() + (long)(randNumber.nextDouble()*parameter.getMax_mem_check_delay());
									
				try
				{
					oos = new ObjectOutputStream(baos);
					oos.writeObject(this.playerInstances[lastPlayer]);
					oos.close();
					oos = null;
					
					logString2 += "\t-needed kB: " + baos.size()/1024 + "\n";
					
					if(baos.size() > (parameter.getMax_player_mem() << 10))
					{
						System.out.println("WARNING: gameTime " + parameter.getGameTime() + " Player " + lastPlayer + " needed too much space and was deleted!!\n");

						logString2 += "Player needed too much space and was deleted!!\n";
						
						playerData[lastPlayer].snakeStatus = IN_ERROR_SPACE;
						this.playerInstances[lastPlayer] = null;
					}
					else
					{
						playerData[lastPlayer].lastCalculatedMemUsage = baos.size();
					}
					baos.reset();
				}
				catch(IOException e)
				{
					e.printStackTrace();
				}
				
				// run the GC
				System.gc();
			}
		}
		
		// tell the snake which direction to move.
		playerData[this.lastPlayer].turnDirection = this.playerInstances[this.lastPlayer].getTurnDirection();
		// tell the snake that it can move
		if(parameter.getGameTime() == 0 || playerData[this.lastPlayer].nextUpdateTime <= parameter.getGameTime()) playerData[this.lastPlayer].move = true;
		
		logString1 += "\t needed  ms: " + this.playerCalculationtime + "\tturn direction: " + playerData[this.lastPlayer].turnDirection;
		
		if(parameter.getPrint_calc_ms() == 1)  System.out.println(logString1 + "\n");

		this.playerLog.storeLine(logString1);
		this.playerLog.storeLine(logString2);
		
		this.playerThread = null;
	}
	
	
	
	/** abborts the game */
	protected void abbort()
	{
		this.playerLog.storeLine("end Game");
		this.playerInfo.playerGameInfo = null;
		this.playerInfo.playerLevelInfo = null;
		this.playerInfo.playerOwnSnakeInfo = null;
		this.playerInfo.playerSnakeInfo = null;
		this.playerInstances = null;
		this.playerThread = null;
		if(this.parameter.getLogging() == 1) this.playerLog.safeAndCloseFile();
	}
	
	/** */
	protected void imergencyPause()
	{
		if(!imergencyPaused)
		{
			imergencyPaused = true;
			if(this.playerThread != null && this.playerThread.isAlive())
			{
				this.playerLog.storeLine("SUSPEND!! SystemTime: " + System.currentTimeMillis());
				this.playerThread.suspend();
			}
		}
		else
		{
			imergencyPaused = false;
			if(this.playerThread != null && this.playerThread.isAlive())
			{
				this.playerLog.storeLine("RESUME!! SystemTime: " + System.currentTimeMillis());
				this.playerThread.resume();
			}
		}
	}
	
	/** */
	protected void kill()
	{
		if(this.playerThread != null && this.playerThread.isAlive())
		{
			this.playerThread.stop();
		}
	}
	//-------------------------------------------------------------------------------------
	//- public static methods -------------------------------------------------------------
	//-------------------------------------------------------------------------------------

	/**
	 * prevents that more than one instance of the class exists in the program
	 * 
	 * @return a new instance if it doesnt already exist, if so, it returns the old one
	 */
	protected static M_PlayerHandler getInstance()
	{ 
		if(instance == null) instance = new M_PlayerHandler();
		return instance;
	}
	
	
	/**
	 * @param playerHandler
	 */
	protected static void destroyInstance(M_PlayerHandler playerHandler)
	{
		playerHandler.imergencyPaused = false;
		playerHandler.lastPlayer = -1;
		playerHandler.playerInstances = null;
		playerHandler.nextSizeControllTime = -1;
		playerHandler.randNumber = null;
		if(playerHandler.playerThread != null && playerHandler.playerThread.isAlive()) playerHandler.playerThread.stop();
		playerHandler.playerThread = null;
		instance = null;

	}

}