package mapping;

import java.util.GregorianCalendar;
import java.util.LinkedList;
import adjusting.Adjuster;

import logging.Logger;
import mapping.data.Server;

/**
 * 
 * @author Majd Kokaly
 * 
 * This thread is responsible for announcing time out events of jobs. It keeps
 * track of sent jobs.It wakes up at the time the earliest job should be done
 * and checks it is done. If it is not done it declares that job as timed-out.
 * This class is Thread-safe.
 * 
 */
public class TimeOutAnnouncer implements Runnable {

	/**
	 * Every sent job has a node storing the maximum time by when it should be
	 * completed.
	 */
	private LinkedList<TimeOutAnnouncer.TimeNode> list = new LinkedList<TimeNode>();

	/**
	 * This value determines how long the the thread will sleep next. This value
	 * is continuously updated by the method upDateCurrentSleepTime()
	 */
	private long currentSleepTime;
	/**
	 * This flag tells the thread if it is alive or should kill itself.
	 */
	private boolean alive = true;

	/**
	 * The mapper object that contains the serversTable that this server
	 * modifies.
	 */
	private Mapper mapper;

	/**
	 * The default time to sleep.
	 */
	private final long DEFAULT_SLEEP_TIME = 5000;

	/**
	 * The thread object responsible for running this Runnable class.
	 */
	private Thread thisThread;

	/**
	 * This logger is responsible for logging time out events.
	 */
	Logger timeOutLogger = new Logger("timeout");

	/**
	 * A default constructor.
	 * 
	 * @param mapper
	 *            To set the mapper field.
	 */
	public TimeOutAnnouncer(Mapper mapper) {
		this.setMapper(mapper);
	}

	public synchronized long getCurrentSleepTime() {
		return currentSleepTime;
	}

	public synchronized void setCurrentSleepTime(long currentSleepTime) {
		this.currentSleepTime = currentSleepTime;
	}

	public synchronized boolean isAlive() {
		return alive;
	}

	public synchronized void setAlive(boolean alive) {
		this.alive = alive;
	}

	public Mapper getMapper() {
		return mapper;
	}

	public void setMapper(Mapper mapper) {
		this.mapper = mapper;
	}

	public Logger getTimeOutLogger() {
		return timeOutLogger;
	}

	public void setTimeOutLogger(Logger timeOutLogger) {
		this.timeOutLogger = timeOutLogger;
	}

	/**
	 * This method is used to add nodes. Each node stores the maximum time by
	 * when it should be completed and of course the ID of that job.
	 * 
	 * @param AfterInTU
	 *            After how many units the job should be completed.
	 * @param jobID
	 *            The ID of the job in subject.
	 * 
	 */
	public void add(double AfterInTU, long jobID) {
		this.addSorted(new TimeNode(AfterInTU * this.getMapper().getTimeUnitInMinutes(), jobID));
	}

	/**
	 * This method is used to add nodes. Each node stores the maximum time by
	 * when it should be completed and of course the ID of that job.
	 * 
	 * @param node
	 *            The node to be added.
	 */
	private void addSorted(TimeNode node) {
		if (list.isEmpty()) {
			list.add(node);
			if (thisThread != null)
				thisThread.interrupt();
			return;
		}
		for (int i = 0; i < list.size(); i++) {
			if (node.time.before(list.get(i).time)) {
				list.add(i, node);
				if (i == 0) {
					if (thisThread != null)
						thisThread.interrupt();
				}
				return;
			}
		}
		list.add(node); // after all the time nodes
	}

	/**
	 * This method starts this thread.
	 */
	public void startThread() {
		thisThread = new Thread(this);
		thisThread.start();
	}

	/**
	 * This method stops this thread.
	 */
	public void stopThread() {
		this.setAlive(false);
		thisThread.interrupt();
		thisThread = null;
	}

	public void print() {
		System.out.println("Size: " + list.size());
		System.out.println("now: " + (new GregorianCalendar()).getTime().toString() + "\n");
		for (int i = 0; i < list.size(); i++) {
			System.out.println(list.get(i).toString());
		}
	}

