package mapping;

import java.io.Serializable;
import java.util.GregorianCalendar;
import java.util.LinkedList;

import adjusting.Adjuster;

import mapping.data.Server;

/**
 * 
 * @author Majd Kokaly
 * 
 * This thread is responsible for announcing the end of an artificial failure
 * period for an artificially down server. It keeps track of failed servers and
 * the time when they should become up again and in turn it wakes up and
 * announces the server up again.
 * 
 */
public class EndOfFailureAnnouncer implements Runnable, Serializable {

	/**
	 * The serialVersionUID is used to universally identify this version of this
	 * class.
	 */
	private static final long serialVersionUID = 9190446048851505684L;

	/**
	 * Every failed server has a node representing its failure and containing
	 * the time of this server to be up again.
	 */
	private LinkedList<TimeNodeForServers> list = new LinkedList<TimeNodeForServers>();

	/**
	 * 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;

	/**
	 * A default constructor.
	 * 
	 * @param mapper
	 *            To set the mapper field.
	 */
	public EndOfFailureAnnouncer(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;
	}

	/**
	 * This method is used to add nodes. Each node represents a time that this
	 * thread should wake up and announce that the server is up.
	 * 
	 * @see TimeNodeForServers
	 * 
	 * @param AfterInMinutes
	 *            The number of minutes for the server to stay down.
	 * @param server
	 *            The Server object in subject.
	 * 
	 */
	public synchronized void add(double AfterInMinutes, Server server) {
		this.add(new TimeNodeForServers(AfterInMinutes, server));
	}

	/**
	 * This method is used to add nodes. Each node represents a time that this
	 * thread should wake up and announce that the server is up.
	 * 
	 * @see TimeNodeForServers
	 * 
	 * @param node
	 *            The node to be added.
	 * 
	 */
	private void add(TimeNodeForServers 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 represents a time in which a
	 * server should become up again. The nodes in the link list is sorted. The
	 * first node is the node with the nearest time. This thread sleeps until it
	 * is the time to declare the server of the first node up. Then it wakes up
	 * and declare it up. 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();
		System.out.println("EndOfFailureAnnouncer.run: Started");
		while (this.isAlive()) {
			// Sleep for the current sleep Time
			try {
				Thread.sleep(this.getCurrentSleepTime());
			} catch (InterruptedException e) {
				System.err.println("EndOfFailureAnnouncer.run: Interupted");
				if (!this.isAlive())
					return;
				this.upDateCurrentSleepTime();
				continue;
			}

			if (list.isEmpty())
				continue;

			GregorianCalendar currentTime = new GregorianCalendar();

			Server server = list.get(0).server;

			boolean up = !Adjuster.isServerSufferingFromAFailure(currentTime, server);

			if (up) { // If it is up, re-add it to server's list

				System.err.println("EndOfFailureAnnouncer: Server: " + server.getHostName() + " is declared UP");

				this.getMapper().getServersTable().setServerDown(server.getIndex(), false);
				this.getMapper().getServersTable().clearServerActiveJobsNumber(server.getIndex());
				this.getMapper().getMappingScheme().serverIsUp(server.getIndex());
				
				if (!list.isEmpty()) {
					list.remove(0);// remove the node
				}

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

				this.upDateCurrentSleepTime();

			} else { // If a server is still down, handle it.
				System.err.println("EndOfFailureAnnouncer. System is stilldown");
				this.upDateCurrentSleepTime();
			}
		}
	}

	/**
	 * This method is used to calculate the time for this thread to sleep.
	 */
	private 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 = 1000;
			}
			this.setCurrentSleepTime(sleepTime);
		}
		GregorianCalendar timeToWakeUp = new GregorianCalendar();
		timeToWakeUp.add(GregorianCalendar.MILLISECOND, (int) this.getCurrentSleepTime());

	}

	/**
	 * This class represents the time when a server should be declared as up. It
	 * is used as the nodes in the linked list of the EndOfFailureAnnouncer.
	 * 
	 * @see EndOfFailureAnnouncer
	 * 
	 * @author Majd Kokaly
	 */
	public static class TimeNodeForServers {
		GregorianCalendar time;
		Server server;

		public TimeNodeForServers(GregorianCalendar cal, Server server) {
			this.time = cal;
			this.server = server;
		}

		public TimeNodeForServers(double AfterInMinutes, Server server) {
			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.server = server;
		}

		public String toString() {
			return "Server: " + server.getHostName() + " at " + time.getTime().toString();
		}
	}

}
