package gov.va.med.esr.common.infra;

// Java Classes
import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.lang.Validate;

import gov.va.med.fw.util.StringUtils;
import gov.va.med.fw.util.date.ImpreciseDateSupport;

/**
 * A date that can be precise or imprecise. An imprecise date is a date with
 * only year, or year and month components. All other formats are considered
 * precise.
 */
public class ImpreciseDate implements Cloneable, Serializable, Comparable, ImpreciseDateSupport {
	/**
	 * An instance of serialVersionUID
	 */
	private static final long serialVersionUID = 7298656148593521292L;

	// The storage for the precise date.
	private Date date;

	// The string format of the imprecise date.
	private String string;

	// The storage for the imprecise and precise dates.
	private Calendar calendar;

	// Boolean that determines if the day portion of the date is precise
	private boolean dayPrecise;

	// Boolean that determines if the month portion of the date is precise
	private boolean monthPrecise;

	// Boolean that determines whether a time portion was specified.
	private boolean timePresent;

	// Boolean that determines whether a time seconds portion was specified.
	private boolean secPresent;
	
	/** Date format wihtout timestamp used for both imprecise and precise dates. */
	public static final String STANDARD_IMPRECISE_DATE_FORMAT_WITH_ALL_COMPONENTS = "yyyyMMdd";

	/** Date format with timestamp used for both imprecise and precise dates. */
	public static final String STANDARD_IMPRECISE_DATE_HHMM_TS_FORMAT_WITH_ALL_COMPONENTS = "yyyyMMddHHmm";

	/** Date format with timestamp used for both imprecise and precise dates. */
	public static final String STANDARD_IMPRECISE_DATE_TS_FORMAT_WITH_ALL_COMPONENTS = "yyyyMMddHHmmss";

	/** Date format without timestamp used for both imprecise and precise dates. */
	public static final String CONVENIENCE_PRECISE_DATE_PATTERN = "MM/dd/yyyy";

	/** Date format with timestamp used for both imprecise and precise dates. */
	public static final String CONVENIENCE_PRECISE_DATE_HHMM_TIMESTAMP_PATTERN = "MM/dd/yyyy HH:mm";

	/** Date format with timestamp used for both imprecise and precise dates. */
	public static final String CONVENIENCE_PRECISE_DATE_TIMESTAMP_PATTERN = "MM/dd/yyyy HH:mm:ss";

	/**
	 * Force usage of one of the other constructors.
	 */
	private ImpreciseDate() {
		super();
	}

	/**
	 * An ImpreciseDate supports either a precise date or an imprecise date.
	 * 
	 * We will try them in the following order:
	 * 
	 * 1) Imprecise date in the format of yyyyMMddHHmm where ddHHmm are optional
	 * "0" filled. For example, the following are all valid: 2005, 200512,
	 * 20051200, 200512000000. 2) Precise date in the format of yyyyMMddHHmm.
	 * For example, 200512010000, 200512012309. 3) Precise date in the format of
	 * MM/dd/yyyy HH:mm. For example, 12/01/2005, 12/01/2005 23:09. Note that
	 * this format should no longer be used and it's implementation will
	 * probably be removed in the future. Callers who have a date in this format
	 * should convert it into the Option 2 format instead.
	 * 
	 * If any of these formats aren't followed, an IllegalArgumentException will
	 * be thrown.
	 * 
	 * @param date
	 *            The string format of the date
	 * @throws IllegalArgumentException
	 *             if the format is invalid.
	 */
	public ImpreciseDate(String date) throws IllegalArgumentException {
		// Ensure something was passed in
		if (StringUtils.isBlank(date)) {
			throw new IllegalArgumentException(
					"Can not create ImpreciseDate from a null or empty string.");
		}

		try {
			// 1) Try the Imprecise format of yyyyMM.
			initialize(((new ImpreciseDateStringFormat(date)).getCalendar()));
		} catch (Exception e) {
			try {
				// 2a) Try the precise format of yyyyMMddHHmmss.
				initialize((new SimpleDateFormat(
						STANDARD_IMPRECISE_DATE_TS_FORMAT_WITH_ALL_COMPONENTS))
						.parse(date));
				this.timePresent = true;
				this.secPresent = true;
			} catch (ParseException e1) {
				try {
					// Try the precise format of yyyyMMddHHmm.
					initialize((new SimpleDateFormat(
							STANDARD_IMPRECISE_DATE_HHMM_TS_FORMAT_WITH_ALL_COMPONENTS))
							.parse(date));
					this.timePresent = true;
					this.secPresent = false;

				} catch (ParseException tsE1) {
					try {
						// 2b) Try the precise format of yyyyMMdd.
						initialize((new SimpleDateFormat(
								STANDARD_IMPRECISE_DATE_FORMAT_WITH_ALL_COMPONENTS))
								.parse(date));
						this.timePresent = false;
					} catch (ParseException e2) {
						try {
							// ***** REVISIT *****
							// Remove support for this format here since it
							// makes this class too flexible
							// and too difficult to maintain. The Option 2
							// format should be the only
							// format supported.

							// 3a) Try the precise format of MM/dd/yyyy
							// HH:mm:ss.
							initialize((new SimpleDateFormat(
									CONVENIENCE_PRECISE_DATE_TIMESTAMP_PATTERN))
									.parse(date));
							this.timePresent = true;
							this.secPresent = true;
						} catch (ParseException e3) {
							try {
								// Try the precise format of MM/dd/yyyy
								// HH:mm.
								initialize((new SimpleDateFormat(
										CONVENIENCE_PRECISE_DATE_HHMM_TIMESTAMP_PATTERN))
										.parse(date));
								this.timePresent = true;
								this.secPresent = false;
							} catch (ParseException tsE3) {
								try {
									// ***** REVISIT *****
									// Remove support for this format here since
									// it
									// makes this class too flexible
									// and too difficult to maintain. The Option
									// 2
									// format should be the only
									// format supported.

									// 3b) Try the precise format of MM/dd/yyyy.
									initialize((new SimpleDateFormat(
											CONVENIENCE_PRECISE_DATE_PATTERN))
											.parse(date));
									this.timePresent = false;
								} catch (ParseException e4) {
									// The format isn't valid.
									throw new IllegalArgumentException(
											"The passed in date '"
													+ date
													+ "' has an invalid format.  Supported formats are '"
													+ STANDARD_IMPRECISE_DATE_TS_FORMAT_WITH_ALL_COMPONENTS
													+ "' and '"
													+ CONVENIENCE_PRECISE_DATE_TIMESTAMP_PATTERN
													+ "'.");
								}
							}
						}
					}
				}
			}
		}
	}

