/* Copyright (c) 2003. All Rights Reserved.
 * 
 * This is the distribution of classes developed at IGPP/UCLA.
 * 
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 * 
 * This library 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
 * Library General Public License for more details.
 *  
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 * 
 * See the COPYING file located in the top-level-directory of
 * the archive of this library for complete text of license.
 *
 */
package gov.nasa.pds.ppi.util;

import java.util.*;
import java.text.*;

/**
 * PPITime is a class that contains a methods for parsing, comparing and
 * generating time strings.
 *
 * @author      Todd King
 * @author      Planetary Data System
 * @version     1.0, 04/21/03
 * @since       1.0
 */
public class PPITime implements Comparable<PPITime>{
 	// Enumeration of possible value type
	/** 1/19/83 11:45:30.234 */	public static final String AMERDATE = "MM/dd/yy HH:mm:ss.SSS";      
	/** 19.1.83 11:45:30.234 */	public static final String EURODATE = "dd-MM-yy HH:mm:ss.SSS";
	/** jan 19, 1983 11:45:30.234 */ public static final String AMER = "MMM dd, yyyy HH:mm:ss.SSS";
	/** 19 jan 1983 11:45:30.234 */	public static final String EURO = "dd MMM yyyy HH:mm:ss.SSS";
	/** 1983 303 11:45:30.234 */ public static final String DOY = "yyyy DD HH:mm:ss.SSS";
	/** 83.1.19 11:45:30.234 */ public static final String JAPANDATE = "yyyy.MM.dd HH:mm:ss.SSS";
	/** 83.19.1 11:45:30.234 */ public static final String NIPPONDATE = "yyyy.dd.MM yyyy HH:mm:ss.SSS";
	/** 83 01 19 00 11 45 30.234 */ public static final String HIGHLOW = "yyyy MM dd HH:mm:ss.SSS";
	/** 83 019 JAN 19 11 45 30.234 */ public static final String ISEEDATE = "yyyy DDD MMM dd HH:mm:ss.SSS";
	/** 1989-JAN-19 11:45:30.234 */	public static final String DFS = "yyyy-MMM-dd HH:mm:ss.SSS";
	/** 1989/01/19 11:45:30.234 */ public static final String ABBRDFS = "yyyy/MM/dd HH:mm:ss.SSS";
	/** 1989-01-19T11:45:30.234 or 1989-019T11:45:30.234 with omissions */	public static final String PDS = "T";
	/** 19890119T114530.234 */ public static final String ISO = "yyyyMMddThhmmss.SSSS";
	/** 758979930.234 */ public static final String BINARY = "B1966";
	/** 19-01-1989 11:45:30.234 */ public static final String CLUSTER = "dd-MM-yyy HH:mm:ss.SSS";

	/*
	 * Default timezone (GMT - no offset)
	 */
	public TimeZone mTimeZone = TimeZone.getTimeZone("GMT-0:00");
		
	/** The Date variable where parsed values are stored. Also the value
	 * of this element is used when generating formatted output.
	 */
	public Calendar	mDate = Calendar.getInstance(mTimeZone);
	
    /** 
     * Creates an instance of a time value.
     * The instance is not initialized and must be either using
     * now() convert(), or copy().
	 *
     * @since           1.0
     */
 	public PPITime() {
	}
		
    /** 
     * Creates an instance of a time value.
     * The instance is initialized as a copy of the
     * passed argument.
     *
     * @param item		the instance of a PPITime value 
     *					to initialize this instance with.
	 *
     * @since           1.0
     */
 	public PPITime(PPITime item) {
		mDate = (Calendar) item.mDate.clone();
	}
	
	/** Execute the class from the command line
	 * @param args make javadoc shutup
	 */	
	public static void main(String[] args) {
		PPITime	time = new PPITime();
		String buffer;
		
		// Check arguments
		if(args.length < 3) {
			System.out.println("Usage: PPITime TimeString InFormat OutFormat");
			return;
		}

		// Process arguments
		System.out.println("args[0]: " + args[0]);
		time.convert(time.findSpec(args[1]), args[0]);
		
		System.out.println("Binary: " + time.format(BINARY));		
		
		System.out.print(args[2] + ": ");
		buffer = time.format(time.findSpec(args[2]));
		System.out.println(buffer);		
	}
	
