package org.msh.utils.date;

import javax.persistence.Embeddable;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;
import java.util.Date;


/**
 * Represent a period of time, i.e, an intervall between 2 dates
 * @author Ricardo Memoria
 *
 */
@Embeddable
public class Period {

	/**
	 * Initial date of the period
	 */
	@Temporal(TemporalType.DATE)
	private Date iniDate;
	
	/**
	 * Final date of the period
	 */
	@Temporal(TemporalType.DATE)
	private Date endDate;

	
	/**
	 * Create a new period defining the initial and final date
	 * @param iniDate
	 * @param endDate
	 */
	public Period(Date iniDate, Date endDate) {
		super();
		this.iniDate = iniDate != null? (Date)iniDate.clone() : null;
		this.endDate = endDate != null? (Date)endDate.clone() : null;
		checkDatesOrder();
	}

	
	/**
	 * Create a new period from an existing period
	 * @param p
	 */
	public Period(Period p) {
		super();
		this.iniDate = (Date)p.getIniDate().clone();
		this.endDate = (Date)p.getEndDate().clone();
	}


	/**
	 * Set the bounds of the period
	 * @param ini Initial date of the period
	 * @param end Final date of the period
	 */
	public void set(Date ini, Date end) {
		iniDate = (Date)ini.clone();
		endDate = (Date)end.clone();
		checkDatesOrder();
	}


	/**
	 * Standard constructor
	 */
	public Period() {
		super();
	}

	
	/**
	 * Check if initial date is before the initial date. If not, they are swapped
	 */
	private void checkDatesOrder() {
		if ((iniDate == null) || (endDate == null))
			return;
		if (iniDate.after(endDate)) {
			Date dt = iniDate;
			iniDate = endDate;
			endDate = dt;
		}
	}
	
	/**
	 * Return the number of days in the period
	 * @return
	 */
	public int getDays() {
		return ((iniDate != null) && (endDate != null) ? DateUtils.daysBetween(iniDate, endDate): 0);
	}

	
	/**
	 * Return number of months in the period
	 * @return
	 */
	public int getMonths() {
		return ((iniDate != null) && (endDate != null) ? DateUtils.monthsBetween(iniDate, DateUtils.incDays(endDate, 1)): 0);
	}


	/**
	 * Return number of years in the period
	 * @return
	 */
	public int getYears() {
		return ((iniDate != null) && (endDate != null) ? DateUtils.monthsBetween(iniDate, DateUtils.incDays(endDate, 1)): 0);
	}

	
	/**
	 * Check if the period is before the given period, even if they intersect
	 * @param p
	 */
	public boolean isBefore(Period p) {
		return (iniDate.before(p.getIniDate()));
	}


	/**
	 * Check if the period is after the given period, even if they intersect
	 * @param p
	 * @return
	 */
	public boolean isAfter(Period p) {
		return (iniDate.after(p.getIniDate()));
	}

	
	/**
	 * Check if a period is empty, i.e, if either the initial or the final date was not defined
	 * @return true if period is empty, otherwise false
	 */
	public boolean isEmpty() {
		return (iniDate == null) || (endDate == null);
	}


	/**
	 * Cut the period from the given date until the final date, turning in a small period
	 * from the initial date to the given date
	 * @param dt Date of cut
	 * @return true if the date is inside the period
	 */
	public boolean cutEnd(Date dt) {
		if (!isDateInside(dt))
			return false;
		
		endDate = dt;
		return true;
	}

	
	/**
	 * Cut the period from the initial date until the given date, turning in a small period
	 * from the given date to the final date
	 * @param dt Date of cut
	 * @return true if the date is inside the period
	 */
	public boolean cutIni(Date dt) {
		if (!isDateInside(dt))
			return false;
		
		iniDate = dt;
		return true;
	}

	
	/**
	 * Move the period to the initial date, keeping the distance between the initial and final date
	 * @param newIniDate the initial date of the period
	 */
	public void movePeriod(Date newIniDate) {
		if (isEmpty())
			return;
		
		int days = DateUtils.daysBetween(this.iniDate, newIniDate);
		if (iniDate.after(newIniDate))
			days = -days;

		iniDate = DateUtils.incDays(iniDate, days);
		endDate = DateUtils.incDays(endDate, days);
	}
	
	
	/**
	 * Move the period to a specific quantity of days, keeping the distance between the initial and final date
	 * @param days
	 */
	public void movePeriod(int days) {
		if (isEmpty())
			return;

		iniDate = DateUtils.incDays(iniDate, days);
		endDate = DateUtils.incDays(endDate, days);
	}


