package cz.cuni.amis.pogamut.ut2004.bot;

import java.util.concurrent.CountDownLatch;

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.agent.AgentStateType;
import cz.cuni.amis.pogamut.base.agent.exceptions.AgentException;
import cz.cuni.amis.pogamut.base.agent.worldview.IStartableWorldView;
import cz.cuni.amis.pogamut.base.agent.worldview.WorldEventListener;
import cz.cuni.amis.pogamut.base.communication.commands.ICommandSerializer;
import cz.cuni.amis.pogamut.base.exceptions.PogamutException;
import cz.cuni.amis.pogamut.base.exceptions.PogamutRuntimeException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.ut2004.bot.exceptions.BotAlreadyStartedException;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.BotKilled;
import cz.cuni.amis.pogamut.ut2004.communication.messages.gbinfomessages.Spawn;
import cz.cuni.amis.pogamut.ut2004.communication.translator.events.BotFirstSpawned;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.Job;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.ImmutableFlag;
import cz.cuni.amis.utils.flag.WaitForFlagChange;

/**
 * This bot class is wrapping the logic thread. Meaning that it is using another thread
 * for the bot's logic - implementing correctly start/stop/kill/pause/resumeAgent().
 * 
 * @author Jimmy
 *
 * @param <WorldView>
 * @param <Body>
 */
@AgentScoped
public abstract class ThreadedUT2004Bot<WorldView extends IStartableWorldView> extends AbstractUT2004Bot<WorldView> {
	
	/**
	 * Start latch we're synchronizing the start of the agent on.
	 */
	private CountDownLatch logicStartLatch = new CountDownLatch(1);
	
	/**
	 * Indicating whether the thread is running.
	 */
	protected Flag<Boolean> logicAlive = new Flag<Boolean>(false);
	
	/**
	 * Whether the thread should be alive - when dropped the thread will terminate
	 * itself.
	 */
	protected boolean logicShouldBeAlive = true;
	
	/**
	 * Used to pause the execution of the logic thread.
	 */
	protected Flag<Boolean> logicShouldRun = new Flag<Boolean>(true);
	
	/**
	 * Dropped when the "logicShouldRun" is false and the doLogic() ends.
	 */
	protected Flag<Boolean> logicRunning = new Flag<Boolean>(true);
	
	/**
	 * Thread of the bot's logic.
	 */
	protected Thread logicThread = null;
	
	/**
	 * How fast the logic should be running? In millis.
	 */
	private long logicPeriodMillis = 100;
	
	/**
	 * When was the last time the doLogic() has been called? In millis.
	 */
	private long lastLogicRunMillis = -99999999;
	
	/**
	 * Whether the bot is alive (true) or dead (false).
	 */
	private Flag<Boolean> botAlive = new Flag<Boolean>(false);
		
	private WorldEventListener<BotFirstSpawned> firstSpawnListener = new WorldEventListener<BotFirstSpawned>() {

		@Override
		public void notify(BotFirstSpawned event) {
			getWorldView().removeListener(BotFirstSpawned.class, this);
			setAgentState(AgentStateType.RUNNING, "Bot is running.");
			botAlive.setFlag(true);
			logicStartLatch.countDown();			
		}
		
	};
	
	private WorldEventListener<Spawn> spawnListener = new WorldEventListener<Spawn>() {

		@Override
		public void notify(Spawn event) {
			getLogger().user().warning("Bot spawned.");
			botAlive.setFlag(true);
			if (getAgentState().getFlag().getType() == AgentStateType.RUNNING) {
				setAgentStateDescription("Bot is running.");
			}
		}
		
	};
	
	private WorldEventListener<BotKilled> killedListener = new WorldEventListener<BotKilled>() {

		@Override
		public void notify(BotKilled event) {
			getLogger().user().warning("Bot killed.");
			botAlive.setFlag(false);
			if (getAgentState().getFlag().getType() == AgentStateType.RUNNING) {
				setAgentStateDescription("Bot killed.");
			}
			botKilled(event);			
		}
		
	};
	
	@Inject
	public ThreadedUT2004Bot(AgentLogger logger, WorldView worldView, ICommandSerializer commandSerializer) {
		super(logger, worldView, commandSerializer);
		getWorldView().addListener(BotFirstSpawned.class, firstSpawnListener);
		getWorldView().addListener(Spawn.class, spawnListener);
		getWorldView().addListener(BotKilled.class, killedListener);
	}
	
	/**
	 * When was the last time the doLogic() has been called? In millis.
	 */
	public long getLastLogicRunMillis() {
		return lastLogicRunMillis;
	}

	public long getLogicPeriodMillis() {
		return logicPeriodMillis;
	}

	/**
	 * Sets how fast the doLogic() method should be called. Default is 100ms meaning
	 * the logic is called 10x per second.
	 * <p><p>
	 * If the logic takes more time then this number it is called as fast as it can be.
	 * 
	 * @param logicPeriodMillis
	 */
	public void setLogicPeriodMillis(long logicPeriodMillis) {
		this.logicPeriodMillis = logicPeriodMillis;
	}
	
	/**
	 * Whether the bot is alive (true) or dead (false).
	 * @return
	 */
	public ImmutableFlag<Boolean> getBotAlive() {
		return botAlive.getImmutable();
	}

	@Override
	public synchronized void pause() throws AgentException {
		logicShouldRun.setFlag(false);
		try {
			new WaitForFlagChange<Boolean>(logicRunning, false).await();
		} catch (InterruptedException e) {
			throw new AgentException("Interrupted during waiting for the logic thread to end.", getLogger().platform(), this);
		}
	}

