/*
 * Redistribution and use of this software and associated documentation
 * ("Software"), with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * 1. Redistributions of source code must retain copyright
 *    statements and notices.  Redistributions must also contain a
 *    copy of this document.
 *
 * 2. Redistributions in binary form must reproduce the
 *    above copyright notice, this list of conditions and the
 *    following disclaimer in the documentation and/or other
 *    materials provided with the distribution.
 *
 * 3. The name "Exolab" must not be used to endorse or promote
 *    products derived from this Software without prior written
 *    permission of Intalio, Inc.  For written permission,
 *    please contact info@exolab.org.
 *
 * 4. Products derived from this Software may not be called "Exolab"
 *    nor may "Exolab" appear in their names without prior written
 *    permission of Intalio, Inc. Exolab is a registered
 *    trademark of Intalio, Inc.
 *
 * 5. Due credit should be given to the Exolab Project
 *    (http://www.exolab.org/).
 *
 * THIS SOFTWARE IS PROVIDED BY INTALIO, INC. AND CONTRIBUTORS
 * ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES, INCLUDING, BUT
 * NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 * FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL
 * INTALIO, INC. OR ITS CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
 * OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Copyright 2000-2002 (C) Intalio, Inc. All Rights Reserved.
 *
 * $Id: DateTimeBase.java 8516 2010-01-25 12:14:04Z wguttmn $
 * Date         Author          Changes
 * 08/30/2001   Arnaud Blandin  added to Calendar() (patch from Sébastien Stormacq [S.Stormacq@aubay-si.lu])
 * 05/29/2001   Arnaud Blandin  Added order methods
 * 05/22/2001   Arnaud Blandin  Created
 */
package org.exolab.castor.types;

import java.text.DateFormat;
import java.text.ParseException;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Date;
import java.util.SimpleTimeZone;
import java.util.TimeZone;

/**
 * The base class for date/time XML Schema types.
 * <p>
 * The validation of the date/time fields is done in the set methods and follows
 * <a href="http://www.iso.ch/markete/8601.pdf">the ISO8601 Date and Time Format</a>.
 * <p>
 * Note: the Castor date/time types are mutable, unlike the date/time types of
 * the JDK in Java2. This is needed by the Marshaling framework.
 *
 * @author <a href="mailto:blandin@intalio.com">Arnaud Blandin</a>
 * @author <a href="mailto:edward.kuns@aspect.com">Edward Kuns</a>
 * @version $Revision: 8516 $
 * @see DateTime
 * @see Date
 * @see Time
 */
public abstract class DateTimeBase implements java.io.Serializable, Cloneable {
    /** Public constant referring to an indeterminate Date/Time comparison. */
    public static final int INDETERMINATE = -1;
    
    /** Public constant referring to a Date/Time comparison result of "less than". */
    public static final int LESS_THAN = 0;
    
    /** Public constant referring to a Date/Time comparison result of "equals". */
    public static final int EQUALS = 1;
    
    /** Public constant referring to a Date/Time comparison result of "greater than". */
    public static final int GREATER_THAN = 2;

    /** When comparing a date/time with a time zone to one without, the recommendation
     * says that 14 hours is the time zone offset to use for comparison. */
    protected static final int    MAX_TIME_ZONE_COMPARISON_OFFSET = 14;

    /** Convenience String for complaints. */
    protected static final String WRONGLY_PLACED  = " is wrongly placed.";

    /** true if this date/time type is negative. */
    private boolean _isNegative = false;
    
    /** The century field. */
    private short _century = 0;
    
    /** The year field. */
    private short _year = 0;
    
    /** The month field. */
    private short _month = 0;
    
    /** The day field. */
    private short _day = 0;
    
    /** the hour field. */
    private short _hour = 0;
    
    /** the minute field. */
    private short _minute = 0;
    
    /** the second field. */
    private short _second = 0;
    
    /** the millsecond field. */
    private short _millsecond = 0;
    
    /** true if the time zone is negative. */
    private boolean _zoneNegative = false;
    
    /** true if this date/time type has a time zone assigned. */
    private boolean _utc = false;
    
    /** the time zone hour field. */
    private short _zoneHour = 0;
    
    /** the time zone minute field. */
    private short _zoneMinute = 0;

    //////////////////////////Abstract methods////////////////////////////////////

    /**
     * Returns a java.util.Date that represents the XML Schema Date datatype.
     * @return a java.util.Date that represents the XML Schema Date datatype.
     */
    public abstract Date toDate();

    /**
     * Sets all the fields by reading the values in an array.
     * @param values an array of shorts with the values.
     */
    public abstract void setValues(short[] values);

    /**
     * returns an array of short with all the fields that describe a date/time
     * type.
     * @return an array of short with all the fields that describe a date/time
     *         type.
     */
    public abstract short[] getValues();

    ////////////////////////////////////////////////////////////////////////////

