/*
 * Copyright (C) 2006 The Android Open Source Project
 *
 * 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 android.text.format;

import android.util.TimeFormatException;

import com.android.i18n.timezone.WallTime;
import com.android.i18n.timezone.ZoneInfoData;
import com.android.i18n.timezone.ZoneInfoDb;

import java.util.Locale;
import java.util.TimeZone;

/**
 * An alternative to the {@link java.util.Calendar} and
 * {@link java.util.GregorianCalendar} classes. An instance of the Time class represents
 * a moment in time, specified with second precision. It is modelled after
 * struct tm. This class is not thread-safe and does not consider leap seconds.
 *
 * <p>This class has a number of issues and it is recommended that
 * {@link java.util.GregorianCalendar} is used instead.
 *
 * <p>Known issues:
 * <ul>
 *     <li>For historical reasons when performing time calculations all arithmetic currently takes
 *     place using 32-bit integers. This limits the reliable time range representable from 1902
 *     until 2037.See the wikipedia article on the
 *     <a href="http://en.wikipedia.org/wiki/Year_2038_problem">Year 2038 problem</a> for details.
 *     Do not rely on this behavior; it may change in the future.
 *     </li>
 *     <li>Calling {@link #switchTimezone(String)} on a date that cannot exist, such as a wall time
 *     that was skipped due to a DST transition, will result in a date in 1969 (i.e. -1, or 1 second
 *     before 1st Jan 1970 UTC).</li>
 *     <li>Much of the formatting / parsing assumes ASCII text and is therefore not suitable for
 *     use with non-ASCII scripts.</li>
 *     <li>No support for pseudo-zones like "GMT-07:00".</li>
 * </ul>
 *
 * @deprecated Use {@link java.util.GregorianCalendar} instead.
 */
@Deprecated
public class Time {
    private static final String Y_M_D_T_H_M_S_000 = "%Y-%m-%dT%H:%M:%S.000";
    private static final String Y_M_D_T_H_M_S_000_Z = "%Y-%m-%dT%H:%M:%S.000Z";
    private static final String Y_M_D = "%Y-%m-%d";

    public static final String TIMEZONE_UTC = "UTC";

    /**
     * The Julian day of the epoch, that is, January 1, 1970 on the Gregorian
     * calendar.
     */
    public static final int EPOCH_JULIAN_DAY = 2440588;

    /**
     * The Julian day of the Monday in the week of the epoch, December 29, 1969
     * on the Gregorian calendar.
     */
    public static final int MONDAY_BEFORE_JULIAN_EPOCH = EPOCH_JULIAN_DAY - 3;

    /**
     * True if this is an allDay event. The hour, minute, second fields are
     * all zero, and the date is displayed the same in all time zones.
     */
    public boolean allDay;

    /**
     * Seconds [0-61] (2 leap seconds allowed)
     */
    public int second;

    /**
     * Minute [0-59]
     */
    public int minute;

    /**
     * Hour of day [0-23]
     */
    public int hour;

    /**
     * Day of month [1-31]
     */
    public int monthDay;

    /**
     * Month [0-11]
     */
    public int month;

    /**
     * Year. For example, 1970.
     */
    public int year;

    /**
     * Day of week [0-6]
     */
    public int weekDay;

    /**
     * Day of year [0-365]
     */
    public int yearDay;

    /**
     * This time is in daylight savings time. One of:
     * <ul>
     * <li><b>positive</b> - in dst</li>
     * <li><b>0</b> - not in dst</li>
     * <li><b>negative</b> - unknown</li>
     * </ul>
     */
    public int isDst;

    /**
     * Offset in seconds from UTC including any DST offset.
     */
    public long gmtoff;

    /**
     * The timezone for this Time.  Should not be null.
     */
    public String timezone;

    /*
     * Define symbolic constants for accessing the fields in this class. Used in
     * getActualMaximum().
     */
    public static final int SECOND = 1;
    public static final int MINUTE = 2;
    public static final int HOUR = 3;
    public static final int MONTH_DAY = 4;
    public static final int MONTH = 5;
    public static final int YEAR = 6;
    public static final int WEEK_DAY = 7;
    public static final int YEAR_DAY = 8;
    public static final int WEEK_NUM = 9;

    public static final int SUNDAY = 0;
    public static final int MONDAY = 1;
    public static final int TUESDAY = 2;
    public static final int WEDNESDAY = 3;
    public static final int THURSDAY = 4;
    public static final int FRIDAY = 5;
    public static final int SATURDAY = 6;

    // An object that is reused for date calculations.
    private TimeCalculator calculator;

    /**
     * Construct a Time object in the timezone named by the string
     * argument "timezone". The time is initialized to Jan 1, 1970.
     * @param timezoneId string containing the timezone to use.
     * @see TimeZone
     */
    public Time(String timezoneId) {
        if (timezoneId == null) {
            throw new NullPointerException("timezoneId is null!");
        }
        initialize(timezoneId);
    }

    /**
     * Construct a Time object in the default timezone. The time is initialized to
     * Jan 1, 1970.
     */
    public Time() {
        initialize(TimeZone.getDefault().getID());
    }

