/*
 * @(#)$Id: BigDateTimeValueType.java,v 1.14 2002/09/08 20:18:55 kk122374 Exp $
 *
 * Copyright 2001 Sun Microsystems, Inc. All Rights Reserved.
 * 
 * This software is the proprietary information of Sun Microsystems, Inc.  
 * Use is subject to license terms.
 * 
 */
package com.sun.msv.datatype.xsd.datetime;

import java.math.BigInteger;
import java.math.BigDecimal;
import java.util.Calendar;
import java.util.SimpleTimeZone;

import com.sun.msv.datatype.xsd.Comparator;

/**
 * DateTimeValueType object that can hold all lexically valid dateTime value.
 * 
 * This class provides:
 * <ol>
 *  <li> Unlimited digits for year (e.g., "year 9999999999999999999999")
 *  <li> Unlimited digits for fraction of second (e.g. 0.00000000000001 sec)
 * </ol>
 * 
 * To provide methods that can change date/time values, normalize method
 * should be modified too.
 * 
 * @author Kohsuke KAWAGUCHI
 */
public class BigDateTimeValueType implements IDateTimeValueType {
	
	/** year value.
	 * this variable is null if no year is specified.
	 *
	 * Since there is no year 0, value 0 indicates year -1. -1 indicates -2, and so forth.
	 */
	private BigInteger year;
	public BigInteger getYear() { return year; }
	
	/** month (always between 0 and 11)
	 * this variable is null if no year is specified
	 */
	private Integer month;
	public Integer getMonth() { return month; }
	
	/** day (always normalized)
	 * this variable is null if no year is specified
	 */
	private Integer day;
	public Integer getDay() { return day; }
	
	/** hour (always between 0 and 23)
	 * this variable is null if no year is specified
	 */
	private Integer hour;
	public Integer getHour() { return hour; }
	
	/** minute (always between 0 and 59)
	 * this variable is null if no year is specified
	 */
	private Integer minute;
	public Integer getMinute() { return minute; }
	
	/** second (always in [0,60) )
	 * this variable is null if no year is specified
	 */
	private BigDecimal second;
	public BigDecimal getSecond() { return second; }
	
	/** time zone specifier */
	private TimeZone zone;
	public TimeZone getTimeZone() { return zone; }
	
	/** creates an instance with the specified BigDateTimeValueType,
	 *  with modified time zone.
	 * 
	 *  created object shares its date/time value component with the original one,
	 *  so special care is necessary not to mutate those values.
	 */
	public BigDateTimeValueType( BigDateTimeValueType base, TimeZone newTimeZone ) {
		this( base.year, base.month, base.day, base.hour, base.minute, base.second, newTimeZone );
	}
	
	public BigDateTimeValueType( BigInteger year, int month, int day, int hour, int minute, BigDecimal second, TimeZone timeZone ) {
		this( year, new Integer(month), new Integer(day), new Integer(hour), new Integer(minute), second, timeZone );
	}
	
	public BigDateTimeValueType( BigInteger year, Integer month, Integer day, Integer hour, Integer minute, BigDecimal second, TimeZone timeZone ) {
		this.year	= year;
		this.month	= month;
		this.day	= day;
		this.hour	= hour;
		this.minute	= minute;
		this.second	= second;
		this.zone	= timeZone;
	}
	
	public BigDateTimeValueType() {}
	
	
	
	public BigDateTimeValueType getBigValue() {
		return this;
	}

	public boolean equals( Object o ) {
		return equals( (IDateTimeValueType)o );
	}
	
	public boolean equals( IDateTimeValueType rhs ) {
		if(!(rhs instanceof BigDateTimeValueType))
			rhs = rhs.getBigValue();
		return equals( this, (BigDateTimeValueType)rhs );
	}
	