    /**
     * Returns true if the given year represents a leap year. A specific year is
     * a leap year if it is either evenly divisible by 400 OR evenly divisible
     * by 4 and not evenly divisible by 100.
     *
     * @param year
     *            the year to test where 0 < year <= 9999
     * @return true if the given year represents a leap year
     */
    public final boolean isLeap(int year) {
        return ((year % 4) == 0 && (year % 100) != 0) || (year % 400) == 0;
    }

    /**
     * Returns true if the given year represents a leap year. A specific year is
     * a leap year if it is either evenly divisible by 400 OR evenly divisible
     * by 4 and not evenly divisible by 100.
     *
     * @param year
     *            the year to test where 0 <= year <= 99
     * @param century
     *            the century to test where 0 <= century <= 99
     * @return true if the given year represents a leap year
     */
    private final boolean isLeap(short century, short year) {
        return isLeap(century * 100 + year);
    }

    //////////////////////////Setter methods////////////////////////////////////

    /**
     * Set the negative field to true.
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             "century+year is negative" field is not allowed.
     */
    public void setNegative() throws UnsupportedOperationException {
        _isNegative = true;
    }

    /**
     * Set the century field. Note: year 0000 is not allowed.
     * @param century the value to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             century field is not allowed
     */
    public void setCentury(short century) throws UnsupportedOperationException {
        String err = "";
        if (century < 0) {
            err = "century " + century + " must not be negative.";
            throw new IllegalArgumentException(err);
        } else if (_year == 0 && century == 0 && _century != 0) {
            err = "century:  0000 is not an allowed year.";
            throw new IllegalArgumentException(err);
        }

        _century = century;
    }

    /**
     * Sets the Year field. Note: year 0000 is not allowed.
     *
     * @param year
     *            the year to set
     * @throws UnsupportedOperationException
     *             in an overridden method in a derived class if that derived
     *             class does not support the year element.
     */
    public void setYear(short year) throws UnsupportedOperationException {
        String err = "";
        if (year < 0) {
            err = "year " + year + " must not be negative.";
            throw new IllegalArgumentException(err);
        } else if (year == -1) {
            if (_century != -1) {
                err = "year can not be omitted unless century is also omitted.";
                throw new IllegalArgumentException(err);
            }
        } else if (year == 0 && _century == 0) {
            err = "year:  0000 is not an allowed year";
            throw new IllegalArgumentException(err);
        } else if (year > 99) {
            err = "year " + year + " is out of range:  0 <= year <= 99.";
            throw new IllegalArgumentException(err);
        }

        _year = year;
    }

    /**
     * Sets the Month Field. Note 1 <= month <= 12.
     * @param month the value to set up
     * @throws UnsupportedOperationException
     *             in an overridden method in a derived class if that derived
     *             class does not support the month element.
     */
    public void setMonth(short month) throws UnsupportedOperationException {
        String err = "";
        if (month == -1) {
            if (_century != -1) {
                 err = "month cannot be omitted unless the previous component is also omitted.\n"
                       + "only higher level components can be omitted.";
                 throw new IllegalArgumentException(err);
            }
        } else if (month < 1 || month > 12) {
            err = "month " + month + " is out of range:  1 <= month <= 12";
            throw new IllegalArgumentException(err);
        }

        _month = month;
    }

    /**
     * Sets the Day Field. Note:  This field is validated before the assignment
     * is done.
     *
     * @param day
     *            the value to set up
     * @throws UnsupportedOperationException
     *             in an overridden method in a derived class if that derived
     *             class does not support the day element.
     */
    public void setDay(short day) throws UnsupportedOperationException {
        String err = "";
        if  (day == -1) {
            if (_month != -1) {
                err = "day cannot be omitted unless the previous component is also omitted.\n"
                      + "only higher level components can be omitted.";
                throw new IllegalArgumentException(err);
            }
        } else if (day < 1) {
            err = "day " + day + " cannot be negative.";
            throw new IllegalArgumentException(err);
        }

        short maxDay = maxDayInMonthFor(_century, _year, _month);
        if (day > maxDay) {
            if (_month != 2) {
                err = "day " + day + " is out of range for month " + _month + ":  "
                      + "1 <= day <= " + maxDay;
                throw new IllegalArgumentException(err);
            } else if (isLeap(_century, _year)) {
                err = "day " + day + " is out of range for February in a leap year:  "
                      + "1 <= day <= 29";
                throw new IllegalArgumentException(err);
            } else {
                err = "day " + day + " is out of range for February in a non-leap year:  "
                      + "1 <= day <= 28";
                throw new IllegalArgumentException(err);
            }
        }

        _day = day;
    }

    /**
     * Sets the hour field for this date/time type.
     *
     * @param hour
     *            the hour to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the hour
     *             field is not allowed
     */
    public void setHour(short hour) throws UnsupportedOperationException {
        if (hour > 23) {
            String err = "hour " + hour + " must be strictly less than 24";
            throw new IllegalArgumentException(err);
        } else if (hour < 0) {
            String err = "hour " + hour + " cannot be negative.";
            throw new IllegalArgumentException(err);
        }

        _hour = hour;
    }

