package pulling.availability_predection;

import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.util.*;

/**
 * This thread is responsible for measuring the Usage of the server, updating
 * the history file and letting the Mapper know when the node is idle. This
 * Thread uses the AvailabilityLogger to access the history file.
 * 
 * @author Majd Kokaly
 * @version 1
 */

public class AvailabilityManager implements Runnable {
	/**
	 * Dummy constant to express that some entries are still unknown in the
	 * array readings
	 */
	final private double UNKNOWN = -1; 

	/** A hard coded minimum time resolution in minutes */
	final private int MINIMUM_TIME_RESOLUTION = 1;

	/** A hard coded minimum time between readings in minutes */
	final private int MINIMUM_TIME_BETWEEN_READINGS = 1;

	private double[] readings;
	/**
	 * The thread can take measurements of availability in two modes:
	 * distributedReadingsMode: Where many readings are taken within the
	 * resolution time and the average is taken into consideration, and
	 * singleReadingsMode: Where only one reading is taken every resolution
	 * period.
	 */
	private boolean distributedReadingsMode = true;
	/**
	 * readingFrequency is how many times, when using distributedReadingsMode,
	 * the thread should measure availability in a time tick.
	 */

	/** This parameter defines the time in minutes when availability is sent to the central Mapper. */
	private int timeresolution = 10;
	
	/**
	 * Defines the number of time in every timeresolution that the availability should be measured
	 */
	private int readingFrequency = 5;

	/**
	 * The availability of the server, as assigned by the user. Default is 1
	 */
	private double availability = 1;

	/** logger object is used to perform the logging */
	private AvailabilityLogger logger; 
	
	/** This boolean field is used to control this Runnable */
	private boolean alive = true;
	
	/** The thread object that runs this Runnable */
	private Thread thisThread;

	/** The mapper hostname. This is used for communications */
	private String mapperHostName;

	/** Port that the mapper uses to listen to availability related notification */
	private int availablityPort = 37933;

	/* -------------- Modes ---------------- */
	/**
	 * The availability module can work in different modes.
	 * Please refer to the thesis document (Section Availability model).
	 * This defines what mode is used.
	 */
	private int mode = WEIGHTED_MEAN;

	/**
	 * Is one mode of operation.
	 */
	public static int WEIGHTED_MEAN = 0;
	/**
	 * When using the WEIGHTED_MEAN. This array is used.
	 * Please refer to the thesis document (Section Availability model).
	 */
	private double[] weights = { 0.4, 0.3, 0.2, 0.1 };
	
	/**
	 * Is one mode of operation.
	 */
	public static int RECURSIVE = 1;
	
	/**
	 * When using the RECURSIVE. The value of c is used..
	 * Please refer to the thesis document (Section Availability model).
	 */
	private double c = 0.5;
	private double previousEstimation = 0.5;
	
	/**
	 * The default constructor
	 */
	public AvailabilityManager() {
		this.setLogger(new AvailabilityLogger(this.getTimeresolution(), this.getPastReadingsCount()));
		this.setMapperHostName(this.getLocalAddress());
		this.setAlive(true);
		initReadings();
	}


	public AvailabilityManager(int timeresolution, String mapperHostName, double availability) {
		this.setTimeresolution(timeresolution);
		this.setLogger(new AvailabilityLogger(this.getTimeresolution(), this.getPastReadingsCount()));
		this.setAvailability(availability);
		this.setMapperHostName(mapperHostName);
		this.setAlive(true);
		initReadings();
	}


	public AvailabilityManager(int timeresolution, String mapperHostName, int mode, double availability) {
		this.setTimeresolution(timeresolution);
		if (mode == AvailabilityManager.RECURSIVE) {
			this.setMode(AvailabilityManager.RECURSIVE);
			
			this.setLogger(new AvailabilityLogger(this.getTimeresolution(), 1));
		} else {
			this.setMode(AvailabilityManager.WEIGHTED_MEAN);
			this.setLogger(new AvailabilityLogger(this.getTimeresolution(), this.getPastReadingsCount()));
		}
		this.setAvailability(availability);
		this.setMapperHostName(mapperHostName);
		this.setAlive(true);
		initReadings();
	}


