/*
 *  Draco Econometrics
 *  Copyright (C) 2006-2007 Approximatrix, LLC
 *  http://www.approximatrix.com
 *  
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.approximatrix.draco.datamodel;

public class DateObject extends Number {

	public static final int FMT_MONTH_FIRST = 1;
	public static final int FMT_DAY_FIRST = 2;
	private static int date_format = FMT_MONTH_FIRST;
	
	private static final char date_separator = '/'; 
	
	public static final int YEARLY = 1;
	public static final int QUARTERLY = 2;
	public static final int MONTHLY = 3;
	public static final int BIMONTHLY = 4;
	public static final int WEEKLY = 5;
	public static final int DAILY = 6;
	
	public static final String YEARLY_STR = "Annual";
	public static final String MONTHLY_STR = "Monthly";
	public static final String BIMONTHLY_STR = "Bi-Monthly";
	public static final String QUARTERLY_STR = "Quarterly";
	public static final String WEEKLY_STR = "Weekly";
	public static final String DAILY_STR = "Daily";

	private DateObject base = null;
	private int type = 1;
	
	private int year;
	private int quarter;
	private int month;
	private int monthhalf;
	private int week;
	private int day;
	
	/** Sets format of the String representation of DateObjects
	 * 
	 * @param format the format to use
	 */
	public static void setDateFormat(int format) {
		date_format = format;
	}
	
	/** Returns the current date formatting standard
	 *
	 * @return the enumerator representing the date format
	 */
	public static int getDateFormat() {
		return date_format;
	}
	
	/** Returns the number of days in a year
	 * 
	 * @param year the year to calculate from
	 * @return either 365 or 366 if it is a leap year
	 */
	private static int getDaysInYear(int year) {
		if(!isLeapYear(year))
			return 365;
		else
			return 366;
	}
	
	/** Determines if a year is actually a leap year
	 * 
	 * @param year the year to test
	 * @return true if it is a leap year
	 * @see http://mathforkids.allinfoabout.com/features/leapyear.html
	 */
	private static boolean isLeapYear(int year) {
		if(year % 4 != 0) 
			return false;
		
		if(year % 400 == 0)
			return false;
		
		return true;
	}
	
	/** Returns the number of days in a given month in a given year
	 * 
	 * @param month the month, from 1 to 12
	 * @param year the year
	 * @return the number of days in the month
	 */ 
	private static int daysInMonth(int month, int year) {
		int days = 0;
		
		switch(month) {
			case 9:	
			case 4:
			case 6:
			case 11:
				days = 30;
				break;
			case 2:
				if(isLeapYear(year))
					days = 29;
				else
					days = 28;
				break;
			default:
				days = 31;
				break;
		}
		
		return days;
	}
	
	/** Returns a String describing the DateObject's type designation
	 * 
	 * @return a String contianing the description
	 */
	public static String getTypeString(int type) {
		switch(type) {
			case YEARLY:
				return YEARLY_STR;
			case MONTHLY:
				return MONTHLY_STR;
			case BIMONTHLY:
				return BIMONTHLY_STR;
			case QUARTERLY:
				return QUARTERLY_STR;
			case WEEKLY:
				return WEEKLY_STR;
			case DAILY:
				return DAILY_STR;
			default:
				return null;
		}
	}
	
	/** Returns the int corresponding to the input String describing 
	 * the DateObject's type designation
	 * 
	 * @return an int designating the type
	 */
	public static int getTypeInt(String type) {
		if(type.compareTo(YEARLY_STR)==0) 
			return YEARLY;
		else if(type.compareTo(MONTHLY_STR)==0) 
			return MONTHLY;
		else if(type.compareTo(BIMONTHLY_STR)==0) 
			return BIMONTHLY;
		else if(type.compareTo(QUARTERLY_STR)==0) 
			return QUARTERLY;
		else if(type.compareTo(WEEKLY_STR)==0) 
			return WEEKLY;
		else if(type.compareTo(DAILY_STR)==0) 
			return DAILY;
		else 
			return -1;
	}
	
	/** Constructor that generates a new DateObject based on an original base
	 * and appropriately increments above the base by the specified number of
	 * time units based on the base type
	 *  
	 * @param base the base DateObject
	 * @param value the number of time units above the base
	 */ 
	public DateObject(DateObject base, int value) {
		initialize(base.getType(), base);
		
		year = base.getYear();
		quarter = base.getQuarter();
		month = base.getMonth();
		monthhalf = base.getMonthHalf();
		week = base.getWeek();
		day = base.getDay();
		
		this.increment(value);
	}
	
	/** Not implemented properly - do not use
	 * 
	 * @param type the DateObject type
	 * @param value the Date in string form
	 * 
	 * @deprecated Never implemented properly - use the ParseDate class
	 */
	public DateObject(int type, String value) {
		initialize(type, null);
	}
	
	/** Constructs a new DateObject without a base (meaning it probably
	 * is a base) based on the year/month/day provided.  Set month and/or
	 * day to zero if not appplicable.
	 * 
	 * @param type the type of the DateObject
	 * @param year the year
	 * @param month the month or zero if NA
	 * @param day the day or zero if NA
	 */
	public DateObject(int type, int year, int month, int day) {
		initialize(type, null);
		
		switch(type) {
			case YEARLY:
				this.year = year;
				break;
			case MONTHLY:
				this.month = month;
				this.year = year;
				break;
			case DAILY:
				this.day = day;
				this.month = month;
				this.year = year;
				break;
		}
	}
	
	/** Constructs a new DateObject without a base (meaning it probably
	 * is a base) using a collection of parameters.  This constructor
	 * may be used to build any type of date object, including weekly
	 * and quarterly.  All non-applicable parameters for a given type
	 * are ignored during construction.
	 * 
	 * @param type the type of the DateObject
	 * @param year the year
	 * @param quarter the quarter or zero if not quarterly data
	 * @param month the month or zero if not monthly data
	 * @param monthhalf the monthhalf or zero if not bimonthly data
	 * @param week the week or zero if not weekly data
	 * @param day the day or zero if not daily data
	 */
	public DateObject(int type, int year, int quarter, int month, int monthhalf, int week, int day) {
		this(type,year,month,day);
		
		switch(type) {
			case QUARTERLY:
				this.quarter = quarter;
				this.year = year;
				break;
			case BIMONTHLY:
				this.monthhalf = monthhalf;
				this.month = month;
				this.year = year;
				break;
			case WEEKLY:
				this.week = week;
				this.year = year;
				break;
			
		}
		
		// Call an increment with a zero value to update all the
		// subunits
		this.increment(0);
	}
	
	/** Initializes a new DateObject based on the specified type and assigns
	 * the base value (or null) to the stored base DateObject
	 * 
	 * @param type the type of the DateObject
	 * @param base the base Date Object or null if this object is a base
	 */
	private void initialize(int type, DateObject base) {
		this.base = base;
		this.type = type;
	}
	
	/** Increments the DateObject by one time unit
	 * 
	 */
	public void increment() {
		this.increment(1);
	}
	
	/** Increments the DateObject by the number of time units specified
	 * and updates any other subunits accordingly
	 * 
	 * @param amt the number of time untis to increment
	 */
	public void increment(int amt) {
		switch(type) {
			case YEARLY:
				year+=amt;
				break;
			case MONTHLY:
				month+=amt;
				break;
			case QUARTERLY:
				quarter+=amt;
				break;
			case BIMONTHLY:
				monthhalf+=amt;
				break;
			case WEEKLY:
				week+=amt;
				break;
			case DAILY:
				day+=amt;
				break;
		}
		
		boolean change = true;
		while(change) {
			change = false;
			
			switch(type) {
				case DAILY:
					// Increment month
					if(day > daysInMonth(month,year)) {
						month++;
						day -= daysInMonth(month,year);
						change = true;
					} else if(day < 1) {
						month--;
						day += daysInMonth(month,year);
						change = true;
					}
					monthhalf = 1+day/daysInMonth(month,year);
				case BIMONTHLY:
					if(type == BIMONTHLY) {
						if(monthhalf > 2) {
							month++;
							monthhalf -= 2;
							change = true;
						} else if(monthhalf < 1) {
							month--;
							monthhalf += 2;
							change = true;
						}
					} 
				case MONTHLY:
					// Increment year
					if(month > 12) {
						year++;
						month -= 12;
						change = true;
					} else if(month < 1) {
						year--;
						month += 12;
						change = true;
					}
					
					// Update other parameters
					quarter = month/3;
					int total_days = 0;
					for(int i=1;i<=month;i++) {
						total_days += daysInMonth(i,year);
					}
					total_days += day;
					week = total_days/7;
					break;
				case WEEKLY:
					if(week > 52) {
						year++;
						week -= 52;
						change = true;
					} else if(week < 1) {
						year --;
						week += 52;
						change = true;
					}
					int total_weekly_days = 0;
					for(month=1;total_weekly_days/7 < week;month++) {
						total_weekly_days += daysInMonth(month,year);
					}
					quarter = 4*(int)(((float)week)/52.0);
					break;
				case QUARTERLY:
					if(quarter > 4) {
						year++;
						quarter -= 4;
						change = true;
					} else if(quarter < 1) {
						year--;
						quarter += 4;
						change = true;
					}
					break;
			}
		}
		
	}
	
	/** Decrements the DateObject by one time unit
	 *
	 */
	public void decrement() {
		this.decrement(1);
	}
	
	/** Decrements the DateObject by the number of time units specified
	 * and updates any other subunits accordingly
	 * 
	 * @param amt the number of time units to decrement (positive will decrrement, negative will increment)
	 */
	public void decrement(int amt) {
		this.increment(-1*amt);
	}
	
	/** Returns the number of time units from the base value of the DateObject
	 * as an int.
	 * 
	 * @return the number of time units from the base or zero if base==null
	 */
	public int intValue() {
		return getCountFrom(base);
	}

	/** Returns the number of time units from the base value of the DateObject
	 * as a long.
	 * 
	 * @return the number of time units from the base or zero if base==null
	 */
	public long longValue() {
		return new Integer(getCountFrom(base)).longValue();
	}

	/** Returns the number of time units from the base value of the DateObject
	 * as a float.
	 * 
	 * @return the number of time units from the base or zero if base==null
	 */
	public float floatValue() {
		return new Integer(getCountFrom(base)).floatValue();
	}

	/** Returns the number of time units from the base value of the DateObject
	 * as a double.
	 * 
	 * @return the number of time units from the base or zero if base==null
	 */
	public double doubleValue() {
		return new Integer(getCountFrom(base)).doubleValue();
	}
	
	/** Returns the String representation of the DateObject consistent with
	 * date_separator and date_format.
	 * 
	 * @return a String representation of the object
	 */
	public String stringValue() {
		StringBuffer builder = new StringBuffer();

		switch(type) {
			case YEARLY:
				builder.append(Integer.toString(year));
				break;
			case MONTHLY:
				builder.append(Integer.toString(month));
				builder.append(date_separator);
				builder.append(Integer.toString(year));
				break;
			case QUARTERLY:
				builder.append('Q');
				builder.append(Integer.toString(quarter));
				builder.append(date_separator);
				builder.append(Integer.toString(year));
				break;
			case BIMONTHLY:
				builder.append(Integer.toString(month));
				builder.append('.');
				builder.append(Integer.toString(monthhalf));
				builder.append(date_separator);
				builder.append(Integer.toString(year));
				break;
			case WEEKLY:
				builder.append(Integer.toString(week));
				builder.append(date_separator);
				builder.append(Integer.toString(year));
				break;
			case DAILY:
				switch(date_format) {
					case FMT_DAY_FIRST:
						builder.append(Integer.toString(day));
						builder.append(date_separator);
						builder.append(Integer.toString(month));
						break;
					case FMT_MONTH_FIRST:
					default:
						builder.append(Integer.toString(month));
						builder.append(date_separator);
						builder.append(Integer.toString(day));
						break;
				}
				builder.append(date_separator);
				builder.append(Integer.toString(year));
				break;
			default:
				return null;
		}
		
		return builder.toString();
	}

	/** Returns an integer which enumerates the type of the DateObject
	 * 
	 * @return an int defining the DateObject as yearly, monthly, etc.
	 */
	public int getType() {
		return type;
	}
	
	/** Returns the base DateObject
	 * 
	 * @return the base or null if this is a base
	 */
	public DateObject getBase() {
		return base;
	}
	
	/** Sets the base DateObject
	 * 
	 * @param the base or null to erase the base
	 */
	public void setBase(DateObject new_base) {
		base = new_base;
	}
	
	/** Returns a String describing the DateObject's type designation
	 * 
	 * @return a String contianing the description
	 */
	public String getTypeString() {
		return getTypeString(this.getType());
	}
	
	/** Returns the year of the Date object
	 * 
	 * @return the year value
	 */
	public int getYear() {
		return year;
	}
	
	/** Returns the Quarter of the object
	 * 
	 * @return the quarter or -1 if data is annual
	 */
	public int getQuarter() {
		if(type == YEARLY) {
			return -1;
		} else {
			return quarter;
		}
	}
	
	/** Returns the Month of the object
	 * 
	 * @return the month or -1 if data is yearly or quarterly
	 */
	public int getMonth() {
		if(type == YEARLY || type == QUARTERLY) {
			return -1;
		} else {
			return month;
		}
	}
	
	/** Returns the Half-Month designation
	 * 
	 * @return the half month or -1 if data is yearly, quarterly, or monthly
	 */
	public int getMonthHalf() {
		if(type == YEARLY || type == QUARTERLY || type == MONTHLY) {
			return -1;
		} else {
			return monthhalf;
		}
	}
	
	/** Returns the Week of the object
	 * 
	 * @return the week or -1 if yearly, quarterly, monthly, or bi-monthly
	 */
	public int getWeek() {
		if(type == WEEKLY || type == DAILY) {
			return week;
		} else {
			return -1;
		}
	}
	
	/** Returns the Day of the object
	 * 
	 * @return the day or -1 if yearly, quarterly, monthly, bi-monthly, or daily
	 */
	public int getDay() {
		if(type == DAILY) {
			return day;
		} else {
			return -1;
		}
	}
	
	/** Determines the number of time units this DateObject is from 
	 * another DateObject.
	 * 
	 * @param given the DateObject from which the measurement is based
	 */
	public int getCountFrom(DateObject given) {
		int count = 0;
		
		if(given == null) return 0;
		
		if(given.getType() != type) {
			System.err.print("Incompatible DateObject Types");
			return Integer.MIN_VALUE;
		}
		
		switch(type) {
			case YEARLY:
				count = year - given.getYear();
				break;
			case QUARTERLY:
				count = 4*(year-given.getYear()) + (quarter-given.getQuarter());
				break;
			case MONTHLY:
				count = 12*(year-given.getYear()) + (month-given.getMonth());
				break;
			case BIMONTHLY:
				count = 24*(year-given.getYear()) + 2*(month-given.getMonth()) + (monthhalf-given.getMonthHalf());
				break;
			case WEEKLY:
				count = 52*(year-given.getYear()) + week-given.getWeek();
				break;
			case DAILY:
				long given_total_days = 0;
				long this_total_days = 0;
				
				// Negative years are not supported in this configuration
				if(given.getYear() < 0 || this.getYear() < 0) {
					System.err.println("Error: DAILY DateObject types cannot utilize negative years.");
					return 0;
				}
				
				// First the given
				for(int i=0;i<given.getYear();i++) {
					given_total_days += getDaysInYear(i);
				}
				for(int i=1;i<given.getMonth();i++) {
					given_total_days += daysInMonth(i,given.getYear());
				}
				given_total_days += given.getDay();
				
				// Next, this object
				for(int i=0;i<this.getYear();i++) {
					this_total_days += getDaysInYear(i);
				}
				for(int i=1;i<this.getMonth();i++) {
					this_total_days += daysInMonth(i,this.getYear());
				}
				this_total_days += this.getDay();
				
				count = (int)(this_total_days - given_total_days);
				break;
		}
		
		return count;
	}
	
	/** Overrides the toString() method for Objects
	 *
	 * @return a String generated by the stringValue() function
	 */
	public String toString() {
		return stringValue();
	}
	
}