    /**
     * set the minute field for this date/time type.
     *
     * @param minute
     *            the minute to set.
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             minute field is not allowed
     */
    public void setMinute(short minute) throws UnsupportedOperationException {
        if (minute > 59) {
            String err = "minute " + minute + " must be strictly less than 60.";
            throw new IllegalArgumentException(err);
        } else if (minute < 0) {
            String err = "minute " + minute + " cannot be negative.";
            throw new IllegalArgumentException(err);
        }

        _minute = minute ;
    }

    /**
     * Sets the seconds field for this date/time type, including fractional
     * seconds.  (In this implementation, fractional seconds are limited
     * to milliseconds and are truncated at millseconds if more precision
     * is provided.)
     *
     * @param second
     *            the second to set
     * @param millsecond
     *            the millisecond to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             second field is not allowed
     */
    public void setSecond(short second,short millsecond) throws UnsupportedOperationException {
        setSecond(second);
        setMilliSecond(millsecond);
    }

    /**
     * Sets the seconds field for this date/time type, not including the
     * fractional seconds.  Any fractional seconds previously set is unmodified.
     *
     * @param second
     *            the second to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             second field is not allowed
     */
    public void setSecond(short second) throws UnsupportedOperationException {
        if (second > 59) {
           String err = "seconds " + second + " must be less than 60";
           throw new IllegalArgumentException(err);
        } else if (second < 0) {
           String err = "seconds "+second+" cannot be negative.";
           throw new IllegalArgumentException(err);
        }

        _second = second;
    }

    /**
     * Sets the millisecond field for this date/time type.
     *
     * @param millisecond
     *            the millisecond to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the
     *             millisecond field is not allowed
     */
    public void setMilliSecond(short millisecond) throws UnsupportedOperationException {
        if (millisecond < 0) {
            String err = "milliseconds " + millisecond + " cannot be negative.";
            throw new IllegalArgumentException(err);
        } else if (millisecond > 999) {
            String err = "milliseconds " + millisecond + " is out of bounds: 0 <= milliseconds <= 999.";
            throw new IllegalArgumentException(err);
        }

        _millsecond = millisecond;
    }

    /**
     * Sets the UTC field.
     */
    public void setUTC() {
        _utc = true;
    }

    /**
     * Sets the time zone negative field to true.
     *
     * @param zoneNegative
     *            indicates whether or not the time zone is negative.
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the time zone fields
     *             is not allowed
     */
    public void setZoneNegative(boolean zoneNegative) {
        _zoneNegative = zoneNegative;
    }

    /**
     * Sets the time zone fields for this date/time type. A call to this method
     * means that the date/time type used is UTC.
     * <p>
     * For a negative time zone, you first assign the absolute value of the time
     * zone using this method and then you call
     * {@link #setZoneNegative(boolean)}.
     *
     * @param hour
     *            The time zone hour to set. Must be positive.
     * @param minute
     *            The time zone minute to set.
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the time
     *             zone fields is not allowed
     */
    public void setZone(short hour, short minute) {
        setZoneHour(hour);
        setZoneMinute(minute);
    }

    /**
     * Sets the time zone hour field for this date/time type. A call to this
     * method means that the date/time type used is UTC.
     * <p>
     * For a negative time zone, you first assign the absolute value of the time
     * zone using this method and then you call
     * {@link #setZoneNegative(boolean)}.
     *
     * @param hour
     *            the time zone hour to set.  Must be positive.
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the time
     *             zone fields is not allowed
     */
    public void setZoneHour(short hour) {
        if (hour > 23) {
            String err = "time zone hour " + hour + " must be strictly less than 24";
            throw new IllegalArgumentException(err);
        } else if (hour < 0) {
            String err = "time zone hour " + hour + " cannot be negative.";
            throw new IllegalArgumentException(err);
        }

        _zoneHour = hour;

        // Any call to setZone means that you use the date/time you use is UTC
        setUTC();
    }

    /**
     * Sets the time zone minute field for this date/time type. A call to this
     * method means that the date/time type used is UTC.
     *
     * @param minute
     *            the time zone minute to set
     * @throws UnsupportedOperationException
     *             this exception is thrown when changing the value of the time
     *             zone fields is not allowed
     */
    public void setZoneMinute(short minute) {
        if (minute > 59) {
            String err = "time zone minute " + minute + " must be strictly lower than 60";
            throw new IllegalArgumentException(err);
        } else if (minute < 0) {
            String err = "time zone minute " + minute + " cannot be negative.";
            throw new IllegalArgumentException(err);
        }

        _zoneMinute = minute;

        // Any call to setZone means that you use the date/time you use is UTC
        setUTC();
    }

    ////////////////////////Getter methods//////////////////////////////////////

    public boolean isNegative() throws UnsupportedOperationException {
        return _isNegative;
    }

    public short getCentury() throws UnsupportedOperationException {
        return _century;
    }

    public short getYear() throws UnsupportedOperationException {
        return _year;
    }

