/*
 *  Licensed to the Apache Software Foundation (ASF) under one or more
 *  contributor license agreements.  See the NOTICE file distributed with
 *  this work for additional information regarding copyright ownership.
 *  The ASF licenses this file to You 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.
 */
/**
*******************************************************************************
* Copyright (C) 1996-2008, International Business Machines Corporation and    *
* others. All Rights Reserved.                                                *
*******************************************************************************
*/

package java.util;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamField;

import org.apache.harmony.luni.util.Msg;

// BEGIN android-note
// The class javadoc and some of the method descriptions are copied from ICU4J
// source files. Changes have been made to the copied descriptions.
// The icu license header was added to this file. 
// END android-note
/**
 * {@code SimpleTimeZone} is a concrete subclass of {@code TimeZone}
 * that represents a time zone for use with a Gregorian calendar. This class
 * does not handle historical changes.
 * <P>
 * Use a negative value for {@code dayOfWeekInMonth} to indicate that
 * {@code SimpleTimeZone} should count from the end of the month
 * backwards. For example, Daylight Savings Time ends at the last
 * (dayOfWeekInMonth = -1) Sunday in October, at 2 AM in standard time.
 * 
 * @see Calendar
 * @see GregorianCalendar
 * @see TimeZone
 * @since Android 1.0
 */
public class SimpleTimeZone extends TimeZone {
    
    private static final long serialVersionUID = -403250971215465050L;

    private int rawOffset;

    private int startYear, startMonth, startDay, startDayOfWeek, startTime;

    private int endMonth, endDay, endDayOfWeek, endTime;

    private int startMode, endMode;

    private static final int DOM_MODE = 1, DOW_IN_MONTH_MODE = 2,
            DOW_GE_DOM_MODE = 3, DOW_LE_DOM_MODE = 4;

    /**
     * The constant for representing a start or end time in GMT time mode.
     * 
     * @since Android 1.0
     */
    public static final int UTC_TIME = 2;

    /**
     * The constant for representing a start or end time in standard local time mode,
     * based on timezone's raw offset from GMT; does not include Daylight
     * savings.
     * 
     * @since Android 1.0
     */
    public static final int STANDARD_TIME = 1;

    /**
     * The constant for representing a start or end time in local wall clock time
     * mode, based on timezone's adjusted offset from GMT; includes
     * Daylight savings.
     * 
     * @since Android 1.0
     */
    public static final int WALL_TIME = 0;

    private boolean useDaylight;

    private GregorianCalendar daylightSavings;

    private int dstSavings = 3600000;

    /**
     * Constructs a {@code SimpleTimeZone} with the given base time zone offset from GMT
     * and time zone ID. Timezone IDs can be obtained from
     * {@code TimeZone.getAvailableIDs}. Normally you should use {@code TimeZone.getDefault} to
     * construct a {@code TimeZone}.
     * 
     * @param offset
     *            the given base time zone offset to GMT.
     * @param name
     *            the time zone ID which is obtained from
     *            {@code TimeZone.getAvailableIDs}.
     * @since Android 1.0
     */
    public SimpleTimeZone(int offset, String name) {
        setID(name);
        rawOffset = offset;
    }