	public AvailabilityManager(int timeresolution, String mapperHostName, double c, double availability) {
		this.setTimeresolution(timeresolution);
		this.setMode(AvailabilityManager.RECURSIVE);
		this.setLogger(new AvailabilityLogger(this.getTimeresolution(), 1));
		this.setAvailability(availability);
		this.setMapperHostName(mapperHostName);
		this.setAlive(true);
		initReadings();
	}


	public AvailabilityManager(int timeresolution, String mapperHostName, double[] weights, double availability) {
		this.setTimeresolution(timeresolution);
		this.setMode(AvailabilityManager.WEIGHTED_MEAN);
		if (weights != null)
			this.setWeights(weights);
		this.setLogger(new AvailabilityLogger(this.getTimeresolution(), 1));
		this.setAvailability(availability);
		this.setMapperHostName(mapperHostName);
		this.setAlive(true);
		initReadings();
	}

	/**
	 * This method starts the Thread
	 */
	public void startThread() {
		thisThread = new Thread(this);
		thisThread.start();
	}
		
	/**
	 * This method stops the Thread
	 */
	public void stopThread() {
		boolean wasAlive = this.isAlive();
		this.setAlive(false);
		if (wasAlive) {
			thisThread.interrupt();
			thisThread = null;
		}
	}

	public AvailabilityLogger getLogger() {
		return logger;
	}

	public void setLogger(AvailabilityLogger logger) {
		this.logger = logger;
	}

	public boolean isAlive() {
		return alive;
	}

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

	public boolean isDistributedReadingsMode() {
		return distributedReadingsMode;
	}

	public void setDistributedReadingsMode(boolean distributedReadingsMode) {
		this.distributedReadingsMode = distributedReadingsMode;
		if (this.distributedReadingsMode == false) // Single mode
			this.setReadingFrequency(1);
	}

	public int getTimeresolution() {
		return timeresolution;
	}

	public void setAvailability(double avail)
	{
		this.availability = avail;
	}
	
	public double getAvailability()
	{
		return this.availability;
	}
	
	public void setTimeresolution(int timeresolution) {
		if (timeresolution >= MINIMUM_TIME_RESOLUTION) {
			this.timeresolution = timeresolution;
			if (this.getLogger() != null)
				this.getLogger().setTimeresolution(timeresolution);

		} else
			System.out.println("Could not set time resolution; Time resolution is: " + this.getTimeresolution());
	}

	public int getReadingFrequency() {
		return readingFrequency;
	}

	public void setReadingFrequency(int readingFrequency) {
		if (this.getTimeresolution() / this.readingFrequency >= MINIMUM_TIME_BETWEEN_READINGS) {
			this.readingFrequency = readingFrequency;
		} else
			System.out.println("Could not set readings frequency; Reading Frequency is: " + this.getReadingFrequency());
	}

	public String getMapperHostName() {
		return mapperHostName;
	}

	public void setMapperHostName(String mapperHostName) {
		this.mapperHostName = mapperHostName;
	}

	public int getAvailablityPort() {
		return availablityPort;
	}

	public void setAvailablityPort(int availablityPort) {
		this.availablityPort = availablityPort;
	}

	public int getMode() {
		return mode;
	}

	public void setMode(int mode) {
		if (mode == AvailabilityManager.WEIGHTED_MEAN || mode == AvailabilityManager.RECURSIVE)
			this.mode = mode;
		else
			this.mode = AvailabilityManager.WEIGHTED_MEAN;
	}

	public int getPastReadingsCount() {
		return weights.length;
	}

	public double[] getWeights() {
		return weights;
	}

	public void setWeights(double[] weights) {
		this.weights = weights;
	}

	public double getC() {
		return c;
	}

	public void setC(double c) {
		this.c = c;
	}

	public double getPreviousEstimation() {
		return previousEstimation;
	}