    public short getMonth() throws UnsupportedOperationException {
        return _month;
    }

    public short getDay() throws UnsupportedOperationException {
        return _day;
    }

    public short getHour() throws UnsupportedOperationException {
        return _hour;
    }

    public short getMinute() throws UnsupportedOperationException {
        return _minute;
    }

    public short getSeconds() throws UnsupportedOperationException {
        return _second;
    }

    public short getMilli() throws UnsupportedOperationException {
        return _millsecond;
    }

    /**
     * Returns true if this date/time type is UTC, that is, has a time zone
     * assigned. A date/time type is UTC if a 'Z' appears at the end of the
     * lexical representation type or if it contains a time zone.
     *
     * @return true if this type has a time zone assigned, else false.
     */
    public boolean isUTC() {
        return _utc;
    }

    public boolean isZoneNegative() {
        return _zoneNegative;
    }

    public short getZoneHour() {
        return _zoneHour;
    }

    public short getZoneMinute() {
        return _zoneMinute;
    }

    ////////////////////////Getter methods//////////////////////////////////////

    public boolean hasIsNegative() {
        return true;
    }

    public boolean hasCentury() {
        return true;
    }

    public boolean hasYear() {
        return true;
    }

    public boolean hasMonth() {
        return true;
    }

    public boolean hasDay() {
        return true;
    }

    public boolean hasHour() {
        return true;
    }

    public boolean hasMinute() {
        return true;
    }

    public boolean hasSeconds() {
        return true;
    }

    public boolean hasMilli() {
        return true;
    }

    ////////////////////////////////////////////////////////////////////////////////

    /**
     * Adds a Duration to this Date/Time type as defined in <a
     * href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes">
     * Adding Duration to dateTimes (W3C XML Schema, part 2 appendix E).</a>
     * This version uses the algorithm defined in the document from W3C. A later
     * version may optimize it.
     * <p>
     * The modified Date/Time instance will keep the same time zone it started
     * with, if any.
     * <p>
     * Don't use getter methods but use direct field access for dateTime in
     * order to have the behaviour defined in the Recommendation document.
     *
     * @param duration
     *            the duration to add
     */
    public void addDuration(Duration duration) {
        int temp  = 0;
        int carry = 0;
        int sign  = (duration.isNegative()) ? -1 : 1;

        // First add the month and year.

        // Months
        try {
            temp = _month + sign * duration.getMonth();
            carry = fQuotient(temp - 1, 12);
            this.setMonth((short) (modulo(temp - 1, 12) + 1));
        } catch (UnsupportedOperationException e) {
            // Ignore
        }

        // Years
        try {
            temp = _century * 100 + _year + sign * duration.getYear() + carry;
            this.setCentury((short) (temp / 100));
            this.setYear((short) (temp % 100));
        } catch (UnsupportedOperationException e) {
            // Ignore
        }

        // Next, pin the day-of-month so it is not outside the new month.

        int tempDay = _day;
        if (tempDay < 1) {
            tempDay = 1;
        } else {
            int maxDay = maxDayInMonthFor(_century, _year, _month);
            if (_day > maxDay) {
                tempDay = maxDay;
            }
        }

        // Next, add the time components

        // Seconds
        try {
            temp = _millsecond + sign * (int)duration.getMilli();
            carry = fQuotient(temp, 1000);
            this.setMilliSecond((short)modulo(temp, 1000));

            temp = _second + sign * duration.getSeconds() + carry;
            carry = fQuotient(temp, 60);
            this.setSecond((short)modulo(temp , 60));
        } catch (UnsupportedOperationException e) {
            // Ignore
        }

        // Minutes
        try {
            temp = _minute + sign * duration.getMinute() + carry;
            carry = fQuotient(temp, 60);
            this.setMinute((short)modulo(temp , 60));
        } catch (UnsupportedOperationException e) {
            // Ignore
        }

        // Hours
        try {
            temp = _hour + sign*duration.getHour() + carry;
            carry = fQuotient(temp, 24);
            this.setHour((short)modulo(temp , 24));
        } catch (UnsupportedOperationException e) {
            // Ignore
        }

        // Finally, set the day-of-month, rolling the month & year as needed

        // Days
        try {
            tempDay += sign * duration.getDay() + carry;

            // Loop until the day-of-month is within bounds
            while (true) {
                short maxDay = maxDayInMonthFor(_century, _year, _month);
                if (tempDay < 1) {
                    tempDay = (short) (tempDay + maxDayInMonthFor(_century, _year, _month - 1));
                    carry = -1;
                } else if (tempDay > maxDay) {
                    tempDay = (short)(tempDay - maxDay);
                    carry = 1;
                } else {
                    break;
                }

                try {
                    temp = _month + carry;
                    this.setMonth((short)(modulo(temp - 1, 12) + 1));
                    temp = this.getCentury() * 100 + this.getYear() + fQuotient(temp - 1, 12);
                    this.setCentury((short) (temp / 100));
                    this.setYear((short) (temp % 100));
                } catch (UnsupportedOperationException e) {
                    // Ignore
                }
            }

            this.setDay((short)tempDay);
        } catch (UnsupportedOperationException e) {
            // Ignore
        }
    } // addDuration