    /**
     * A copy constructor.  Construct a Time object by copying the given
     * Time object.  No normalization occurs.
     *
     * @param other
     */
    public Time(Time other) {
        initialize(other.timezone);
        set(other);
    }

    /** Initialize the Time to 00:00:00 1/1/1970 in the specified timezone. */
    private void initialize(String timezoneId) {
        this.timezone = timezoneId;
        this.year = 1970;
        this.monthDay = 1;
        // Set the daylight-saving indicator to the unknown value -1 so that
        // it will be recomputed.
        this.isDst = -1;

        // A reusable object that performs the date/time calculations.
        calculator = new TimeCalculator(timezoneId);
    }

    /**
     * Ensures the values in each field are in range. For example if the
     * current value of this calendar is March 32, normalize() will convert it
     * to April 1. It also fills in weekDay, yearDay, isDst and gmtoff.
     *
     * <p>
     * If "ignoreDst" is true, then this method sets the "isDst" field to -1
     * (the "unknown" value) before normalizing.  It then computes the
     * time in milliseconds and sets the correct value for "isDst" if the
     * fields resolve to a valid date / time.
     *
     * <p>
     * See {@link #toMillis(boolean)} for more information about when to
     * use <tt>true</tt> or <tt>false</tt> for "ignoreDst" and when {@code -1}
     * might be returned.
     *
     * @return the UTC milliseconds since the epoch, or {@code -1}
     */
    public long normalize(boolean ignoreDst) {
        calculator.copyFieldsFromTime(this);
        long timeInMillis = calculator.toMillis(ignoreDst);
        calculator.copyFieldsToTime(this);
        return timeInMillis;
    }

    /**
     * Convert this time object so the time represented remains the same, but is
     * instead located in a different timezone. This method automatically calls
     * normalize() in some cases.
     *
     * <p>This method can return incorrect results if the date / time cannot be normalized.
     */
    public void switchTimezone(String timezone) {
        calculator.copyFieldsFromTime(this);
        calculator.switchTimeZone(timezone);
        calculator.copyFieldsToTime(this);
        this.timezone = timezone;
    }

    private static final int[] DAYS_PER_MONTH = { 31, 28, 31, 30, 31, 30, 31,
            31, 30, 31, 30, 31 };

    /**
     * Return the maximum possible value for the given field given the value of
     * the other fields. Requires that it be normalized for MONTH_DAY and
     * YEAR_DAY.
     * @param field one of the constants for HOUR, MINUTE, SECOND, etc.
     * @return the maximum value for the field.
     */
    public int getActualMaximum(int field) {
        switch (field) {
        case SECOND:
            return 59; // leap seconds, bah humbug
        case MINUTE:
            return 59;
        case HOUR:
            return 23;
        case MONTH_DAY: {
            int n = DAYS_PER_MONTH[this.month];
            if (n != 28) {
                return n;
            } else {
                int y = this.year;
                return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 29 : 28;
            }
        }
        case MONTH:
            return 11;
        case YEAR:
            return 2037;
        case WEEK_DAY:
            return 6;
        case YEAR_DAY: {
            int y = this.year;
            // Year days are numbered from 0, so the last one is usually 364.
            return ((y % 4) == 0 && ((y % 100) != 0 || (y % 400) == 0)) ? 365 : 364;
        }
        case WEEK_NUM:
            throw new RuntimeException("WEEK_NUM not implemented");
        default:
            throw new RuntimeException("bad field=" + field);
        }
    }

    /**
     * Clears all values, setting the timezone to the given timezone. Sets isDst
     * to a negative value to mean "unknown".
     * @param timezoneId the timezone to use.
     */
    public void clear(String timezoneId) {
        if (timezoneId == null) {
            throw new NullPointerException("timezone is null!");
        }
        this.timezone = timezoneId;
        this.allDay = false;
        this.second = 0;
        this.minute = 0;
        this.hour = 0;
        this.monthDay = 0;
        this.month = 0;
        this.year = 0;
        this.weekDay = 0;
        this.yearDay = 0;
        this.gmtoff = 0;
        this.isDst = -1;
    }

    /**
     * Compare two {@code Time} objects and return a negative number if {@code
     * a} is less than {@code b}, a positive number if {@code a} is greater than
     * {@code b}, or 0 if they are equal.
     *
     * <p>
     * This method can return an incorrect answer when the date / time fields of
     * either {@code Time} have been set to a local time that contradicts the
     * available timezone information.
     *
     * @param a first {@code Time} instance to compare
     * @param b second {@code Time} instance to compare
     * @throws NullPointerException if either argument is {@code null}
     * @throws IllegalArgumentException if {@link #allDay} is true but {@code
     *             hour}, {@code minute}, and {@code second} are not 0.
     * @return a negative result if {@code a} is earlier, a positive result if
     *         {@code b} is earlier, or 0 if they are equal.
     */
    public static int compare(Time a, Time b) {
        if (a == null) {
            throw new NullPointerException("a == null");
        } else if (b == null) {
            throw new NullPointerException("b == null");
        }
        a.calculator.copyFieldsFromTime(a);
        b.calculator.copyFieldsFromTime(b);

        return TimeCalculator.compare(a.calculator, b.calculator);
    }

