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

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

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.communication.I_PlayFieldConstants;
import de.farafin.snEADy.control.C_Human;
import de.farafin.snEADy.inOut.C_FileClassLoader;
import de.farafin.snEADy.player.Player;
import de.farafin.snEADy.world.C_World;
import de.farafin.snEADy.GameParameter;

/**
 * @author roland, lars
 * 
 * GameEngine is the Module that organize the game flow and all its data transportation.
 * its for organizing the communication between @see C_Wworld, @see M_PlayerHandler and @see M_Main
 *
 *
 *  @version $Revision: 1.75 $ 
 * */
public final class M_GameEngine extends Thread implements I_Constants, I_GameStats, I_PlayFieldConstants
{
	//-------------------------------------------------------------------------------------
	//- attributes ------------------------------------------------------------------------
	//-------------------------------------------------------------------------------------
	 
	/** an instance of the world, the class that computes all moves and stuff
	 * world also includes the arena as final. if you like to create a new arena, you need
	 * to create a new world-object.
	 * 
	 * @see C_World
	 */
	private C_World world = null;

	/** data of the managed player
	 * 
	 * @see de.farafin.snEADy.communication.D_PlayerData */
	private D_PlayerData[] playerData = null;
	
	
	/** The player Instances which are loaded */
	private Player[] playerInstances = null;
	
	/** The instances of human Player */
	private C_Human[] humanInstances = null;

	// modules
	/** The instance of the player handler.. the class that handles the player management
	 * @see M_PlayerHandler 
	 */
	private M_PlayerHandler playerHandler = null;


	// game Variables
	/** the Time the player will have time to think for their next move */
	//private long thinkingTime;
	
	/** the gameInfo instance from whitch a copy is given to other modules */
	private D_GameInfo gameInfo;
	
	/** parameter set of GameParameter */
	private final GameParameter parameter;

	// others
	/** own instance to prevent multible instanzes */
	private static M_GameEngine instance;
	
	/** flag that controlls if the class is suspended */
	private boolean imergencyPaused = false;

	
	//-------------------------------------------------------------------------------------
	//- constructor -----------------------------------------------------------------------
	//-------------------------------------------------------------------------------------
	
	/**
	 * constructor of M_GameEngine
	 * @see #getInstance()
	 */
	private M_GameEngine()
	{
		this.world = null;
		this.parameter = M_Main.getInstance().getParameter();
		this.playerHandler = M_PlayerHandler.getInstance();

		this.playerData = null;
		this.parameter.setGameTime(0);
		//this.thinkingTime = 1000;

	}

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

	/** to send other modules the current gameState, its necessary to copy the data and
	 * send the data to all modules. this method just copys the data and stores them in a
	 * gameInfo instance.
	 * 
	 * @return the gameInfos for the other modules
	 */
	private D_GameInfo generateGameInfo()
	{
		int i;
		if(this.gameInfo == null) this.gameInfo = new D_GameInfo();
		else{return reGenerateGameInfo(); }

		this.gameInfo.level = this.world.generateLevel();
		this.gameInfo.gameTime = parameter.getGameTime();
		this.gameInfo.exitTime = parameter.getExit_time();
		this.gameInfo.suddenDeath = parameter.getSuddend_time();
		this.gameInfo.playerData = new D_PlayerData[this.playerData.length];
		for(i = 0; i < this.playerData.length; i++)
		{
			this.gameInfo.playerData[i] = (D_PlayerData)this.playerData[i].clone();
		}

		return this.gameInfo;
	}

	/**
	 * @return the gameInfo which is updated to the actual data and its stored in this.gameInfo
	 */
	private D_GameInfo reGenerateGameInfo()
	{
		int i;
		if(this.gameInfo == null) return generateGameInfo();

		world.updateInfos(this.gameInfo);
		this.gameInfo.gameTime = parameter.getGameTime();
		this.gameInfo.exitTime = parameter.getExit_time();
		this.gameInfo.suddenDeath = parameter.getSuddend_time();
		
		if(this.gameInfo.playerData.length != this.playerData.length)
		{
			this.gameInfo.playerData = new D_PlayerData[this.playerData.length];
			for(i = 0; i < this.playerData.length; i++)
			{
				this.gameInfo.playerData[i] = (D_PlayerData)this.playerData.clone();
			}
		}
		else 
		{
			for(i = 0; i < this.playerData.length; i++)
			{
				this.gameInfo.playerData[i].copyOnMe(this.playerData[i]);
			}
		}
		
		return this.gameInfo;
	}
	