	public boolean equals( BigDateTimeValueType lhs, BigDateTimeValueType rhs ) {
		return compare(lhs,rhs)==Comparator.EQUAL;
	}
	
	
	/** gets a human-readable representation of this object.
	 *
	 * return value is not intended to be compliant with the canonical representation
	 * of "dateTime" type.
	 */
	public String toString() {
		StringBuffer r = new StringBuffer();
		
		if( year!=null )	r.append(year);
		r.append('-');
		if( month!=null )	r.append(month.intValue()+1);
		r.append('-');
		if( day!=null )		r.append(day.intValue()+1);
		r.append('T');
		if( hour!=null )	r.append(hour);
		r.append(':');
		if( minute!=null )	r.append(minute);
		r.append(':');
		if( second!=null )	r.append(second);
		
		if( zone!=null ) {
			if( zone.minutes==0 )	r.append('Z');
			else {
				if( zone.minutes<0 )	r.append('-');
				else					r.append('+');
				r.append(Math.abs(zone.minutes/60));
				r.append(Math.abs(zone.minutes%60));
			}
		}
		
		return new String(r);
	}
	
	public int hashCode() {
		// to be consistent with equals method, we have to normalize
		// value before computation.
		BigDateTimeValueType n = (BigDateTimeValueType)this.normalize();
		return Util.objHashCode(n.year) + Util.objHashCode(n.month) + Util.objHashCode(n.day)
			+  Util.objHashCode(n.hour) + Util.objHashCode(n.minute) + Util.objHashCode(n.second)
			+  Util.objHashCode(n.zone);
	}
	
	public int compare( IDateTimeValueType o ) {
		if(!(o instanceof BigDateTimeValueType) )
			o = o.getBigValue();
		
		return compare( this, (BigDateTimeValueType)o );
	}
	
	/**
	 * compares two BigDateTimeValueType and returns one of the constant defined in
	 * {@link Comparator}.
	 * 
	 * Order-relation between two dateTime is defined in
	 * http://www.w3.org/TR/xmlschema-2/#dateTime
	 */
	protected static int compare( BigDateTimeValueType lhs, BigDateTimeValueType rhs ) {
		lhs = (BigDateTimeValueType)lhs.normalize();
		rhs = (BigDateTimeValueType)rhs.normalize();
		
		if( (lhs.zone!=null && rhs.zone!=null) || (lhs.zone==null && rhs.zone==null) ) {
			if(!Util.objEqual(lhs.year,rhs.year))		return Util.objCompare(lhs.year,rhs.year);
			if(!Util.objEqual(lhs.month,rhs.month))		return Util.objCompare(lhs.month,rhs.month);
			if(!Util.objEqual(lhs.day,rhs.day))			return Util.objCompare(lhs.day,rhs.day);
			if(!Util.objEqual(lhs.hour,rhs.hour))		return Util.objCompare(lhs.hour,rhs.hour);
			if(!Util.objEqual(lhs.minute,rhs.minute))	return Util.objCompare(lhs.minute,rhs.minute);
			if(!Util.objEqual(lhs.second,rhs.second))	return Util.objCompare(lhs.second,rhs.second);
		
			return Comparator.EQUAL;
		}
		
		if( lhs.zone==null ) {
			int r;
			
			r = compare( (BigDateTimeValueType)new BigDateTimeValueType(lhs,Util.timeZoneNeg14).normalize(), rhs );
			if( r==Comparator.EQUAL || r==Comparator.LESS )
				return Comparator.LESS;	// lhs < rhs
			
			r = compare( (BigDateTimeValueType)new BigDateTimeValueType(lhs,Util.timeZonePos14).normalize(), rhs );
			if( r==Comparator.EQUAL || r==Comparator.GREATER )
				return Comparator.GREATER;	// lhs > rhs
			
			return Comparator.UNDECIDABLE;		// lhs <> rhs
		} else {
			int r;
			
			r = compare( lhs, (BigDateTimeValueType)new BigDateTimeValueType(rhs,Util.timeZonePos14) );
			if( r==Comparator.EQUAL || r==Comparator.LESS )
				return Comparator.LESS;	// lhs < rhs
			
			r = compare( lhs, (BigDateTimeValueType)new BigDateTimeValueType(rhs,Util.timeZoneNeg14) );
			if( r==Comparator.EQUAL || r==Comparator.GREATER )
				return Comparator.GREATER;	// lhs > rhs
			
			return Comparator.UNDECIDABLE;	// lhs <> rhs
		}
	}
	