    /**
     * Print the current value given the format string provided. See
     * strftime(3) manual page for what means what. The final string must be
     * less than 256 characters.
     * @param format a string containing the desired format.
     * @return a String containing the current time expressed in the current locale.
     */
    public String format(String format) {
        calculator.copyFieldsFromTime(this);
        return calculator.format(format);
    }

    /**
     * Return the current time in YYYYMMDDTHHMMSS&lt;tz&gt; format
     */
    @Override
    public String toString() {
        // toString() uses its own TimeCalculator rather than the shared one. Otherwise weird stuff
        // happens during debugging when the debugger calls toString().
        TimeCalculator calculator = new TimeCalculator(this.timezone);
        calculator.copyFieldsFromTime(this);
        return calculator.toStringInternal();
    }

    /**
     * Parses a date-time string in either the RFC 2445 format or an abbreviated
     * format that does not include the "time" field.  For example, all of the
     * following strings are valid:
     *
     * <ul>
     *   <li>"20081013T160000Z"</li>
     *   <li>"20081013T160000"</li>
     *   <li>"20081013"</li>
     * </ul>
     *
     * Returns whether or not the time is in UTC (ends with Z).  If the string
     * ends with "Z" then the timezone is set to UTC.  If the date-time string
     * included only a date and no time field, then the <code>allDay</code>
     * field of this Time class is set to true and the <code>hour</code>,
     * <code>minute</code>, and <code>second</code> fields are set to zero;
     * otherwise (a time field was included in the date-time string)
     * <code>allDay</code> is set to false. The fields <code>weekDay</code>,
     * <code>yearDay</code>, and <code>gmtoff</code> are always set to zero,
     * and the field <code>isDst</code> is set to -1 (unknown).  To set those
     * fields, call {@link #normalize(boolean)} after parsing.
     *
     * To parse a date-time string and convert it to UTC milliseconds, do
     * something like this:
     *
     * <pre>
     *   Time time = new Time();
     *   String date = "20081013T160000Z";
     *   time.parse(date);
     *   long millis = time.normalize(false);
     * </pre>
     *
     * @param s the string to parse
     * @return true if the resulting time value is in UTC time
     * @throws android.util.TimeFormatException if s cannot be parsed.
     */
    public boolean parse(String s) {
        if (s == null) {
            throw new NullPointerException("time string is null");
        }
        if (parseInternal(s)) {
            timezone = TIMEZONE_UTC;
            return true;
        }
        return false;
    }

    /**
     * Parse a time in the current zone in YYYYMMDDTHHMMSS format.
     */
    private boolean parseInternal(String s) {
        int len = s.length();
        if (len < 8) {
            throw new TimeFormatException("String is too short: \"" + s +
                    "\" Expected at least 8 characters.");
        }

        boolean inUtc = false;

        // year
        int n = getChar(s, 0, 1000);
        n += getChar(s, 1, 100);
        n += getChar(s, 2, 10);
        n += getChar(s, 3, 1);
        year = n;

        // month
        n = getChar(s, 4, 10);
        n += getChar(s, 5, 1);
        n--;
        month = n;

        // day of month
        n = getChar(s, 6, 10);
        n += getChar(s, 7, 1);
        monthDay = n;

        if (len > 8) {
            if (len < 15) {
                throw new TimeFormatException(
                        "String is too short: \"" + s
                                + "\" If there are more than 8 characters there must be at least"
                                + " 15.");
            }
            checkChar(s, 8, 'T');
            allDay = false;

            // hour
            n = getChar(s, 9, 10);
            n += getChar(s, 10, 1);
            hour = n;

            // min
            n = getChar(s, 11, 10);
            n += getChar(s, 12, 1);
            minute = n;

            // sec
            n = getChar(s, 13, 10);
            n += getChar(s, 14, 1);
            second = n;

            if (len > 15) {
                // Z
                checkChar(s, 15, 'Z');
                inUtc = true;
            }
        } else {
            allDay = true;
            hour = 0;
            minute = 0;
            second = 0;
        }

        weekDay = 0;
        yearDay = 0;
        isDst = -1;
        gmtoff = 0;
        return inUtc;
    }

    private void checkChar(String s, int spos, char expected) {
        char c = s.charAt(spos);
        if (c != expected) {
            throw new TimeFormatException(String.format(
                    "Unexpected character 0x%02d at pos=%d.  Expected 0x%02d (\'%c\').",
                    (int) c, spos, (int) expected, expected));
        }
    }

    private static int getChar(String s, int spos, int mul) {
        char c = s.charAt(spos);
        if (Character.isDigit(c)) {
            return Character.getNumericValue(c) * mul;
        } else {
            throw new TimeFormatException("Parse error at pos=" + spos);
        }
    }