    /**
     * Constructs a {@code SimpleTimeZone} with the given base time zone offset from GMT,
     * time zone ID, and times to start and end the daylight savings time. Timezone IDs can
     * be obtained from {@code TimeZone.getAvailableIDs}. Normally you should use
     * {@code TimeZone.getDefault} to create a {@code TimeZone}. For a time zone that does not
     * use daylight saving time, do not use this constructor; instead you should
     * use {@code SimpleTimeZone(rawOffset, ID)}.
     * <p>
     * By default, this constructor specifies day-of-week-in-month rules. That
     * is, if the {@code startDay} is 1, and the {@code startDayOfWeek} is {@code SUNDAY}, then this
     * indicates the first Sunday in the {@code startMonth}. A {@code startDay} of -1 likewise
     * indicates the last Sunday. However, by using negative or zero values for
     * certain parameters, other types of rules can be specified.
     * <p>
     * Day of month: To specify an exact day of the month, such as March 1, set
     * {@code startDayOfWeek} to zero.
     * <p>
     * Day of week after day of month: To specify the first day of the week
     * occurring on or after an exact day of the month, make the day of the week
     * negative. For example, if {@code startDay} is 5 and {@code startDayOfWeek} is {@code -MONDAY},
     * this indicates the first Monday on or after the 5th day of the
     * {@code startMonth}.
     * <p>
     * Day of week before day of month: To specify the last day of the week
     * occurring on or before an exact day of the month, make the day of the
     * week and the day of the month negative. For example, if {@code startDay} is {@code -21}
     * and {@code startDayOfWeek} is {@code -WEDNESDAY}, this indicates the last Wednesday on or
     * before the 21st of the {@code startMonth}.
     * <p>
     * The above examples refer to the {@code startMonth}, {@code startDay}, and {@code startDayOfWeek};
     * the same applies for the {@code endMonth}, {@code endDay}, and {@code endDayOfWeek}.
     * <p>
     * The daylight savings time difference is set to the default value: one hour.          
     * 
     * @param offset
     *            the given base time zone offset to GMT.
     * @param name
     *            the time zone ID which is obtained from
     *            {@code TimeZone.getAvailableIDs}.
     * @param startMonth
     *            the daylight savings starting month. The month indexing is 0-based. eg, 0
     *            for January.
     * @param startDay
     *            the daylight savings starting day-of-week-in-month. Please see
     *            the member description for an example.
     * @param startDayOfWeek
     *            the daylight savings starting day-of-week. Please see the
     *            member description for an example.
     * @param startTime
     *            the daylight savings starting time in local wall time, which
     *            is standard time in this case. Please see the member
     *            description for an example.
     * @param endMonth
     *            the daylight savings ending month. The month indexing is 0-based. eg, 0 for
     *            January.
     * @param endDay
     *            the daylight savings ending day-of-week-in-month. Please see
     *            the member description for an example.
     * @param endDayOfWeek
     *            the daylight savings ending day-of-week. Please see the member
     *            description for an example.
     * @param endTime
     *            the daylight savings ending time in local wall time, which is
     *            daylight time in this case. Please see the member description
     *            for an example.
     * @throws IllegalArgumentException
     *             if the month, day, dayOfWeek, or time parameters are out of
     *             range for the start or end rule.
     * @since Android 1.0
     */
    public SimpleTimeZone(int offset, String name, int startMonth,
            int startDay, int startDayOfWeek, int startTime, int endMonth,
            int endDay, int endDayOfWeek, int endTime) {
        this(offset, name, startMonth, startDay, startDayOfWeek, startTime,
                endMonth, endDay, endDayOfWeek, endTime, 3600000);
    }

    /**
     * Constructs a {@code SimpleTimeZone} with the given base time zone offset from GMT,
     * time zone ID, times to start and end the daylight savings time, and 
     * the daylight savings time difference in milliseconds
     * 
     * @param offset
     *            the given base time zone offset to GMT.
     * @param name
     *            the time zone ID which is obtained from
     *            {@code TimeZone.getAvailableIDs}.
     * @param startMonth
     *            the daylight savings starting month. Month is 0-based. eg, 0
     *            for January.
     * @param startDay
     *            the daylight savings starting day-of-week-in-month. Please see
     *            the description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param startDayOfWeek
     *            the daylight savings starting day-of-week. Please see the
     *            description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param startTime
     *            The daylight savings starting time in local wall time, which
     *            is standard time in this case. Please see the description of 
     *            {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param endMonth
     *            the daylight savings ending month. Month is 0-based. eg, 0 for
     *            January.
     * @param endDay
     *            the daylight savings ending day-of-week-in-month. Please see
     *            the description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param endDayOfWeek
     *            the daylight savings ending day-of-week. Please see the description of 
     *            {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param endTime
     *            the daylight savings ending time in local wall time, which is
     *            daylight time in this case. Please see the description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)}
     *            for an example.
     * @param daylightSavings
     *            the daylight savings time difference in milliseconds.
     * @exception IllegalArgumentException
     *                the month, day, dayOfWeek, or time parameters are out of
     *                range for the start or end rule.
     * @since Android 1.0
     */
    public SimpleTimeZone(int offset, String name, int startMonth,
            int startDay, int startDayOfWeek, int startTime, int endMonth,
            int endDay, int endDayOfWeek, int endTime, int daylightSavings) {
        this(offset, name);
        if (daylightSavings <= 0) {
            throw new IllegalArgumentException(Msg.getString("K00e9", daylightSavings)); //$NON-NLS-1$
        }
        dstSavings = daylightSavings;

        setStartRule(startMonth, startDay, startDayOfWeek, startTime);
        setEndRule(endMonth, endDay, endDayOfWeek, endTime);
    }