	/** normalized DateTimeValue of this object.
	 * 
	 * once when the normalized value is computed,
	 * the value is kept in this varible so that
	 * successive calls to normalize method need not
	 * have to compute it again.
	 * 
	 * This approach assumes that modification to the date/time component
	 * will never be made.
	 */
	private IDateTimeValueType normalizedValue = null;
	
	public IDateTimeValueType normalize() {
		// see if this object is already normalized
		if( zone==null || zone.minutes==0 )		return this;
		
		// see if there is cached normalized value
		if( normalizedValue!=null )			return normalizedValue;
		
		// for normalization to work correctly,
		// we have to extend the precision.
		// otherwise, addition will remove unspecified fields,
		// and the result becomes incorrect. For example,
		// --03-- + (+08:00)    -->  --02--
		
		// which is apparently not what we wanted.
		
		// update: it seems to me that this unintuitive behavior is 
		//         not going to be corrected in XML Schema 1.0
		
		
		// faster performance can be achieved by writing optimized inline addition code.
		normalizedValue = 
			this.add( BigTimeDurationValueType.fromMinutes(-zone.minutes) );
		
		((BigDateTimeValueType)normalizedValue).zone=TimeZone.GMT;
		
		return normalizedValue;
	}
	
	private static BigInteger nullAs0( BigInteger o ) {
		if(o!=null)	return o;
		else		return BigInteger.ZERO;
	}
	
	private static BigDecimal nullAs0( BigDecimal o ) {
		if(o!=null)	return o;
		else		return Util.decimal0;
	}
	
	private static BigInteger[] divideAndRemainder( BigInteger x1, BigInteger x2 ) {
		BigInteger[] r = x1.divideAndRemainder(x2);
		if(r[1].signum()<0) {
			// in BigInteger, -2/10 = -2, which is not preferable.
			// we want -2/10 to be 8, with quodrant of -1.
			r[1] = r[1].add(x2);
			r[0] = r[0].subtract(BigInteger.ONE);
		}
		return r;
	}
	