	/**
	 * Check if a given period is inside the period, including the days at the limit
	 * @param ini
	 * @param end
	 * @return
	 */
	public boolean contains(Date ini, Date end) {
		return (!ini.before(iniDate)) && (!end.after(endDate));
	}


	/**
	 * Check if a given period is inside the period, including the days at the limit
	 * @param p
	 * @return
	 */
	public boolean contains(Period p) {
		return contains(p.getIniDate(), p.getEndDate());
	}

	
	/**
	 * Check if a given period intersects the period 
	 * @param ini initial date of the given period
	 * @param end final date of the given period
	 * @return true if the period defined by (ini, end) intersect with the period
	 */
	public boolean isIntersected(Date ini, Date end) {
		return (!ini.after(endDate)) && (!end.before(iniDate));
	}

	
	/**
	 * Check if a given period intersects the period, i.e, if the periods share a common period
	 * @param p period to check if is intersected with the period
	 * @return true if periods share a common intersection
	 */
	public boolean isIntersected(Period p) {
		return isIntersected(p.getIniDate(), p.getEndDate());
	}

	
	/**
	 * Check if the period is totally inside the given period
	 * @param p outer period
	 * @return true if period is inside period p
	 */
	public boolean isInside(Period p) {
		if (isEmpty())
			return false;
		
		return ((!iniDate.before(p.getIniDate())) && (!endDate.after(p.getEndDate())));
	}


	/**
	 * Initialize the period from an existing period
	 * @param p
	 */
	public void set(Period p) {
		iniDate = (Date)p.getIniDate().clone();
		endDate = (Date)p.getEndDate().clone();
	}


	/**
	 * Intersect the period with the given period. The period is changed according
	 * to the result of the intersection
	 * @param ini initial date of the period to be intersected
	 * @param end final date of the period to be intersected
	 * @return
	 */
	public boolean intersect(Date ini, Date end) {
		if (!isIntersected(ini, end))
			return false;

		if (ini.after(iniDate))
			iniDate = ini;
		if (end.before(endDate))
			endDate = end;
			
		return true;
	}


	/**
	 * Intersect the period with the given period. The period is changed according
	 * to the result of the intersection
	 * @param p {@link Period} instance to intersect with the period
	 * @return true if period was intersected, otherwise false if the period doesn't intersect
	 */
	public boolean intersect(Period p) {
		return intersect(p.getIniDate(), p.getEndDate());
	}
	
	
	/**
	 * Check if a date is inside the period
	 * @param dt
	 * @return
	 */
	public boolean isDateInside(Date dt) {
		return (!dt.before(iniDate)) && (!dt.after(endDate));
	}


	/**
	 * Return the initial date
	 * @return
	 */
	public Date getIniDate() {
		return iniDate;
	}
	

	/**
	 * Set the initial date
	 * @param iniDate
	 */
	public void setIniDate(Date iniDate) {
		this.iniDate = (iniDate != null? (Date)iniDate.clone(): iniDate);
		checkDatesOrder();
	}


	/**
	 * Return the ending date
	 * @return
	 */
	public Date getEndDate() {
		return endDate;
	}


	/**
	 * Set the ending date
	 * @param endDate
	 */
	public void setEndDate(Date endDate) {
		this.endDate = (endDate != null? (Date)endDate.clone(): endDate);
		checkDatesOrder();
	}


	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((endDate == null) ? 0 : endDate.hashCode());
		result = prime * result + ((iniDate == null) ? 0 : iniDate.hashCode());
		return result;
	}


	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Period other = (Period) obj;
		if (endDate == null) {
			if (other.endDate != null)
				return false;
		} else if (!endDate.equals(other.endDate))
			return false;
		if (iniDate == null) {
			if (other.iniDate != null)
				return false;
		} else if (!iniDate.equals(other.iniDate))
			return false;
		return true;
	}


	@Override
	public String toString() {
		return (iniDate != null? iniDate.toString(): "null") + "..." +
				(endDate != null? endDate.toString(): "null");
	}
}