    /**
     * Construct a {@code SimpleTimeZone} with the given base time zone offset from GMT,
     * time zone ID, times to start and end the daylight savings time including a 
     * mode specifier, the daylight savings time difference in milliseconds.
     * The mode specifies either {@link #WALL_TIME}, {@link #STANDARD_TIME}, or
     * {@link #UTC_TIME}.
     * 
     * @param offset
     *            the given base time zone offset to GMT.
     * @param name
     *            the time zone ID which is obtained from
     *            {@code TimeZone.getAvailableIDs}.
     * @param startMonth
     *            the daylight savings starting month. The month indexing is 0-based. eg, 0
     *            for January.
     * @param startDay
     *            the daylight savings starting day-of-week-in-month. Please see
     *            the description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param startDayOfWeek
     *            the daylight savings starting day-of-week. Please see the
     *            description of {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param startTime
     *            the time of day in milliseconds on which daylight savings 
     *            time starts, based on the {@code startTimeMode}.
     * @param startTimeMode
     *            the mode (UTC, standard, or wall time) of the start time
     *            value.
     * @param endDay 
     *            the day of the week on which daylight savings time ends. 
     * @param endMonth
     *            the daylight savings ending month. The month indexing is 0-based. eg, 0 for
     *            January.
     * @param endDayOfWeek
     *            the daylight savings ending day-of-week. Please see the description of 
     *            {@link #SimpleTimeZone(int, String, int, int, int, int, int, int, int, int)} for an example.
     * @param endTime
     *            the time of day in milliseconds on which daylight savings 
     *            time ends, based on the {@code endTimeMode}.
     * @param endTimeMode
     *            the mode (UTC, standard, or wall time) of the end time value.
     * @param daylightSavings
     *            the daylight savings time difference in milliseconds.
     * @throws IllegalArgumentException
     *             if the month, day, dayOfWeek, or time parameters are out of
     *             range for the start or end rule.
     * @since Android 1.0
     */
    public SimpleTimeZone(int offset, String name, int startMonth,
            int startDay, int startDayOfWeek, int startTime, int startTimeMode,
            int endMonth, int endDay, int endDayOfWeek, int endTime,
            int endTimeMode, int daylightSavings) {

        this(offset, name, startMonth, startDay, startDayOfWeek, startTime,
                endMonth, endDay, endDayOfWeek, endTime, daylightSavings);
        startMode = startTimeMode;
        endMode = endTimeMode;
    }

    /**
     * Returns a new {@code SimpleTimeZone} with the same ID, {@code rawOffset} and daylight
     * savings time rules as this SimpleTimeZone.
     * 
     * @return a shallow copy of this {@code SimpleTimeZone}.
     * @see java.lang.Cloneable
     * @since Android 1.0
     */
    @Override
    public Object clone() {
        SimpleTimeZone zone = (SimpleTimeZone) super.clone();
        if (daylightSavings != null) {
            zone.daylightSavings = (GregorianCalendar) daylightSavings.clone();
        }
        return zone;
    }

    /**
     * Compares the specified object to this {@code SimpleTimeZone} and returns whether they
     * are equal. The object must be an instance of {@code SimpleTimeZone} and have the
     * same internal data.
     * 
     * @param object
     *            the object to compare with this object.
     * @return {@code true} if the specified object is equal to this
     *         {@code SimpleTimeZone}, {@code false} otherwise.
     * @see #hashCode
     * @since Android 1.0
     */
    @Override
    public boolean equals(Object object) {
        if (!(object instanceof SimpleTimeZone)) {
            return false;
        }
        SimpleTimeZone tz = (SimpleTimeZone) object;
        return getID().equals(tz.getID())
                && rawOffset == tz.rawOffset
                && useDaylight == tz.useDaylight
                && (!useDaylight || (startYear == tz.startYear
                        && startMonth == tz.startMonth
                        && startDay == tz.startDay && startMode == tz.startMode
                        && startDayOfWeek == tz.startDayOfWeek
                        && startTime == tz.startTime && endMonth == tz.endMonth
                        && endDay == tz.endDay
                        && endDayOfWeek == tz.endDayOfWeek
                        && endTime == tz.endTime && endMode == tz.endMode && dstSavings == tz.dstSavings));
    }