	/** checks the gameSate
	 * @return if the process should be ended or not
	 *  */
	private boolean checkGameState()
	{
		// if game should be paused.. here its holdet until the thread is notifyed.
		switch(M_Main.getInstance().getGameState())
		{
			case GAME_RUNNING:
				break;
			case GAME_PAUSE:
				try
				{
					while(M_Main.getInstance().getGameState() == GAME_PAUSE)
						sleep(100);
				}
				catch(InterruptedException e)
				{
					if(DEBUG) System.out.println("DEBUG M_GameEngin: " + e);
				}
				break;
			case GAME_SAFE:
				break;
			case GAME_RECOVER:
				break;
			case PROG_END:
			case GAME_ABBORT:
				this.playerHandler.abbort();
				// TODO: after lars had implemented a version where null pointer is allowed, delete and replace it with abive.
				M_Main.getInstance().update(this.reGenerateGameInfo(), this.humanInstances);
				this.gameInfo = null;
				this.parameter.setGameTime(0);
				this.playerInstances = null;
				this.humanInstances = null;
				this.playerData = null;
				//this.thinkingTime = 1000;
				this.world = null;
				//M_Main.getInstance().update(null);
				System.gc();
				synchronized(this)
				{
					try
					{
						wait(100);
						//Thread.currentThread().sleep(1000);
					}
					catch(InterruptedException e)
					{
						e.printStackTrace();
					}
				}
				return true;
			default:;
		}	
		
		return false;
	}
	
	//-------------------------------------------------------------------------------------
	//- public methods --------------------------------------------------------------------
	//-------------------------------------------------------------------------------------
	
	/** The main cycle of the game. its a run-method from Thread, M_GameEngine is extended from.
	 * 
	 * @see java.lang.Thread#run()
	 */
	public void run()
	{		
		int i=0, j=0;
		int snakeCount = 0;
		long time = 0;
		
		// main cycle loop, maybe true is not so good choice... dont know what would be better at moment
		while(true)
		{
			time = -System.currentTimeMillis();
			for(i=0; i<this.playerInstances.length; i++)
			{
			//	if(this.playerData[i].snakeStatus > IN_EXIT)
				//	M_Main.getInstance().control("end");

				if(this.isInterrupted())
				{
					/* TODO #roland: what happends if the main cycle is interrupted? */
					break;
				}

				// check the gamestate. if true is returned, game should stop.
				if(checkGameState()) return;
				
				
				playerHandler.runPlayer(this.gameInfo, this.playerData);

				
				// if only one snake is left, the exit should be opened.
				snakeCount = 0;
				for(j=0; j<this.playerData.length; j++)
				{
					if(this.playerData[j].snakeStatus == IN_ACTION) snakeCount++;
				}
				if(snakeCount <= 1 && parameter.getGameTime() < parameter.getExit_time())
				{
					parameter.setSuddend_time(parameter.getGameTime() + (parameter.getSuddend_time() - parameter.getExit_time()));
					parameter.setExit_time(parameter.getGameTime());
				}

				// check the gamestate. if true is returned, game should stop.
				if(checkGameState()) return;
				
				
				// do not update the first gamecycle to give player the possibility to analyse the playfield without changed
				// of mooving players.
				if(parameter.getGameTime() > 0) // special rule for the first move of player.
				{
					reGenerateGameInfo();
					world.update(this.gameInfo);
				}
				
				// if subtidles is on, each world update a graphics update follows. 
				if(parameter.getShow_subcycles() == 1)
				{
					reGenerateGameInfo();
					M_Main.getInstance().update(this.gameInfo, this.humanInstances);
				}
			}

			if(parameter.getGameTime() == 0) // special rule for the first move of player. 
			{
				reGenerateGameInfo();
				world.update(this.gameInfo);
			}

			// check the gamestate. if true is returned, game should stop.
			if(checkGameState()) return;
			// if subtidles is off, here the update of main and graphics. its only made once a gameTimeCycle
			if(parameter.getShow_subcycles() == 0)
			{
				reGenerateGameInfo();
				M_Main.getInstance().update(this.gameInfo, this.humanInstances);
			}
			
			time += System.currentTimeMillis();
			// if the game was faster than min_cycle_ms, wait a bit.
			
			if(parameter.getNo_thread_calc() == 0 && time < parameter.getMin_cycle_ms())
			{
				try
				{
					//System.gc();
					sleep(parameter.getMin_cycle_ms() - time);
				}
				catch(InterruptedException e)
				{
					e.printStackTrace();
				}
			}
			
			parameter.setGameTime(parameter.getGameTime() + 1);
		}
	}