	/**
	 * Constructor to create a precise date.
	 * 
	 * @param date
	 *            The precise date value.
	 */
	public ImpreciseDate(Date date) {
		super();
		initialize(date);
	}

	/**
	 * Constructor to create either a precise or imprecise date based on the
	 * calendar provided. To construct an imprecise date, the month or day part
	 * of the calendar should not be set (i.e. calendar.isSet(Calendar.MONTH) or
	 * calendar.isSet(Calendar.DAY_OF_MONTH) should equal false). <p/> If at
	 * least the year, month, and day is provided, then the value is precise.
	 * 
	 * @param calendar
	 *            The calendar used to create the date.
	 */
	public ImpreciseDate(Calendar calendar) {
		super();
		initialize(calendar);
	}

	/**
	 * Constructor to create an ImpreciseDate based on another ImpreciseDate.
	 * 
	 * @param impreciseDate
	 *            The ImpreciseDate to construct a new ImpreciseDate with.
	 */
	public ImpreciseDate(ImpreciseDate impreciseDate) {
		super();

		Validate.notNull(impreciseDate,
				"The passed in impreciseDate cannot be null.");

		// Store the properties from the passed in ImpreciseDate
		this.date = impreciseDate.date;
		this.calendar = impreciseDate.calendar;
		this.string = impreciseDate.string;
		this.dayPrecise = impreciseDate.dayPrecise;
		this.monthPrecise = impreciseDate.monthPrecise;
		this.timePresent = impreciseDate.timePresent;
		this.secPresent = impreciseDate.secPresent;
	}

	/**
	 * Initialize this class with a precise date.
	 * 
	 * @param date
	 *            The precise date.
	 */
	private void initialize(Date date) {
		// Ensure a date was passed in
		Validate.notNull(date, "The passed in date cannot be null.");

		// Store a cloned version of the date
		this.date = (Date) date.clone();

		// Store the calendar
		this.calendar = Calendar.getInstance();
		this.calendar.setLenient(false);
		this.calendar.setTime(this.date);

		// Store the other properties
		this.string = null;
		this.dayPrecise = true;
		this.monthPrecise = true;
		this.timePresent = true;
		this.secPresent = true;
	}