    @Override
    public int getDSTSavings() {
        if (!useDaylight) {
            return 0;
        }
        return dstSavings;
    }

    @Override
    public int getOffset(int era, int year, int month, int day, int dayOfWeek,
            int time) {
        if (era != GregorianCalendar.BC && era != GregorianCalendar.AD) {
            throw new IllegalArgumentException(Msg.getString("K00ea", era)); //$NON-NLS-1$
        }
        checkRange(month, dayOfWeek, time);
        if (month != Calendar.FEBRUARY || day != 29 || !isLeapYear(year)) {
            checkDay(month, day);
        }

        if (!useDaylightTime() || era != GregorianCalendar.AD
                || year < startYear) {
            return rawOffset;
        }
        if (endMonth < startMonth) {
            if (month > endMonth && month < startMonth) {
                return rawOffset;
            }
        } else {
            if (month < startMonth || month > endMonth) {
                return rawOffset;
            }
        }

        int ruleDay = 0, daysInMonth, firstDayOfMonth = mod7(dayOfWeek - day);
        if (month == startMonth) {
            switch (startMode) {
            case DOM_MODE:
                ruleDay = startDay;
                break;
            case DOW_IN_MONTH_MODE:
                if (startDay >= 0) {
                    ruleDay = mod7(startDayOfWeek - firstDayOfMonth) + 1
                            + (startDay - 1) * 7;
                } else {
                    daysInMonth = GregorianCalendar.DaysInMonth[startMonth];
                    if (startMonth == Calendar.FEBRUARY && isLeapYear(year)) {
                        daysInMonth += 1;
                    }
                    ruleDay = daysInMonth
                            + 1
                            + mod7(startDayOfWeek
                                    - (firstDayOfMonth + daysInMonth))
                            + startDay * 7;
                }
                break;
            case DOW_GE_DOM_MODE:
                ruleDay = startDay
                        + mod7(startDayOfWeek
                                - (firstDayOfMonth + startDay - 1));
                break;
            case DOW_LE_DOM_MODE:
                ruleDay = startDay
                        + mod7(startDayOfWeek
                                - (firstDayOfMonth + startDay - 1));
                if (ruleDay != startDay) {
                    ruleDay -= 7;
                }
                break;
            }
            if (ruleDay > day || ruleDay == day && time < startTime) {
                return rawOffset;
            }
        }

        int ruleTime = endTime - dstSavings;
        int nextMonth = (month + 1) % 12;
        if (month == endMonth || (ruleTime < 0 && nextMonth == endMonth)) {
            switch (endMode) {
            case DOM_MODE:
                ruleDay = endDay;
                break;
            case DOW_IN_MONTH_MODE:
                if (endDay >= 0) {
                    ruleDay = mod7(endDayOfWeek - firstDayOfMonth) + 1
                            + (endDay - 1) * 7;
                } else {
                    daysInMonth = GregorianCalendar.DaysInMonth[endMonth];
                    if (endMonth == Calendar.FEBRUARY && isLeapYear(year)) {
                        daysInMonth++;
                    }
                    ruleDay = daysInMonth
                            + 1
                            + mod7(endDayOfWeek
                                    - (firstDayOfMonth + daysInMonth)) + endDay
                            * 7;
                }
                break;
            case DOW_GE_DOM_MODE:
                ruleDay = endDay
                        + mod7(endDayOfWeek - (firstDayOfMonth + endDay - 1));
                break;
            case DOW_LE_DOM_MODE:
                ruleDay = endDay
                        + mod7(endDayOfWeek - (firstDayOfMonth + endDay - 1));
                if (ruleDay != endDay) {
                    ruleDay -= 7;
                }
                break;
            }

            int ruleMonth = endMonth;
            if (ruleTime < 0) {
                int changeDays = 1 - (ruleTime / 86400000);
                ruleTime = (ruleTime % 86400000) + 86400000;
                ruleDay -= changeDays;
                if (ruleDay <= 0) {
                    if (--ruleMonth < Calendar.JANUARY) {
                        ruleMonth = Calendar.DECEMBER;
                    }
                    ruleDay += GregorianCalendar.DaysInMonth[ruleMonth];
                    if (ruleMonth == Calendar.FEBRUARY && isLeapYear(year)) {
                        ruleDay++;
                    }
                }
            }

            if (month == ruleMonth) {
                if (ruleDay < day || ruleDay == day && time >= ruleTime) {
                    return rawOffset;
                }
            } else if (nextMonth != ruleMonth) {
                return rawOffset;
            }
        }
        return rawOffset + dstSavings;
    }

