package cz.cuni.amis.pogamut.base.communication.mediator;

import java.util.logging.Level;

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.communication.exceptions.CommunicationException;
import cz.cuni.amis.pogamut.base.communication.exceptions.MediatorException;
import cz.cuni.amis.pogamut.base.communication.translator.IWorldChangeEvent;
import cz.cuni.amis.pogamut.base.communication.translator.IWorldEventOutput;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.base.utils.logging.LogCategory;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.ImmutableFlag;
import cz.cuni.amis.utils.flag.WaitForFlagChange;

/**
 * This class should wrap the reading thread that will continuously read
 * messages from the connection (that is represented by IWorldEventOutput
 * object) passing them to the IWorldMessageReceiver.
 */
@AgentScoped
public class Mediator implements IMediator {

	/**
	 * Name prefix for the worker thread and for the logs.
	 */
	public static final String WORKER_THREAD_NAME_PREFIX = "Mediator.Worker";

	/**
	 * Name prefix for the mediator.
	 */
	public static final String MEDIATOR_LOG_PREFIX = "Mediator: ";

	/**
	 * Receiver for the messages that are parsed from the world connection.
	 */
	protected IMediatorOutput receiver = null;

	/**
	 * Thread of the worker.
	 */
	protected Thread workerThread = null;

	/**
	 * Worker instance - it implements Runnable interface and is continuously
	 * reading messages from the connection object and passing them to the
	 * receiver.
	 */
	protected Worker worker = null;

	/**
	 * Mutex for start synchronization.
	 */
	protected Object startSynchronization = new Object();

	/**
	 * Logger of the connection - access it via protected log() methods rather
	 * then directly.
	 */
	private AgentLogger agentLogger = null;

	/**
	 * Log category for the mediator (platform log).
	 */
	private LogCategory log = null;

	/**
	 * Used to get events from the world.
	 */
	private IWorldEventOutput producer;

	/**
	 * Flag that is telling us whether the worker (and thus the
	 * WorldCommunication) is running.
	 */
	private Flag<Boolean> running = new Flag<Boolean>(false);

	/**
	 * The object in passed to the constructor (IWorldEventOutput) is world
	 * event producer.
	 * <p>
	 * <p>
	 * The mediator will read events from this producer and pass them to the
	 * IWorldEventInput specified during the start() of the mediator.
	 * 
	 * @param connection
	 * @param messageParser
	 * @param commandSerializer
	 * @throws CommunicationException
	 */
	@Inject
	public Mediator(IWorldEventOutput producer, IMediatorOutput receiver, AgentLogger logger)
			throws CommunicationException {
		agentLogger = logger;
		log = agentLogger.in();
		this.producer = producer;
	}

    @Override
    public void setMediatorOutput(IMediatorOutput receiver) {
    	this.receiver = receiver;
	}

	@Override
	public void start() throws CommunicationException {
		synchronized (startSynchronization) {
			if (workerThread != null) {
				throw new MediatorException(
						"Mediator worker thread already exists, can't start.",
						log,
						this);
			}
			producer.start();
			log.finer(MEDIATOR_LOG_PREFIX + "starting mediator thread ("
					+ WORKER_THREAD_NAME_PREFIX + ").");
			worker = new Worker();
			workerThread = new Thread(worker, WORKER_THREAD_NAME_PREFIX + " ("
					+ String.valueOf(producer) + ")");
			workerThread.start();
		}
	}

	@Override
	public void stop() {
		synchronized (startSynchronization) {
			if (worker != null) worker.stop();
			try {
				new WaitForFlagChange<Boolean>(running, false).await();
			} catch (InterruptedException e) {				
			}			
		}
	}

	@Override
	public void kill() {
		synchronized (startSynchronization) {
			if (worker != null) {
				worker.kill();
			}
			producer.stop();
			try {
				new WaitForFlagChange<Boolean>(running, false).await();
			} catch (InterruptedException e) {
			}
		}
	}

	public boolean isRunning() {
		return running.getFlag();
	}

	@Override
	public ImmutableFlag<Boolean> getRunning() {
		return running.getImmutable();
	}

	public LogCategory getLogger() {
		return log;
	}

	private class Worker implements Runnable {

		/**
		 * Simple flag that is telling us whether the Worker should run,
		 * controlled from stopXXX() methods.
		 */
		private volatile boolean shouldRun = true;
		
		/**
		 * Drops the shouldRun flag.
		 */
		public void stop() {
			this.shouldRun = false;
		}

		/**
		 * Drops the shouldRun flag, waits for 500ms and then interrupts the
		 * thread in hope it helps.
		 */
		public void kill() {
			this.stop();
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
			}
			Thread worker = workerThread;
			if (worker != null && worker.isAlive()) worker.interrupt();
		}
		
		/**
		 * Contains main while cycle that is continuously reading messages from
		 * the connection (using parser), notifying listeners and then passing
		 * them to the message receiver.
		 */
		@Override
		public void run() {
			
			// set the running flag, we've been started
			running.setFlag(true);

			// notify that gateway started
			logWorker(Level.INFO, "worker thread (" + WORKER_THREAD_NAME_PREFIX
					+ ") started.");

			try {
				IMediatorOutput currentHandler;
				IWorldChangeEvent worldEvent;

				while (shouldRun && !Thread.interrupted()) {

					currentHandler = receiver;
					if (currentHandler == null) {
						if (shouldRun) {
							// handler lost...
							logWorker(Level.SEVERE,
									"message receiver lost (is null).");
						}
						break;
					}

					worldEvent = null;

					try {
						worldEvent = producer.getEvent();
						logWorker(Level.INFO, "world event received: "
								+ String.valueOf(worldEvent), worldEvent);
					} catch (CommunicationException ce) {
						ce.logExceptionOnce(log);
						if (shouldRun) {
							logWorker(Level.SEVERE, "exception occured, shutting down");							
						}
						break;
					} catch (Exception e) {
						if (shouldRun) {
							// filtering out the exception that are raised due
							// to closing the connection
							// from other thread
							logWorker(
								Level.SEVERE, 
								ExceptionToString.process("exception raised in world event output ("
											              + producer.toString() + ")", e)
							);
						}
						break;
					}

					// are we alive?
					if (!shouldRun || Thread.interrupted())
						break;
					// yes we are, continue

					try {
						currentHandler.notify(worldEvent);
					} catch (Exception e) {
						logWorker(Level.SEVERE, ExceptionToString.process(
								"exception raised in message receiver ("
										+ currentHandler.toString() + ")", e));
						break;
					}
				}
			} catch (Exception e) {
				logWorker(Level.SEVERE, ExceptionToString.process("unhandled exception caught", e));
			}

			// clean after yourself
			shouldRun = false;
			worker = null;
			workerThread = null;

			// just to be sure that the event output will be closed as well
			try {
				producer.stop();
			} catch (Exception e) {
				logWorker(Level.SEVERE, ExceptionToString.process("can't close event output", e));
			}

			// set the running flag, we're finished
			running.setFlag(false);

			// notify that gateway stopped
			logWorker(Level.WARNING, "worker stopped.");
		}

		private void logWorker(Level level, String message) {
			log.log(level, WORKER_THREAD_NAME_PREFIX + ": " + message);
		}

		private void logWorker(Level level, String message, Object obj) {
			if (obj == null)
				log.log(level, WORKER_THREAD_NAME_PREFIX + ": " + message);
			else
				log.log(level, WORKER_THREAD_NAME_PREFIX + ": " + message, obj);
		}

	}

}