	/** init the game
	 * @param initData 
	 */
	protected void initGame(D_GameInfo initData)
	{
		parameter.setGameTime(0);
		long time = 0;
		Player[] playerArray = new Player[initData.playerData.length];
		C_Human[] humanArray = new C_Human[initData.playerData.length];
		D_PlayerData[] infoArray = new D_PlayerData[initData.playerData.length];
		int playerCount = 0;
		int humanCount=0;
		int i=0;
		Class c = null;
		C_FileClassLoader loader = new C_FileClassLoader("player");

		ByteArrayOutputStream baos = null;
		ObjectOutputStream oos = null;

		// check time to init the game
		time = -System.currentTimeMillis();
		
		// if a player cant be initialized or gains too much space at the beginning, it is deleted.
		for(i=0; i<initData.playerData.length; i++)
		{
			// get the actuall class of the player.
			try
			{
				// lade playerklassen neu
				c = loader.loadClass(initData.playerData[i].playerClass.getName(), true);
				if(DEBUG) System.out.println("DEBUG M_GameEngine.initPlayer: load Class" + initData.playerData[i].playerClass.getName());
			}
			catch(Exception e)
			{
				System.out.println("WARNING: player wasnt loaded, using the player that was loaded before starting the game. \n\t" + e);
				c = initData.playerData[i].playerClass;
			}
			
			// in c is the actuall class stored. Loading it now.
			try
			{
				// make a new Instance of the player
				playerArray[playerCount] = (Player)c.newInstance();
				// if the player is human
				if(c.getName().endsWith("C_Human"))
				{
					// set the name to the specified name
					((C_Human)playerArray[playerCount]).setName(initData.playerData[i].name);
					humanArray[humanCount] = (C_Human)playerArray[playerCount];
					humanCount++;
				}
			}
			catch(Throwable e) // if an error accured whyle building a new instance..
			{
				// what happends if the player cant be initialized?
				System.out.println("WARNING: Player couldnt be initialzed! please check your default constructor routins. \n");
				e.printStackTrace();
				playerArray[playerCount] = null;
				continue;
			}
			
			// remember which data are usefull
			infoArray[playerCount] = initData.playerData[i];

			// checking new player size
			if(parameter.getMax_player_mem() > 0)
			{
				baos = new ByteArrayOutputStream();

				if(!c.getName().endsWith("C_Human"))
				{
					try
					{
						oos = new ObjectOutputStream(baos);
						oos.writeObject(playerArray[playerCount]);
						oos.close();
						oos = null;
						
						if(parameter.getPrint_player_mem() == 1) System.out.println("startGameCheck: Player " + playerCount + " needed kB: " + baos.size()/1024);
						// TODO: write init log!
						
						if(baos.size() > (parameter.getMax_player_mem() << 10))
						{
							System.out.println("WARNING: startGameCheck: Player " + playerCount + " needed too much space and will not start the game!!\n");
							infoArray[playerCount].snakeStatus = IN_ERROR_SPACE;
							playerArray[playerCount] = null;
						}
						baos.reset();
					}
					catch(IOException e)
					{
						if(DEBUG) System.out.println("WARNING M_GameEngine.initPlayer: exception while startChecking playersize!!");
						e.printStackTrace();
					}
					catch(OutOfMemoryError e)
					{
						System.out.println("WARNING: Player " + i + " cursed a OutOfMemoryError!!! chack your constructor!!\n" + e);
						infoArray[playerCount].snakeStatus = IN_ERROR_SPACE;
						playerArray[playerCount] = null;
						System.gc();
						try{sleep(100);}
						catch(InterruptedException e2){System.out.println("WARNING: Error whyle sleeping!" + e2);}
					}
				}
			}
			// gives player the monitor
			playerArray[playerCount].setMonitor(M_Main.getInstance().getMonitor());
			
			// gets the name from the player
			infoArray[playerCount].name = playerArray[playerCount].getName();
			
			// note that one more player was sucessfull loaded.
			playerCount++;
		}
		
		this.playerData = new D_PlayerData[playerCount];
		this.playerInstances = new Player[playerCount];		
		for(i=0; i<playerCount; i++)
		{
			this.playerInstances[i] = playerArray[i];
			this.playerData[i] = infoArray[i];
		}

		this.humanInstances = new C_Human[humanCount];
		for(i=0; i<humanCount; i++)
		{
			this.humanInstances[i] = humanArray[i];
		}
		
		// init playerHandler and World
		initData.playerData = this.playerData;
		world = new C_World(initData, this.parameter);
		playerHandler.initGame(this.playerInstances);

		time += System.currentTimeMillis();
		if(DEBUG) System.out.println("DEBUG M_GameEngine.initGame: init Time: " + time);
		time = -System.currentTimeMillis();

		// try to clean system
		System.gc();
		try{sleep(100);}
		catch(InterruptedException e){System.out.println("WARNING: Error whyle sleeping!" + e);}

		time += System.currentTimeMillis();
		if(DEBUG) System.out.println("DEBUG M_GameEngine.initGame: gc-Time: " + time);
		
		// give mainEngine the final data chich were computed
		// after that, mainEngine should be fully initialized..
		generateGameInfo();
		this.gameInfo.gameRunning = true;
		
		M_Main.getInstance().update(this.gameInfo, this.humanInstances);
	}
	