	public IDateTimeValueType add( ITimeDurationValueType _rhs ) {
		if( _rhs instanceof BigTimeDurationValueType ) {
			// big + big
			BigTimeDurationValueType rhs = (BigTimeDurationValueType)_rhs;

			BigInteger[] quoAndMod = divideAndRemainder(
				Util.int2bi(this.month).add(rhs.month),
				Util.the12);
			
			BigInteger oyear; int omonth;
			int ohour, ominute; BigDecimal osecond;
			
			omonth = quoAndMod[1].intValue();
			oyear = quoAndMod[0].add(nullAs0(this.year)).add(nullAs0(rhs.year));
			
			
			BigDecimal sec = nullAs0(this.second).add(nullAs0(rhs.second));
			
			// quo = floor((this.second+rhs.second)/60)
			//     = floor( (this.second+rhs.second)*10^scale / (60*10^scale) )
			//     = (this.second+rhs.second).unscaled / 60*10^scale
			
			quoAndMod = divideAndRemainder(
				sec.unscaledValue(),
				Util.the60.multiply(Util.the10.pow(sec.scale())) );
			
			osecond = new BigDecimal(quoAndMod[1], sec.scale());
			
			
			quoAndMod = divideAndRemainder(
				quoAndMod[0].add(Util.int2bi(this.minute)).add(rhs.minute), Util.the60 );
			ominute = quoAndMod[1].intValue();
			
			quoAndMod = divideAndRemainder(
				quoAndMod[0].add(Util.int2bi(this.hour)).add(rhs.hour), Util.the24 );
			ohour = quoAndMod[1].intValue();
			
			int tempDays;
			int md = Util.maximumDayInMonthFor(oyear,omonth);
			{
				int dayValue = (this.day!=null)?this.day.intValue():0;
				if( dayValue<0 )		tempDays=0;
				else if( dayValue>=md )	tempDays=md-1;
				else					tempDays=dayValue;
			}
			
			BigInteger oday = rhs.day.add(quoAndMod[0]).add(Util.int2bi(tempDays));
			while(true) {
				int carry;
				if( oday.signum()==-1 )	{ // day<0
					oday = oday.add(Util.int2bi(Util.maximumDayInMonthFor(oyear,(omonth+11)%12)));
					carry = -1;
				} else {
					BigInteger bmd = Util.int2bi(Util.maximumDayInMonthFor(oyear,omonth));
					if( oday.compareTo(bmd)>=0 ) {
						oday = oday.subtract(bmd);
						carry = +1;
					} else
						break;
				}
				
				omonth += carry;
				if( omonth<0 ) {
					omonth += 12;
					oyear = oyear.subtract(BigInteger.ONE);
				}
				oyear = oyear.add( Util.int2bi(omonth/12) );
				omonth %= 12;
			}
			
			// set those fields blank which are not originally specified.
			return new BigDateTimeValueType(
				this.year!=null  ? oyear:null,
				this.month!=null ? new Integer(omonth):null,
				this.day!=null   ? new Integer(oday.intValue()):null,
				this.hour!=null  ? new Integer(ohour):null,
				this.minute!=null? new Integer(ominute):null,
				this.second!=null? osecond:null,
				this.zone );
		} else {
			// big + small
			// TODO : implement this to achive better performance
			
			// just for now, convert it to BigTimeDurationValue and then compute the result.
			return add( _rhs.getBigValue() );
		}
	}
    
    
    public Calendar toCalendar() {
        // set fields of Calendar.
        // In BigDateTimeValueType, the first day of the month is 0,
        // where it is 1 in java.util.Calendar.
        
        Calendar cal = new java.util.GregorianCalendar(createJavaTimeZone());
        cal.clear();    // reset all fields. This method does not reset the time zone.
        
        if( getYear()!=null )     cal.set( cal.YEAR, getYear().intValue() );
        if( getMonth()!=null )    cal.set( cal.MONTH, getMonth().intValue() );
        if( getDay()!=null )      cal.set( cal.DAY_OF_MONTH, getDay().intValue()+1/*offset*/ );
        if( getHour()!=null )     cal.set( cal.HOUR_OF_DAY, getHour().intValue() );
        if( getMinute()!=null )   cal.set( cal.MINUTE, getMinute().intValue() );
        if( getSecond()!=null ) {
            cal.set( cal.SECOND, getSecond().intValue() );
            cal.set( cal.MILLISECOND, getSecond().movePointRight(3).intValue()%1000 );
        }
        
        return cal;
    }
    
    /** creates the equivalent Java TimeZone object. */
    protected java.util.TimeZone createJavaTimeZone() {
        if(getTimeZone()!=null)
            return new SimpleTimeZone( getTimeZone().minutes*60*1000, "custom" );
        else
            // if the time zone is not present, assume the system default.
            return SimpleTimeZone.getDefault();
    }
    
    
     
/*
	public static void main( String[] args )
	{
		Object o1 = new BigDateTimeValueType( new BigInteger("2001"), new Integer(5), new Integer(1), null, null, null, null );
		Object o2 = new BigDateTimeValueType( new BigInteger("2001"), new Integer(5), new Integer(1), null, null, null, null );
		
		System.out.println(o1.hashCode());
		System.out.println(o2.hashCode());
		System.out.println(o1.equals(o2));
		System.out.println(o2.equals(o1));
		
		java.util.Set s = new java.util.HashSet();
		s.add(o1);
		System.out.println( s.contains(o2) );
	}

	public static void main( String[] args )
	{
		Object o1 = new BigDateTimeValueType( new BigInteger("1512"), new Integer(1), new Integer(4), null, null, null, TimeZone.create(-12*60) );
		Object o2 = new BigDateTimeValueType( new BigInteger("1512"), new Integer(1), new Integer(5), null, null, null, TimeZone.create(+12*60) );
		
		System.out.println(o1.hashCode());
		System.out.println(o2.hashCode());
		System.out.println(o1.equals(o2));
		System.out.println(o2.equals(o1));
	}
 */
}