    @Override
    public int getOffset(long time) {
        if (!useDaylightTime()) {
            return rawOffset;
        }
        if (daylightSavings == null) {
            daylightSavings = new GregorianCalendar(this);
        }
        return daylightSavings.getOffset(time + rawOffset);
    }

    @Override
    public int getRawOffset() {
        return rawOffset;
    }

    /**
     * Returns an integer hash code for the receiver. Objects which are equal
     * return the same value for this method.
     * 
     * @return the receiver's hash.
     * @see #equals
     * @since Android 1.0
     */
    @Override
    public synchronized int hashCode() {
        int hashCode = getID().hashCode() + rawOffset;
        if (useDaylight) {
            hashCode += startYear + startMonth + startDay + startDayOfWeek
                    + startTime + startMode + endMonth + endDay + endDayOfWeek
                    + endTime + endMode + dstSavings;
        }
        return hashCode;
    }

    @Override
    public boolean hasSameRules(TimeZone zone) {
        if (!(zone instanceof SimpleTimeZone)) {
            return false;
        }
        SimpleTimeZone tz = (SimpleTimeZone) zone;
        if (useDaylight != tz.useDaylight) {
            return false;
        }
        if (!useDaylight) {
            return rawOffset == tz.rawOffset;
        }
        return rawOffset == tz.rawOffset && dstSavings == tz.dstSavings
                && startYear == tz.startYear && startMonth == tz.startMonth
                && startDay == tz.startDay && startMode == tz.startMode
                && startDayOfWeek == tz.startDayOfWeek
                && startTime == tz.startTime && endMonth == tz.endMonth
                && endDay == tz.endDay && endDayOfWeek == tz.endDayOfWeek
                && endTime == tz.endTime && endMode == tz.endMode;
    }

    @Override
    public boolean inDaylightTime(Date time) {
        // check for null pointer
        long millis = time.getTime();
        if (!useDaylightTime()) {
            return false;
        }
        if (daylightSavings == null) {
            daylightSavings = new GregorianCalendar(this);
        }
        return daylightSavings.getOffset(millis + rawOffset) != rawOffset;
    }

    private boolean isLeapYear(int year) {
        if (year > 1582) {
            return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
        }
        return year % 4 == 0;
    }

    private int mod7(int num1) {
        int rem = num1 % 7;
        return (num1 < 0 && rem < 0) ? 7 + rem : rem;
    }

    /**
     * Sets the daylight savings offset in milliseconds for this {@code SimpleTimeZone}.
     * 
     * @param milliseconds
     *            the daylight savings offset in milliseconds.
     * @since Android 1.0
     */
    public void setDSTSavings(int milliseconds) {
        if (milliseconds > 0) {
            dstSavings = milliseconds;
        } else {
            throw new IllegalArgumentException();
        }
    }

    private void checkRange(int month, int dayOfWeek, int time) {
        if (month < Calendar.JANUARY || month > Calendar.DECEMBER) {
            throw new IllegalArgumentException(Msg.getString("K00e5", month)); //$NON-NLS-1$
        }
        if (dayOfWeek < Calendar.SUNDAY || dayOfWeek > Calendar.SATURDAY) {
            throw new IllegalArgumentException(Msg.getString("K00e7", dayOfWeek)); //$NON-NLS-1$
        }
        if (time < 0 || time >= 24 * 3600000) {
            throw new IllegalArgumentException(Msg.getString("K00e8", time)); //$NON-NLS-1$
        }
    }

    private void checkDay(int month, int day) {
        if (day <= 0 || day > GregorianCalendar.DaysInMonth[month]) {
            throw new IllegalArgumentException(Msg.getString("K00e6", day)); //$NON-NLS-1$
        }
    }

