package adjusting.availability_adjusting;

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

/**
 * 
 * This class resembles the periods of time that a Server object should be
 * artificially failed in them. For example: [12:01 - 12:03] , [ 14:05 - 14:10 ] , [
 * 18:23: 18:33] is a trace meaning that the server in subject will be
 * unavailable or failed in the times 12:01 to 12:03, 14:05 to 14:10 and 18:23
 * to 18:33.
 * 
 * @author Majd Kokaly
 * 
 */
public class FailureTrace implements Serializable 
{

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

	/**
	 * The array of FailurePeriod objects which constructs this Failure Trace.
	 */
	private ArrayList<FailurePeriod> failingPeriods;

	public FailureTrace() 
	{
		this.setFailurePeriods(new ArrayList<FailurePeriod>());
	}

	public ArrayList<FailurePeriod> getFailurePeriods() 
	{
		return failingPeriods;
	}

	public void setFailurePeriods(ArrayList<FailurePeriod> failingPeriods) 
	{
		this.failingPeriods = failingPeriods;
	}

	public int getNumbersOfPeriods() 
	{
		return this.failingPeriods.size();
	}

	/**
	 * 
	 * @return the mean of the lengths of all the FailurePeriod objects in this
	 *         trace. (length is: endTime - beginingTime)
	 */
	public double getMeanOfPeriodsInMinutes() 
	{
		
		if (this.getNumbersOfPeriods() == 0)
			return 0;
		
		double sum = 0;
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{
			sum += this.getFailurePeriods().get(i).getLengthInMinutes();
		}
		
		return (sum / this.getNumbersOfPeriods());
	}

	/**
	 * 
	 * @return the mean of the lengths of periods between artificial periods.
	 *         (i.e the mean of up-time periods)
	 */
	public double getMeanIfInterPeriodsInMinutes() 
	{
		
		if (this.getNumbersOfPeriods() == 0)
			return 0;

		double sum = 0;
		
		for (int i = 0; i < this.getNumbersOfPeriods() - 1; i++) 
		{
			sum += (this.getFailurePeriods().get(i + 1).getBeginningTime().getTimeInMillis() - this.getFailurePeriods()
					.get(i).getEndTime().getTimeInMillis()) / 60000.0;
		}

		return (sum / (this.getNumbersOfPeriods() - 1));
	}

	/**
	 * 
	 * @param period
	 *            Is FailurePeriod Object
	 * @return true if addition succeeded, false otherwise. Additions might fail
	 *         if the new period overlaps an existing
	 *         FailurePeriod
	 * 
	 */
	public boolean add(FailurePeriod period) 
	{
		
		if (this.getFailurePeriods().isEmpty()) 
		{
			this.getFailurePeriods().add(period);
			return true;
		}
		
		/* This for loop checks the trace before adding the new period. Overlapping periods are not allowed. */
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{
			if (period.getEndTime().before(this.getFailurePeriods().get(i).getEndTime())) 
			{
				if (period.isOverLapped(this.getFailurePeriods().get(i))) 
				{
					return false;
				} 
				else
				{
					this.getFailurePeriods().add(i, period);
					return true;
				}
			}
		}
		
		this.getFailurePeriods().add(period);
		return true;
	}

	public void print()
	{
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{
			this.getFailurePeriods().get(i).print();
		}
	}

	/**
	 * 
	 * @param time
	 *            Is a GregorianCalendar (i.e time object)
	 * @return True if time is in any FailurePeriod object in this trace and
	 *         false otherwise.
	 */
	public boolean IsTimeInFailure(GregorianCalendar time) 
	{
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{
			
			if (this.getFailurePeriods().get(i).isTimeInPeriod(time))
				return true;
			
			if (time.before(this.getFailurePeriods().get(i).getEndTime()))
				return false;
		}
		
		return false;
	}

	/**
	 * 
	 * @param time
	 *            Calendar object
	 * @return the end of the failure period that the calendar is in. If the calendar is not in
	 *         any failure period, null is returned.
	 */
	public GregorianCalendar getEndOfFailure(GregorianCalendar time) 
	{
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{
			if (this.getFailurePeriods().get(i).isTimeInPeriod(time))
				return getFailurePeriods().get(i).getEndTime();
			
			if (time.before(this.getFailurePeriods().get(i).getEndTime()))
				return null;
		}
		return null;
	}

	/**
	 * 
	 * @param period
	 *            Is a FailurePeriod object.
	 * @return True if the period overlaps with some FailurePeriod object in this
	 *         trace, false otherwise.
	 */
	public boolean doesPeriodOverlapAFailurePeriod(FailurePeriod period) 
	{
		
		for (int i = 0; i < this.getNumbersOfPeriods(); i++) 
		{		
			if (this.getFailurePeriods().get(i).isOverLapped(period))
				return true;
		}
		
		return false;
	}

	/**
	 * 
	 * @param startTime Is a Time 
	 * @param endTime Is a Time
	 * @return true if the time range between startTime and endTime overlaps with some FailurePeriod object in this
	 *         trace, false otherwise.
	 */
	public boolean doesRangeOverlapAFailurePeriod(GregorianCalendar startTime, GregorianCalendar endTime) 
	{
		FailurePeriod range = new FailurePeriod(startTime, endTime);
		return this.doesPeriodOverlapAFailurePeriod(range);
	}

	/**
	 * 
	 * This method fills a trace of failure periods.
	 * 
	 * @param startTime
	 *            The start time of the trace
	 * @param endTime
	 *            The end time of the trace
	 * @param periodLengthMean
	 *            The mean of all the periods' lengths
	 * @param interPeriodLengthMean
	 *            The mean between the periods of failures
	 * @param minutesInTimeUnit
	 *            The minutes per Time Unit of the system
	 */

	public void fillTrace(GregorianCalendar startTime, GregorianCalendar endTime, double periodLengthMean,
			double interPeriodLengthMean, double minutesInTimeUnit) 
	{
		this.setFailurePeriods(new ArrayList<FailurePeriod>());

		GregorianCalendar temp = startTime;
		probability_distribution.ExponentialDist periodLengthDist = new probability_distribution.ExponentialDist(
				1.0 / (periodLengthMean * minutesInTimeUnit));
		probability_distribution.ExponentialDist interPeriodLengthDist = new probability_distribution.ExponentialDist(
				1.0 / (interPeriodLengthMean * minutesInTimeUnit));

		GregorianCalendar startOfPeriod;
		GregorianCalendar endOfPeriod;

		int count = 0;
		double interPeriodLength, sumInterPeriodLength = 0;
		double periodLength, sumPeriodLength = 0;

		while (temp.before(endTime)) 
		{

			interPeriodLength = interPeriodLengthDist.getTimeToNextEvent();
			periodLength = periodLengthDist.getTimeToNextEvent();

			sumInterPeriodLength += interPeriodLength;
			sumPeriodLength += periodLength;
			count++;

			temp.add(GregorianCalendar.SECOND, (int) Math
					.round((interPeriodLength - Math.floor(interPeriodLength)) * 60));
			temp.add(GregorianCalendar.MINUTE, (int) interPeriodLength);

			startOfPeriod = (GregorianCalendar) temp.clone();
			endOfPeriod = (GregorianCalendar) temp.clone();

			endOfPeriod.add(GregorianCalendar.SECOND, (int) Math.round((periodLength - Math.floor(periodLength)) * 60));
			endOfPeriod.add(GregorianCalendar.MINUTE, (int) periodLength);

			temp = (GregorianCalendar) endOfPeriod.clone();

			this.add(new FailurePeriod(startOfPeriod, endOfPeriod));

		}
	}

}
