/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed under the Apache License, Version 2.0 (the "License");
 *   you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *  limitations under the License.
 */

package org.apache.xmlbeans;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

/**
 * Used to build {@link GDate GDates}.
 * <p>
 * Like GDate, a GDateBuilder represents a Gregorian Date, Time,
 * and Timezone, or subset of information (Year, Month, Day,
 * Time, Timezone, or some combination). Wherever it provides
 * guidance, the XML Schema 1.0 specification (plus published
 * errata) is followed.
 * <p>
 * Instances may separately set or clear the year, month,
 * day-of-month, and time-of-day. Not all operations are
 * meaningful on all combinations. In particular, timezone
 * normalization is only possible if there is a time, or
 * a time together with a full date.
 */
public final class GDateBuilder implements GDateSpecification, java.io.Serializable {
    private static final long serialVersionUID = 1L;

    private int _bits;
    private int _CY;
    private int _M;
    private int _D;
    private int _h;
    private int _m;
    private int _s;
    private BigDecimal _fs;
    private int _tzsign;
    private int _tzh;
    private int _tzm;

    /**
     * Constructs a GDateBuilder specifying no date or time
     */
    public GDateBuilder() {
    }

    /**
     * Builds another GDateBuilder with the same value
     * as this one.
     */
    public Object clone() {
        return new GDateBuilder(this);
    }

    /**
     * Builds a GDate from this GDateBuilder.
     */
    public GDate toGDate() {
        return new GDate(this);
    }

    /**
     * Constructs a GDateBuilder by copying another GDateSpecification.
     */
    public GDateBuilder(GDateSpecification gdate) {
        if (gdate.hasTimeZone()) {
            setTimeZone(gdate.getTimeZoneSign(), gdate.getTimeZoneHour(), gdate.getTimeZoneMinute());
        }

        if (gdate.hasTime()) {
            setTime(gdate.getHour(), gdate.getMinute(), gdate.getSecond(), gdate.getFraction());
        }

        if (gdate.hasDay()) {
            setDay(gdate.getDay());
        }

        if (gdate.hasMonth()) {
            setMonth(gdate.getMonth());
        }

        if (gdate.hasYear()) {
            setYear(gdate.getYear());
        }
    }

    // Forms:

    // Date part:
    // Year:       (-?\d{4,})
    // YearMonth:  (-?\d{4,})-(\d{2})
    // Date:       (-?\d{4,})-(\d{2})-(\d{2})
    // Month:      --(\d{2})(--)?              //errata R-48
    // MonthDay:   --(\d{2})-(\d{2})
    // Day:        ---(\d{2})

    // Time part:
    // Time:       (\d{2}):(\d{2}):(\d{2})(.\d*)?

    // Timezone part:
    // TZ:         (Z)|([+-]\d{2}):(\d{2})

    /**
     * Constructs a GDateBuilder from a lexical
     * representation. The lexical space contains the
     * union of the lexical spaces of all the schema
     * date/time types (except for duration).
     */
    public GDateBuilder(CharSequence string) {
        this(new GDate(string));
    }


    public GDateBuilder(Calendar calendar) {
        this(new GDate(calendar));
    }