	public void setPreviousEstimation(double previousEstimation) {
		this.previousEstimation = previousEstimation;
	}

	/**
	 * Set every element in the reading array to be unknown
	 */
	public void initReadings() {
		this.readings = new double[this.getReadingFrequency()];
		for (int i = 0; i < readings.length; i++) {
			readings[i] = UNKNOWN;
		}
	}

	/**
	 * This method returns the average of the readings elements ignoring UNKOWN
	 * elements
	 * 
	 * @return average of readings elements.
	 */
	public double getAverage() {
		double sum = 0;
		int count = 0;
		int i;
		for (i = 0; i < readings.length; i++) {
			// To handle the special case at the beginning when not
			// every reading is taken.
			if (readings[i] != UNKNOWN) {
				sum += readings[i];
				count++;
			}
		}
		return sum / (double) count;
	}
	
	/**
	 * Please refer to Section Availability Model in the thesis document.
	 * 
	 * @param calendar is the time for which the estimation is desired
	 * @return the estimated availability
	 */
	public double getEstimatedAvailability(GregorianCalendar calendar) {
		if (this.getMode() == AvailabilityManager.WEIGHTED_MEAN) {
			double sum = 0;
			double weightSum = 0;
			for (int j = 1; j <= this.getPastReadingsCount(); j++) {
				sum += this.getLogger().getReading(j, WeekDay.getWeekDay(calendar), TimeStamp.getTimeStamp(calendar))
						* this.getWeights()[j - 1];
				weightSum += this.getWeights()[j - 1];
			}
			return sum / weightSum;
		}

		if (this.getMode() == AvailabilityManager.RECURSIVE) {
			double estimation = this.getC() * this.getPreviousEstimation() + (1 - this.getC())
					* this.getLogger().getReading(1, WeekDay.getWeekDay(calendar), TimeStamp.getTimeStamp(calendar));

			this.setPreviousEstimation(estimation);
			return estimation;
		} else
			return -1;
	}

	/**
	 * The task of this thread is basic. It wakes every specific period
	 * depending on the time resolution of the system It measures the CPU usage
	 * and logs it using the AvailabilityLogger object (logger).
	 */

	private boolean midnightHasJustPassed = false;