	@Override
	public synchronized void resume() throws AgentException {
		logicShouldRun.setFlag(true);
		try {
			new WaitForFlagChange<Boolean>(logicRunning, true).await();
		} catch (InterruptedException e) {
			throw new AgentException("Interrupted during waiting for the logic thread to be resumed", getLogger().platform(), this);
		}
	}

	@Override
	public synchronized void start() throws AgentException {		
		if (logicThread != null) {
			throw new BotAlreadyStartedException(this + " has already been started before", getLogger().platform(), this);
		}
		super.start();
		logicThread = new Thread(new LogicThread(), this + " logic thread");
		logicThread.start();
		try {
			new WaitForFlagChange<Boolean>(logicRunning, true).await();
		} catch (InterruptedException e) {
			throw new AgentException("Interrupted during waiting for the logic to start.", getLogger().platform());
		}
	}

	@Override
	public void stop() {
		logicShouldBeAlive = false;
		try {
			logicStartLatch.countDown();
		} catch (Exception e) {			
		}
		try {
			resume();
		} catch (AgentException e1) {
		}
		super.stop();
		botAlive.setFlag(true);		
		try {
			new WaitForFlagChange<Boolean>(logicAlive, false).await();
		} catch (InterruptedException e) {
		}
	}
	
	@SuppressWarnings("unchecked")
	@Override
	public void kill() {
		new Job(){
			@Override
			protected void job() throws Exception {
				stop();
			}				
		}.startJob();
		logicThread.interrupt();		
	}

	
	/**
	 * Hook to add custom code before the logic thread starts to call doLogic(). Called
	 * before the logicRunning flag is raised and the state of the agent is changed
	 * to RUNNING.
	 * <p><p>
	 * <b>RESERVED FOR THE POGAMUT-CORE!</b>
	 * <p><p>
	 * Currently: Implements waiting for the INIT command.
	 */
	protected void logicThreadCreated() throws PogamutException {
		try {
			logicStartLatch.await();
		} catch (InterruptedException e) {
			throw new AgentException("interrupted during the waiting for the INIT command to be sent", e, getLogger().platform());
		}
	}
	
	/**
	 * Method that is called from the worker thread. Note that this method will be called only iff
	 * the bot is alive. (Even though the bot may be killed during the execution of this method.)
	 */
	protected abstract void doBotLogic() throws PogamutException;
	
	/**
	 * Method that is called whenever the bot is killed.
	 * @throws PogamutException
	 */
	protected abstract void botKilled(BotKilled event);
	
	/**
	 * Worker thread of the bot - before running the logic it calls logicThreadCreated()
	 * then calls iteratively doLogic().
	 * 
	 * @author Jimmy
	 */
	private class LogicThread implements Runnable {
		
		private void stopping(AgentStateType state, String description) {
			logicShouldBeAlive = false;
			logicAlive.setFlag(false);
			if (!getAgentState().getFlag().getType().isEndState()) {
				setAgentState(state, description);
			}			
			getLogger().platform().warning("Logic thread terminated");			
		}

		@Override
		public void run() {
			logicAlive.setFlag(true);

			getLogger().platform().fine("Logic thread created");
			
			try {
				logicThreadCreated();
			} catch(PogamutException e) {
				e.logExceptionOnce(getLogger().platform());
				stopping(AgentStateType.FAILED, e.getMessage());				
				return;
			} catch(PogamutRuntimeException e) {
				e.logExceptionOnce(getLogger().platform());
				stopping(AgentStateType.FAILED, e.getMessage());				
				return;
			} catch(Exception e) {
				getLogger().platform().severe(ExceptionToString.process("Exception occured in logicThreadCreated()", e));
				stopping(AgentStateType.FAILED, e.getMessage());
				return;
			}
			
			getLogger().platform().warning("Logic thread running");
			
			logicRunning.setFlag(true);
			
			try {
				while (logicShouldBeAlive && !Thread.interrupted()) {
					
					if (!logicShouldRun.getFlag()) {
						// pause the bot
						logicRunning.setFlag(false);
						setAgentState(AgentStateType.PAUSED, "Bot is paused.");
						new WaitForFlagChange<Boolean>(logicShouldRun, true).await();						
						logicRunning.setFlag(true);
						setAgentState(AgentStateType.RUNNING, (getBotAlive().getFlag() ? "Bot is running." : "Bot killed."));
					}
						
					if (System.currentTimeMillis() - lastLogicRunMillis < logicPeriodMillis) {
						Thread.sleep(logicPeriodMillis - (System.currentTimeMillis() - lastLogicRunMillis));
					}
					
					lastLogicRunMillis = System.currentTimeMillis();
					if (!getBotAlive().getFlag()) {
						// bot is dead, wait for the bot to be spawned again
						new WaitForFlagChange<Boolean>(getBotAlive(), true).await();
					}
					
					if (logicShouldBeAlive && !Thread.interrupted()) {
						// fires the bot logic
						doBotLogic();					
					} else {
						break;
					}
				} 
			} catch (PogamutException e) {
				if (logicShouldBeAlive) {
					e.logExceptionOnce(getLogger().platform());
					setAgentState(AgentStateType.FAILED, e.getMessage());
				}
			} catch (PogamutRuntimeException e) {
				if (logicShouldBeAlive) {
					e.logExceptionOnce(getLogger().platform());
					setAgentState(AgentStateType.FAILED, e.getMessage());
				}
			} catch (Exception e) {
				if (logicShouldBeAlive) {
					getLogger().platform().severe(ExceptionToString.process("exception occured during the logic of the agent", e));
					setAgentState(AgentStateType.FAILED, e.getMessage());
				}
			}
									
			stopping(AgentStateType.END, "Bot stopped");
		}
		
	}

}