    /**
     * Constructs a GDateBuilder with the specified year, month, day,
     * hours, minutes, seconds, and optional fractional seconds, in
     * an unspecified timezone.
     * <p>
     * Note that by not specifying the timezone the GDateBuilder
     * becomes partially unordered with respect to timesthat do have a
     * specified timezone.
     *
     * @param year     The year
     * @param month    The month, from 1-12
     * @param day      The day of month, from 1-31
     * @param hour     The hour of day, from 0-23
     * @param minute   The minute of hour, from 0-59
     * @param second   The second of minute, from 0-59
     * @param fraction The fraction of second, 0.0 to 0.999... (may be null)
     */
    public GDateBuilder(
        int year,
        int month,
        int day,
        int hour,
        int minute,
        int second,
        BigDecimal fraction) {
        _bits = HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME;

        if (year == 0) {
            throw new IllegalArgumentException();
        }

        _CY = (year > 0 ? year : year + 1);
        _M = month;
        _D = day;
        _h = hour;
        _m = minute;
        _s = second;
        _fs = fraction == null ? GDate._zero : fraction;

        if (!isValid()) {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Constructs an absolute GDateBuilder with the specified year,
     * month, day, hours, minutes, seconds, and optional fractional
     * seconds, and in the timezone specified.
     * <p>
     * Note that you can reexpress the GDateBuilder in any timezone using
     * normalizeToTimeZone(). The normalize() method normalizes to UTC.
     * <p>
     * If you wish to have a time or date that isn't in a specified timezone,
     * then use the constructor that does not include the timezone arguments.
     *
     * @param year     the year
     * @param month    the month, from 1-12
     * @param day      the day of month, from 1-31
     * @param hour     the hour of day, from 0-23
     * @param minute   the minute of hour, from 0-59
     * @param second   the second of minute, from 0-59
     * @param fraction the fraction of second, 0.0 to 0.999... (may be null)
     * @param tzSign   the timezone offset sign, either +1, 0, or -1
     * @param tzHour   the timezone offset hour
     * @param tzMinute the timezone offset minute
     */
    public GDateBuilder(
        int year,
        int month,
        int day,
        int hour,
        int minute,
        int second,
        BigDecimal fraction,
        int tzSign,
        int tzHour,
        int tzMinute) {
        _bits = HAS_TIMEZONE | HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME;

        if (year == 0) {
            throw new IllegalArgumentException();
        }

        _CY = (year > 0 ? year : year + 1);
        _M = month;
        _D = day;
        _h = hour;
        _m = minute;
        _s = second;
        _fs = fraction == null ? GDate._zero : fraction;
        _tzsign = tzSign;
        _tzh = tzHour;
        _tzm = tzMinute;

        if (!isValid()) {
            throw new IllegalArgumentException();
        }
    }

    /**
     * Constructs a GDateBuilder based on a java.util.Date.
     * <p>
     * The current offset of the default timezone is used as the timezone.
     * <p>
     * For example, if eastern daylight time is in effect at the given
     * date, the timezone on the east coast of the United States
     * translates to GMT-05:00 (EST) + 1:00 (DT offset) == GMT-04:00.
     *
     * @param date the date object to copy
     */
    public GDateBuilder(Date date) {
        setDate(date);
    }

    /**
     * True if the instance is immutable.
     */
    public boolean isImmutable() {
        return false;
    }

    /**
     * Returns a combination of flags indicating the information
     * contained by this GDate.  The five flags are
     * HAS_TIMEZONE, HAS_YEAR, HAS_MONTH, HAS_DAY, and HAS_TIME.
     */
    public int getFlags() {
        return _bits;
    }

    /**
     * True if this date/time specification specifies a timezone.
     */
    public final boolean hasTimeZone() {
        return ((_bits & HAS_TIMEZONE) != 0);
    }

    /**
     * True if this date/time specification specifies a year.
     */
    public final boolean hasYear() {
        return ((_bits & HAS_YEAR) != 0);
    }

    /**
     * True if this date/time specification specifies a month-of-year.
     */
    public final boolean hasMonth() {
        return ((_bits & HAS_MONTH) != 0);
    }

    /**
     * True if this date/time specification specifies a day-of-month.
     */
    public final boolean hasDay() {
        return ((_bits & HAS_DAY) != 0);
    }

    /**
     * True if this date/time specification specifies a time-of-day.
     */
    public final boolean hasTime() {
        return ((_bits & HAS_TIME) != 0);
    }

    /**
     * True if this date/time specification specifies a full date (year, month, day)
     */
    public final boolean hasDate() {
        return ((_bits & (HAS_DAY | HAS_MONTH | HAS_YEAR)) == (HAS_DAY | HAS_MONTH | HAS_YEAR));
    }

    /**
     * Gets the year. Should be a four-digit year specification.
     */
    public final int getYear() {
        return (_CY > 0 ? _CY : _CY - 1);
    }

    /**
     * Gets the month-of-year. January is 1.
     */
    public final int getMonth() {
        return _M;
    }

    /**
     * Gets the day-of-month. The first day of each month is 1.
     */
    public final int getDay() {
        return _D;
    }

    /**
     * Gets the hour-of-day. Midnight is 0, and 11PM is 23.
     */
    public final int getHour() {
        return _h;
    }

    /**
     * Gets the minute-of-hour. Range from 0 to 59.
     */
    public final int getMinute() {
        return _m;
    }

    /**
     * Gets the second-of-minute. Range from 0 to 59.
     */
    public final int getSecond() {
        return _s;
    }


    /**
     * Gets the fraction-of-second. Range from 0 (inclusive) to 1 (exclusive).
     */
    public final BigDecimal getFraction() {
        return _fs;
    }


    /**
     * Gets the rounded millisecond value. Range from 0 to 999
     */
    public final int getMillisecond() {
        if (_fs == null || GDate._zero.equals(_fs)) {
            return 0;
        }
        return _fs.setScale(3, RoundingMode.HALF_UP).unscaledValue().intValue();
    }

    /**
     * Gets the time zone sign. For time zones east of GMT,
     * this is positive; for time zones west, this is negative.
     */
    public final int getTimeZoneSign() {
        return _tzsign;
    }

    /**
     * Gets the time zone hour.
     * This is always positive: for the sign, look at
     * getTimeZoneSign().
     */
    public final int getTimeZoneHour() {
        return _tzh;
    }

    /**
     * Gets the time zone minutes.
     * This is always positive: for the sign, look at
     * getTimeZoneSign().
     */
    public final int getTimeZoneMinute() {
        return _tzm;
    }


    /**
     * Sets the year. Should be a four-digit year specification.
     *
     * @param year the year
     */
    public void setYear(int year) {
        if (year < GDate.MIN_YEAR || year > GDate.MAX_YEAR) {
            throw new IllegalArgumentException("year out of range");
        }
        if (year == 0) {
            throw new IllegalArgumentException("year cannot be 0");
        }
        _bits |= HAS_YEAR;
        _CY = (year > 0 ? year : year + 1);
    }

    /**
     * Sets the month-of-year. January is 1.
     *
     * @param month the month, from 1-12
     */
    public void setMonth(int month) {
        if (month < 1 || month > 12) {
            throw new IllegalArgumentException("month out of range");
        }
        _bits |= HAS_MONTH;
        _M = month;
    }

    /**
     * Sets the day-of-month. The first day of each month is 1.
     *
     * @param day the day of month, from 1-31
     */
    public void setDay(int day) {
        if (day < 1 || day > 31) {
            throw new IllegalArgumentException("day out of range");
        }
        _bits |= HAS_DAY;
        _D = day;
    }

    /**
     * Sets the time. Hours in the day range from 0 to 23;
     * minutes and seconds range from 0 to 59; and fractional
     * seconds range from 0 (inclusive) to 1 (exclusive).
     * The fraction can be null and is assumed to be zero.
     *
     * @param hour     the hour of day, from 0-23 or 24 only if min, sec and fraction are 0
     * @param minute   the minute of hour, from 0-59
     * @param second   the second of minute, from 0-59
     * @param fraction the fraction of second, 0.0 to 0.999... (may be null)
     */
    public void setTime(int hour, int minute, int second, BigDecimal fraction) {
        if (hour < 0 || hour > 24) {
            throw new IllegalArgumentException("hour out of range");
        }
        if (minute < 0 || minute > 59) {
            throw new IllegalArgumentException("minute out of range");
        }
        if (second < 0 || second > 59) {
            throw new IllegalArgumentException("second out of range");
        }
        if (fraction != null && (fraction.signum() < 0 || GDate._one.compareTo(fraction) <= 0)) {
            throw new IllegalArgumentException("fraction out of range");
        }
        if (hour == 24 && (minute != 0 || second != 0 || (fraction != null && (GDate._zero.compareTo(fraction) != 0)))) {
            throw new IllegalArgumentException("when hour is 24, min sec and fracton must be 0");
        }

        _bits |= HAS_TIME;
        _h = hour;
        _m = minute;
        _s = second;
        _fs = fraction == null ? GDate._zero : fraction;
    }

    /**
     * Sets the time zone without changing the other time
     * fields. If you with to adjust other time fields to express
     * the same actual moment in time in a different time zone,
     * use normalizeToTimeZone.
     * <p>
     * Timezones must be between -14:00 and +14:00. Sign
     * must be -1 or 1 (or 0 for UTC only), and the offset hours
     * and minute arguments must be nonnegative.
     *
     * @param tzSign   the timezone offset sign, either +1, 0, or -1
     * @param tzHour   the timezone offset hour
     * @param tzMinute the timezone offset minute
     */
    public void setTimeZone(int tzSign, int tzHour, int tzMinute) {
        if (!((tzSign == 0 && tzHour == 0 && tzMinute == 0) ||
              ((tzSign == -1 || tzSign == 1) &&
               (tzHour >= 0 && tzMinute >= 0) &&
               (tzHour == 14 && tzMinute == 0 || tzHour < 14 && tzMinute < 60)))) {
            throw new IllegalArgumentException("time zone out of range (-14:00 to +14:00). (" +
                                               (tzSign < 0 ? "-" : "+") + tzHour + ":" + tzMinute + ")");
        }

        _bits |= HAS_TIMEZONE;
        _tzsign = tzSign;
        _tzh = tzHour;
        _tzm = tzMinute;
    }

    /**
     * Sets the time zone based on a number of offset minutes rather
     * than sign/hour/minute; for example, setTimeZone(-60) is the
     * same as setTimeZone(-1, 1, 0).
     */
    public void setTimeZone(int tzTotalMinutes) {
        if (tzTotalMinutes < -14 * 60 || tzTotalMinutes > 14 * 60) {
            throw new IllegalArgumentException("time zone out of range (-840 to 840 minutes). (" + tzTotalMinutes + ")");
        }

        int tzSign = Integer.compare(tzTotalMinutes, 0);
        tzTotalMinutes *= tzSign;
        int tzH = tzTotalMinutes / 60;
        int tzM = tzTotalMinutes - tzH * 60;

        setTimeZone(tzSign, tzH, tzM);
    }

    /**
     * Clears the year. After clearing, hasYear returns false and the
     * value of getYear is undefined.
     */
    public void clearYear() {
        _bits &= ~HAS_YEAR;
        _CY = 0;
    }

    /**
     * Clears the month-of-year. After clearing. hasMonth returns false and
     * the value of getMonth is undefined.
     */
    public void clearMonth() {
        _bits &= ~HAS_MONTH;
        _M = 0;
    }

    /**
     * Clears the day-of-month. After clearing. hasDay returns false and
     * the value of getDay is undefined.
     */
    public void clearDay() {
        _bits &= ~HAS_DAY;
        _D = 0;
    }

    /**
     * Clears the time-of-day.
     * After clearing. hasTime returns false and
     * the value of getTime is undefined.
     */
    public void clearTime() {
        _bits &= ~HAS_TIME;
        _h = 0;
        _m = 0;
        _s = 0;
        _fs = null;
    }

    /**
     * Clears the timezone. After clearing. hasTimeZone returns false and
     * the value of getTimeZoneHour and getTimeZoneMinute are undefined.
     * Does not change the other time fields.
     */
    public void clearTimeZone() {
        _bits &= ~HAS_TIMEZONE;
        _tzsign = 0;
        _tzh = 0;
        _tzm = 0;
    }

    /**
     * True if all date fields lie within their legal ranges.  A GDateBuilder
     * can be invalid, for example, if you change the month to February
     * and the day-of-month is 31.
     */
    public boolean isValid() {
        return isValidGDate(this);
    }

    /* package */
    static boolean isValidGDate(GDateSpecification date) {
        if (date.hasYear() && date.getYear() == 0) {
            return false;
        }

        if (date.hasMonth() && (date.getMonth() < 1 || date.getMonth() > 12)) {
            return false;
        }

        if (date.hasDay() &&
            (date.getDay() < 1 || date.getDay() > 31 ||
             date.getDay() > 28 &&
             date.hasMonth() &&
             (date.hasYear() ?
                 date.getDay() > _maxDayInMonthFor((date.getYear() > 0 ?
                         date.getYear() :
                         date.getYear() + 1),
                     date.getMonth()) :
                 date.getDay() > _maxDayInMonth(date.getMonth())))) {
            return false;
        }

        if (date.hasTime() && ((date.getHour() < 0 || date.getHour() > 23 ||
                                date.getMinute() < 0 || date.getMinute() > 59 ||
                                date.getSecond() < 0 || date.getSecond() > 59 ||
                                date.getFraction().signum() < 0 || date.getFraction().compareTo(GDate._one) >= 0)) &&
            // check for 24:00:00 valid format
            !(date.getHour() == 24 && date.getMinute() == 0 && date.getSecond() == 0 &&
              date.getFraction().compareTo(GDate._zero) == 0)) {
            return false;
        }

        if (date.hasTimeZone() &&
            (!((date.getTimeZoneSign() == 0 && date.getTimeZoneHour() == 0 && date.getTimeZoneMinute() == 0) ||
               ((date.getTimeZoneSign() == -1 || date.getTimeZoneSign() == 1) &&
                // NB: allow +00:00 and -00:00
                // (date.getTimeZoneHour() == 0 && date.getTimeZoneMinute() > 0 || date.getTimeZoneHour() > 0 && date.getTimeZoneMinute() >= 0) &&
                (date.getTimeZoneHour() >= 0 && date.getTimeZoneMinute() >= 0) &&
                (date.getTimeZoneHour() == 14 && date.getTimeZoneMinute() == 0 || date.getTimeZoneHour() < 14 && date.getTimeZoneMinute() < 60))))) {
            return false;
        }

        // everyting looks kosher
        return true;
    }


    /**
     * Normalizes the instance, ensuring date and time fields are within
     * their normal ranges.
     * <p>
     * If no timezone or no time is specified, or if a partial date is specified, this
     * method does nothing, and leaves the timezone information as-is.
     * <p>
     * If a time or time and date is specified, this method normalizes the timezone
     * to UTC.
     */
    public void normalize() {
        // DateTime or Time, with TimeZone: normalize to UTC.
        // In the process all the fields will be normalized.
        if (hasDay() == hasMonth() && hasDay() == hasYear() &&
            hasTimeZone() && hasTime()) {
            normalizeToTimeZone(0, 0, 0);
        } else {
            // No timezone, or incomplete date.
            _normalizeTimeAndDate();
        }

        // remove trailing zeros from fractional seconds
        if (hasTime() && _fs != null && _fs.scale() > 0) {
            if (_fs.signum() == 0) {
                _fs = GDate._zero;
            } else {
                BigInteger bi = _fs.unscaledValue();
                String str = bi.toString();
                int lastzero;
                for (lastzero = str.length(); lastzero > 0; lastzero -= 1) {
                    if (str.charAt(lastzero - 1) != '0') {
                        break;
                    }
                }
                if (lastzero < str.length()) {
                    _fs = _fs.setScale(_fs.scale() - str.length() + lastzero, RoundingMode.UNNECESSARY);
                }
            }
        }
    }

    /**
     * Normalizes the instance when hour is 24. If day is present, hour 24 is equivalent to hour 00 next day.
     */
    void normalize24h() {
        if (!hasTime() || getHour() != 24) {
            return;
        }

        _normalizeTimeAndDate();
    }


    private void _normalizeTimeAndDate() {
        long carry = 0;

        if (hasTime()) {
            carry = _normalizeTime();
        }

        if (hasDay()) {
            _D = Math.addExact(_D, Math.toIntExact(carry));
        }

        if (hasDate()) {
            _normalizeDate();
        } else if (hasMonth()) {
            // with incomplete dates, just months can be normalized:
            // days stay denormalized.
            if (_M < 1 || _M > 12) {
                int temp = _M;
                _M = _modulo(temp, 1, 13);
                if (hasYear()) {
                    _CY = _CY + (int) _fQuotient(temp, 1, 13);
                }
            }
        }
    }

    /**
     * If the time and timezone are known, this method changes the timezone to the
     * specified UTC offset, altering minutes, hours, day, month, and year as
     * necessary to ensure that the actual described moment in time is the same.
     * <p>
     * It is an error to operate on instances without a time or timezone, or
     * with a partially specified date.
     *
     * @param tzSign   the timezone offset sign, either +1, 0, or -1
     * @param tzHour   the timezone offset hour
     * @param tzMinute the timezone offset minute
     */
    public void normalizeToTimeZone(int tzSign, int tzHour, int tzMinute) {
        if (!((tzSign == 0 && tzHour == 0 && tzMinute == 0) ||
              ((tzSign == -1 || tzSign == 1) &&
               (tzHour >= 0 && tzMinute >= 0) &&
               (tzHour == 14 && tzMinute == 0 || tzHour < 14 && tzMinute < 60)))) {
            throw new IllegalArgumentException("time zone must be between -14:00 and +14:00");
        }

        if (!hasTimeZone() || !hasTime()) {
            throw new IllegalStateException("cannot normalize time zone without both time and timezone");
        }

        if (!(hasDay() == hasMonth() && hasDay() == hasYear())) {
            throw new IllegalStateException("cannot do date math without a complete date");
        }

        int hshift = tzSign * tzHour - _tzsign * _tzh;
        int mshift = tzSign * tzMinute - _tzsign * _tzm;

        _tzsign = tzSign;
        _tzh = tzHour;
        _tzm = tzMinute;
        addDuration(1, 0, 0, 0, hshift, mshift, 0, null);
    }

    /**
     * Normalizes to a time zone specified by a number of offset minutes rather
     * than sign/hour/minute; for example, normalizeToTimeZone(-60) is the
     * same as normalizeToTimeZone(-1, 1, 0).
     */
    public void normalizeToTimeZone(int tzTotalMinutes) {
        if (tzTotalMinutes < -14 * 60 || tzTotalMinutes > 14 * 60) {
            throw new IllegalArgumentException("time zone out of range (-840 to 840 minutes). (" + tzTotalMinutes + ")");
        }

        int tzSign = Integer.compare(tzTotalMinutes, 0);
        tzTotalMinutes *= tzSign;
        int tzH = tzTotalMinutes / 60;
        int tzM = tzTotalMinutes - tzH * 60;

        normalizeToTimeZone(tzSign, tzH, tzM);
    }


    /**
     * Adds a given duration to the date/time.
     *
     * @param duration the duration to add
     */
    public void addGDuration(GDurationSpecification duration) {
        addDuration(duration.getSign(), duration.getYear(), duration.getMonth(), duration.getDay(),
            duration.getHour(), duration.getMinute(), duration.getSecond(), duration.getFraction());
    }

    /**
     * Subtracts a given duration from the date/time.
     *
     * @param duration the duration to subtract
     */
    public void subtractGDuration(GDurationSpecification duration) {
        addDuration(-duration.getSign(), duration.getYear(), duration.getMonth(), duration.getDay(),
            duration.getHour(), duration.getMinute(), duration.getSecond(), duration.getFraction());
    }


    /**
     * Normalizes the date by carrying over to the year any months outside 1..12
     * and carrying over to the month any days outside 1..(days-in-month).
     */
    private void _normalizeDate() {
        if (_M < 1 || _M > 12 || _D < 1 || _D > _maxDayInMonthFor(_CY, _M)) {
            // fix months first
            int temp = _M;
            _M = _modulo(temp, 1, 13);
            _CY = _CY + (int) _fQuotient(temp, 1, 13);

            // then pull days out
            int extradays = _D - 1;
            _D = 1;

            // then use the julian date function to fix
            setJulianDate(getJulianDate() + extradays);
        }
    }

    /**
     * Normalizes time so that fractions are 0..1(exc), seconds/minutes 0..59,
     * and hours 0..24. Returns the number of days to carry over from normalizing
     * away more than 24 hours.
     */
    private long _normalizeTime() {
        long carry = 0;
        long temp;

        // fractions
        if (_fs != null && (_fs.signum() < 0 || _fs.compareTo(GDate._one) >= 0)) {
            BigDecimal bdcarry = _fs.setScale(0, RoundingMode.FLOOR);
            _fs = _fs.subtract(bdcarry);
            carry = bdcarry.longValue();
        }

        if (carry != 0 || _s < 0 || _s > 59 || _m < 0 || _m > 50 || _h < 0 || _h > 23) {
            // seconds
            temp = _s + carry;
            carry = _fQuotient(temp, 60);
            _s = _mod(temp, 60, carry);

            // minutes
            temp = _m + carry;
            carry = _fQuotient(temp, 60);
            _m = _mod(temp, 60, carry);

            // hours
            temp = _h + carry;
            carry = _fQuotient(temp, 24);
            _h = _mod(temp, 24, carry);
        }

        return carry;
    }

    /**
     * Adds a given duration to the date/time.
     *
     * @param sign     +1 to add, -1 to subtract
     * @param year     the number of years to add
     * @param month    the number of months to add
     * @param day      the number of days to add
     * @param hour     the number of hours to add
     * @param minute   the number of minutes to add
     * @param second   the number of seconds to add
     * @param fraction the number of fractional seconds to add (may be null)
     */
    public void addDuration(int sign, int year, int month, int day,
                            int hour, int minute, int second, BigDecimal fraction) {
        boolean timemath = hour != 0 || minute != 0 || second != 0 || fraction != null && fraction.signum() != 0;
        if (timemath && !hasTime()) {
            throw new IllegalStateException("cannot do time math without a complete time");
        }
        boolean datemath = hasDay() && (day != 0 || timemath);
        if (datemath && !hasDate()) {
            throw new IllegalStateException("cannot do date math without a complete date");
        }

        int temp;

        // months + years are easy
        if (month != 0 || year != 0) {
            // Prepare the _D to be pegged before changing month
            if (hasDay()) {
                _normalizeDate();
            }

            // Add months and years
            temp = _M + sign * month;
            _M = _modulo(temp, 1, 13);
            _CY = _CY + sign * year + (int) _fQuotient(temp, 1, 13);

            // In new month, day may need to be pegged before proceeding
            if (hasDay()) {
                assert (_D >= 1);
                temp = _maxDayInMonthFor(_CY, _M);
                if (_D > temp) {
                    _D = temp;
                }
            }
        }

        long carry = 0;

        if (timemath) {
            // fractions
            if (fraction != null && fraction.signum() != 0) {
                if (_fs.signum() == 0 && sign == 1) {
                    _fs = fraction;
                } else {
                    _fs = (sign == 1) ? _fs.add(fraction) : _fs.subtract(fraction);
                }
            }

            // seconds, minutes, hours
            _s += sign * second;
            _m += sign * minute;
            _h += sign * hour;

            // normalize time
            carry = _normalizeTime();
        }

        if (datemath) {
            // days: may require renormalization
            _D = Math.addExact(_D, Math.toIntExact(Math.addExact(Math.multiplyExact(sign, day), carry)));
            _normalizeDate();
        }
    }

    /**
     * Given {year,month} computes maximum
     * number of days for given month
     */
    private static int _maxDayInMonthFor(int year, int month) {
        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        }

        return month == 2 ? (_isLeapYear(year) ? 29 : 28) : 31;

    }

    /**
     * Given {year,month} computes maximum
     * number of days for given month
     */
    private static int _maxDayInMonth(int month) {
        if (month == 4 || month == 6 || month == 9 || month == 11) {
            return 30;
        }

        return month == 2 ? 29 : 31;

    }

    /**
     * Returns the Julian date corresponding to this Gregorian date.
     * The Julian date (JD) is a continuous count of days from
     * 1 January 4713 BC.
     */
    public final int getJulianDate() {
        return julianDateForGDate(this);
    }


    /**
     * Sets the Gregorian date based on the given Julian date.
     * The Julian date (JD) is a continuous count of days from
     * 1 January 4713 BC.
     *
     * @param julianday the julian day number
     */
    public void setJulianDate(int julianday) {
        if (julianday < 0) {
            throw new IllegalArgumentException("date before year -4713");
        }

        int temp;
        int qepoc;

        // from http://aa.usno.navy.mil/faq/docs/JD_Formula.html
        temp = julianday + 68569;
        qepoc = 4 * temp / 146097;
        temp = temp - (146097 * qepoc + 3) / 4;
        _CY = 4000 * (temp + 1) / 1461001;
        temp = temp - 1461 * _CY / 4 + 31;
        _M = 80 * temp / 2447;
        _D = temp - 2447 * _M / 80;
        temp = _M / 11;
        _M = _M + 2 - 12 * temp;
        _CY = 100 * (qepoc - 49) + _CY + temp;

        _bits |= HAS_DAY | HAS_MONTH | HAS_YEAR;
    }


    /**
     * Sets the current time and date based on a java.util.Date instance.
     * <p>
     * The timezone offset used is based on the default TimeZone. (The
     * default TimeZone is consulted to incorporate daylight savings offsets
     * if applicable for the current date as well as the base timezone offset.)
     * <p>
     * If you wish to normalize the timezone, e.g., to UTC, follow this with
     * a call to normalizeToTimeZone.
     *
     * @param date the Date object to copy
     */
    public void setDate(Date date) {
        // Default timezone
        TimeZone dtz = TimeZone.getDefault();
        int offset = dtz.getOffset(date.getTime());
        int offsetsign = 1;
        if (offset < 0) {
            offsetsign = -1;
            offset = -offset;
        }
        int offsetmin = offset / (1000 * 60);
        int offsethr = offsetmin / 60;
        offsetmin = offsetmin - offsethr * 60;

        setTimeZone(offsetsign, offsethr, offsetmin);

        // paranoia: tz.getOffset can return fractions of minutes, but we must round
        int roundedoffset = offsetsign * (offsethr * 60 + offsetmin) * 60 * 1000;

        // midnight
        setTime(0, 0, 0, GDate._zero);

        // Set to January 1, 1970.
        // setJulianDate(2440588);
        _bits |= HAS_DAY | HAS_MONTH | HAS_YEAR;
        _CY = 1970;
        _M = 1;
        _D = 1;

        // Add a duration representing the number of milliseconds
        addGDuration(new GDuration(1, 0, 0, 0, 0, 0, 0,
            BigDecimal.valueOf(date.getTime() + roundedoffset, 3)));

        // special case: ss.000 -> ss
        if (_fs.signum() == 0) {
            _fs = GDate._zero;
        }
    }

    /**
     * Copies a GDateSpecification, completely replacing the current
     * information in this GDateBuilder.
     *
     * @param gdate the GDateSpecification to copy
     */
    public void setGDate(GDateSpecification gdate) {
        _bits = gdate.getFlags() & (HAS_TIMEZONE | HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME);
        int year = gdate.getYear();
        _CY = (year > 0 ? year : year + 1);
        _M = gdate.getMonth();
        _D = gdate.getDay();
        _h = gdate.getHour();
        _m = gdate.getMinute();
        _s = gdate.getSecond();
        _fs = gdate.getFraction();
        _tzsign = gdate.getTimeZoneSign();
        _tzh = gdate.getTimeZoneHour();
        _tzm = gdate.getTimeZoneMinute();
    }


    /**
     * Retrieves the value of the current time as an {@link XmlCalendar}.
     * <p>
     * {@link XmlCalendar} is a subclass of {@link java.util.GregorianCalendar}
     * which is slightly customized to match XML schema date rules.
     * <p>
     * The returned {@link XmlCalendar} has only those time and date fields
     * set that are reflected in the GDate object.  Because of the way the
     * {@link java.util.Calendar} contract works, any information in the isSet() vanishes
     * as soon as you view any unset field using get() methods.
     * This means that if it is important to understand which date fields
     * are set, you must call isSet() first before get().
     */
    public XmlCalendar getCalendar() {
        return new XmlCalendar(this);
    }

    /**
     * Retrieves the value of the current time as a java.util.Date
     * instance.
     */
    public Date getDate() {
        return dateForGDate(this);
    }

    /* package */
    static int julianDateForGDate(GDateSpecification date) {
        if (!date.hasDate()) {
            throw new IllegalStateException("cannot do date math without a complete date");
        }

        // from http://aa.usno.navy.mil/faq/docs/JD_Formula.html
        int day = date.getDay();
        int month = date.getMonth();
        int year = date.getYear();
        year = (year > 0 ? year : year + 1);
        int result = day - 32075 + 1461 * (year + 4800 + (month - 14) / 12) / 4 +
                     367 * (month - 2 - (month - 14) / 12 * 12) / 12 - 3 * ((year + 4900 + (month - 14) / 12) / 100) / 4;

        if (result < 0) {
            throw new IllegalStateException("date too far in the past (year allowed to -4713)");
        }

        return result;
    }

    /* package */
    static Date dateForGDate(GDateSpecification date) {
        long jDate = julianDateForGDate(date);
        long to1970Date = jDate - 2440588;
        long to1970Ms = 1000 * 60 * 60 * 24 * to1970Date;

        to1970Ms += date.getMillisecond();
        to1970Ms += date.getSecond() * 1000L;
        to1970Ms += date.getMinute() * 60 * 1000L;
        to1970Ms += date.getHour() * 60 * 60 * 1000L;
        if (date.hasTimeZone()) {
            to1970Ms -= date.getTimeZoneMinute() * date.getTimeZoneSign() * 60 * 1000L;
            to1970Ms -= date.getTimeZoneHour() * date.getTimeZoneSign() * 60 * 60 * 1000L;
        } else {
            TimeZone def = TimeZone.getDefault();
            int offset = def.getOffset(to1970Ms);
            to1970Ms -= offset;
        }

        return new Date(to1970Ms);
    }

    /**
     * True for leap years.
     */
    private static boolean _isLeapYear(int year) {
        // BUGBUG: Julian calendar?
        return ((year % 4 == 0) && ((year % 100 != 0) || (year % 400 == 0)));
    }

    /**
     * fQuotient(a, b) = the greatest integer less than or equal to a/b
     */
    private static long _fQuotient(long a, int b) {
        return ((a < 0) == (b < 0)) ? (a / b) : -((b - a - 1) / b);

    }

    /**
     * modulo(a, b) = a - fQuotient(a,b)*b
     */
    private static int _mod(long a, int b, long quotient) {
        return (int) (a - quotient * b);
    }

    /**
     * modulo(a - low, high - low) + low
     */
    private static int _modulo(long temp, int low, int high) {
        long a = temp - low;
        int b = high - low;
        return (_mod(a, b, _fQuotient(a, b)) + low);
    }

    /**
     * Quotient(a - low, high - low)
     */
    private static long _fQuotient(long temp, int low, int high) {
        return _fQuotient(temp - low, high - low);
    }

    /**
     * Sets to the first possible moment that matches the given
     * specification.
     */
    private void _setToFirstMoment() {
        // 1584 was the first leap year during which the Gregorian
        // calendar was in use: seems like the most reasonable "first"
        // year to use in absence of a year.

        if (!hasYear()) {
            setYear(1584);
        }

        if (!hasMonth()) {
            setMonth(1);
        }

        if (!hasDay()) {
            setDay(1);
        }

        if (!hasTime()) {
            setTime(0, 0, 0, GDate._zero);
        }
    }

    /**
     * Comparison to another GDate.
     * <ul>
     * <li>Returns -1 if this &lt; date. (less-than)
     * <li>Returns 0 if this == date. (equal)
     * <li>Returns 1 if this &gt; date. (greater-than)
     * <li>Returns 2 if this &lt;&gt; date. (incomparable)
     * </ul>
     * Two instances are incomparable if they have different amounts
     * of information.
     *
     * @param datespec the date to compare against
     */
    public final int compareToGDate(GDateSpecification datespec) {
        return compareGDate(this, datespec);
    }


    /* package */
    static int compareGDate(GDateSpecification tdate, GDateSpecification datespec) {
        // same amount of information: looks good
        int bitdiff = tdate.getFlags() ^ datespec.getFlags();

        if ((bitdiff & (HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME | HAS_TIMEZONE)) == 0) {
            // If the other date needs to be normalized to
            // our timezone, make a clone and do so if possible
            if (tdate.hasTimeZone() &&
                (datespec.getTimeZoneHour() != tdate.getTimeZoneHour() ||
                 datespec.getTimeZoneMinute() != tdate.getTimeZoneMinute() ||
                 datespec.getTimeZoneSign() != tdate.getTimeZoneSign())) {
                datespec = new GDateBuilder(datespec);

                int flags = tdate.getFlags() & (HAS_YEAR | HAS_MONTH | HAS_DAY);
                if (flags != 0 && flags != (HAS_YEAR | HAS_MONTH | HAS_DAY) || !tdate.hasTime()) {
                    // in these cases we'll need to fill in fields
                    ((GDateBuilder) datespec)._setToFirstMoment();
                    tdate = new GDateBuilder(tdate);
                    ((GDateBuilder) tdate)._setToFirstMoment();
                }

                ((GDateBuilder) datespec).normalizeToTimeZone(tdate.getTimeZoneSign(), tdate.getTimeZoneHour(), tdate.getTimeZoneMinute());
            }

            // compare by field
            return fieldwiseCompare(tdate, datespec);
        }

        // different amounts of information (except timezone): not comparable
        if ((bitdiff & (HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME)) != 0) {
            return 2;
        }

        // The schema spec says we should try to compare with-timezone and
        // without-timezone specifications... Well, OK, sure, if they say so.

        // We don't have a timezone but the other does: reverse the call
        if (!tdate.hasTimeZone()) {
            int result = compareGDate(datespec, tdate);
            return result == 2 ? 2 : -result;
        }

        // Now tdate is guaranteed to have a timezone and datespec not.

        // To muck with the times, make clones
        GDateBuilder pdate = new GDateBuilder(tdate);

        // To cover the one uncovered case: if one date is 02/28 and the
        // other date is 03/01, shift days closer by one to simulate being
        // the last day of the month within a leap year
        if ((tdate.getFlags() & (HAS_YEAR | HAS_MONTH | HAS_DAY)) == (HAS_MONTH | HAS_DAY)) {
            if (tdate.getDay() == 28 && tdate.getMonth() == 2) {
                if (datespec.getDay() == 1 && datespec.getMonth() == 3) {
                    pdate.setDay(29);
                }
            } else if (datespec.getDay() == 28 && datespec.getMonth() == 2) {
                if (tdate.getDay() == 1 && tdate.getMonth() == 3) {
                    pdate.setMonth(2);
                    pdate.setDay(29);
                }
            }
        }

        // For timespans, compare by first instant of time
        // possible. Therefore, fill in Midnight, January 1, 1584 (a leap year)
        // in absence of other information.
        pdate._setToFirstMoment();

        // P < Q if P < (Q with time zone +14:00)
        GDateBuilder qplusdate = new GDateBuilder(datespec);
        qplusdate._setToFirstMoment();
        qplusdate.setTimeZone(1, 14, 0);
        qplusdate.normalizeToTimeZone(tdate.getTimeZoneSign(), tdate.getTimeZoneHour(), tdate.getTimeZoneMinute());
        if (fieldwiseCompare(pdate, qplusdate) == -1) {
            return -1;
        }

        // P > Q if P > (Q with time zone -14:00)
        GDateBuilder qminusdate = qplusdate;
        qminusdate.setGDate(datespec);
        qminusdate._setToFirstMoment();
        qminusdate.setTimeZone(-1, 14, 0);
        qminusdate.normalizeToTimeZone(tdate.getTimeZoneSign(), tdate.getTimeZoneHour(), tdate.getTimeZoneMinute());
        if (fieldwiseCompare(pdate, qminusdate) == 1) {
            return 1;
        }

        // P <> Q otherwise
        return 2;
    }

    /**
     * Does a simple most-significant-digit-first comparison,
     * ignoring any timezone or has/doesn't have issues.
     * The data must have been digested first.
     */
    private static int fieldwiseCompare(GDateSpecification tdate, GDateSpecification date) {
        if (tdate.hasYear()) {
            int CY = date.getYear();
            int TCY = tdate.getYear();
            if (TCY < CY) {
                return -1;
            }
            if (TCY > CY) {
                return 1;
            }
        }
        if (tdate.hasMonth()) {
            int M = date.getMonth();
            int TM = tdate.getMonth();
            if (TM < M) {
                return -1;
            }
            if (TM > M) {
                return 1;
            }
        }
        if (tdate.hasDay()) {
            int D = date.getDay();
            int TD = tdate.getDay();
            if (TD < D) {
                return -1;
            }
            if (TD > D) {
                return 1;
            }
        }
        if (tdate.hasTime()) {
            int h = date.getHour();
            int th = tdate.getHour();
            if (th < h) {
                return -1;
            }
            if (th > h) {
                return 1;
            }
            int m = date.getMinute();
            int tm = tdate.getMinute();
            if (tm < m) {
                return -1;
            }
            if (tm > m) {
                return 1;
            }
            int s = date.getSecond();
            int ts = tdate.getSecond();
            if (ts < s) {
                return -1;
            }
            if (ts > s) {
                return 1;
            }
            BigDecimal fs = date.getFraction();
            BigDecimal tfs = tdate.getFraction();
            if (tfs == null && fs == null) {
                return 0;
            }
            return (tfs == null ? GDate._zero : tfs).compareTo(fs == null ? GDate._zero : fs);
        }

        return 0;
    }

    /**
     * Returns the builtin type code for the shape of the information
     * contained in this instance, or 0 if the
     * instance doesn't contain information corresponding to a
     * Schema type.
     * <p>
     * Value will be equal to
     * {@link SchemaType#BTC_NOT_BUILTIN},
     * {@link SchemaType#BTC_G_YEAR},
     * {@link SchemaType#BTC_G_YEAR_MONTH},
     * {@link SchemaType#BTC_G_MONTH},
     * {@link SchemaType#BTC_G_MONTH_DAY},
     * {@link SchemaType#BTC_G_DAY},
     * {@link SchemaType#BTC_DATE},
     * {@link SchemaType#BTC_DATE_TIME}, or
     * {@link SchemaType#BTC_TIME}.
     */
    public final int getBuiltinTypeCode() {
        return btcForFlags(_bits);
    }

    /* package */
    static int btcForFlags(int flags) {
        switch (flags & (HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME)) {
            case HAS_YEAR:
                return SchemaType.BTC_G_YEAR;
            case HAS_YEAR | HAS_MONTH:
                return SchemaType.BTC_G_YEAR_MONTH;
            case HAS_MONTH:
                return SchemaType.BTC_G_MONTH;
            case HAS_MONTH | HAS_DAY:
                return SchemaType.BTC_G_MONTH_DAY;
            case HAS_DAY:
                return SchemaType.BTC_G_DAY;
            case HAS_YEAR | HAS_MONTH | HAS_DAY:
                return SchemaType.BTC_DATE;
            case HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME:
                return SchemaType.BTC_DATE_TIME;
            case HAS_TIME:
                return SchemaType.BTC_TIME;
            default:
                return SchemaType.BTC_NOT_BUILTIN;
        }
    }

    /**
     * Clears the fields in this GDateBuilder that are not applicable
     * for the given SchemaType date code.  The code should be
     * {@link SchemaType#BTC_G_YEAR},
     * {@link SchemaType#BTC_G_YEAR_MONTH},
     * {@link SchemaType#BTC_G_MONTH},
     * {@link SchemaType#BTC_G_MONTH_DAY},
     * {@link SchemaType#BTC_G_DAY},
     * {@link SchemaType#BTC_DATE},
     * {@link SchemaType#BTC_DATE_TIME}, or
     * {@link SchemaType#BTC_TIME}.
     *
     * @param typeCode the type code to apply
     */
    public void setBuiltinTypeCode(int typeCode) {
        switch (typeCode) {
            case SchemaType.BTC_G_YEAR:
                //HAS_YEAR
                clearMonth();
                clearDay();
                clearTime();
                return;
            case SchemaType.BTC_G_YEAR_MONTH:
                //HAS_YEAR | HAS_MONTH
                clearDay();
                clearTime();
                return;
            case SchemaType.BTC_G_MONTH:
                //HAS_MONTH
                clearYear();
                clearDay();
                clearTime();
                return;
            case SchemaType.BTC_G_MONTH_DAY:
                //HAS_MONTH | HAS_DAY
                clearYear();
                clearTime();
                return;
            case SchemaType.BTC_G_DAY:
                //HAS_DAY
                clearYear();
                clearMonth();
                clearTime();
                return;
            case SchemaType.BTC_DATE:
                //HAS_YEAR | HAS_MONTH | HAS_DAY
                clearTime();
                return;
            case SchemaType.BTC_DATE_TIME:
                //HAS_YEAR | HAS_MONTH | HAS_DAY | HAS_TIME
                return;
            case SchemaType.BTC_TIME:
                //HAS_TIME
                clearYear();
                clearMonth();
                clearDay();
                return;
            default:
                throw new IllegalArgumentException("codeType must be one of SchemaType BTC_  DATE TIME related types.");
        }
    }


    /* package */ static final BigInteger TEN = BigInteger.valueOf(10);

    /**
     * The canonical string representation. Specific moments or
     * times-of-day in a specified timezone are normalized to
     * UTC time to produce a canonical string form for them.
     * Other recurring time specifications keep their timezone
     * information.
     */
    public String canonicalString() {
        boolean needNormalize =
            (hasTimeZone() && getTimeZoneSign() != 0 && hasTime() &&
             ((hasDay() == hasMonth() && hasDay() == hasYear())));

        if (!needNormalize && getFraction() != null && getFraction().scale() > 0) {
            BigInteger bi = getFraction().unscaledValue();
            needNormalize = (bi.mod(TEN).signum() == 0);
        }

        if (!needNormalize) {
            return toString();
        }

        GDateBuilder cdate = new GDateBuilder(this);
        cdate.normalize();
        return cdate.toString();
    }

    /**
     * The natural string representation. This represents the information
     * that is available, including timezone. For types that correspond
     * to defined schema types (schemaBuiltinTypeCode() &gt; 0),
     * this provides the natural lexical representation.
     * <p>
     * When both time and timezone are specified, this string is not
     * the canonical representation unless the timezone is UTC (Z)
     * (since the same moment in time can be expressed in different
     * timezones). To get a canonical string, use the canonicalString()
     * method.
     */
    public final String toString() {
        return GDate.formatGDate(this);
    }

}