    /**
     * Parse a time in RFC 3339 format.  This method also parses simple dates
     * (that is, strings that contain no time or time offset).  For example,
     * all of the following strings are valid:
     *
     * <ul>
     *   <li>"2008-10-13T16:00:00.000Z"</li>
     *   <li>"2008-10-13T16:00:00.000+07:00"</li>
     *   <li>"2008-10-13T16:00:00.000-07:00"</li>
     *   <li>"2008-10-13"</li>
     * </ul>
     *
     * <p>
     * If the string contains a time and time offset, then the time offset will
     * be used to convert the time value to UTC.
     * </p>
     *
     * <p>
     * If the given string contains just a date (with no time field), then
     * the {@link #allDay} field is set to true and the {@link #hour},
     * {@link #minute}, and  {@link #second} fields are set to zero.
     * </p>
     *
     * <p>
     * Returns true if the resulting time value is in UTC time.
     * </p>
     *
     * @param s the string to parse
     * @return true if the resulting time value is in UTC time
     * @throws android.util.TimeFormatException if s cannot be parsed.
     */
     public boolean parse3339(String s) {
         if (s == null) {
             throw new NullPointerException("time string is null");
         }
         if (parse3339Internal(s)) {
             timezone = TIMEZONE_UTC;
             return true;
         }
         return false;
     }

     private boolean parse3339Internal(String s) {
         int len = s.length();
         if (len < 10) {
             throw new TimeFormatException("String too short --- expected at least 10 characters.");
         }
         boolean inUtc = false;

         // year
         int n = getChar(s, 0, 1000);
         n += getChar(s, 1, 100);
         n += getChar(s, 2, 10);
         n += getChar(s, 3, 1);
         year = n;

         checkChar(s, 4, '-');

         // month
         n = getChar(s, 5, 10);
         n += getChar(s, 6, 1);
         --n;
         month = n;

         checkChar(s, 7, '-');

         // day
         n = getChar(s, 8, 10);
         n += getChar(s, 9, 1);
         monthDay = n;

         if (len >= 19) {
             // T
             checkChar(s, 10, 'T');
             allDay = false;

             // hour
             n = getChar(s, 11, 10);
             n += getChar(s, 12, 1);

             // Note that this.hour is not set here. It is set later.
             int hour = n;

             checkChar(s, 13, ':');

             // minute
             n = getChar(s, 14, 10);
             n += getChar(s, 15, 1);
             // Note that this.minute is not set here. It is set later.
             int minute = n;

             checkChar(s, 16, ':');

             // second
             n = getChar(s, 17, 10);
             n += getChar(s, 18, 1);
             second = n;

             // skip the '.XYZ' -- we don't care about subsecond precision.

             int tzIndex = 19;
             if (tzIndex < len && s.charAt(tzIndex) == '.') {
                 do {
                     tzIndex++;
                 } while (tzIndex < len && Character.isDigit(s.charAt(tzIndex)));
             }

             int offset = 0;
             if (len > tzIndex) {
                 char c = s.charAt(tzIndex);
                 // NOTE: the offset is meant to be subtracted to get from local time
                 // to UTC.  we therefore use 1 for '-' and -1 for '+'.
                 switch (c) {
                     case 'Z':
                         // Zulu time -- UTC
                         offset = 0;
                         break;
                     case '-':
                         offset = 1;
                         break;
                     case '+':
                         offset = -1;
                         break;
                     default:
                         throw new TimeFormatException(String.format(
                                 "Unexpected character 0x%02d at position %d.  Expected + or -",
                                 (int) c, tzIndex));
                 }
                 inUtc = true;

                 if (offset != 0) {
                     if (len < tzIndex + 6) {
                         throw new TimeFormatException(
                                 String.format("Unexpected length; should be %d characters",
                                         tzIndex + 6));
                     }

                     // hour
                     n = getChar(s, tzIndex + 1, 10);
                     n += getChar(s, tzIndex + 2, 1);
                     n *= offset;
                     hour += n;

                     // minute
                     n = getChar(s, tzIndex + 4, 10);
                     n += getChar(s, tzIndex + 5, 1);
                     n *= offset;
                     minute += n;
                 }
             }
             this.hour = hour;
             this.minute = minute;

             if (offset != 0) {
                 normalize(false);
             }
         } else {
             allDay = true;
             this.hour = 0;
             this.minute = 0;
             this.second = 0;
         }

         this.weekDay = 0;
         this.yearDay = 0;
         this.isDst = -1;
         this.gmtoff = 0;
         return inUtc;
     }

    /**
     * Returns the timezone string that is currently set for the device.
     */
    public static String getCurrentTimezone() {
        return TimeZone.getDefault().getID();
    }

    /**
     * Sets the time of the given Time object to the current time.
     */
    public void setToNow() {
        set(System.currentTimeMillis());
    }