    /** 
     * Parses a string into a Date using the given pattern.
     * The pattern can be specified using one of the predefined
     * formats or it can be specified using the syntax used
     * by  @see SimpleDateFormat.
	 * 
	 * @param pattern	the text containing the pattern to parse
	 *                  buffer with.
	 * @param buffer	the text containing the string to parse.
	 *
	 * @return			<code>true</code> if the string could be parsed.
	 *					<code>false</code> if any error occured.
	 *
     * @since           1.0
     */
	public boolean convert(String pattern, String buffer) 
	{
	
		long	milli;
		SimpleDateFormat	parser;
		Date	date;
		String	part[];
		String	piece[];
		int		n;
		
		if(buffer.length() == 0) return false;
		if(pattern.length() == 0) return false;
		
		try {
			if(pattern.charAt(0) == 'B') {	// Special case - binary seconds from reference year
				int		year;
				double	seconds;
				
				if(pattern.length() < 5) year = 1966;	// Cline Time
				else year = Integer.parseInt(pattern.substring(1));
				
				seconds = Double.parseDouble(buffer);
				milli = (long) (seconds * 1000);
				mDate.setTimeInMillis(milli);
				mDate.add(Calendar.YEAR, year - 1970);
			} else if(pattern.charAt(0) == 'T') {	// Special case - PDS style time
				int year = 0;
				int doy = 1;
				int month = 0;
				int day = 1;
				int hour = 0;
				int minute = 0;
				double	seconds = 0.0;
	
				if(buffer.compareToIgnoreCase("EOM") == 0) {
					eternity();
					return true;
				}
								
				if(buffer.compareToIgnoreCase("LAUNCH") == 0) {
					dawn();
					return true;
				}
				
				// Handle old PDS times with a Z at the end.
				if(buffer.charAt(buffer.length()-1) == 'Z'){
					if(buffer.length() < 2)
						return false;
					buffer = buffer.substring(0,buffer.length()-1);
				}
				
				part = buffer.split("T");
				piece = part[0].split("-");
				if(piece.length > 0) year = Integer.parseInt(piece[0]);
				
				if(piece.length == 2) doy = Integer.parseInt(piece[1]);
				if(piece.length > 2) {
					month = Integer.parseInt(piece[1]); day = Integer.parseInt(piece[2]);
				}
				if(part.length > 1) {	// Time portion
					piece = part[1].split(":");
					if(piece.length > 0) hour = Integer.parseInt(piece[0]);
					if(piece.length > 1) minute = Integer.parseInt(piece[1]);
					if(piece.length > 2) seconds = Double.parseDouble(piece[2]);
				}
				// Reformat string
				if(month == 0) {
					buffer = year + " " + doy + " ";
					pattern = "yyyy DDD HH:mm:ss.SSS";
				} else {
					buffer = year + " " + month + " " + day + " ";
					pattern = "yyyy MM dd HH:mm:ss.SSS";
				}
				buffer	+=hour + ":" + minute + ":"	+ seconds;
				parser = new SimpleDateFormat(pattern);
				parser.setTimeZone(mTimeZone);
				mDate.setTime(parser.parse(buffer));
			} else {
				parser = new SimpleDateFormat(pattern);	
				parser.setTimeZone(mTimeZone);
				mDate.setTime(parser.parse(buffer));
				n = mDate.get(Calendar.YEAR);
				if(n >= 20 && n < 100) mDate.add(Calendar.YEAR, 1900);
				if(n >= 0 && n < 20) mDate.add(Calendar.YEAR, 2000);
			}
		} catch(Exception e) {
			return false;
		}
		
		return true;
	}
	