	public void run() {
		// initiating the log file.
		getLogger().initHistoryFile();

		boolean firstTime = true;
		// Main loop
		while (this.isAlive()) {
			this.initReadings();
			int i = 0;
			/*
			 * Change i to align the readings For example if the time resolution
			 * is 10 minutes and frequency was 5 and the thread was started at
			 * 12:07, the variable i should be advanced 3 times indicating that
			 * 3 readings were missed in current interval.
			 */
			if (firstTime) {

				i = (int) (this.getSleepTimeTillNextSystemTick() / ((((double) this.getTimeresolution()) / ((double) this
						.getReadingFrequency())) * 60 * 1000));
				i = this.getReadingFrequency() - i - 1;
				firstTime = false;
			} else
				i = 0;

			for (; i < this.getReadingFrequency(); i++) {
				// Sleep for sampleTime milliseconds (e.g System resolution/
				// Frequency)
				try {

					long timeTillNextReading = this.getSleepTimeTillNextReading();

					Thread.sleep(timeTillNextReading);
					readings[i] = CPU_Usage.getSystemIdlePercentage() * this.getAvailability();

					if (this.midnightHasJustPassed) {
						i = this.getReadingFrequency(); // to break the loop
					}
				} catch (InterruptedException e) {
					if (!isAlive())
						return;
				}
			}
			GregorianCalendar now = new GregorianCalendar();
			if (this.midnightHasJustPassed) {
				now.set(GregorianCalendar.HOUR_OF_DAY, 0);
				now.set(GregorianCalendar.MINUTE, 0);
				now.set(GregorianCalendar.SECOND, 0);
				now.set(GregorianCalendar.MILLISECOND, 0);
				now.add(GregorianCalendar.MINUTE, -1);
			} else
				now.add(GregorianCalendar.MINUTE, -1 * this.getTimeresolution());
			try {

				TimeStamp ts = new TimeStamp(now.get(Calendar.HOUR_OF_DAY), now.get(Calendar.MINUTE));

				logger.insertNewReading(this.getAverage(), WeekDay.getWeekDay(now), ts);

				this.notifyMapperWithThenNewAvailability(this.getAverage());
				System.out
						.println("A reading has been logged and sent: " + this.getAverage() + " for " + ts.toString());

			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	public static void print(GregorianCalendar cal) {
		System.out.println(cal.getTime().toString());
	}

	/**
	 * tickLengthInMinutes are the minutes in a tick or the time resolution for
	 * the system
	 * 
	 * @return the time in milliseconds to sleep to wake up at the right time
	 *         for the next time tick
	 */
	private long getSleepTimeTillNextSystemTick() {
		double tickLengthInMinutes = this.getTimeresolution();
		/*
		 * Setting beginning Of the day to the time when current day began
		 * (00:00:00 or 12:00:00 AM)
		 */
		GregorianCalendar beginningOfDay = new GregorianCalendar();
		beginningOfDay.set(GregorianCalendar.HOUR_OF_DAY, 0);
		beginningOfDay.set(GregorianCalendar.MINUTE, 0);
		beginningOfDay.set(GregorianCalendar.SECOND, 0);
		beginningOfDay.set(GregorianCalendar.MILLISECOND, 0);
		/* now is a GregorianCalendar object for current moment */
		GregorianCalendar now = new GregorianCalendar();

		/* Calculating sleep time */

		long diff = now.getTimeInMillis() - beginningOfDay.getTimeInMillis();

		double ticksInDiff = ((double) diff) / ((double) tickLengthInMinutes * 60.0 * 1000.0);

		double sleepTime = (Math.ceil(ticksInDiff) - ticksInDiff) * tickLengthInMinutes * 60.0 * 1000.0;

		return min(Math.round(sleepTime), this.getTimeTillMidnight(now));
		
	}

	/**
	 * @return the time in milliseconds to sleep to wake up at the right time
	 *         for the next time tick
	 */
	private long getSleepTimeTillNextReading() {

		/*
		 * Setting beginning Of the day to the time when current day began
		 * (00:00:00 or 12:00:00 AM)
		 */
		GregorianCalendar beginningOfDay = new GregorianCalendar();
		beginningOfDay.set(GregorianCalendar.HOUR_OF_DAY, 0);
		beginningOfDay.set(GregorianCalendar.MINUTE, 0);
		beginningOfDay.set(GregorianCalendar.SECOND, 0);
		beginningOfDay.set(GregorianCalendar.MILLISECOND, 0);

		/* now is a GregorianCalendar object for current moment */
		GregorianCalendar now = new GregorianCalendar();

		/* Calculating sleep time */
		// Calculating difference between now and the beginning of the day in
		// milli seconds
		long diff = now.getTimeInMillis() - beginningOfDay.getTimeInMillis();
		// Calculating readingPeriod time. (i.e. how many minutes per reading)
		double readingPeriod = (double) this.getTimeresolution() / (double) this.getReadingFrequency();
		// Calculating how many readings periods happened until now (as a double
		// value)
		double readingsPerioudsInDiff = ((double) diff) / ((double) readingPeriod * 60 * 1000);
		// Calculating the sleepTime until the next reading using
		// readingsPerioudsInDiff.
		// For example suppose readingsPerioudsInDiff = 130.7 reading, and Time
		// Resolution was 10 minutes and frequency was 5.
		// That means a reading per 2 minutes
		// That means the time till the next reading is .3 readingPeriod (0.3 =
		// ceil(130.7) - 130.7.)
		// 0.3 readingPeriod is 0.3 * 2 minutes = 0.6 minutes = 0.6 minutes * 60
		// second/minute * 1000 millsecond/second = sleepTime
		double sleepTime = (Math.ceil(readingsPerioudsInDiff) - readingsPerioudsInDiff) * readingPeriod * 60 * 1000;

		if (getTimeTillMidnight(now) <= Math.round(sleepTime)) {
			this.midnightHasJustPassed = true;
			System.out.println("Midnight");
			return getTimeTillMidnight(now);
		} else {
			this.midnightHasJustPassed = false;
			return Math.round(sleepTime);
		}
	}

	/**
	 * 
	 * @param cal
	 *            is the time you wish to know the difference from it to
	 *            midnight
	 * @return difference between cal and next midnight
	 */
	private long getTimeTillMidnight(GregorianCalendar cal) {
		GregorianCalendar nextMidnight = (GregorianCalendar) cal.clone();
		nextMidnight.set(GregorianCalendar.HOUR_OF_DAY, 0);
		nextMidnight.set(GregorianCalendar.MINUTE, 0);
		nextMidnight.set(GregorianCalendar.SECOND, 0);
		nextMidnight.set(GregorianCalendar.MILLISECOND, 0);
		nextMidnight.set(GregorianCalendar.DAY_OF_MONTH, cal.get(GregorianCalendar.DAY_OF_MONTH) + 1);

		return nextMidnight.getTimeInMillis() - cal.getTimeInMillis();
	}

	/**
	 * 
	 * @param a
	 * @param b
	 * @return minimum between a or b. If equals returns a
	 */
	private long min(long a, long b) {
		if (a <= b)
			return a;
		else
			return b;
	}


	public void printAllTimeIntervals() {
		GregorianCalendar beginningOfDay = new GregorianCalendar();
		beginningOfDay.set(GregorianCalendar.HOUR_OF_DAY, 0);
		beginningOfDay.set(GregorianCalendar.MINUTE, 0);
		beginningOfDay.set(GregorianCalendar.SECOND, 0);
		beginningOfDay.set(GregorianCalendar.MILLISECOND, 0);

		GregorianCalendar nextMidnight = (GregorianCalendar) beginningOfDay.clone();
		nextMidnight.set(GregorianCalendar.HOUR_OF_DAY, 0);
		nextMidnight.set(GregorianCalendar.MINUTE, 0);
		nextMidnight.set(GregorianCalendar.SECOND, 0);
		nextMidnight.set(GregorianCalendar.MILLISECOND, 0);
		nextMidnight.set(GregorianCalendar.DAY_OF_MONTH, beginningOfDay.get(GregorianCalendar.DAY_OF_MONTH) + 1);

		while (beginningOfDay.before(nextMidnight)) {
			beginningOfDay.add(GregorianCalendar.MILLISECOND, this.getTimeresolution() * 60 * 1000);
			print(beginningOfDay);
		}
	}

	/**
	 * This method notifies Mapper with the availability.
	 * 
	 * @param availability
	 *           The new aj value.
	 * @throws UnknownHostException 
	 * 
	 */
	public void notifyMapperWithThenNewAvailability(double availability) throws UnknownHostException {
		Socket socket = null;
		DataOutputStream out = null;
		try {
			socket = new Socket(this.getMapperHostName(), this.getAvailablityPort());
			out = new DataOutputStream(socket.getOutputStream());
			out.writeBytes(this.getLocalAddress() + "#" + availability);
		} catch (UnknownHostException e) {
			System.err.println("Don't know about mapper: " + this.getMapperHostName());
			e.printStackTrace();
		} catch (IOException e) {
			System.err.println("Couldn't get I/O for " + "the connection to Mapper: " + this.getMapperHostName());
			System.out.println(this.getAvailablityPort());
			e.printStackTrace();
		} finally {

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

	private String getLocalAddress() {
		try {
			return InetAddress.getLocalHost().getHostName();
		} catch (UnknownHostException e) {
			e.printStackTrace();
			return null;
		}
	}

	public String toString() {
		return this.getMapperHostName() + " Resolution: " + this.getTimeresolution() + " Frequency: "
				+ this.getReadingFrequency();
	}

	public void print() {
		System.out.print(this.toString());
	}


}