    /**
     * Converts this time to milliseconds. Suitable for interacting with the
     * standard java libraries. The time is in UTC milliseconds since the epoch.
     * This does an implicit normalization to compute the milliseconds but does
     * <em>not</em> change any of the fields in this Time object.  If you want
     * to normalize the fields in this Time object and also get the milliseconds
     * then use {@link #normalize(boolean)}.
     *
     * <p>
     * If "ignoreDst" is false, then this method uses the current setting of the
     * "isDst" field and will adjust the returned time if the "isDst" field is
     * wrong for the given time.  See the sample code below for an example of
     * this.
     *
     * <p>
     * If "ignoreDst" is true, then this method ignores the current setting of
     * the "isDst" field in this Time object and will instead figure out the
     * correct value of "isDst" (as best it can) from the fields in this
     * Time object.  The only case where this method cannot figure out the
     * correct value of the "isDst" field is when the time is inherently
     * ambiguous because it falls in the hour that is repeated when switching
     * from Daylight-Saving Time to Standard Time.
     *
     * <p>
     * Here is an example where <tt>toMillis(true)</tt> adjusts the time,
     * assuming that DST changes at 2am on Sunday, Nov 4, 2007.
     *
     * <pre>
     * Time time = new Time();
     * time.set(4, 10, 2007);  // set the date to Nov 4, 2007, 12am
     * time.normalize(false);       // this sets isDst = 1
     * time.monthDay += 1;     // changes the date to Nov 5, 2007, 12am
     * millis = time.toMillis(false);   // millis is Nov 4, 2007, 11pm
     * millis = time.toMillis(true);    // millis is Nov 5, 2007, 12am
     * </pre>
     *
     * <p>
     * To avoid this problem, use <tt>toMillis(true)</tt>
     * after adding or subtracting days or explicitly setting the "monthDay"
     * field.  On the other hand, if you are adding
     * or subtracting hours or minutes, then you should use
     * <tt>toMillis(false)</tt>.
     *
     * <p>
     * You should also use <tt>toMillis(false)</tt> if you want
     * to read back the same milliseconds that you set with {@link #set(long)}
     * or {@link #set(Time)} or after parsing a date string.
     *
     * <p>
     * This method can return {@code -1} when the date / time fields have been
     * set to a local time that conflicts with available timezone information.
     * For example, when daylight savings transitions cause an hour to be
     * skipped: times within that hour will return {@code -1} if isDst =
     * {@code -1}.
     */
    public long toMillis(boolean ignoreDst) {
        calculator.copyFieldsFromTime(this);
        return calculator.toMillis(ignoreDst);
    }

    /**
     * Sets the fields in this Time object given the UTC milliseconds.  After
     * this method returns, all the fields are normalized.
     * This also sets the "isDst" field to the correct value.
     *
     * @param millis the time in UTC milliseconds since the epoch.
     */
    public void set(long millis) {
        allDay = false;
        calculator.timezone = timezone;
        calculator.setTimeInMillis(millis);
        calculator.copyFieldsToTime(this);
    }

    /**
     * Format according to RFC 2445 DATE-TIME type.
     *
     * <p>The same as format("%Y%m%dT%H%M%S"), or format("%Y%m%dT%H%M%SZ") for a Time with a
     * timezone set to "UTC".
     */
    public String format2445() {
        calculator.copyFieldsFromTime(this);
        return calculator.format2445(!allDay);
    }

    /**
     * Copy the value of that to this Time object. No normalization happens.
     */
    public void set(Time that) {
        this.timezone = that.timezone;
        this.allDay = that.allDay;
        this.second = that.second;
        this.minute = that.minute;
        this.hour = that.hour;
        this.monthDay = that.monthDay;
        this.month = that.month;
        this.year = that.year;
        this.weekDay = that.weekDay;
        this.yearDay = that.yearDay;
        this.isDst = that.isDst;
        this.gmtoff = that.gmtoff;
    }

    /**
     * Sets the fields. Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
     * Call {@link #normalize(boolean)} if you need those.
     */
    public void set(int second, int minute, int hour, int monthDay, int month, int year) {
        this.allDay = false;
        this.second = second;
        this.minute = minute;
        this.hour = hour;
        this.monthDay = monthDay;
        this.month = month;
        this.year = year;
        this.weekDay = 0;
        this.yearDay = 0;
        this.isDst = -1;
        this.gmtoff = 0;
    }

    /**
     * Sets the date from the given fields.  Also sets allDay to true.
     * Sets weekDay, yearDay and gmtoff to 0, and isDst to -1.
     * Call {@link #normalize(boolean)} if you need those.
     *
     * @param monthDay the day of the month (in the range [1,31])
     * @param month the zero-based month number (in the range [0,11])
     * @param year the year
     */
    public void set(int monthDay, int month, int year) {
        this.allDay = true;
        this.second = 0;
        this.minute = 0;
        this.hour = 0;
        this.monthDay = monthDay;
        this.month = month;
        this.year = year;
        this.weekDay = 0;
        this.yearDay = 0;
        this.isDst = -1;
        this.gmtoff = 0;
    }

    /**
     * Returns true if the time represented by this Time object occurs before
     * the given time.
     *
     * <p>
     * Equivalent to {@code Time.compare(this, that) < 0}. See
     * {@link #compare(Time, Time)} for details.
     *
     * @param that a given Time object to compare against
     * @return true if this time is less than the given time
     */
    public boolean before(Time that) {
        return Time.compare(this, that) < 0;
    }