    /** 
     * Formats the time in the requested format and returns the string.
	 * 
	 * @param pattern	the text containing the pattern to format
	 *					the time as.
	 *
	 * @return			a string containing the formatted time value.
	 *
     * @since           1.0
     */
	public String format(String pattern) {
		SimpleDateFormat	parser;
		String		buffer;
		String		temp;
		int			n;
		
		if(pattern == null) return "Invalid pattern";
		
		if(pattern.charAt(0) == 'B') {	// Special case - binary seconds from reference year
			int		year;
			long	seconds;
			long	milli;
			long	diff;
			
			if(pattern.length() < 5) year = 1966;	// Cline Time
			else year = Integer.parseInt(pattern.substring(1));
			
			Calendar refYear = Calendar.getInstance(mTimeZone);
			refYear.set(year, 1, 1, 0, 0, 0);
			Calendar sysYear = Calendar.getInstance(mTimeZone);
			sysYear.set(1970, 1, 1, 0, 0, 0);

			milli = mDate.getTimeInMillis();
			milli -= refYear.getTimeInMillis();
			milli += sysYear.getTimeInMillis();
			seconds = milli / 1000;
			diff = milli - (seconds * 1000);
			buffer = Long.toString(seconds);
			buffer += ".";
			temp = Long.toString(diff);
			for(int i = 0; i < 3 - temp.length(); i++) buffer += "0";
			n = temp.length();
			if(n > 3) n = 3;
			buffer += temp.substring(0, n);
		} else if((n = pattern.indexOf('T')) != -1) {	// Special case - PDS style time
			if(pattern.compareTo("T") == 0) {	// Fule time format
				parser = new SimpleDateFormat("yyyy-MM-dd");
				parser.setTimeZone(mTimeZone);
				buffer = parser.format(mDate.getTime()) + "T";
				parser = new SimpleDateFormat("HH:mm:ss.SSS");
				parser.setTimeZone(mTimeZone);
				buffer += parser.format(mDate.getTime());
			} else {	// Treat as two part pattern split by "T"
				buffer = "";
				temp = pattern.substring(0, n);
				if(temp.length() > 0) {
					parser = new SimpleDateFormat(pattern.substring(0, n));
					parser.setTimeZone(mTimeZone);
					buffer += parser.format(mDate.getTime());
				}
				buffer += "T";
				temp = pattern.substring(n+1);
				if(temp.length() > 0) {
					parser = new SimpleDateFormat(temp);
					parser.setTimeZone(mTimeZone);
					buffer += parser.format(mDate.getTime());
				}
			}
		} else {
			parser = new SimpleDateFormat(pattern);
			parser.setTimeZone(mTimeZone);
			buffer = parser.format(mDate.getTime());
		}
		return buffer;
	}
	
    /** 
     * Returns the time format specification that matches the given
     * standard format name. If no match is found the passed string is
     * returned.
	 * 
	 * @param name		the name of the standard format.
	 *
	 * @return			a string containing the format specification that matches
	 * 					the passed name. If no match is found the passed string
	 *					is returned.
	 *
     * @since           1.0
     */
	static public String findSpec(String name) {
		// Placed in order of most commonly used
		if(name.compareToIgnoreCase("PDS") == 0)		return PDS;
		if(name.compareToIgnoreCase("BINARY") == 0)		return BINARY;
		if(name.compareToIgnoreCase("CLUSTER") == 0)	return CLUSTER;
		if(name.compareToIgnoreCase("DFS") == 0)		return DFS;
		if(name.compareToIgnoreCase("ABBRDFS") == 0)	return ABBRDFS;
		if(name.compareToIgnoreCase("ISO") == 0)		return ISO;
		if(name.compareToIgnoreCase("DOY") == 0)		return DOY;
		if(name.compareToIgnoreCase("AMERDATE") == 0)	return AMERDATE;      
		if(name.compareToIgnoreCase("EURODATE") == 0)	return EURODATE;
		if(name.compareToIgnoreCase("AMER") == 0) 		return AMER;
		if(name.compareToIgnoreCase("EURO") == 0)		return EURO;
		if(name.compareToIgnoreCase("JAPANDATE") == 0)	return JAPANDATE;
		if(name.compareToIgnoreCase("NIPPONDATE") == 0)	return NIPPONDATE;
		if(name.compareToIgnoreCase("HIGHLOW") == 0)	return HIGHLOW;
		if(name.compareToIgnoreCase("ISEEDATE") == 0)	return ISEEDATE;

		return name;	// Return passed string - assume its a time spec.
	}
	
    /** 
     * Sets the time to the earliest possible time.
	 * 
     * @since           1.0
     */
	public void dawn() {
		Calendar calendar = Calendar.getInstance(mTimeZone);
		
		calendar.set(Calendar.YEAR, calendar.getMinimum(Calendar.YEAR));
		calendar.set(Calendar.MONTH, calendar.getMinimum(Calendar.MONTH));
		calendar.set(Calendar.DAY_OF_MONTH, calendar.getMinimum(Calendar.DAY_OF_MONTH));
		calendar.set(Calendar.HOUR_OF_DAY, calendar.getMinimum(Calendar.HOUR_OF_DAY));
		calendar.set(Calendar.MINUTE, calendar.getMinimum(Calendar.MINUTE));
		calendar.set(Calendar.SECOND, calendar.getMinimum(Calendar.SECOND));
		
		mDate.setTime(calendar.getTime());
	}
	