	/** recover a before safed game
	 * @param recoverData
	 */
	protected void recover(D_RecoverData recoverData)
	{
	// TODO Auto-generated method stub

	}
	
	/** saves a game
	 * @param dataStorage
	 */
	protected void store(D_RecoverData dataStorage)
	{
	// TODO Auto-generated method stub

	}
	
	/**
	 * @return the actual gameInfo
	 */
	protected D_GameInfo getGameInfo()
	{
		return this.reGenerateGameInfo();
	}

	/** pauses/starts the actual player using deprecated methods! DO NOT USE IF YOU DO NOT REALLY NEED!!! */
	protected void imergencyPause()
	{
		playerHandler.imergencyPause(); // suspends the actual running player
		if(!imergencyPaused)
		{
			imergencyPaused = true;
			this.suspend();  // suspends the game Thread
		}
		else
		{
			imergencyPaused = false;
			this.resume();  // resums the game Thread
		}
	}
	
	
	/** stops all threads that are running in the game!  DO NOT USE IF YOU DO NOT REALLY NEED!!! */
	protected void kill()
	{
		this.playerHandler.kill();	// stops the player thread
		this.stop(); // stops the game thread
	}
	//-------------------------------------------------------------------------------------
	//- 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_GameEngine getInstance()
	{
		if(instance == null) instance = new M_GameEngine();
		return instance;
	}

	/** destroys the engine - instance to make it free for a new one.
	 * 
	 * @param engine
	 */
	protected static void destroyInstance(M_GameEngine engine)
	{
		engine.gameInfo = null;
		engine.humanInstances = null;
		engine.imergencyPaused = false;
		engine.playerData = null;
		M_PlayerHandler.destroyInstance(engine.playerHandler);
		engine.playerHandler = null;
		engine.playerInstances = null;
		engine.world = null;
		instance = null;
	}
}