	/**
	 * This thread has a list of nodes. Every node stores the time when a a job
	 * should be completed. The nodes in the linked list are sorted by the time.
	 * The first node is the node with the nearest time. This thread sleeps
	 * until it is the time to declare the job as timed out if it is not
	 * completed. If new nodes are inserted the list is modified and the sleep
	 * times of the thread are modified as well.
	 */
	public void run() {
		this.upDateCurrentSleepTime();
		String status;
		while (this.isAlive()) {
			// Sleep for the current sleep Time
			try {
				Thread.sleep(this.getCurrentSleepTime());
			} catch (InterruptedException e) {
				if (!this.isAlive())
					return;
				this.upDateCurrentSleepTime();
				continue;
			}
			// Check current job

			if (list.isEmpty())
				continue;
			status = this.getMapper().getJobsTable().getJobStatus(list.get(0).jobID);

			if (!status.equals(mapping.data.Job.DONE) && !status.equals(mapping.data.Job.QUEUED)) {
				// There is a timeout. Handle it!

				// Check if server is available
				GregorianCalendar current = new GregorianCalendar();
				Server server = this.getMapper().getJobsTable().getJobServer(list.get(0).jobID);
				if (!Adjuster.isServerSufferingFromAFailure(current, server)) {
					// If available return it to available servers

						System.err.println("TimeOutAnnouncer: " + server.getHostName() + " is declared UP!:)");
						this.getMapper().getServersTable().setServerDown(server.getIndex(), false);
						this.getMapper().getServersTable().clearServerActiveJobsNumber(server.getIndex());
						this.getMapper().getMappingScheme().serverIsUp(server.getIndex());

						/* -------- Availability Queue Management------- */
						this.getMapper().getAvailableServersQueue().enqueue(server.getIndex());
					//}

				} else { // If not notify EndOfailureAnnouncers
					GregorianCalendar endOfFailure = Adjuster.getEndOFFailurePeriod(current, server);
					long differenceInMilliSeconds = endOfFailure.getTimeInMillis() - current.getTimeInMillis();
					this.getMapper().getEndOfFailureAnnouncer().add(differenceInMilliSeconds / 1000.0 / 60.0, server);
					System.err.println("TimeOutAnnouncer: " + server.getHostName() + " is declared DOWN!!!");
					this.getMapper().getServersTable().setServerDown(server.getIndex(), true);
					this.getMapper().getMappingScheme().serverIsDown(server.getIndex());
				}

				// Notify Mapper that job timed out
				this.getTimeOutLogger().log("Job " + list.get(0).jobID + "timed out");

				System.err.println("TimeOutAnnouncer.run: Time out happened. Job: " + list.get(0).jobID
						+ " It was assigned to server " + this.mapper.getJobsTable().getJobHostName(list.get(0).jobID));

				this.getMapper().jobTimedOut(list.get(0).jobID);

				this.list.remove(0);
			} else {
				if (!list.isEmpty())
					list.remove(0);// remove the node
				this.upDateCurrentSleepTime();
			}
		}
	}

	/**
	 * This method is used to calculate the time for this thread to sleep.
	 */
	public void upDateCurrentSleepTime() {
		long sleepTime;
		if (list.isEmpty()) {
			this.setCurrentSleepTime(DEFAULT_SLEEP_TIME);
		} else {
			sleepTime = list.get(0).time.getTimeInMillis() - (new GregorianCalendar()).getTimeInMillis();
			if (sleepTime < 0) {
				sleepTime = 1;
			}
			this.setCurrentSleepTime(sleepTime);
		}
		GregorianCalendar timeToWakeUp = new GregorianCalendar();
		timeToWakeUp.add(GregorianCalendar.MILLISECOND, (int) this.getCurrentSleepTime());
	}

	/**
	 * This class stores the time when a job should be completed.
	 * 
	 * @author Majd Kokaly
	 */
	public static class TimeNode {
		GregorianCalendar time;
		long jobID;

		public TimeNode(GregorianCalendar cal, long jobID) {
			this.time = cal;
			this.jobID = jobID;
		}

		public TimeNode(double AfterInMinutes, long jobID) {
			this.time = new GregorianCalendar();
			this.time.add(GregorianCalendar.SECOND, (int) ((AfterInMinutes - Math.floor(AfterInMinutes)) * 60));
			this.time.add(GregorianCalendar.MINUTE, (int) Math.floor(AfterInMinutes));
			this.jobID = jobID;
		}

		public String toString() {
			return "Job: " + jobID + " at " + time.getTime().toString();
		}
	}

}