	/**
	 * Initialize this class with either a precise or imprecise date.
	 * 
	 * @param calendar
	 *            The date.
	 */
	private void initialize(Calendar calendar) {
		// Ensure a calendar was passed in
		Validate.notNull(calendar, "The passed in calendar cannot be null.");

		// Parse the calendar from the least precise to the most precise
		if (!calendar.isSet(Calendar.YEAR)) {
			// At least a year must be specified
			throw new IllegalArgumentException(
					"The passed in calendar must contain a year.");
		} else {
			if (!calendar.isSet(Calendar.MONTH)) {
				// Calendar contains only a year (Imprecise)
				this.string = (new ImpreciseDateStringFormat(
						(Calendar) calendar.clone())).getString();
				this.date = null;
				this.monthPrecise = false;
				this.dayPrecise = false;
				this.timePresent = false;

				// Store the calendar
				this.calendar = (Calendar) calendar.clone();
				this.calendar.clear();
				this.calendar.setLenient(false);
				this.calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
			} else {
				if (!calendar.isSet(Calendar.DAY_OF_MONTH)) {
					// Calendar contains only the month and year (Imprecise)

					// Test for valid month
					Calendar testCalendar = (Calendar) calendar.clone();
					testCalendar.setLenient(false);
					testCalendar.set(Calendar.MONTH, 0);
					testCalendar.set(Calendar.DAY_OF_MONTH, 1);
					this.getDateFromCalendar(testCalendar);

					// Store the properties
					this.date = null;
					this.string = (new ImpreciseDateStringFormat(
							(Calendar) calendar.clone())).getString();
					this.monthPrecise = true;
					this.dayPrecise = false;
					this.timePresent = false;

					// Store the calendar
					this.calendar = (Calendar) calendar.clone();
					this.calendar.clear();
					this.calendar.setLenient(false);
					this.calendar.set(Calendar.YEAR, calendar
							.get(Calendar.YEAR));
					this.calendar.set(Calendar.MONTH, calendar
							.get(Calendar.MONTH));
				} else {
					// Calendar is precise
					this.calendar = (Calendar) calendar.clone();
					this.calendar.setLenient(false);

					// Determine whether the time was specified
					if ((this.calendar.isSet(Calendar.HOUR_OF_DAY))
							&& (this.calendar.isSet(Calendar.MINUTE))) {
						this.timePresent = true;
						if (this.calendar.isSet(Calendar.SECOND)) {
							this.secPresent = true;
						}
						else {
							this.secPresent = false;
						}
					} else {
						this.timePresent = false;
					}

					// Initialize the parts of the calendar that aren't set to
					// 0's.
					if (!this.calendar.isSet(Calendar.HOUR_OF_DAY)) {
						this.calendar.set(Calendar.HOUR_OF_DAY, 0);
					}
					if (!this.calendar.isSet(Calendar.MINUTE)) {
						this.calendar.set(Calendar.MINUTE, 0);
					}
					if (!this.calendar.isSet(Calendar.SECOND)) {
						this.calendar.set(Calendar.SECOND, 0);
					}
					if (!this.calendar.isSet(Calendar.MILLISECOND)) {
						this.calendar.set(Calendar.MILLISECOND, 0);
					}

					// Store the properties
					this.date = this.getDateFromCalendar(this.calendar);
					this.string = null;
					this.dayPrecise = true;
					this.monthPrecise = true;
				}
			}
		}
	}

	/**
	 * A calendar representing the precise or imprecise date.
	 * 
	 * @return the Calendar
	 */
	public Calendar getCalendar() {
		return (Calendar) this.calendar.clone();
	}

	/**
	 * The precise date value. Will return null if value is not precise.
	 * 
	 * @return the precise date.
	 */
	public Date getDate() {
		return (this.date == null) ? null : (Date) this.date.clone();
	}

	/**
	 * Returns whether the value is imprecise.
	 * 
	 * @return whether the value is imprecise.
	 */
	public boolean isImprecise() {
		return (this.date == null);
	}

	/**
	 * Returns whether the value is precise.
	 * 
	 * @return whether the value is precise.
	 */
	public boolean isPrecise() {
		return !this.isImprecise();
	}

	/**
	 * Returns whether the day is precise.
	 * 
	 * @return whether the day is precise.
	 */
	public boolean isDayPrecise() {
		return this.dayPrecise;
	}

	/**
	 * Returns whether the month is precise.
	 * 
	 * @return whether the month is precise.
	 */
	public boolean isMonthPrecise() {
		return this.monthPrecise;
	}

	/**
	 * Returns whether the time is present.
	 * 
	 * @return whether the time is present.
	 */
	public boolean isTimePresent() {
		return this.timePresent;
	}

	public boolean isSecPresent() {
		return this.secPresent;
	}
	
	/**
	 * Returns the string representation of the imprecise date in the following
	 * format: yyyyMM. Will return null if the value is precise.
	 * 
	 * @return the string representation of the value.
	 */
	public String getString() {
		return this.string;
	}

	/**
	 * Converts the passed in calendar into a date.
	 * 
	 * @param calendar
	 *            The calender
	 * 
	 * @return The date
	 * 
	 * @throws IllegalArgumentException
	 *             if the passed in calendar can't be converted into a date.
	 */
	private Date getDateFromCalendar(Calendar calendar) {
		try {
			return calendar.getTime();
		} catch (IllegalArgumentException e) {
			throw new IllegalArgumentException(
					"The passed in calendar represents an invalid date.");
		}
	}

