package mapping;

import java.io.DataInputStream;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.GregorianCalendar;

import logging.Event;
import logging.Logger;

import adjusting.Adjuster;

/**
 * @author Majd Kokaly
 * 
 * This server is responsible for receiving information about completed jobs. A
 * server (node) sends a message to this server to announce the completion of a
 * job.
 */
public class CompletionServer implements Runnable {

	/**
	 * This flag tells the thread if it is alive or should kill itself.
	 */
	private boolean alive = true;

	/**
	 * The port that this servers listen on. By default the value is 37931.
	 */
	private int completionServerPort = 37931;

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

	/**
	 * A Thread objects that runs this Runnable class.
	 */
	private Thread thisThread;

	/**
	 * A final value of the maximum tolerated difference between the timestamp
	 * of the message received and the time of the receipt.
	 */
	private static final int NOTICALBE_DIFFERENCE_IN_TIME = 3000; // 3 seconds

	/** A logger object responsible for logging completion events */
	private Logger completionEventsLogger;

	/** A logger object responsible for logging failures events */
	private Logger failuresLogger;

	/**
	 * Default constructor.
	 * 
	 * @param mapper
	 *            to set the mapper field
	 */
	public CompletionServer(Mapper mapper) {
		this.mapper = mapper;
		this.setCompletionEventsLogger(new Logger("completionServer"));
		this.setFailuresLogger(new Logger("failureServer"));
	}

	public synchronized boolean isAlive() {
		return alive;
	}

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

	public synchronized int getCompletionServerPort() {
		return completionServerPort;
	}

	public void setCompletionServerPort(int completionServerPort) {
		this.completionServerPort = completionServerPort;
	}

	public Mapper getMapper() {
		return mapper;
	}

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

	public Logger getCompletionEventsLogger() {
		return completionEventsLogger;
	}

	public void setCompletionEventsLogger(Logger completionEventsLogger) {
		this.completionEventsLogger = completionEventsLogger;
	}

	public Logger getFailuresLogger() {
		return failuresLogger;
	}

	public void setFailuresLogger(Logger failuresLogger) {
		this.failuresLogger = failuresLogger;
	}

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

	/**
	 * This method stops the the server and the thread.
	 */
	public void stopServer() {
		boolean wasAlive = this.isAlive();
		this.setAlive(false);
		if (wasAlive) {
			thisThread.interrupt();
			thisThread = null;

		}
	}