    /** 
     * Sets the time to the latest possible time.
	 * 
     * @since           1.0
     */
	public void eternity() {
		Calendar calendar = Calendar.getInstance(mTimeZone);
		
		calendar.set(Calendar.YEAR, 9000); // Using getMaximum does not work properly
		calendar.set(Calendar.MONTH, calendar.getMaximum(Calendar.MONTH));
		calendar.set(Calendar.DAY_OF_MONTH, calendar.getMaximum(Calendar.DAY_OF_MONTH));
		calendar.set(Calendar.HOUR_OF_DAY, calendar.getMaximum(Calendar.HOUR_OF_DAY));
		calendar.set(Calendar.MINUTE, calendar.getMaximum(Calendar.MINUTE));
		calendar.set(Calendar.SECOND, calendar.getMaximum(Calendar.SECOND));
		
		mDate.setTime(calendar.getTime());
	}
	
    /** 
     * Compare a time to this instance for ordering.
	 * 
	 * @param anotherTime	the PPITime to compare to this time.
	 *
	 * @return			the value 0 if the passed argument is equal to 
	 *					this instance; a value less than 0 if the this instance
	 *					is before the passed argument; and a value greater than 0 if 
	 *					the this instance is after the passed argument.
	 *
     * @since           1.0
     */
	public int compareTo(PPITime anotherTime) {
		return mDate.getTime().compareTo(anotherTime.mDate.getTime());
	}
	
	/** Override of equals to make sure that it is consistant with compareTo i.e.:
	 *    (x.compareTo(y) == 0) == (x.equals(y))
	 */
	public boolean equals(Object obj){
		if(this == obj) return true;
		if(!(obj instanceof PPITime)) return false;
		PPITime other = (PPITime)obj;
		
		return (mDate.compareTo(other.mDate) == 0);
	}
	
	/** Override of hashCode to make sure it is consistant with equals */
	public int hashCode(){ return mDate.hashCode(); }
	
    /** 
     * Makes a copy of a PPITime item.
	 *
	 * @param item	the instance of the PPITime item to copy.
	 *
     * @since           1.0
     */
	public void copy(PPITime item) {
		mDate = (Calendar) item.mDate.clone();
	}
	
    /** 
     * Advances the time by a specified number of minutes.
	 *
	 * @param minutes	the number of minutes to advance the time by.
	 *					The value may include a fractional minute.
	 *
     * @since           1.0
     */
	public void advance(double minutes) {
		int min = (int) minutes;
		double seconds = (minutes - min) * 60.0;
		int sec = (int) seconds;
		int milli = (int) ((seconds - sec) * 1000);
		
		mDate.add(Calendar.MINUTE, min);
		mDate.add(Calendar.SECOND, sec);
		mDate.add(Calendar.MILLISECOND, milli);
	}
	
    /** 
     * Sets the date to the current system time.
	 *
     * @since           1.0
     */
	public void now() {
		mDate.setTime(Calendar.getInstance(mTimeZone).getTime());
	}
	
    /** 
     * Returns the number of milliseconds between a time and this time.
	 *
	 * @param other the instance of the PPITime item to compare to.
	 *
     * @since           1.0
     */
	public long span(PPITime other) {
		return (mDate.getTimeInMillis() - other.mDate.getTimeInMillis());
	}
	
	/** Print the time in ISO/DIS 8601 day of month standard format. */
	public String toString(){
		// Since the calendar is constructed via getInstance the fields should always
		// be available.
		DecimalFormat df = new DecimalFormat("00");
		DecimalFormat dfm = new DecimalFormat("000");
		
		return mDate.get(Calendar.YEAR)+"-"+df.format(mDate.get(Calendar.MONTH)+1)+"-"+
			df.format(mDate.get(Calendar.DAY_OF_MONTH))+"T"+
			df.format(mDate.get(Calendar.HOUR_OF_DAY))+":"+
			df.format(mDate.get(Calendar.MINUTE))+":"+
			df.format(mDate.get(Calendar.SECOND))+"."+
			dfm.format(mDate.get(Calendar.MILLISECOND));
	}
}