    /**
     * Returns true if the time represented by this Time object occurs after
     * the given time.
     *
     * <p>
     * Equivalent to {@code Time.compare(this, that) > 0}. See
     * {@link #compare(Time, Time)} for details.
     *
     * @param that a given Time object to compare against
     * @return true if this time is greater than the given time
     */
    public boolean after(Time that) {
        return Time.compare(this, that) > 0;
    }

    /**
     * This array is indexed by the weekDay field (SUNDAY=0, MONDAY=1, etc.)
     * and gives a number that can be added to the yearDay to give the
     * closest Thursday yearDay.
     */
    private static final int[] sThursdayOffset = { -3, 3, 2, 1, 0, -1, -2 };

    /**
     * Computes the week number according to ISO 8601.  The current Time
     * object must already be normalized because this method uses the
     * yearDay and weekDay fields.
     *
     * <p>
     * In IS0 8601, weeks start on Monday.
     * The first week of the year (week 1) is defined by ISO 8601 as the
     * first week with four or more of its days in the starting year.
     * Or equivalently, the week containing January 4.  Or equivalently,
     * the week with the year's first Thursday in it.
     * </p>
     *
     * <p>
     * The week number can be calculated by counting Thursdays.  Week N
     * contains the Nth Thursday of the year.
     * </p>
     *
     * @return the ISO week number.
     */
    public int getWeekNumber() {
        // Get the year day for the closest Thursday
        int closestThursday = yearDay + sThursdayOffset[weekDay];

        // Year days start at 0
        if (closestThursday >= 0 && closestThursday <= 364) {
            return closestThursday / 7 + 1;
        }

        // The week crosses a year boundary.
        Time temp = new Time(this);
        temp.monthDay += sThursdayOffset[weekDay];
        temp.normalize(true /* ignore isDst */);
        return temp.yearDay / 7 + 1;
    }

    /**
     * Return a string in the RFC 3339 format.
     * <p>
     * If allDay is true, expresses the time as Y-M-D</p>
     * <p>
     * Otherwise, if the timezone is UTC, expresses the time as Y-M-D-T-H-M-S UTC</p>
     * <p>
     * Otherwise the time is expressed the time as Y-M-D-T-H-M-S +- GMT</p>
     * @return string in the RFC 3339 format.
     */
    public String format3339(boolean allDay) {
        if (allDay) {
            return format(Y_M_D);
        } else if (TIMEZONE_UTC.equals(timezone)) {
            return format(Y_M_D_T_H_M_S_000_Z);
        } else {
            String base = format(Y_M_D_T_H_M_S_000);
            String sign = (gmtoff < 0) ? "-" : "+";
            int offset = (int) Math.abs(gmtoff);
            int minutes = (offset % 3600) / 60;
            int hours = offset / 3600;

            return String.format(Locale.US, "%s%s%02d:%02d", base, sign, hours, minutes);
        }
    }

    /**
     * Returns true if the instant of the supplied time would be for the
     * Gregorian calendar date January 1, 1970 <em>for a user observing UTC
     * </em>, i.e. the timezone of the time object is ignored.
     * <p>
     * See {@link #getJulianDay(long, long)} for how to determine the Julian day
     * for the timezone of the time object.
     * <p>
     * This method can return an incorrect answer when the date / time fields have
     * been set to a local time that contradicts the available timezone information.
     *
     * @param time the time to test
     * @return true if epoch.
     */
    public static boolean isEpoch(Time time) {
        long millis = time.toMillis(true);
        return getJulianDay(millis, 0 /* UTC offset */) == EPOCH_JULIAN_DAY;
    }

    /**
     * Computes the Julian day number for a point in time in a particular
     * timezone. The Julian day for a given calendar date is the same for
     * every timezone. For example, the Julian day for July 1, 2008 is
     * 2454649.
     *
     * <p>Callers must pass the time in UTC millisecond (as can be returned
     * by {@link #toMillis(boolean)} or {@link #normalize(boolean)})
     * and the offset from UTC of the timezone in seconds at that time (as
     * might be in {@link #gmtoff}).
     *
     * <p>The Julian day is useful for testing if two events occur on the
     * same calendar date and for determining the relative time of an event
     * from the present ("yesterday", "3 days ago", etc.).
     *
     * @param millis the time in UTC milliseconds
     * @param gmtoffSeconds the offset from UTC in seconds
     * @return the Julian day
     * @deprecated Use {@link java.time.temporal.JulianFields#JULIAN_DAY} instead.
     */
    @Deprecated
    public static int getJulianDay(long millis, long gmtoffSeconds) {
        long offsetMillis = gmtoffSeconds * 1000;
        long adjustedMillis = millis + offsetMillis;
        long julianDay = adjustedMillis / DateUtils.DAY_IN_MILLIS;
        // Negative adjustedMillis values must round towards Integer.MIN_VALUE.
        if (adjustedMillis < 0 && adjustedMillis % DateUtils.DAY_IN_MILLIS != 0) {
            julianDay--;
        }
        return (int) (julianDay + EPOCH_JULIAN_DAY);
    }