	/**
	 * A utility method to convert string received to date objects. The dates
	 * and time stamps received should be in the following format: "yyyy-MM-dd
	 * HH:mm:ss Z".
	 * 
	 * @param dateString
	 *            The string of the date to be converted.
	 * @return A GregorianCalendar objects corresponding to the input string.
	 */
	private GregorianCalendar convertToCal(String dateString) {
		Date date = null;
		try {
			DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z"); // Formatter

			date = (Date) formatter.parse(dateString);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		GregorianCalendar cal = new GregorianCalendar();
		cal.setTime(date);
		return cal;
	}

	/**
	 * A utility method to convert string received to date objects. The dates
	 * and time stamps received should be in the following format: "yyyy-MM-dd
	 * HH:mm:ss Z". In addition to the conversion, this method add a particular
	 * amount of milli seconds to the date parsed. This is used to offset the
	 * difference in time between the mapper and machines.
	 * 
	 * @param dateString
	 *            The string of the date to be converted.
	 * @param millis
	 *            The amount of milliseconds to be added
	 * @return A GregorianCalendar object obtained by parsing the input string
	 *         and adding "millis" milliseconds.
	 */
	private GregorianCalendar convertToCalAndAdd(String dateString, long millis) {
		Date date = null;
		try {
			// Some examples
			DateFormat formatter = null; // Formatter used by Puller
			formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss Z");
			date = (Date) formatter.parse(dateString);
		} catch (ParseException e) {
			e.printStackTrace();
		}
		GregorianCalendar cal = new GregorianCalendar();
		cal.setTime(date);
		cal.add(GregorianCalendar.MILLISECOND, (int) millis);
		return cal;
	}

	/**
	 * The ServerSocket object used to listen to incoming connections.
	 */
	ServerSocket serverSocket = null;

	/**
	 * The Socket object used to handle connections.
	 */
	Socket socket = null;

	/**
	 * The input stream used to read data from the socket.
	 */
	DataInputStream in = null;

	/**
	 * This thread listens on its port to receive completion announcements. It
	 * notifies the Mapper object, unless an artificial failure has happened to
	 * the executer server. In that case the completion announcement is dropped.
	 */
	public void run() {
		this.setAlive(true);

		int serverID = 0;
		String serverAddress = new String();
		long jobID = 0;
		long jobSecondaryID = 0;
		String dateStarted = new String();
		String dateDone = new String();

		String timeStamp = new String();

		long differenceInTime = 0;

		try {
			serverSocket = new ServerSocket(getCompletionServerPort());
		} catch (IOException e1) {
			e1.printStackTrace();
		}

		while (this.isAlive()) {

			serverID = 0;
			serverAddress = "";
			jobID = 0;
			jobSecondaryID = 0;
			dateStarted = "";
			dateDone = "";
			timeStamp = "";

			try {
				socket = serverSocket.accept();

				if (!this.isAlive())
					return;

				in = new DataInputStream(socket.getInputStream());
				/* ---- Format of message sent is ------ */
				// timestamp#serverId#localAddress#jobID#secondaryJobID#dateStarted#dateDone
				GregorianCalendar current = new GregorianCalendar();

				int r;

				// Parsing timeStamp
				while ((r = in.read()) != -1 && r != '#') { // # is the
					// delimeter between
					// address and id
					timeStamp = timeStamp + ((char) r);
				}

				// Calculate the difference in time
				GregorianCalendar timeStampCal = convertToCal(timeStamp);
				differenceInTime = current.getTimeInMillis() - timeStampCal.getTimeInMillis();

				if (Math.abs(differenceInTime) > NOTICALBE_DIFFERENCE_IN_TIME) {
					this.getCompletionEventsLogger().log(
							serverAddress + " has a difference of " + (differenceInTime / 1000) + " sconds.");
				}
				// Later added to date started and date done

				// Parsing server ID
				while ((r = in.read()) != -1 && r != '#') {
					if ((char) r - '0' >= 0) {
						serverID = 10 * serverID + ((char) r - '0');
					}
				}
				// Parsing server address
				while ((r = in.read()) != -1 && r != '#') { // # is the
					// delimeter between
					// address and id
					serverAddress = serverAddress + ((char) r);
				}
				// Parsing Job ID
				while ((r = in.read()) != -1 && r != '#') {
					// if ((char)r - '0' >= 0) {
					jobID = 10 * jobID + ((char) r - '0');
					// }
				}
				// Parsing job secondary id (Xgrid ID)
				while ((r = in.read()) != -1 && r != '#') {
					// if ((char)r - '0' >= 0) {
					jobSecondaryID = 10 * jobSecondaryID + ((char) r - '0');
					// }
				}
				// Parsing date submitted
				while ((r = in.read()) != -1 && r != '#') { // # is the
					// delimeter between
					// address and id
					// if ((char)r - '0' >= 0 || (char)r =='-' || (char)r == '
					// ') {
					dateStarted = dateStarted + (char) r;
					// }
				}

				// Parsing date Done
				while ((r = in.read()) != -1 && r != '#') { // # is the
					dateDone = dateDone + (char) r;
				}

			} catch (IOException e1) {
				e1.printStackTrace();
			} finally {
				if (in != null)
					try {
						in.close();
					} catch (IOException e1) {
						e1.printStackTrace();
					}
				if (socket != null) {
					try {
						socket.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}

		
			/*------ Artificial announcers -----*/
			if (this.getMapper().isArtificialFailuresActive()) {
				
				/*
				 * Check if this is too late, in case of re-submission. This if
				 * statement checks the completed task was completed by the server
				 * it was sent to. When time outs are enabled, the mapper could
				 * announce a job as timed out and re submit it to another server.
				 * The original timed out server could be too late and return the
				 * result, however, because the job was resubmitted the mapper
				 * discard this.
				 */
				if (!this.getMapper().getJobsTable().getJobHostName(jobID).equalsIgnoreCase(serverAddress)) {
					System.out.println("CompletionServer Job Hostname: " + this.getMapper().getJobsTable().getJobHostName(jobID));
					System.out.println(serverAddress);
					continue;// too late this job has been already annoucned
								// timed out
				}

				// check with adjuster to see the if a "failure" happened
				if (Adjuster.didAFailureHappen(convertToCal(dateStarted), convertToCal(dateDone), this.getMapper()
						.getServer(serverID))) {
					// log it
					this.getFailuresLogger().log(
							"Server " + serverAddress + " failed between \t " + dateStarted + "\t" + dateDone);
					System.err.println("CompletionServer.run(): A failure happen in server: " + serverAddress);

					// Don't tell the Mapper. Assuming the server did not
					// reply.
					continue;
				}
			}

			// late result, the mapper consider this job timed out. Drop it.
			// if (this.getMapper().getJobsTable().isJobTimedOut(jobID))
			// continue;
			try {
				this.getMapper().jobIsDone(serverID, jobID, convertToCalAndAdd(dateStarted, differenceInTime),
						convertToCalAndAdd(dateDone, differenceInTime));
			} catch (Exception ex) {
				ex.printStackTrace(); // this might happen when jobs sent from
				// previous tests
			}
			this.getCompletionEventsLogger().logEvent(new Event("Job is done.", jobID));

		}

	}
}