	/**
	 * Returns the string value of this ImpreciseDate.
	 * 
	 * @return The string value.
	 */
	public String toString() {
		return (this.date == null) ? this.string : this.date.toString();
	}

	/**
	 * Compares the passed in object for equality.
	 * 
	 * @param o
	 *            the object to compare.
	 * 
	 * @return True if the objects are equal or false if not.
	 */
	public boolean equals(Object o) {
		return ((this == o) || ((o instanceof ImpreciseDate) && this
				.equals((ImpreciseDate) o)));
	}

	/**
	 * Compares the passed in ImpreciseDate for equality.
	 * 
	 * @param o
	 *            The imprecise date to compare.
	 * 
	 * @return True if the objects are equal or false if not.
	 */
	private boolean equals(ImpreciseDate o) {
		return (this.date == null) ? this.string.equals(o.string) : this.date
				.equals(o.date);
	}

	/**
	 * @see java.lang.Object#hashCode()
	 */
	public int hashCode() {
		return (this.date == null) ? this.string.hashCode() : this.date
				.hashCode();
	}

	/**
	 * @see java.lang.Object#clone()
	 */
	public Object clone() {
		return new ImpreciseDate(this);
	}

	/**
	 * @see java.lang.Comparable#compareTo(java.lang.Object)
	 */
	public int compareTo(Object o) {
		// Same pattern as java.lang pacakge (allow for ClassCastException)
		return compareTo((ImpreciseDate) o);
	}

	/**
	 * @see java.lang.Comparable#compareTo(Object)
	 */
	public int compareTo(ImpreciseDate o) {
		int val = 0;

		if (o == null) {
			val = -1;
		} else {
			if (!equals(o)) {
				// 1.1 Imprecise Dates
				// When a date is entered into the system without the “day”
				// component, for the purposes of computations and comparisons,
				// the system will treat the day as “15.”
				// When a date is entered into the system without the “month”
				// component, for the purposes of computations and comparisons,
				// the system will treat the month as “06.”
				Date thisDate = ImpreciseDateUtils.getDateWithDefault(this);
				Date oDate = ImpreciseDateUtils.getDateWithDefault(o);

				val = (thisDate.after(oDate) ? 1 : -1);
			}
		}
		return val;
	}

	/**
	 * Returns the date in a standard format. The standard format will use the
	 * yyyyMMddHHmm format, but only the parts of the date that are present will
	 * be returned.
	 * 
	 * @return the standard format for the date.
	 */
	public String toStandardFormat() {
		String val = null;
		if (isImprecise()) {
			val = getString();
		} else {
			if (isTimePresent()) {
				if (isSecPresent()) {
					val = new SimpleDateFormat(
						STANDARD_IMPRECISE_DATE_TS_FORMAT_WITH_ALL_COMPONENTS)
						.format(getDate());
				}
				else {
					val = new SimpleDateFormat(
							STANDARD_IMPRECISE_DATE_HHMM_TS_FORMAT_WITH_ALL_COMPONENTS)
							.format(getDate());
				}
			} else {
				val = new SimpleDateFormat(
						STANDARD_IMPRECISE_DATE_FORMAT_WITH_ALL_COMPONENTS)
						.format(getDate());
			}
		}
		return val;
	}
	
	public String getyyyyMMddFormat()
	{
		//CCR12716 Fix (Z05 not triggered upon mailing Address update in UI)
		//precise date
		Date dt = getDate();
		if (dt == null) {
			//imprecise date
			Calendar cal = getCalendar();
			if (cal != null) {
				dt = cal.getTime();
			}
		}
		if (dt != null) {
			return new SimpleDateFormat(
					STANDARD_IMPRECISE_DATE_FORMAT_WITH_ALL_COMPONENTS)
			.format(dt);
		} else {
			//precise or imprecise date is not present
			//scenario may not possible
			return "";
		}
	}
	
	// allows for java bean access
	public String getStandardFormat() {
		return toStandardFormat();
	}

	/**
	 * Gets the string format of the year.
	 * 
	 * @return The year.
	 */
	public String getYear() {
		return toStandardFormat().substring(0, 4);
	}
	
	/**
	 * Gets the string format of the month.
	 * 
	 * @return The month.
	 */
	public String getMonth() {
		return toStandardFormat().substring(4, 6);
	}
	
	/**
	 * Gets the string format of the day.
	 * 
	 * @return The day.
	 */
	public String getDay() {
		return toStandardFormat().substring(6, 8);
	}

	/* (non-Javadoc)
	 * @see gov.va.med.fw.util.ImpreciseDateSupport#getImpreciseDate()
	 */
	public String getImpreciseDate() {
		return getString();
	}
}