    /**
     * <p>Sets the time from the given Julian day number, which must be based on
     * the same timezone that is set in this Time object.  The "gmtoff" field
     * need not be initialized because the given Julian day may have a different
     * GMT offset than whatever is currently stored in this Time object anyway.
     * After this method returns all the fields will be normalized and the time
     * will be set to 12am at the beginning of the given Julian day.
     * </p>
     *
     * <p>
     * The only exception to this is if 12am does not exist for that day because
     * of daylight saving time.  For example, Cairo, Eqypt moves time ahead one
     * hour at 12am on April 25, 2008 and there are a few other places that
     * also change daylight saving time at 12am.  In those cases, the time
     * will be set to 1am.
     * </p>
     *
     * @param julianDay the Julian day in the timezone for this Time object
     * @return the UTC milliseconds for the beginning of the Julian day
     */
    public long setJulianDay(int julianDay) {
        // Don't bother with the GMT offset since we don't know the correct
        // value for the given Julian day.  Just get close and then adjust
        // the day.
        long millis = (julianDay - EPOCH_JULIAN_DAY) * DateUtils.DAY_IN_MILLIS;
        set(millis);

        // Figure out how close we are to the requested Julian day.
        // We can't be off by more than a day.
        int approximateDay = getJulianDay(millis, gmtoff);
        int diff = julianDay - approximateDay;
        monthDay += diff;

        // Set the time to 12am and re-normalize.
        hour = 0;
        minute = 0;
        second = 0;
        millis = normalize(true);
        return millis;
    }

    /**
     * Returns the week since {@link #EPOCH_JULIAN_DAY} (Jan 1, 1970) adjusted
     * for first day of week. This takes a julian day and the week start day and
     * calculates which week since {@link #EPOCH_JULIAN_DAY} that day occurs in,
     * starting at 0. *Do not* use this to compute the ISO week number for the
     * year.
     *
     * @param julianDay The julian day to calculate the week number for
     * @param firstDayOfWeek Which week day is the first day of the week, see
     *            {@link #SUNDAY}
     * @return Weeks since the epoch
     */
    public static int getWeeksSinceEpochFromJulianDay(int julianDay, int firstDayOfWeek) {
        int diff = THURSDAY - firstDayOfWeek;
        if (diff < 0) {
            diff += 7;
        }
        int refDay = EPOCH_JULIAN_DAY - diff;
        return (julianDay - refDay) / 7;
    }

    /**
     * Takes a number of weeks since the epoch and calculates the Julian day of
     * the Monday for that week. This assumes that the week containing the
     * {@link #EPOCH_JULIAN_DAY} is considered week 0. It returns the Julian day
     * for the Monday week weeks after the Monday of the week containing the
     * epoch.
     *
     * @param week Number of weeks since the epoch
     * @return The julian day for the Monday of the given week since the epoch
     */
    public static int getJulianMondayFromWeeksSinceEpoch(int week) {
        return MONDAY_BEFORE_JULIAN_EPOCH + week * 7;
    }

    /**
     * A class that handles date/time calculations.
     *
     * This class originated as a port of a native C++ class ("android.Time") to pure Java. It is
     * separate from the enclosing class because some methods copy the result of calculations back
     * to the enclosing object, but others do not: thus separate state is retained.
     */
    private static class TimeCalculator {
        public final WallTime wallTime;
        public String timezone;

        // Information about the current timezone.
        private ZoneInfoData mZoneInfoData;

        public TimeCalculator(String timezoneId) {
            this.mZoneInfoData = lookupZoneInfoData(timezoneId);
            this.wallTime = new WallTime();
        }

        public long toMillis(boolean ignoreDst) {
            if (ignoreDst) {
                wallTime.setIsDst(-1);
            }

            int r = wallTime.mktime(mZoneInfoData);
            if (r == -1) {
                return -1;
            }
            return r * 1000L;
        }

        public void setTimeInMillis(long millis) {
            // Preserve old 32-bit Android behavior.
            int intSeconds = (int) (millis / 1000);

            updateZoneInfoFromTimeZone();
            wallTime.localtime(intSeconds, mZoneInfoData);
        }

        public String format(String format) {
            if (format == null) {
                format = "%c";
            }
            TimeFormatter formatter = new TimeFormatter();
            return formatter.format(format, wallTime, mZoneInfoData);
        }

        private void updateZoneInfoFromTimeZone() {
            if (!mZoneInfoData.getID().equals(timezone)) {
                this.mZoneInfoData = lookupZoneInfoData(timezone);
            }
        }

        private static ZoneInfoData lookupZoneInfoData(String timezoneId) {
            ZoneInfoData zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData(timezoneId);
            if (zoneInfoData == null) {
                zoneInfoData = ZoneInfoDb.getInstance().makeZoneInfoData("GMT");
            }
            if (zoneInfoData == null) {
                throw new AssertionError("GMT not found: \"" + timezoneId + "\"");
            }
            return zoneInfoData;
        }

        public void switchTimeZone(String timezone) {
            int seconds = wallTime.mktime(mZoneInfoData);
            this.timezone = timezone;
            updateZoneInfoFromTimeZone();
            wallTime.localtime(seconds, mZoneInfoData);
        }