    private void setEndMode() {
        if (endDayOfWeek == 0) {
            endMode = DOM_MODE;
        } else if (endDayOfWeek < 0) {
            endDayOfWeek = -endDayOfWeek;
            if (endDay < 0) {
                endDay = -endDay;
                endMode = DOW_LE_DOM_MODE;
            } else {
                endMode = DOW_GE_DOM_MODE;
            }
        } else {
            endMode = DOW_IN_MONTH_MODE;
        }
        useDaylight = startDay != 0 && endDay != 0;
        if (endDay != 0) {
            checkRange(endMonth, endMode == DOM_MODE ? 1 : endDayOfWeek,
                    endTime);
            if (endMode != DOW_IN_MONTH_MODE) {
                checkDay(endMonth, endDay);
            } else {
                if (endDay < -5 || endDay > 5) {
                    throw new IllegalArgumentException(Msg.getString("K00f8", endDay)); //$NON-NLS-1$
                }
            }
        }
        if (endMode != DOM_MODE) {
            endDayOfWeek--;
        }
    }

    /**
     * Sets the rule which specifies the end of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time ends.
     * @param dayOfMonth
     *            the {@code Calendar} day of the month on which daylight savings time
     *            ends.
     * @param time
     *            the time of day in milliseconds standard time on which
     *            daylight savings time ends.
     * @since Android 1.0
     */
    public void setEndRule(int month, int dayOfMonth, int time) {
        endMonth = month;
        endDay = dayOfMonth;
        endDayOfWeek = 0; // Initialize this value for hasSameRules()
        endTime = time;
        setEndMode();
    }

    /**
     * Sets the rule which specifies the end of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time ends.
     * @param day
     *            the occurrence of the day of the week on which daylight
     *            savings time ends.
     * @param dayOfWeek
     *            the {@code Calendar} day of the week on which daylight savings time
     *            ends.
     * @param time
     *            the time of day in milliseconds standard time on which
     *            daylight savings time ends.
     * @since Android 1.0
     */
    public void setEndRule(int month, int day, int dayOfWeek, int time) {
        endMonth = month;
        endDay = day;
        endDayOfWeek = dayOfWeek;
        endTime = time;
        setEndMode();
    }

    /**
     * Sets the rule which specifies the end of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time ends.
     * @param day
     *            the {@code Calendar} day of the month.
     * @param dayOfWeek
     *            the {@code Calendar} day of the week on which daylight savings time
     *            ends.
     * @param time
     *            the time of day in milliseconds on which daylight savings time
     *            ends.
     * @param after
     *            selects the day after or before the day of month.
     * @since Android 1.0
     */
    public void setEndRule(int month, int day, int dayOfWeek, int time,
            boolean after) {
        endMonth = month;
        endDay = after ? day : -day;
        endDayOfWeek = -dayOfWeek;
        endTime = time;
        setEndMode();
    }

    /**
     * Sets the offset for standard time from GMT for this {@code SimpleTimeZone}.
     * 
     * @param offset
     *            the offset from GMT of standard time in milliseconds.
     * @since Android 1.0
     */
    @Override
    public void setRawOffset(int offset) {
        rawOffset = offset;
    }

    private void setStartMode() {
        if (startDayOfWeek == 0) {
            startMode = DOM_MODE;
        } else if (startDayOfWeek < 0) {
            startDayOfWeek = -startDayOfWeek;
            if (startDay < 0) {
                startDay = -startDay;
                startMode = DOW_LE_DOM_MODE;
            } else {
                startMode = DOW_GE_DOM_MODE;
            }
        } else {
            startMode = DOW_IN_MONTH_MODE;
        }
        useDaylight = startDay != 0 && endDay != 0;
        if (startDay != 0) {
            checkRange(startMonth, startMode == DOM_MODE ? 1 : startDayOfWeek,
                    startTime);
            if (startMode != DOW_IN_MONTH_MODE) {
                checkDay(startMonth, startDay);
            } else {
                if (startDay < -5 || startDay > 5) {
                    throw new IllegalArgumentException(Msg.getString("K00f8", startDay)); //$NON-NLS-1$
                }
            }
        }
        if (startMode != DOM_MODE) {
            startDayOfWeek--;
        }
    }

    /**
     * Sets the rule which specifies the start of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time starts.
     * @param dayOfMonth
     *            the {@code Calendar} day of the month on which daylight savings time
     *            starts.
     * @param time
     *            the time of day in milliseconds on which daylight savings time
     *            starts.
     * @since Android 1.0
     */
    public void setStartRule(int month, int dayOfMonth, int time) {
        startMonth = month;
        startDay = dayOfMonth;
        startDayOfWeek = 0; // Initialize this value for hasSameRules()
        startTime = time;
        setStartMode();
    }