    ///////////////////////W3C XML SCHEMA Helpers///////////////////////////////

    /**
     * Helper function defined in W3C XML Schema Recommendation part 2.
     * @see <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes">
     * W3C XML Schema Recommendation part 2</a>
     */
     private int fQuotient(int a, int b) {
         return (int) Math.floor((float)a / (float)b);
     }

     /**
      * Helper function defined in W3C XML Schema Recommendation part 2.
      * @see <a href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes">
      * W3C XML Schema Recommendation part 2</a>
      */
     private int modulo(int a, int b) {
         return a - fQuotient(a,b) * b;
     }

     /**
     * Returns the maximum day in the given month of the given year.
     *
     * @param year
     * @param month
     * @return the maximum day in the given month of the given year.
     */
    private final short maxDayInMonthFor(short century, short year, int month) {
        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        } else if (month == 2) {
            return (short) ((isLeap(century,year)) ? 29 : 28);
        } else {
            return 31;
        }
    }

    ////////////////////////////////////////////////////////////////////////////

    /**
     * Normalizes a date/time datatype as defined in W3C XML Schema
     * Recommendation document: if a timeZone is present but it is not Z then we
     * convert the date/time datatype to Z using the addition operation defined
     * in <a
     * href="http://www.w3.org/TR/xmlschema-2/#adding-durations-to-dateTimes">
     * Adding Duration to dateTimes (W3C XML Schema, part 2 appendix E).</a>
     *
     * @see #addDuration
     */
    public void normalize() {
        if (!isUTC() || (_zoneHour == 0 && _zoneMinute == 0)) {
            return;
        }

        Duration temp = new Duration();
        temp.setHour(_zoneHour);
        temp.setMinute(_zoneMinute);
        if (!isZoneNegative()) {
            temp.setNegative();
        }

        this.addDuration(temp);

        //reset the zone
        this.setZone((short)0, (short)0);
        this.setZoneNegative(false);
    }

    /**
     * Compares two date/time data types. The algorithm of comparison is defined
     * in <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">W3C XML Schema
     * Recommendation (section 3.2.7.3)</a>
     * <p>
     * The returned value will be one of:
     * <ul>
     * <li>INDETERMINATE (-1): this ? dateTime</li>
     * <li>LESS_THAN (0): this < dateTime</li>
     * <li>EQUALS (1): this == dateTime</li>
     * <li>GREATER_THAN (2): this > dateTime</li>
     * </ul>
     * <p>
     * FIXME:  This code does not compare time zones properly for date/time types
     * that do not contain a time.
     *
     * @param dateTime
     *            the dateTime to compare with the current instance.
     * @return the status of the comparison.
     */
    public int compareTo(DateTimeBase dateTime) {
        if (dateTime == null) {
            throw new IllegalArgumentException("a Date/Time datatype cannot be compared with a null value");
        }

        // Make copies of the date/times we compare so we DO NOT MODIFY THE CURRENT VALUES!
        DateTimeBase tempDate1;
        DateTimeBase tempDate2;

        try {
            tempDate1 = clone(this);
            if (tempDate1.isUTC()) {
                tempDate1.normalize();
            }

            tempDate2 = clone(dateTime);
            if (tempDate2.isUTC()) {
                tempDate2.normalize();
            }
//        } catch (InstantiationException e) {
//            // This is a Castor coding error if this occurs -- it should never occur
//            throw new RuntimeException(e);
//        } catch (IllegalAccessException e) {
//            // This is a Castor coding error if this occurs -- it should never occur
//            throw new RuntimeException(e);
        } catch (CloneNotSupportedException e) {
            // This is a Castor coding error if this occurs -- it should never occur
            throw new RuntimeException("Unexpected 'clone not supported' Exception");
        }

        // If both date/time types are in Z-form (or both not), we just compare the fields.
        if (tempDate1.isUTC() == tempDate2.isUTC()) {
            return compareFields(tempDate1, tempDate2);
        }

        // If datetime1 has a time zone and datetime2 does not
        if (tempDate1.isUTC()) {
            tempDate2.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate2.normalize();
            int result = compareFields(tempDate1, tempDate2);
            if (result == LESS_THAN) {
                return result;
            }

            // Restore time from previous offsetting
            tempDate2.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate2.setZoneNegative(true);
            tempDate2.normalize();

            tempDate2.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate2.setZoneNegative(true);
            tempDate2.normalize();
            result = compareFields(tempDate1, tempDate2);
            if (result == GREATER_THAN) {
                return result;
            }
            return INDETERMINATE;
        }

        // If datetime2 has a time zone and datetime1 does not
        if (tempDate2.isUTC()) {
            tempDate1.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate1.normalize();
            int result = compareFields(tempDate1, tempDate2);
            if (result == GREATER_THAN) {
                return result;
            }

            // Restore time from previous offsetting
            tempDate1.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate1.setZoneNegative(true);
            tempDate1.normalize();

            tempDate1.setZone((short)MAX_TIME_ZONE_COMPARISON_OFFSET,(short)0);
            tempDate1.setZoneNegative(true);
            tempDate1.normalize();
            result = compareFields(tempDate1, tempDate2);
            if (result == LESS_THAN) {
                return result;
            }
            return INDETERMINATE;
        }

        return INDETERMINATE;
    }

    /**
     * Copies a dateTime instance of some type -- all fields may or may not be
     * present. Always copy all fields, even if they are 0, and create a full
     * DateTime with all fields. This allows quick comparisons (no exceptions
     * thrown, which are expensive).
     */
    private DateTimeBase copyDateTimeInstance(DateTimeBase dateTime) {
        DateTimeBase newDateTime = new DateTime();
        newDateTime._isNegative   = dateTime._isNegative;
        newDateTime._century      = dateTime._century;
        newDateTime._year         = dateTime._year;
        newDateTime._month        = dateTime._month;
        newDateTime._day          = dateTime._day;
        newDateTime._hour         = dateTime._hour;
        newDateTime._minute       = dateTime._minute;
        newDateTime._second       = dateTime._second;
        newDateTime._millsecond   = dateTime._millsecond;
        newDateTime._zoneNegative = dateTime._zoneNegative;
        newDateTime._utc          = dateTime._utc;
        newDateTime._zoneHour     = dateTime._zoneHour;
        newDateTime._zoneMinute   = dateTime._zoneMinute;
        return newDateTime;
    }

    public DateTimeBase clone(DateTimeBase dateTime) throws CloneNotSupportedException {
        DateTimeBase newDateTime = (DateTimeBase) super.clone();
//        newDateTime = (DateTimeBase) dateTime.getClass().newInstance();
        newDateTime.setValues(dateTime.getValues());
        if (dateTime.hasIsNegative() && dateTime.isNegative()) {
           newDateTime.setNegative();
        }
        if (dateTime.isUTC()) {
            newDateTime.setUTC();
            newDateTime.setZone(dateTime.getZoneHour(), dateTime.getZoneMinute());
            newDateTime.setZoneNegative(dateTime.isZoneNegative());
        }
        return newDateTime;
    }

    private static int compareFields(DateTimeBase date1, DateTimeBase date2) {
        short field1;
        short field2;

        if (date1.hasCentury() != date2.hasCentury()) {
            return INDETERMINATE;
        }

        if (date1.hasCentury() && date2.hasCentury()) {
            field1 = date1.getCentury();
            field2 = date2.getCentury();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasYear() != date2.hasYear()) {
            return INDETERMINATE;
        }

        if (date1.hasYear() && date2.hasYear()) {
            field1 = date1.getYear();
            field2 = date2.getYear();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasMonth() != date2.hasMonth()) {
            return INDETERMINATE;
        }

        if (date1.hasMonth() && date2.hasMonth()) {
            field1 = date1.getMonth();
            field2 = date2.getMonth();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasDay() != date2.hasDay()) {
            return INDETERMINATE;
        }

        if (date1.hasDay() && date2.hasDay()) {
            field1 = date1.getDay();
            field2 = date2.getDay();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasHour() != date2.hasHour()) {
            return INDETERMINATE;
        }

        if (date1.hasHour() && date2.hasHour()) {
            field1 = date1.getHour();
            field2 = date2.getHour();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasMinute() != date2.hasMinute()) {
            return INDETERMINATE;
        }

        if (date1.hasMinute() && date2.hasMinute()) {
            field1 = date1.getMinute();
            field2 = date2.getMinute();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasSeconds() != date2.hasSeconds()) {
            return INDETERMINATE;
        }

        if (date1.hasSeconds() && date2.hasSeconds()) {
            field1 = date1.getSeconds();
            field2 = date2.getSeconds();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        if (date1.hasMilli() != date2.hasMilli()) {
            return INDETERMINATE;
        }

        if (date1.hasMilli() && date2.hasMilli()) {
            field1 = date1.getMilli();
            field2 = date2.getMilli();
            if (field1 < field2) {
                return LESS_THAN;
            } else if (field1 > field2) {
                return GREATER_THAN;
            }
        }

        return EQUALS;
    }

    /**
     * {@inheritDoc}
     * Overrides the java.lang.Object#hashcode method.
     */
    public int hashCode() {
        return _year^_month^_day^_hour^_minute^_second^_millsecond^_zoneHour^_zoneMinute;
    }

    /**
     * {@inheritDoc}
     * Overrides the java.lang.Object#equals method.
     * @see #equals(Object)
     */
    public boolean equals(Object object) {
        // No need to check if we are comparing two instances of the same class.
        // (if the class is not the same then #equal will return false).
        if (object instanceof DateTimeBase) {
            return equal((DateTimeBase) object);
        }
        return false;
    }

    /**
     * Returns true if the present instance of date/time type is equal to the
     * parameter.
     * <p>
     * The equals relation is as defined in the W3C XML Schema Recommendation,
     * part2.
     *
     * @param dateTime
     *            the date/time type to compare with the present instance
     * @return true if the present instance is equal to the parameter false if
     *         not
     */
    protected boolean equal(DateTimeBase dateTime) {
        return EQUALS == this.compareTo(dateTime);
    } //equals

    /**
     * converts this Date/Time into a local java Calendar.
     * @return a local calendar representing this Date or Time
     */
    public Calendar toCalendar(){
        Calendar result = new GregorianCalendar();
        result.setTime(toDate());
        return result;
    } //toCalendar()

    ////////////// COMMON CODE USED BY EXTENDING CLASSES ///////////////////////

    protected static int parseYear(final String str, final DateTimeBase result, final char[] chars,
                                   final int index, final String complaint) throws ParseException {
        int idx = index;

        if (chars[idx] == '-') {
            idx++;
            result.setNegative();
        }

        if (str.length() < idx + 4
            || !Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])
            || !Character.isDigit(chars[idx + 2]) || !Character.isDigit(chars[idx + 3])) {
            throw new ParseException(complaint + str + "\nThe Year must be 4 digits long", idx);
        }

        short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        short value2 = (short) ((chars[idx+2] - '0') * 10 + (chars[idx+3] - '0'));

        if (value1 == 0 && value2 == 0) {
            throw new ParseException(complaint + str + "\n'0000' is not allowed as a year.", idx);
        }

        result.setCentury(value1);
        result.setYear(value2);

        idx += 4;

        return idx;
    }

    protected static int parseMonth(final String str, final DateTimeBase result, final char[] chars,
                                    final int index, final String complaint) throws ParseException {
        int idx = index;

        if (chars[idx] != '-') {
            throw new ParseException(complaint + str + "\n '-' " + DateTimeBase.WRONGLY_PLACED, idx);
        }

        idx++;

        if (str.length() < idx + 2 || !Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
            throw new ParseException(complaint + str + "\nThe Month must be 2 digits long", idx);
        }

        short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        result.setMonth(value1);

        idx += 2;
        return idx;
    }

    protected static int parseDay(final String str, final DateTimeBase result, final char[] chars,
                                  final int index, final String complaint) throws ParseException {
        int idx = index;

        if (chars[idx] != '-') {
            throw new ParseException(complaint + str + "\n '-' " + DateTimeBase.WRONGLY_PLACED, idx);
        }

        idx++;

        if (str.length() < idx + 2 || !Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
            throw new ParseException(complaint + str + "\nThe Day must be 2 digits long", idx);
        }

        short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        result.setDay(value1);

        idx += 2;
        return idx;
    }

    protected static int parseTime(final String str, final DateTimeBase result, final char[] chars,
                                   final int index, final String complaint) throws ParseException {
        int idx = index;

        if (str.length() < idx + 8) {
            throw new ParseException(complaint + str + "\nA Time field must be at least 8 characters long", idx);
        }

        if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
            throw new ParseException(complaint + str + "\nThe Hour must be 2 digits long", idx);
        }

        short hour;

        hour = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        
        boolean isHourOfValue24 = false;
        if (hour == 24) {
        	result.setHour((short) 0);
        	Duration oneDay = new Duration();
            oneDay.setDay((short) 1);
            result.addDuration(oneDay);
            isHourOfValue24 = true;
        } else {
        	result.setHour(hour);
        }

        idx += 2;

        // Minutes
        if (chars[idx] != ':') {
            throw new ParseException(complaint + str + "\n ':#1' " + DateTimeBase.WRONGLY_PLACED, idx);
        }

        idx++;

        if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
            throw new ParseException(complaint+str+"\nThe Minute must be 2 digits long", idx);
        }

        short minutes = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        
        if (isHourOfValue24 && minutes != 0) {
            throw new ParseException(complaint + str + "\nWhen an hour of 24 is used, minutes must be strictly of value 00.", idx);
        }
        result.setMinute(minutes);

        idx += 2;

        // Seconds
        if (chars[idx] != ':') {
            throw new ParseException(complaint + str + "\n ':#2' " + DateTimeBase.WRONGLY_PLACED, idx);
        }

        idx++;

        if (!Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])) {
            throw new ParseException(complaint + str + "\nThe Second must be 2 digits long", idx);
        }

        short seconds = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
        if (isHourOfValue24 && seconds != 0) {
            throw new ParseException(complaint + str + "\nWhen an hour of 24 is used, seconds must be strictly of value 00.", idx);
        }
        result.setSecond(seconds);

        idx += 2;

        if (idx < chars.length && chars[idx] == '.') {
            idx++;

            long decimalValue = 0;
            long powerOfTen   = 1;
            while (idx < chars.length && Character.isDigit(chars[idx])) {
                decimalValue = decimalValue * 10 + (chars[idx] - '0');
                powerOfTen *= 10;
                idx++;
            }

            // Explicitly truncate to milliseconds if more digits were provided
            if (powerOfTen > 1000) {
                decimalValue /= (powerOfTen / 1000);
                powerOfTen = 1000;
            } else if (powerOfTen < 1000) {
                decimalValue *= (1000 / powerOfTen);
                powerOfTen = 1000;
            }
            result.setMilliSecond((short)decimalValue);
        }

        return idx;
    }

    protected static int parseTimeZone(final String str, final DateTimeBase result, final char[] chars,
                                       final int index, final String complaint) throws ParseException {
        // If we're at the end of the string, there's no time zone to parse
        if (index >= chars.length) {
            return index;
        }

        int idx = index;

        if (chars[idx] == 'Z') {
            result.setUTC();
            return ++idx;
        }

        if (chars[idx] == '+' || chars[idx] == '-') {
            if (chars[idx] == '-') {
                result.setZoneNegative(true);
            }
            idx++;
            if (idx + 5 > chars.length || chars[idx + 2] != ':'
                || !Character.isDigit(chars[idx]) || !Character.isDigit(chars[idx + 1])
                || !Character.isDigit(chars[idx + 3]) || !Character.isDigit(chars[idx + 4])) {
                throw new ParseException(complaint+str+"\nTimeZone must have the format (+/-)hh:mm", idx);
            }
            short value1 = (short) ((chars[idx] - '0') * 10 + (chars[idx+1] - '0'));
            short value2 = (short) ((chars[idx+3] - '0') * 10 + (chars[idx+4] - '0'));
            result.setZone(value1,value2);
            idx += 5;
        }

        return idx;
    }

    /**
     * Sets the time zone in the provided DateFormat.
     * @param df
     */
    protected void setDateFormatTimeZone(DateFormat df) {
        // If no time zone, nothing to do
        if (! isUTC()) {
            return;
        }

        int offset = (this.getZoneMinute() + this.getZoneHour() * 60) * 60 * 1000;
        offset = isZoneNegative() ? -offset : offset;

        SimpleTimeZone timeZone = new SimpleTimeZone(0,"UTC");
        timeZone.setRawOffset(offset);
        timeZone.setID(TimeZone.getAvailableIDs(offset)[0]);
        df.setTimeZone(timeZone);
    }

    /**
     * Sets the time zone in the provided Calendar.
     * @param calendar
     */
    protected void setDateFormatTimeZone(Calendar calendar) {
        // If no time zone, nothing to do
        if (! isUTC()) {
            return;
        }

        int offset = (this.getZoneMinute() + this.getZoneHour() * 60) * 60 * 1000;
        offset = isZoneNegative() ? -offset : offset;

        SimpleTimeZone timeZone = new SimpleTimeZone(0,"UTC");
        timeZone.setRawOffset(offset);
        String[] availableIDs = TimeZone.getAvailableIDs(offset);
        if (availableIDs != null && availableIDs.length > 0) {
            timeZone.setID(availableIDs[0]);
        }
        calendar.setTimeZone(timeZone);
    }

    protected void appendDateString(StringBuffer result) {
        if (isNegative()) {
            result.append('-');
        }

        if ((this.getCentury() / 10) == 0) {
            result.append(0);
        }
        result.append(this.getCentury());

        if ((this.getYear() / 10) == 0) {
            result.append(0);
        }
        result.append(this.getYear());

        result.append('-');
        if ((this.getMonth() / 10) == 0) {
            result.append(0);
        }
        result.append(this.getMonth());

        result.append('-');
        if ((this.getDay()/10) == 0) {
            result.append(0);
        }
        result.append(this.getDay());
    }

    protected void appendTimeString(StringBuffer result) {
        if ((this.getHour()/10) == 0) {
            result.append(0);
        }
        result.append(this.getHour());

        result.append(':');

        if ((this.getMinute() / 10) == 0) {
            result.append(0);
        }
        result.append(this.getMinute());

        result.append(':');

        if ((this.getSeconds() / 10) == 0) {
            result.append(0);
        }
        result.append(this.getSeconds());

        if (this.getMilli() != 0) {
            result.append('.');
            if (this.getMilli() < 100){
                result.append('0');
                if (this.getMilli() < 10){
                    result.append('0');
                }
            }
            result.append(this.getMilli());
        }
    }

    protected void appendTimeZoneString(StringBuffer result) {
        if (!isUTC()) {
            return;
        }

        // By default we append a 'Z' to indicate UTC
        if (this.getZoneHour() == 0 && this.getZoneMinute() == 0) {
            result.append('Z');
            return;
        }

        if (isZoneNegative()) {
            result.append('-');
        } else {
            result.append('+');
        }

        if ((this.getZoneHour()/10) == 0) {
            result.append(0);
        }
        result.append(this.getZoneHour());

        result.append(':');
        if ((this.getZoneMinute()/10) == 0) {
            result.append(0);
        }
        result.append(this.getZoneMinute());
    }

} //-- DateTimeBase