        public String format2445(boolean hasTime) {
            char[] buf = new char[hasTime ? 16 : 8];
            int n = wallTime.getYear();

            buf[0] = toChar(n / 1000);
            n %= 1000;
            buf[1] = toChar(n / 100);
            n %= 100;
            buf[2] = toChar(n / 10);
            n %= 10;
            buf[3] = toChar(n);

            n = wallTime.getMonth() + 1;
            buf[4] = toChar(n / 10);
            buf[5] = toChar(n % 10);

            n = wallTime.getMonthDay();
            buf[6] = toChar(n / 10);
            buf[7] = toChar(n % 10);

            if (!hasTime) {
                return new String(buf, 0, 8);
            }

            buf[8] = 'T';

            n = wallTime.getHour();
            buf[9] = toChar(n / 10);
            buf[10] = toChar(n % 10);

            n = wallTime.getMinute();
            buf[11] = toChar(n / 10);
            buf[12] = toChar(n % 10);

            n = wallTime.getSecond();
            buf[13] = toChar(n / 10);
            buf[14] = toChar(n % 10);

            if (TIMEZONE_UTC.equals(timezone)) {
                // The letter 'Z' is appended to the end.
                buf[15] = 'Z';
                return new String(buf, 0, 16);
            } else {
                return new String(buf, 0, 15);
            }
        }

        private char toChar(int n) {
            return (n >= 0 && n <= 9) ? (char) (n + '0') : ' ';
        }

        /**
         * A method that will return the state of this object in string form. Note: it has side
         * effects and so has deliberately not been made the default {@link #toString()}.
         */
        public String toStringInternal() {
            // This implementation possibly displays the un-normalized fields because that is
            // what it has always done.
            return String.format("%04d%02d%02dT%02d%02d%02d%s(%d,%d,%d,%d,%d)",
                    wallTime.getYear(),
                    wallTime.getMonth() + 1,
                    wallTime.getMonthDay(),
                    wallTime.getHour(),
                    wallTime.getMinute(),
                    wallTime.getSecond(),
                    timezone,
                    wallTime.getWeekDay(),
                    wallTime.getYearDay(),
                    wallTime.getGmtOffset(),
                    wallTime.getIsDst(),
                    toMillis(false /* use isDst */) / 1000
            );

        }

        public static int compare(TimeCalculator aObject, TimeCalculator bObject) {
            if (aObject.timezone.equals(bObject.timezone)) {
                // If the timezones are the same, we can easily compare the two times.
                int diff = aObject.wallTime.getYear() - bObject.wallTime.getYear();
                if (diff != 0) {
                    return diff;
                }

                diff = aObject.wallTime.getMonth() - bObject.wallTime.getMonth();
                if (diff != 0) {
                    return diff;
                }

                diff = aObject.wallTime.getMonthDay() - bObject.wallTime.getMonthDay();
                if (diff != 0) {
                    return diff;
                }

                diff = aObject.wallTime.getHour() - bObject.wallTime.getHour();
                if (diff != 0) {
                    return diff;
                }

                diff = aObject.wallTime.getMinute() - bObject.wallTime.getMinute();
                if (diff != 0) {
                    return diff;
                }

                diff = aObject.wallTime.getSecond() - bObject.wallTime.getSecond();
                if (diff != 0) {
                    return diff;
                }

                return 0;
            } else {
                // Otherwise, convert to milliseconds and compare that. This requires that object be
                // normalized. Note: For dates that do not exist: toMillis() can return -1, which
                // can be confused with a valid time.
                long am = aObject.toMillis(false /* use isDst */);
                long bm = bObject.toMillis(false /* use isDst */);
                long diff = am - bm;
                return (diff < 0) ? -1 : ((diff > 0) ? 1 : 0);
            }

        }

        public void copyFieldsToTime(Time time) {
            time.second = wallTime.getSecond();
            time.minute = wallTime.getMinute();
            time.hour = wallTime.getHour();
            time.monthDay = wallTime.getMonthDay();
            time.month = wallTime.getMonth();
            time.year = wallTime.getYear();

            // Read-only fields that are derived from other information above.
            time.weekDay = wallTime.getWeekDay();
            time.yearDay = wallTime.getYearDay();

            // < 0: DST status unknown, 0: is not in DST, 1: is in DST
            time.isDst = wallTime.getIsDst();
            // This is in seconds and includes any DST offset too.
            time.gmtoff = wallTime.getGmtOffset();
        }

        public void copyFieldsFromTime(Time time) {
            wallTime.setSecond(time.second);
            wallTime.setMinute(time.minute);
            wallTime.setHour(time.hour);
            wallTime.setMonthDay(time.monthDay);
            wallTime.setMonth(time.month);
            wallTime.setYear(time.year);
            wallTime.setWeekDay(time.weekDay);
            wallTime.setYearDay(time.yearDay);
            wallTime.setIsDst(time.isDst);
            wallTime.setGmtOffset((int) time.gmtoff);

            if (time.allDay && (time.second != 0 || time.minute != 0 || time.hour != 0)) {
                throw new IllegalArgumentException("allDay is true but sec, min, hour are not 0.");
            }

            timezone = time.timezone;
            updateZoneInfoFromTimeZone();
        }
    }
}