    /**
     * Sets the rule which specifies the start of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time starts.
     * @param day
     *            the occurrence of the day of the week on which daylight
     *            savings time starts.
     * @param dayOfWeek
     *            the {@code Calendar} day of the week on which daylight savings time
     *            starts.
     * @param time
     *            the time of day in milliseconds on which daylight savings time
     *            starts.
     * @since Android 1.0
     */
    public void setStartRule(int month, int day, int dayOfWeek, int time) {
        startMonth = month;
        startDay = day;
        startDayOfWeek = dayOfWeek;
        startTime = time;
        setStartMode();
    }

    /**
     * Sets the rule which specifies the start of daylight savings time.
     * 
     * @param month
     *            the {@code Calendar} month in which daylight savings time starts.
     * @param day
     *            the {@code Calendar} day of the month.
     * @param dayOfWeek
     *            the {@code Calendar} day of the week on which daylight savings time
     *            starts.
     * @param time
     *            the time of day in milliseconds on which daylight savings time
     *            starts.
     * @param after
     *            selects the day after or before the day of month.
     * @since Android 1.0
     */
    public void setStartRule(int month, int day, int dayOfWeek, int time,
            boolean after) {
        startMonth = month;
        startDay = after ? day : -day;
        startDayOfWeek = -dayOfWeek;
        startTime = time;
        setStartMode();
    }

    /**
     * Sets the starting year for daylight savings time in this {@code SimpleTimeZone}.
     * Years before this start year will always be in standard time.
     * 
     * @param year
     *            the starting year.
     * @since Android 1.0
     */
    public void setStartYear(int year) {
        startYear = year;
        useDaylight = true;
    }

    /**
     * Returns the string representation of this {@code SimpleTimeZone}.
     * 
     * @return the string representation of this {@code SimpleTimeZone}.
     * @since Android 1.0
     */
    @Override
    public String toString() {
        return getClass().getName()
                + "[id=" //$NON-NLS-1$
                + getID()
                + ",offset=" //$NON-NLS-1$
                + rawOffset
                + ",dstSavings=" //$NON-NLS-1$
                + dstSavings
                + ",useDaylight=" //$NON-NLS-1$
                + useDaylight
                + ",startYear=" //$NON-NLS-1$
                + startYear
                + ",startMode=" //$NON-NLS-1$
                + startMode
                + ",startMonth=" //$NON-NLS-1$
                + startMonth
                + ",startDay=" //$NON-NLS-1$
                + startDay
                + ",startDayOfWeek=" //$NON-NLS-1$
                + (useDaylight && (startMode != DOM_MODE) ? startDayOfWeek + 1
                        : 0) + ",startTime=" + startTime + ",endMode=" //$NON-NLS-1$ //$NON-NLS-2$
                + endMode + ",endMonth=" + endMonth + ",endDay=" + endDay //$NON-NLS-1$ //$NON-NLS-2$
                + ",endDayOfWeek=" //$NON-NLS-1$
                + (useDaylight && (endMode != DOM_MODE) ? endDayOfWeek + 1 : 0)
                + ",endTime=" + endTime + "]";  //$NON-NLS-1$//$NON-NLS-2$
    }

    @Override
    public boolean useDaylightTime() {
        return useDaylight;
    }

    private static final ObjectStreamField[] serialPersistentFields = {
            new ObjectStreamField("dstSavings", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("endDay", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("endDayOfWeek", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("endMode", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("endMonth", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("endTime", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("monthLength", byte[].class), //$NON-NLS-1$
            new ObjectStreamField("rawOffset", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("serialVersionOnStream", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startDay", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startDayOfWeek", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startMode", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startMonth", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startTime", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("startYear", Integer.TYPE), //$NON-NLS-1$
            new ObjectStreamField("useDaylight", Boolean.TYPE), }; //$NON-NLS-1$

    private void writeObject(ObjectOutputStream stream) throws IOException {
        int sEndDay = endDay, sEndDayOfWeek = endDayOfWeek + 1, sStartDay = startDay, sStartDayOfWeek = startDayOfWeek + 1;
        if (useDaylight
                && (startMode != DOW_IN_MONTH_MODE || endMode != DOW_IN_MONTH_MODE)) {
            Calendar cal = new GregorianCalendar(this);
            if (endMode != DOW_IN_MONTH_MODE) {
                cal.set(Calendar.MONTH, endMonth);
                cal.set(Calendar.DATE, endDay);
                sEndDay = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
                if (endMode == DOM_MODE) {
                    sEndDayOfWeek = cal.getFirstDayOfWeek();
                }
            }
            if (startMode != DOW_IN_MONTH_MODE) {
                cal.set(Calendar.MONTH, startMonth);
                cal.set(Calendar.DATE, startDay);
                sStartDay = cal.get(Calendar.DAY_OF_WEEK_IN_MONTH);
                if (startMode == DOM_MODE) {
                    sStartDayOfWeek = cal.getFirstDayOfWeek();
                }
            }
        }
        ObjectOutputStream.PutField fields = stream.putFields();
        fields.put("dstSavings", dstSavings); //$NON-NLS-1$
        fields.put("endDay", sEndDay); //$NON-NLS-1$
        fields.put("endDayOfWeek", sEndDayOfWeek); //$NON-NLS-1$
        fields.put("endMode", endMode); //$NON-NLS-1$
        fields.put("endMonth", endMonth); //$NON-NLS-1$
        fields.put("endTime", endTime); //$NON-NLS-1$
        fields.put("monthLength", GregorianCalendar.DaysInMonth); //$NON-NLS-1$
        fields.put("rawOffset", rawOffset); //$NON-NLS-1$
        fields.put("serialVersionOnStream", 1); //$NON-NLS-1$
        fields.put("startDay", sStartDay); //$NON-NLS-1$
        fields.put("startDayOfWeek", sStartDayOfWeek); //$NON-NLS-1$
        fields.put("startMode", startMode); //$NON-NLS-1$
        fields.put("startMonth", startMonth); //$NON-NLS-1$
        fields.put("startTime", startTime); //$NON-NLS-1$
        fields.put("startYear", startYear); //$NON-NLS-1$
        fields.put("useDaylight", useDaylight); //$NON-NLS-1$
        stream.writeFields();
        stream.writeInt(4);
        byte[] values = new byte[4];
        values[0] = (byte) startDay;
        values[1] = (byte) (startMode == DOM_MODE ? 0 : startDayOfWeek + 1);
        values[2] = (byte) endDay;
        values[3] = (byte) (endMode == DOM_MODE ? 0 : endDayOfWeek + 1);
        stream.write(values);
    }

    private void readObject(ObjectInputStream stream) throws IOException,
            ClassNotFoundException {
        ObjectInputStream.GetField fields = stream.readFields();
        rawOffset = fields.get("rawOffset", 0); //$NON-NLS-1$
        useDaylight = fields.get("useDaylight", false); //$NON-NLS-1$
        if (useDaylight) {
            endMonth = fields.get("endMonth", 0); //$NON-NLS-1$
            endTime = fields.get("endTime", 0); //$NON-NLS-1$
            startMonth = fields.get("startMonth", 0); //$NON-NLS-1$
            startTime = fields.get("startTime", 0); //$NON-NLS-1$
            startYear = fields.get("startYear", 0); //$NON-NLS-1$
        }
        if (fields.get("serialVersionOnStream", 0) == 0) { //$NON-NLS-1$
            if (useDaylight) {
                startMode = endMode = DOW_IN_MONTH_MODE;
                endDay = fields.get("endDay", 0); //$NON-NLS-1$
                endDayOfWeek = fields.get("endDayOfWeek", 0) - 1; //$NON-NLS-1$
                startDay = fields.get("startDay", 0); //$NON-NLS-1$
                startDayOfWeek = fields.get("startDayOfWeek", 0) - 1; //$NON-NLS-1$
            }
        } else {
            dstSavings = fields.get("dstSavings", 0); //$NON-NLS-1$
            if (useDaylight) {
                endMode = fields.get("endMode", 0); //$NON-NLS-1$
                startMode = fields.get("startMode", 0); //$NON-NLS-1$
                int length = stream.readInt();
                byte[] values = new byte[length];
                stream.readFully(values);
                if (length >= 4) {
                    startDay = values[0];
                    startDayOfWeek = values[1];
                    if (startMode != DOM_MODE) {
                        startDayOfWeek--;
                    }
                    endDay = values[2];
                    endDayOfWeek = values[3];
                    if (endMode != DOM_MODE) {
                        endDayOfWeek--;
                    }
                }
            }
        }
    }

}
