// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
 *******************************************************************************
 * Copyright (C) 2007-2015, International Business Machines Corporation and    *
 * others. All Rights Reserved.                                                *
 *******************************************************************************
 */
package com.ibm.icu.util;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;
import java.util.MissingResourceException;
import java.util.StringTokenizer;

import com.ibm.icu.impl.Grego;

/**
 * <code>VTimeZone</code> is a class implementing RFC2445 VTIMEZONE.  You can create a
 * <code>VTimeZone</code> instance from a time zone ID supported by <code>TimeZone</code>.
 * With the <code>VTimeZone</code> instance created from the ID, you can write out the rule
 * in RFC2445 VTIMEZONE format.  Also, you can create a <code>VTimeZone</code> instance
 * from RFC2445 VTIMEZONE data stream, which allows you to calculate time
 * zone offset by the rules defined by the data.<br><br>
 *
 * Note: The consumer of this class reading or writing VTIMEZONE data is responsible to
 * decode or encode Non-ASCII text.  Methods reading/writing VTIMEZONE data in this class
 * do nothing with MIME encoding.
 *
 * @stable ICU 3.8
 */
public class VTimeZone extends BasicTimeZone {

    private static final long serialVersionUID = -6851467294127795902L;

    /**
     * Create a <code>VTimeZone</code> instance by the time zone ID.
     *
     * @param tzid The time zone ID, such as America/New_York
     * @return A <code>VTimeZone</code> initialized by the time zone ID, or null
     * when the ID is unknown.
     *
     * @stable ICU 3.8
     */
    public static VTimeZone create(String tzid) {
        BasicTimeZone basicTimeZone = TimeZone.getFrozenICUTimeZone(tzid, true);
        if (basicTimeZone == null) {
            return null;
        }
        VTimeZone vtz = new VTimeZone(tzid);
        vtz.tz = (BasicTimeZone) basicTimeZone.cloneAsThawed();
        vtz.olsonzid = vtz.tz.getID();

        return vtz;
    }

    /**
     * Create a <code>VTimeZone</code> instance by RFC2445 VTIMEZONE data.
     *
     * @param reader The Reader for VTIMEZONE data input stream
     * @return A <code>VTimeZone</code> initialized by the VTIMEZONE data or
     * null if failed to load the rule from the VTIMEZONE data.
     *
     * @stable ICU 3.8
     */
    public static VTimeZone create(Reader reader) {
        VTimeZone vtz = new VTimeZone();
        if (vtz.load(reader)) {
            return vtz;
        }
        return null;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public int getOffset(int era, int year, int month, int day, int dayOfWeek,
            int milliseconds) {
        return tz.getOffset(era, year, month, day, dayOfWeek, milliseconds);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public void getOffset(long date, boolean local, int[] offsets) {
        tz.getOffset(date, local, offsets);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 69
     */
    @Override
    public void getOffsetFromLocal(long date,
            LocalOption nonExistingTimeOpt, LocalOption duplicatedTimeOpt, int[] offsets) {
        tz.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public int getRawOffset() {
        return tz.getRawOffset();
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public boolean inDaylightTime(Date date) {
        return tz.inDaylightTime(date);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public void setRawOffset(int offsetMillis) {
        if (isFrozen()) {
            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
        }
        tz.setRawOffset(offsetMillis);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public boolean useDaylightTime() {
        return tz.useDaylightTime();
    }

    /**
     * {@inheritDoc}
     * @stable ICU 49
     */
    @Override
    public boolean observesDaylightTime() {
        return tz.observesDaylightTime();
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public boolean hasSameRules(TimeZone other) {
        if (this == other) {
            return true;
        }
        if (other instanceof VTimeZone) {
            return tz.hasSameRules(((VTimeZone)other).tz);
        }
        return tz.hasSameRules(other);
    }

    /**
     * Gets the RFC2445 TZURL property value.  When a <code>VTimeZone</code> instance was created from
     * VTIMEZONE data, the value is set by the TZURL property value in the data.  Otherwise,
     * the initial value is null.
     *
     * @return The RFC2445 TZURL property value
     *
     * @stable ICU 3.8
     */
    public String getTZURL() {
        return tzurl;
    }

    /**
     * Sets the RFC2445 TZURL property value.
     *
     * @param url The TZURL property value.
     *
     * @stable ICU 3.8
     */
    public void setTZURL(String url) {
        if (isFrozen()) {
            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
        }
        tzurl = url;
    }

    /**
     * Gets the RFC2445 LAST-MODIFIED property value.  When a <code>VTimeZone</code> instance was created
     * from VTIMEZONE data, the value is set by the LAST-MODIFIED property value in the data.
     * Otherwise, the initial value is null.
     *
     * @return The Date represents the RFC2445 LAST-MODIFIED date.
     *
     * @stable ICU 3.8
     */
    public Date getLastModified() {
        return lastmod;
    }

    /**
     * Sets the date used for RFC2445 LAST-MODIFIED property value.
     *
     * @param date The <code>Date</code> object represents the date for RFC2445 LAST-MODIFIED property value.
     *
     * @stable ICU 3.8
     */
    public void setLastModified(Date date) {
        if (isFrozen()) {
            throw new UnsupportedOperationException("Attempt to modify a frozen VTimeZone instance.");
        }
        lastmod = date;
    }

    /**
     * Writes RFC2445 VTIMEZONE data for this time zone
     *
     * @param writer A <code>Writer</code> used for the output
     * @throws IOException If there were problems creating a buffered writer or writing to it.
     *
     * @stable ICU 3.8
     */
    public void write(Writer writer) throws IOException {
        BufferedWriter bw = new BufferedWriter(writer);
        if (vtzlines != null) {
            for (String line : vtzlines) {
                if (line.startsWith(ICAL_TZURL + COLON)) {
                    if (tzurl != null) {
                        bw.write(ICAL_TZURL);
                        bw.write(COLON);
                        bw.write(tzurl);
                        bw.write(NEWLINE);
                    }
                } else if (line.startsWith(ICAL_LASTMOD + COLON)) {
                    if (lastmod != null) {
                        bw.write(ICAL_LASTMOD);
                        bw.write(COLON);
                        bw.write(getUTCDateTimeString(lastmod.getTime()));
                        bw.write(NEWLINE);
                    }
                } else {
                    bw.write(line);
                    bw.write(NEWLINE);
                }
            }
            bw.flush();
        } else {
            String[] customProperties = null;
            if (olsonzid != null && ICU_TZVERSION != null) {
                customProperties = new String[1];
                customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION + "]";
            }
            writeZone(writer, tz, customProperties);
        }
    }

    /**
     * Writes RFC2445 VTIMEZONE data applicable for dates after
     * the specified start time.
     *
     * @param writer    The <code>Writer</code> used for the output
     * @param start     The start time
     *
     * @throws IOException If there were problems reading and writing to the writer.
     *
     * @stable ICU 3.8
     */
    public void write(Writer writer, long start) throws IOException {
        // Extract rules applicable to dates after the start time
        TimeZoneRule[] rules = tz.getTimeZoneRules(start);

        // Create a RuleBasedTimeZone with the subset rule
        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
        for (int i = 1; i < rules.length; i++) {
            rbtz.addTransitionRule(rules[i]);
        }
        String[] customProperties = null;
        if (olsonzid != null && ICU_TZVERSION != null) {
            customProperties = new String[1];
            customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
                "/Partial@" + start + "]";
        }
        writeZone(writer, rbtz, customProperties);
    }

    /**
     * Writes RFC2445 VTIMEZONE data applicable near the specified date.
     * Some common iCalendar implementations can only handle a single time
     * zone property or a pair of standard and daylight time properties using
     * BYDAY rule with day of week (such as BYDAY=1SUN).  This method produce
     * the VTIMEZONE data which can be handled these implementations.  The rules
     * produced by this method can be used only for calculating time zone offset
     * around the specified date.
     *
     * @param writer    The <code>Writer</code> used for the output
     * @param time      The date
     *
     * @throws IOException If there were problems reading or writing to the writer.
     *
     * @stable ICU 3.8
     */
    public void writeSimple(Writer writer, long time) throws IOException {
        // Extract simple rules
        TimeZoneRule[] rules = tz.getSimpleTimeZoneRulesNear(time);

        // Create a RuleBasedTimeZone with the subset rule
        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tz.getID(), (InitialTimeZoneRule)rules[0]);
        for (int i = 1; i < rules.length; i++) {
            rbtz.addTransitionRule(rules[i]);
        }
        String[] customProperties = null;
        if (olsonzid != null && ICU_TZVERSION != null) {
            customProperties = new String[1];
            customProperties[0] = ICU_TZINFO_PROP + COLON + olsonzid + "[" + ICU_TZVERSION +
                "/Simple@" + time + "]";
        }
        writeZone(writer, rbtz, customProperties);
    }

    // BasicTimeZone methods

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneTransition getNextTransition(long base, boolean inclusive) {
        return tz.getNextTransition(base, inclusive);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
        return tz.getPreviousTransition(base, inclusive);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public boolean hasEquivalentTransitions(TimeZone other, long start, long end) {
        if (this == other) {
            return true;
        }
        return tz.hasEquivalentTransitions(other, start, end);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneRule[] getTimeZoneRules() {
        return tz.getTimeZoneRules();
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public TimeZoneRule[] getTimeZoneRules(long start) {
        return tz.getTimeZoneRules(start);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public Object clone() {
        if (isFrozen()) {
            return this;
        }
        return cloneAsThawed();
    }

    // private stuff ------------------------------------------------------

    private BasicTimeZone tz;
    private List<String> vtzlines;
    private String olsonzid = null;
    private String tzurl = null;
    private Date lastmod = null;

    private static String ICU_TZVERSION;
    private static final String ICU_TZINFO_PROP = "X-TZINFO";

    // Default DST savings
    private static final int DEF_DSTSAVINGS = 60*60*1000; // 1 hour

    // Default time start
    private static final long DEF_TZSTARTTIME = 0;

    // minimum/max
    private static final long MIN_TIME = Long.MIN_VALUE;
    private static final long MAX_TIME = Long.MAX_VALUE;

    // Symbol characters used by RFC2445 VTIMEZONE
    private static final String COLON = ":";
    private static final String SEMICOLON = ";";
    private static final String EQUALS_SIGN = "=";
    private static final String COMMA = ",";
    private static final String NEWLINE = "\r\n";   // CRLF

    // RFC2445 VTIMEZONE tokens
    private static final String ICAL_BEGIN_VTIMEZONE = "BEGIN:VTIMEZONE";
    private static final String ICAL_END_VTIMEZONE = "END:VTIMEZONE";
    private static final String ICAL_BEGIN = "BEGIN";
    private static final String ICAL_END = "END";
    private static final String ICAL_VTIMEZONE = "VTIMEZONE";
    private static final String ICAL_TZID = "TZID";
    private static final String ICAL_STANDARD = "STANDARD";
    private static final String ICAL_DAYLIGHT = "DAYLIGHT";
    private static final String ICAL_DTSTART = "DTSTART";
    private static final String ICAL_TZOFFSETFROM = "TZOFFSETFROM";
    private static final String ICAL_TZOFFSETTO = "TZOFFSETTO";
    private static final String ICAL_RDATE = "RDATE";
    private static final String ICAL_RRULE = "RRULE";
    private static final String ICAL_TZNAME = "TZNAME";
    private static final String ICAL_TZURL = "TZURL";
    private static final String ICAL_LASTMOD = "LAST-MODIFIED";

    private static final String ICAL_FREQ = "FREQ";
    private static final String ICAL_UNTIL = "UNTIL";
    private static final String ICAL_YEARLY = "YEARLY";
    private static final String ICAL_BYMONTH = "BYMONTH";
    private static final String ICAL_BYDAY = "BYDAY";
    private static final String ICAL_BYMONTHDAY = "BYMONTHDAY";

    private static final String[] ICAL_DOW_NAMES =
    {"SU", "MO", "TU", "WE", "TH", "FR", "SA"};

    // Month length in regular year
    private static final int[] MONTHLENGTH = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};

    static {
        // Initialize ICU_TZVERSION
        try {
            ICU_TZVERSION = TimeZone.getTZDataVersion();
        } catch (MissingResourceException e) {
            ///CLOVER:OFF
            ICU_TZVERSION = null;
            ///CLOVER:ON
        }
    }

    /* Hide the constructor */
    private VTimeZone() {
    }

    private VTimeZone(String tzid) {
        super(tzid);
    }

    /*
     * Read the input stream to locate the VTIMEZONE block and
     * parse the contents to initialize this VTimeZone object.
     * The reader skips other RFC2445 message headers.  After
     * the parse is completed, the reader points at the beginning
     * of the header field just after the end of VTIMEZONE block.
     * When VTIMEZONE block is found and this object is successfully
     * initialized by the rules described in the data, this method
     * returns true.  Otherwise, returns false.
     */
    private boolean load(Reader reader) {
        // Read VTIMEZONE block into string array
        try {
            vtzlines = new LinkedList<>();
            boolean eol = false;
            boolean start = false;
            boolean success = false;
            StringBuilder line = new StringBuilder();
            while (true) {
                int ch = reader.read();
                if (ch == -1) {
                    // end of file
                    if (start && line.toString().startsWith(ICAL_END_VTIMEZONE)) {
                        vtzlines.add(line.toString());
                        success = true;
                    }
                    break;
                }
                if (ch == 0x0D) {
                    // CR, must be followed by LF by the definition in RFC2445
                    continue;
                }

                if (eol) {
                    if (ch != 0x09 && ch != 0x20) {
                        // NOT followed by TAB/SP -> new line
                        if (start) {
                            if (line.length() > 0) {
                                vtzlines.add(line.toString());
                            }
                        }
                        line.setLength(0);
                        if (ch != 0x0A) {
                            line.append((char)ch);
                        }
                    }
                    eol = false;
                } else {
                    if (ch == 0x0A) {
                        // LF
                        eol = true;
                        if (start) {
                            if (line.toString().startsWith(ICAL_END_VTIMEZONE)) {
                                vtzlines.add(line.toString());
                                success = true;
                                break;
                            }
                        } else {
                            if (line.toString().startsWith(ICAL_BEGIN_VTIMEZONE)) {
                                vtzlines.add(line.toString());
                                line.setLength(0);
                                start = true;
                                eol = false;
                            }
                        }
                    } else {
                        line.append((char)ch);
                    }
                }
            }
            if (!success) {
                return false;
            }
        } catch (IOException ioe) {
            ///CLOVER:OFF
            return false;
            ///CLOVER:ON
        }
        return parse();
    }

    // parser state
    private static final int INI = 0;   // Initial state
    private static final int VTZ = 1;   // In VTIMEZONE
    private static final int TZI = 2;   // In STANDARD or DAYLIGHT
    private static final int ERR = 3;   // Error state

    /*
     * Parse VTIMEZONE data and create a RuleBasedTimeZone
     */
    private boolean parse() {
        ///CLOVER:OFF
        if (vtzlines == null || vtzlines.size() == 0) {
            return false;
        }
        ///CLOVER:ON

        // timezone ID
        String tzid = null;

        int state = INI;
        boolean dst = false;    // current zone type
        String from = null;     // current zone from offset
        String to = null;       // current zone offset
        String tzname = null;   // current zone name
        String dtstart = null;  // current zone starts
        boolean isRRULE = false;    // true if the rule is described by RRULE
        List<String> dates = null;  // list of RDATE or RRULE strings
        List<TimeZoneRule> rules = new ArrayList<>();   // rule list
        int initialRawOffset = 0;  // initial offset
        int initialDSTSavings = 0;  // initial offset
        long firstStart = MAX_TIME; // the earliest rule start time

        for (String line : vtzlines) {
            int valueSep = line.indexOf(COLON);
            if (valueSep < 0) {
                continue;
            }
            String name = line.substring(0, valueSep);
            String value = line.substring(valueSep + 1);

            switch (state) {
            case INI:
                if (name.equals(ICAL_BEGIN) && value.equals(ICAL_VTIMEZONE)) {
                    state = VTZ;
                }
                break;
            case VTZ:
                if (name.equals(ICAL_TZID)) {
                    tzid = value;
                } else if (name.equals(ICAL_TZURL)) {
                    tzurl = value;
                } else if (name.equals(ICAL_LASTMOD)) {
                    // Always in 'Z' format, so the offset argument for the parse method
                    // can be any value.
                    lastmod = new Date(parseDateTimeString(value, 0));
                } else if (name.equals(ICAL_BEGIN)) {
                    boolean isDST = value.equals(ICAL_DAYLIGHT);
                    if (value.equals(ICAL_STANDARD) || isDST) {
                        // tzid must be ready at this point
                        if (tzid == null) {
                            state = ERR;
                            break;
                        }
                        // initialize current zone properties
                        dates = null;
                        isRRULE = false;
                        from = null;
                        to = null;
                        tzname = null;
                        dst = isDST;
                        state = TZI;
                    } else {
                        // BEGIN property other than STANDARD/DAYLIGHT
                        // must not be there.
                        state = ERR;
                        break;
                    }
                } else if (name.equals(ICAL_END) /* && value.equals(ICAL_VTIMEZONE) */) {
                    break;
                }
                break;

            case TZI:
                if (name.equals(ICAL_DTSTART)) {
                    dtstart = value;
                } else if (name.equals(ICAL_TZNAME)) {
                    tzname = value;
                } else if (name.equals(ICAL_TZOFFSETFROM)) {
                    from = value;
                } else if (name.equals(ICAL_TZOFFSETTO)) {
                    to = value;
                } else if (name.equals(ICAL_RDATE)) {
                    // RDATE mixed with RRULE is not supported
                    if (isRRULE) {
                        state = ERR;
                        break;
                    }
                    if (dates == null) {
                        dates = new LinkedList<>();
                    }
                    // RDATE value may contain multiple date delimited
                    // by comma
                    StringTokenizer st = new StringTokenizer(value, COMMA);
                    while (st.hasMoreTokens()) {
                        String date = st.nextToken();
                        dates.add(date);
                    }
                } else if (name.equals(ICAL_RRULE)) {
                    // RRULE mixed with RDATE is not supported
                    if (!isRRULE && dates != null) {
                        state = ERR;
                        break;
                    } else if (dates == null) {
                        dates = new LinkedList<>();
                    }
                    isRRULE = true;
                    dates.add(value);
                } else if (name.equals(ICAL_END)) {
                    // Mandatory properties
                    if (dtstart == null || from == null || to == null) {
                        state = ERR;
                        break;
                    }
                    // if tzname is not available, create one from tzid
                    if (tzname == null) {
                        tzname = getDefaultTZName(tzid, dst);
                    }

                    // create a time zone rule
                    TimeZoneRule rule = null;
                    int fromOffset = 0;
                    int toOffset = 0;
                    int rawOffset = 0;
                    int dstSavings = 0;
                    long start = 0;
                    try {
                        // Parse TZOFFSETFROM/TZOFFSETTO
                        fromOffset = offsetStrToMillis(from);
                        toOffset = offsetStrToMillis(to);

                        if (dst) {
                            // If daylight, use the previous offset as rawoffset if positive
                            if (toOffset - fromOffset > 0) {
                                rawOffset = fromOffset;
                                dstSavings = toOffset - fromOffset;
                            } else {
                                // This is rare case..  just use 1 hour DST savings
                                rawOffset = toOffset - DEF_DSTSAVINGS;
                                dstSavings = DEF_DSTSAVINGS;
                            }
                        } else {
                            rawOffset = toOffset;
                            dstSavings = 0;
                        }

                        // start time
                        start = parseDateTimeString(dtstart, fromOffset);

                        // Create the rule
                        Date actualStart = null;
                        if (isRRULE) {
                            rule = createRuleByRRULE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
                        } else {
                            rule = createRuleByRDATE(tzname, rawOffset, dstSavings, start, dates, fromOffset);
                        }
                        if (rule != null) {
                            actualStart = rule.getFirstStart(fromOffset, 0);
                            if (actualStart.getTime() < firstStart) {
                                // save from offset information for the earliest rule
                                firstStart = actualStart.getTime();
                                // If this is STD, assume the time before this transition
                                // is DST when the difference is 1 hour.  This might not be
                                // accurate, but VTIMEZONE data does not have such info.
                                if (dstSavings > 0) {
                                    initialRawOffset = fromOffset;
                                    initialDSTSavings = 0;
                                } else {
                                    if (fromOffset - toOffset == DEF_DSTSAVINGS) {
                                        initialRawOffset = fromOffset - DEF_DSTSAVINGS;
                                        initialDSTSavings = DEF_DSTSAVINGS;
                                    } else {
                                        initialRawOffset = fromOffset;
                                        initialDSTSavings = 0;
                                    }
                                }
                            }
                        }
                    } catch (IllegalArgumentException iae) {
                        // bad format - rule == null..
                    }

                    if (rule == null) {
                        state = ERR;
                        break;
                    }
                    rules.add(rule);
                    state = VTZ;
                }
                break;
            }

            if (state == ERR) {
                vtzlines = null;
                return false;
            }
        }

        // Must have at least one rule
        if (rules.size() == 0) {
            return false;
        }

        // Create a initial rule
        InitialTimeZoneRule initialRule = new InitialTimeZoneRule(getDefaultTZName(tzid, false),
                initialRawOffset, initialDSTSavings);

        // Finally, create the RuleBasedTimeZone
        RuleBasedTimeZone rbtz = new RuleBasedTimeZone(tzid, initialRule);

        int finalRuleIdx = -1;
        int finalRuleCount = 0;
        for (int i = 0; i < rules.size(); i++) {
            TimeZoneRule r = rules.get(i);
            if (r instanceof AnnualTimeZoneRule) {
                if (((AnnualTimeZoneRule)r).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
                    finalRuleCount++;
                    finalRuleIdx = i;
                }
            }
        }
        if (finalRuleCount > 2) {
            // Too many final rules
            return false;
        }

        if (finalRuleCount == 1) {
            if (rules.size() == 1) {
                // Only one final rule, only governs the initial rule,
                // which is already initialized, thus, we do not need to
                // add this transition rule
                rules.clear();
            } else {
                // Normalize the final rule
                AnnualTimeZoneRule finalRule = (AnnualTimeZoneRule)rules.get(finalRuleIdx);
                int tmpRaw = finalRule.getRawOffset();
                int tmpDST = finalRule.getDSTSavings();

                // Find the last non-final rule
                Date finalStart = finalRule.getFirstStart(initialRawOffset, initialDSTSavings);
                Date start = finalStart;
                for (int i = 0; i < rules.size(); i++) {
                    if (finalRuleIdx == i) {
                        continue;
                    }
                    TimeZoneRule r = rules.get(i);
                    Date lastStart = r.getFinalStart(tmpRaw, tmpDST);
                    if (lastStart.after(start)) {
                        start = finalRule.getNextStart(lastStart.getTime(),
                                r.getRawOffset(),
                                r.getDSTSavings(),
                                false);
                    }
                }
                TimeZoneRule newRule;
                if (start == finalStart) {
                    // Transform this into a single transition
                    newRule = new TimeArrayTimeZoneRule(
                            finalRule.getName(),
                            finalRule.getRawOffset(),
                            finalRule.getDSTSavings(),
                            new long[] {finalStart.getTime()},
                            DateTimeRule.UTC_TIME);
                } else {
                    // Update the end year
                    int fields[] = Grego.timeToFields(start.getTime(), null);
                    newRule = new AnnualTimeZoneRule(
                            finalRule.getName(),
                            finalRule.getRawOffset(),
                            finalRule.getDSTSavings(),
                            finalRule.getRule(),
                            finalRule.getStartYear(),
                            fields[0]);
                }
                rules.set(finalRuleIdx, newRule);
            }
        }

        for (TimeZoneRule r : rules) {
            rbtz.addTransitionRule(r);
        }

        tz = rbtz;
        setID(tzid);
        return true;
    }

    /*
     * Create a default TZNAME from TZID
     */
    private static String getDefaultTZName(String tzid, boolean isDST) {
        if (isDST) {
            return tzid + "(DST)";
        }
        return tzid + "(STD)";
    }

    /*
     * Create a TimeZoneRule by the RRULE definition
     */
    private static TimeZoneRule createRuleByRRULE(String tzname,
            int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
        if (dates == null || dates.size() == 0) {
            return null;
        }
        // Parse the first rule
        String rrule = dates.get(0);

        long until[] = new long[1];
        int[] ruleFields = parseRRULE(rrule, until);
        if (ruleFields == null) {
            // Invalid RRULE
            return null;
        }

        int month = ruleFields[0];
        int dayOfWeek = ruleFields[1];
        int nthDayOfWeek = ruleFields[2];
        int dayOfMonth = ruleFields[3];

        if (dates.size() == 1) {
            // No more rules
            if (ruleFields.length > 4) {
                // Multiple BYMONTHDAY values

                if (ruleFields.length != 10 || month == -1 || dayOfWeek == 0) {
                    // Only support the rule using 7 continuous days
                    // BYMONTH and BYDAY must be set at the same time
                    return null;
                }
                int firstDay = 31; // max possible number of dates in a month
                int days[] = new int[7];
                for (int i = 0; i < 7; i++) {
                    days[i] = ruleFields[3 + i];
                    // Resolve negative day numbers.  A negative day number should
                    // not be used in February, but if we see such case, we use 28
                    // as the base.
                    days[i] = days[i] > 0 ? days[i] : MONTHLENGTH[month] + days[i] + 1;
                    firstDay = days[i] < firstDay ? days[i] : firstDay;
                }
                // Make sure days are continuous
                for (int i = 1; i < 7; i++) {
                    boolean found = false;
                    for (int j = 0; j < 7; j++) {
                        if (days[j] == firstDay + i) {
                            found = true;
                            break;
                        }
                    }
                    if (!found) {
                        // days are not continuous
                        return null;
                    }
                }
                // Use DOW_GEQ_DOM rule with firstDay as the start date
                dayOfMonth = firstDay;
            }
        } else {
            // Check if BYMONTH + BYMONTHDAY + BYDAY rule with multiple RRULE lines.
            // Otherwise, not supported.
            if (month == -1 || dayOfWeek == 0 || dayOfMonth == 0) {
                // This is not the case
                return null;
            }
            // Parse the rest of rules if number of rules is not exceeding 7.
            // We can only support 7 continuous days starting from a day of month.
            if (dates.size() > 7) {
                return null;
            }

            // Note: To check valid date range across multiple rule is a little
            // bit complicated.  For now, this code is not doing strict range
            // checking across month boundary

            int earliestMonth = month;
            int daysCount = ruleFields.length - 3;
            int earliestDay = 31;
            for (int i = 0; i < daysCount; i++) {
                int dom = ruleFields[3 + i];
                dom = dom > 0 ? dom : MONTHLENGTH[month] + dom + 1;
                earliestDay = dom < earliestDay ? dom : earliestDay;
            }

            int anotherMonth = -1;
            for (int i = 1; i < dates.size(); i++) {
                rrule = dates.get(i);
                long[] unt = new long[1];
                int[] fields = parseRRULE(rrule, unt);

                // If UNTIL is newer than previous one, use the one
                if (unt[0] > until[0]) {
                    until = unt;
                }

                // Check if BYMONTH + BYMONTHDAY + BYDAY rule
                if (fields[0] == -1 || fields[1] == 0 || fields[3] == 0) {
                    return null;
                }
                // Count number of BYMONTHDAY
                int count = fields.length - 3;
                if (daysCount + count > 7) {
                    // We cannot support BYMONTHDAY more than 7
                    return null;
                }
                // Check if the same BYDAY is used.  Otherwise, we cannot
                // support the rule
                if (fields[1] != dayOfWeek) {
                    return null;
                }
                // Check if the month is same or right next to the primary month
                if (fields[0] != month) {
                    if (anotherMonth == -1) {
                        int diff = fields[0] - month;
                        if (diff == -11 || diff == -1) {
                            // Previous month
                            anotherMonth = fields[0];
                            earliestMonth = anotherMonth;
                            // Reset earliest day
                            earliestDay = 31;
                        } else if (diff == 11 || diff == 1) {
                            // Next month
                            anotherMonth = fields[0];
                        } else {
                            // The day range cannot exceed more than 2 months
                            return null;
                        }
                    } else if (fields[0] != month && fields[0] != anotherMonth) {
                        // The day range cannot exceed more than 2 months
                        return null;
                    }
                }
                // If earlier month, go through days to find the earliest day
                if (fields[0] == earliestMonth) {
                    for (int j = 0; j < count; j++) {
                        int dom = fields[3 + j];
                        dom = dom > 0 ? dom : MONTHLENGTH[fields[0]] + dom + 1;
                        earliestDay = dom < earliestDay ? dom : earliestDay;
                    }
                }
                daysCount += count;
            }
            if (daysCount != 7) {
                // Number of BYMONTHDAY entries must be 7
                return null;
            }
            month = earliestMonth;
            dayOfMonth = earliestDay;
        }

        // Calculate start/end year and missing fields
        int[] dfields = Grego.timeToFields(start + fromOffset, null);
        int startYear = dfields[0];
        if (month == -1) {
            // If MYMONTH is not set, use the month of DTSTART
            month = dfields[1];
        }
        if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth == 0) {
            // If only YEARLY is set, use the day of DTSTART as BYMONTHDAY
            dayOfMonth = dfields[2];
        }
        int timeInDay = dfields[5];

        int endYear = AnnualTimeZoneRule.MAX_YEAR;
        if (until[0] != MIN_TIME) {
            Grego.timeToFields(until[0], dfields);
            endYear = dfields[0];
        }

        // Create the AnnualDateTimeRule
        DateTimeRule adtr = null;
        if (dayOfWeek == 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
            // Day in month rule, for example, 15th day in the month
            adtr = new DateTimeRule(month, dayOfMonth, timeInDay, DateTimeRule.WALL_TIME);
        } else if (dayOfWeek != 0 && nthDayOfWeek != 0 && dayOfMonth == 0) {
            // Nth day of week rule, for example, last Sunday
            adtr = new DateTimeRule(month, nthDayOfWeek, dayOfWeek, timeInDay, DateTimeRule.WALL_TIME);
        } else if (dayOfWeek != 0 && nthDayOfWeek == 0 && dayOfMonth != 0) {
            // First day of week after day of month rule, for example,
            // first Sunday after 15th day in the month
            adtr = new DateTimeRule(month, dayOfMonth, dayOfWeek, true, timeInDay, DateTimeRule.WALL_TIME);
        } else {
            // RRULE attributes are insufficient
            return null;
        }

        return new AnnualTimeZoneRule(tzname, rawOffset, dstSavings, adtr, startYear, endYear);
    }

    /*
     * Parse individual RRULE
     *
     * On return -
     *
     * int[0] month calculated by BYMONTH - 1, or -1 when not found
     * int[1] day of week in BYDAY, or 0 when not found
     * int[2] day of week ordinal number in BYDAY, or 0 when not found
     * int[i >= 3] day of month, which could be multiple values, or 0 when not found
     *
     *  or
     *
     * null on any error cases, for example, FREQ=YEARLY is not available
     *
     * When UNTIL attribute is available, the time will be set to until[0],
     * otherwise, MIN_TIME
     */
    private static int[] parseRRULE(String rrule, long[] until) {
        int month = -1;
        int dayOfWeek = 0;
        int nthDayOfWeek = 0;
        int[] dayOfMonth = null;

        long untilTime = MIN_TIME;
        boolean yearly = false;
        boolean parseError = false;
        StringTokenizer st= new StringTokenizer(rrule, SEMICOLON);

        while (st.hasMoreTokens()) {
            String attr, value;
            String prop = st.nextToken();
            int sep = prop.indexOf(EQUALS_SIGN);
            if (sep != -1) {
                attr = prop.substring(0, sep);
                value = prop.substring(sep + 1);
            } else {
                parseError = true;
                break;
            }

            if (attr.equals(ICAL_FREQ)) {
                // only support YEARLY frequency type
                if (value.equals(ICAL_YEARLY)) {
                    yearly = true;
                } else {
                    parseError = true;
                    break;
                }
            } else if (attr.equals(ICAL_UNTIL)) {
                // ISO8601 UTC format, for example, "20060315T020000Z"
                try {
                    untilTime = parseDateTimeString(value, 0);
                } catch (IllegalArgumentException iae) {
                    parseError = true;
                    break;
                }
            } else if (attr.equals(ICAL_BYMONTH)) {
                // Note: BYMONTH may contain multiple months, but only single month make sense for
                // VTIMEZONE property.
                if (value.length() > 2) {
                    parseError = true;
                    break;
                }
                try {
                    month = Integer.parseInt(value) - 1;
                    if (month < 0 || month >= 12) {
                        parseError = true;
                        break;
                    }
                } catch (NumberFormatException nfe) {
                    parseError = true;
                    break;
                }
            } else if (attr.equals(ICAL_BYDAY)) {
                // Note: BYDAY may contain multiple day of week separated by comma.  It is unlikely used for
                // VTIMEZONE property.  We do not support the case.

                // 2-letter format is used just for representing a day of week, for example, "SU" for Sunday
                // 3 or 4-letter format is used for represeinging Nth day of week, for example, "-1SA" for last Saturday
                int length = value.length();
                if (length < 2 || length > 4) {
                    parseError = true;
                    break;
                }
                if (length > 2) {
                    // Nth day of week
                    int sign = 1;
                    if (value.charAt(0) == '+') {
                        sign = 1;
                    } else if (value.charAt(0) == '-') {
                        sign = -1;
                    } else if (length == 4) {
                        parseError = true;
                        break;
                    }
                    try {
                        int n = Integer.parseInt(value.substring(length - 3, length - 2));
                        if (n == 0 || n > 4) {
                            parseError = true;
                            break;
                        }
                        nthDayOfWeek = n * sign;
                    } catch(NumberFormatException nfe) {
                        parseError = true;
                        break;
                    }
                    value = value.substring(length - 2);
                }
                int wday;
                for (wday = 0; wday < ICAL_DOW_NAMES.length; wday++) {
                    if (value.equals(ICAL_DOW_NAMES[wday])) {
                        break;
                    }
                }
                if (wday < ICAL_DOW_NAMES.length) {
                    // Sunday(1) - Saturday(7)
                    dayOfWeek = wday + 1;
                } else {
                    parseError = true;
                    break;
                }
            } else if (attr.equals(ICAL_BYMONTHDAY)) {
                // Note: BYMONTHDAY may contain multiple days delimited by comma
                //
                // A value of BYMONTHDAY could be negative, for example, -1 means
                // the last day in a month
                StringTokenizer days = new StringTokenizer(value, COMMA);
                int count = days.countTokens();
                dayOfMonth = new int[count];
                int index = 0;
                while(days.hasMoreTokens()) {
                    try {
                        dayOfMonth[index++] = Integer.parseInt(days.nextToken());
                    } catch (NumberFormatException nfe) {
                        parseError = true;
                        break;
                    }
                }
            }
        }

        if (parseError) {
            return null;
        }
        if (!yearly) {
            // FREQ=YEARLY must be set
            return null;
        }

        until[0] = untilTime;

        int[] results;
        if (dayOfMonth == null) {
            results = new int[4];
            results[3] = 0;
        } else {
            results = new int[3 + dayOfMonth.length];
            for (int i = 0; i < dayOfMonth.length; i++) {
                results[3 + i] = dayOfMonth[i];
            }
        }
        results[0] = month;
        results[1] = dayOfWeek;
        results[2] = nthDayOfWeek;
        return results;
    }

    /*
     * Create a TimeZoneRule by the RDATE definition
     */
    private static TimeZoneRule createRuleByRDATE(String tzname,
            int rawOffset, int dstSavings, long start, List<String> dates, int fromOffset) {
        // Create an array of transition times
        long[] times;
        if (dates == null || dates.size() == 0) {
            // When no RDATE line is provided, use start (DTSTART)
            // as the transition time
            times = new long[1];
            times[0] = start;
        } else {
            times = new long[dates.size()];
            int idx = 0;
            try {
                for (String date : dates) {
                    times[idx++] = parseDateTimeString(date, fromOffset);
                }
            } catch (IllegalArgumentException iae) {
                return null;
            }
        }
        return new TimeArrayTimeZoneRule(tzname, rawOffset, dstSavings, times, DateTimeRule.UTC_TIME);
    }

    /*
     * Write the time zone rules in RFC2445 VTIMEZONE format
     */
    private void writeZone(Writer w, BasicTimeZone basictz, String[] customProperties) throws IOException {
        // Write the header
        writeHeader(w);

        if (customProperties != null && customProperties.length > 0) {
            for (int i = 0; i < customProperties.length; i++) {
                if (customProperties[i] != null) {
                    w.write(customProperties[i]);
                    w.write(NEWLINE);
                }
            }
        }

        long t = MIN_TIME;
        String dstName = null;
        int dstFromOffset = 0;
        int dstFromDSTSavings = 0;
        int dstToOffset = 0;
        int dstStartYear = 0;
        int dstMonth = 0;
        int dstDayOfWeek = 0;
        int dstWeekInMonth = 0;
        int dstMillisInDay = 0;
        long dstStartTime = 0;
        long dstUntilTime = 0;
        int dstCount = 0;
        AnnualTimeZoneRule finalDstRule = null;

        String stdName = null;
        int stdFromOffset = 0;
        int stdFromDSTSavings = 0;
        int stdToOffset = 0;
        int stdStartYear = 0;
        int stdMonth = 0;
        int stdDayOfWeek = 0;
        int stdWeekInMonth = 0;
        int stdMillisInDay = 0;
        long stdStartTime = 0;
        long stdUntilTime = 0;
        int stdCount = 0;
        AnnualTimeZoneRule finalStdRule = null;

        int[] dtfields = new int[6];
        boolean hasTransitions = false;

        // Going through all transitions
        while(true) {
            TimeZoneTransition tzt = basictz.getNextTransition(t, false);
            if (tzt == null) {
                break;
            }
            hasTransitions = true;
            t = tzt.getTime();
            String name = tzt.getTo().getName();
            boolean isDst = (tzt.getTo().getDSTSavings() != 0);
            int fromOffset = tzt.getFrom().getRawOffset() + tzt.getFrom().getDSTSavings();
            int fromDSTSavings = tzt.getFrom().getDSTSavings();
            int toOffset = tzt.getTo().getRawOffset() + tzt.getTo().getDSTSavings();
            Grego.timeToFields(tzt.getTime() + fromOffset, dtfields);
            int weekInMonth = Grego.getDayOfWeekInMonth(dtfields[0], dtfields[1], dtfields[2]);
            int year = dtfields[0];
            boolean sameRule = false;
            if (isDst) {
                if (finalDstRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
                    if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
                        finalDstRule = (AnnualTimeZoneRule)tzt.getTo();
                    }
                }
                if (dstCount > 0) {
                    if (year == dstStartYear + dstCount
                            && name.equals(dstName)
                            && dstFromOffset == fromOffset
                            && dstToOffset == toOffset
                            && dstMonth == dtfields[1]
                            && dstDayOfWeek == dtfields[3]
                            && dstWeekInMonth == weekInMonth
                            && dstMillisInDay == dtfields[5]) {
                        // Update until time
                        dstUntilTime = t;
                        dstCount++;
                        sameRule = true;
                    }
                    if (!sameRule) {
                        if (dstCount == 1) {
                            writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
                                    dstStartTime, true);
                        } else {
                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
                        }
                    }
                }
                if (!sameRule) {
                    // Reset this DST information
                    dstName = name;
                    dstFromOffset = fromOffset;
                    dstFromDSTSavings = fromDSTSavings;
                    dstToOffset = toOffset;
                    dstStartYear = year;
                    dstMonth = dtfields[1];
                    dstDayOfWeek = dtfields[3];
                    dstWeekInMonth = weekInMonth;
                    dstMillisInDay = dtfields[5];
                    dstStartTime = dstUntilTime = t;
                    dstCount = 1;
                }
                if (finalStdRule != null && finalDstRule != null) {
                    break;
                }
            } else {
                if (finalStdRule == null && tzt.getTo() instanceof AnnualTimeZoneRule) {
                    if (((AnnualTimeZoneRule)tzt.getTo()).getEndYear() == AnnualTimeZoneRule.MAX_YEAR) {
                        finalStdRule = (AnnualTimeZoneRule)tzt.getTo();
                    }
                }
                if (stdCount > 0) {
                    if (year == stdStartYear + stdCount
                            && name.equals(stdName)
                            && stdFromOffset == fromOffset
                            && stdToOffset == toOffset
                            && stdMonth == dtfields[1]
                            && stdDayOfWeek == dtfields[3]
                            && stdWeekInMonth == weekInMonth
                            && stdMillisInDay == dtfields[5]) {
                        // Update until time
                        stdUntilTime = t;
                        stdCount++;
                        sameRule = true;
                    }
                    if (!sameRule) {
                        if (stdCount == 1) {
                            writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
                                    stdStartTime, true);
                        } else {
                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
                        }
                    }
                }
                if (!sameRule) {
                    // Reset this STD information
                    stdName = name;
                    stdFromOffset = fromOffset;
                    stdFromDSTSavings = fromDSTSavings;
                    stdToOffset = toOffset;
                    stdStartYear = year;
                    stdMonth = dtfields[1];
                    stdDayOfWeek = dtfields[3];
                    stdWeekInMonth = weekInMonth;
                    stdMillisInDay = dtfields[5];
                    stdStartTime = stdUntilTime = t;
                    stdCount = 1;
                }
                if (finalStdRule != null && finalDstRule != null) {
                    break;
                }
            }
        }
        if (!hasTransitions) {
            // No transition - put a single non transition RDATE
            int offset = basictz.getOffset(0 /* any time */);
            boolean isDst = (offset != basictz.getRawOffset());
            writeZonePropsByTime(w, isDst, getDefaultTZName(basictz.getID(), isDst),
                    offset, offset, DEF_TZSTARTTIME - offset, false);
        } else {
            if (dstCount > 0) {
                if (finalDstRule == null) {
                    if (dstCount == 1) {
                        writeZonePropsByTime(w, true, dstName, dstFromOffset, dstToOffset,
                                dstStartTime, true);
                    } else {
                        writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
                                dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);
                    }
                } else {
                    if (dstCount == 1) {
                        writeFinalRule(w, true, finalDstRule,
                                dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, dstStartTime);
                    } else {
                        // Use a single rule if possible
                        if (isEquivalentDateRule(dstMonth, dstWeekInMonth, dstDayOfWeek, finalDstRule.getRule())) {
                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, MAX_TIME);
                        } else {
                            // Not equivalent rule - write out two different rules
                            writeZonePropsByDOW(w, true, dstName, dstFromOffset, dstToOffset,
                                    dstMonth, dstWeekInMonth, dstDayOfWeek, dstStartTime, dstUntilTime);

                            Date nextStart = finalDstRule.getNextStart(dstUntilTime,
                                    dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, false);

                            assert nextStart != null;
                            if (nextStart != null) {
                                writeFinalRule(w, true, finalDstRule,
                                        dstFromOffset - dstFromDSTSavings, dstFromDSTSavings, nextStart.getTime());
                            }
                        }
                    }
                }
            }
            if (stdCount > 0) {
                if (finalStdRule == null) {
                    if (stdCount == 1) {
                        writeZonePropsByTime(w, false, stdName, stdFromOffset, stdToOffset,
                                stdStartTime, true);
                    } else {
                        writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
                                stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);
                    }
                } else {
                    if (stdCount == 1) {
                        writeFinalRule(w, false, finalStdRule,
                                stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, stdStartTime);
                    } else {
                        // Use a single rule if possible
                        if (isEquivalentDateRule(stdMonth, stdWeekInMonth, stdDayOfWeek, finalStdRule.getRule())) {
                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, MAX_TIME);
                        } else {
                            // Not equivalent rule - write out two different rules
                            writeZonePropsByDOW(w, false, stdName, stdFromOffset, stdToOffset,
                                    stdMonth, stdWeekInMonth, stdDayOfWeek, stdStartTime, stdUntilTime);

                            Date nextStart = finalStdRule.getNextStart(stdUntilTime,
                                    stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, false);

                            assert nextStart != null;
                            if (nextStart != null) {
                                writeFinalRule(w, false, finalStdRule,
                                        stdFromOffset - stdFromDSTSavings, stdFromDSTSavings, nextStart.getTime());

                            }
                        }
                    }
                }
            }
        }
        writeFooter(w);
    }

    /*
     * Check if the DOW rule specified by month, weekInMonth and dayOfWeek is equivalent
     * to the DateTimerule.
     */
    private static boolean isEquivalentDateRule(int month, int weekInMonth, int dayOfWeek, DateTimeRule dtrule) {
        if (month != dtrule.getRuleMonth() || dayOfWeek != dtrule.getRuleDayOfWeek()) {
            return false;
        }
        if (dtrule.getTimeRuleType() != DateTimeRule.WALL_TIME) {
            // Do not try to do more intelligent comparison for now.
            return false;
        }
        if (dtrule.getDateRuleType() == DateTimeRule.DOW
                && dtrule.getRuleWeekInMonth() == weekInMonth) {
            return true;
        }
        int ruleDOM = dtrule.getRuleDayOfMonth();
        if (dtrule.getDateRuleType() == DateTimeRule.DOW_GEQ_DOM) {
            if (ruleDOM%7 == 1 && (ruleDOM + 6)/7 == weekInMonth) {
                return true;
            }
            if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 6
                    && weekInMonth == -1*((MONTHLENGTH[month]-ruleDOM+1)/7)) {
                return true;
            }
        }
        if (dtrule.getDateRuleType() == DateTimeRule.DOW_LEQ_DOM) {
            if (ruleDOM%7 == 0 && ruleDOM/7 == weekInMonth) {
                return true;
            }
            if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - ruleDOM)%7 == 0
                    && weekInMonth == -1*((MONTHLENGTH[month] - ruleDOM)/7 + 1)) {
                return true;
            }
        }
        return false;
    }

    /*
     * Write a single start time
     */
    private static void writeZonePropsByTime(Writer writer, boolean isDst, String tzname,
            int fromOffset, int toOffset, long time, boolean withRDATE) throws IOException {
        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, time);
        if (withRDATE) {
            writer.write(ICAL_RDATE);
            writer.write(COLON);
            writer.write(getDateTimeString(time + fromOffset));
            writer.write(NEWLINE);
        }
        endZoneProps(writer, isDst);
    }

    /*
     * Write start times defined by a DOM rule using VTIMEZONE RRULE
     */
    private static void writeZonePropsByDOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
            int month, int dayOfMonth, long startTime, long untilTime) throws IOException {
        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);

        beginRRULE(writer, month);
        writer.write(ICAL_BYMONTHDAY);
        writer.write(EQUALS_SIGN);
        writer.write(Integer.toString(dayOfMonth));

        if (untilTime != MAX_TIME) {
            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
        }
        writer.write(NEWLINE);

        endZoneProps(writer, isDst);
    }

    /*
     * Write start times defined by a DOW rule using VTIMEZONE RRULE
     */
    private static void writeZonePropsByDOW(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
            int month, int weekInMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
        beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);

        beginRRULE(writer, month);
        writer.write(ICAL_BYDAY);
        writer.write(EQUALS_SIGN);
        writer.write(Integer.toString(weekInMonth));    // -4, -3, -2, -1, 1, 2, 3, 4
        writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]);    // SU, MO, TU...

        if (untilTime != MAX_TIME) {
            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
        }
        writer.write(NEWLINE);

        endZoneProps(writer, isDst);
    }

    /*
     * Write start times defined by a DOW_GEQ_DOM rule using VTIMEZONE RRULE
     */
    private static void writeZonePropsByDOW_GEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
            int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
        // Check if this rule can be converted to DOW rule
        if (dayOfMonth%7 == 1) {
            // Can be represented by DOW rule
            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
                    month, (dayOfMonth + 6)/7, dayOfWeek, startTime, untilTime);
        } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 6) {
            // Can be represented by DOW rule with negative week number
            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
                    month, -1*((MONTHLENGTH[month] - dayOfMonth + 1)/7), dayOfWeek, startTime, untilTime);
        } else {
            // Otherwise, use BYMONTHDAY to include all possible dates
            beginZoneProps(writer, isDst, tzname, fromOffset, toOffset, startTime);

            // Check if all days are in the same month
            int startDay = dayOfMonth;
            int currentMonthDays = 7;

            if (dayOfMonth <= 0) {
                // The start day is in previous month
                int prevMonthDays = 1 - dayOfMonth;
                currentMonthDays -= prevMonthDays;

                int prevMonth = (month - 1) < 0 ? 11 : month - 1;

                // Note: When a rule is separated into two, UNTIL attribute needs to be
                // calculated for each of them.  For now, we skip this, because we basically use this method
                // only for final rules, which does not have the UNTIL attribute
                writeZonePropsByDOW_GEQ_DOM_sub(writer, prevMonth, -prevMonthDays, dayOfWeek, prevMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);

                // Start from 1 for the rest
                startDay = 1;
            } else if (dayOfMonth + 6 > MONTHLENGTH[month]) {
                // Note: This code does not actually work well in February.  For now, days in month in
                // non-leap year.
                int nextMonthDays = dayOfMonth + 6 - MONTHLENGTH[month];
                currentMonthDays -= nextMonthDays;

                int nextMonth = (month + 1) > 11 ? 0 : month + 1;

                writeZonePropsByDOW_GEQ_DOM_sub(writer, nextMonth, 1, dayOfWeek, nextMonthDays, MAX_TIME /* Do not use UNTIL */, fromOffset);
            }
            writeZonePropsByDOW_GEQ_DOM_sub(writer, month, startDay, dayOfWeek, currentMonthDays, untilTime, fromOffset);
            endZoneProps(writer, isDst);
        }
    }

    /*
     * Called from writeZonePropsByDOW_GEQ_DOM
     */
    private static void writeZonePropsByDOW_GEQ_DOM_sub(Writer writer, int month,
            int dayOfMonth, int dayOfWeek, int numDays, long untilTime, int fromOffset) throws IOException {

        int startDayNum = dayOfMonth;
        boolean isFeb = (month == Calendar.FEBRUARY);
        if (dayOfMonth < 0 && !isFeb) {
            // Use positive number if possible
            startDayNum = MONTHLENGTH[month] + dayOfMonth + 1;
        }
        beginRRULE(writer, month);
        writer.write(ICAL_BYDAY);
        writer.write(EQUALS_SIGN);
        writer.write(ICAL_DOW_NAMES[dayOfWeek - 1]);    // SU, MO, TU...
        writer.write(SEMICOLON);
        writer.write(ICAL_BYMONTHDAY);
        writer.write(EQUALS_SIGN);

        writer.write(Integer.toString(startDayNum));
        for (int i = 1; i < numDays; i++) {
            writer.write(COMMA);
            writer.write(Integer.toString(startDayNum + i));
        }

        if (untilTime != MAX_TIME) {
            appendUNTIL(writer, getDateTimeString(untilTime + fromOffset));
        }
        writer.write(NEWLINE);
    }

    /*
     * Write start times defined by a DOW_LEQ_DOM rule using VTIMEZONE RRULE
     */
    private static void writeZonePropsByDOW_LEQ_DOM(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset,
            int month, int dayOfMonth, int dayOfWeek, long startTime, long untilTime) throws IOException {
        // Check if this rule can be converted to DOW rule
        if (dayOfMonth%7 == 0) {
            // Can be represented by DOW rule
            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
                    month, dayOfMonth/7, dayOfWeek, startTime, untilTime);
        } else if (month != Calendar.FEBRUARY && (MONTHLENGTH[month] - dayOfMonth)%7 == 0){
            // Can be represented by DOW rule with negative week number
            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
                    month, -1*((MONTHLENGTH[month] - dayOfMonth)/7 + 1), dayOfWeek, startTime, untilTime);
        } else if (month == Calendar.FEBRUARY && dayOfMonth == 29) {
            // Special case for February
            writeZonePropsByDOW(writer, isDst, tzname, fromOffset, toOffset,
                    Calendar.FEBRUARY, -1, dayOfWeek, startTime, untilTime);
        } else {
            // Otherwise, convert this to DOW_GEQ_DOM rule
            writeZonePropsByDOW_GEQ_DOM(writer, isDst, tzname, fromOffset, toOffset,
                    month, dayOfMonth - 6, dayOfWeek, startTime, untilTime);
        }
    }

    /*
     * Write the final time zone rule using RRULE, with no UNTIL attribute
     */
    private static void writeFinalRule(Writer writer, boolean isDst, AnnualTimeZoneRule rule,
            int fromRawOffset, int fromDSTSavings, long startTime) throws IOException{
        DateTimeRule dtrule = toWallTimeRule(rule.getRule(), fromRawOffset, fromDSTSavings);

        // If the rule's mills in a day is out of range, adjust start time.
        // Olson tzdata supports 24:00 of a day, but VTIMEZONE does not.
        // See ticket#7008/#7518

        int timeInDay = dtrule.getRuleMillisInDay();
        if (timeInDay < 0) {
            startTime = startTime + (0 - timeInDay);
        } else if (timeInDay >= Grego.MILLIS_PER_DAY) {
            startTime = startTime - (timeInDay - (Grego.MILLIS_PER_DAY - 1));
        }

        int toOffset = rule.getRawOffset() + rule.getDSTSavings();
        switch (dtrule.getDateRuleType()) {
        case DateTimeRule.DOM:
            writeZonePropsByDOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), startTime, MAX_TIME);
            break;
        case DateTimeRule.DOW:
            writeZonePropsByDOW(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
                    dtrule.getRuleMonth(), dtrule.getRuleWeekInMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
            break;
        case DateTimeRule.DOW_GEQ_DOM:
            writeZonePropsByDOW_GEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
            break;
        case DateTimeRule.DOW_LEQ_DOM:
            writeZonePropsByDOW_LEQ_DOM(writer, isDst, rule.getName(), fromRawOffset + fromDSTSavings, toOffset,
                    dtrule.getRuleMonth(), dtrule.getRuleDayOfMonth(), dtrule.getRuleDayOfWeek(), startTime, MAX_TIME);
            break;
        }
    }

    /*
     * Convert the rule to its equivalent rule using WALL_TIME mode
     */
    private static DateTimeRule toWallTimeRule(DateTimeRule rule, int rawOffset, int dstSavings) {
        if (rule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
            return rule;
        }
        int wallt = rule.getRuleMillisInDay();
        if (rule.getTimeRuleType() == DateTimeRule.UTC_TIME) {
            wallt += (rawOffset + dstSavings);
        } else if (rule.getTimeRuleType() == DateTimeRule.STANDARD_TIME) {
            wallt += dstSavings;
        }

        int month = -1, dom = 0, dow = 0, dtype = -1;
        int dshift = 0;
        if (wallt < 0) {
            dshift = -1;
            wallt += Grego.MILLIS_PER_DAY;
        } else if (wallt >= Grego.MILLIS_PER_DAY) {
            dshift = 1;
            wallt -= Grego.MILLIS_PER_DAY;
        }

        month = rule.getRuleMonth();
        dom = rule.getRuleDayOfMonth();
        dow = rule.getRuleDayOfWeek();
        dtype = rule.getDateRuleType();

        if (dshift != 0) {
            if (dtype == DateTimeRule.DOW) {
                // Convert to DOW_GEW_DOM or DOW_LEQ_DOM rule first
                int wim = rule.getRuleWeekInMonth();
                if (wim > 0) {
                    dtype = DateTimeRule.DOW_GEQ_DOM;
                    dom = 7 * (wim - 1) + 1;
                } else {
                    dtype = DateTimeRule.DOW_LEQ_DOM;
                    dom = MONTHLENGTH[month] + 7 * (wim + 1);
                }

            }
            // Shift one day before or after
            dom += dshift;
            if (dom == 0) {
                month--;
                month = month < Calendar.JANUARY ? Calendar.DECEMBER : month;
                dom = MONTHLENGTH[month];
            } else if (dom > MONTHLENGTH[month]) {
                month++;
                month = month > Calendar.DECEMBER ? Calendar.JANUARY : month;
                dom = 1;
            }
            if (dtype != DateTimeRule.DOM) {
                // Adjust day of week
                dow += dshift;
                if (dow < Calendar.SUNDAY) {
                    dow = Calendar.SATURDAY;
                } else if (dow > Calendar.SATURDAY) {
                    dow = Calendar.SUNDAY;
                }
            }
        }
        // Create a new rule
        DateTimeRule modifiedRule;
        if (dtype == DateTimeRule.DOM) {
            modifiedRule = new DateTimeRule(month, dom, wallt, DateTimeRule.WALL_TIME);
        } else {
            modifiedRule = new DateTimeRule(month, dom, dow,
                    (dtype == DateTimeRule.DOW_GEQ_DOM), wallt, DateTimeRule.WALL_TIME);
        }
        return modifiedRule;
    }

    /*
     * Write the opening section of zone properties
     */
    private static void beginZoneProps(Writer writer, boolean isDst, String tzname, int fromOffset, int toOffset, long startTime) throws IOException {
        writer.write(ICAL_BEGIN);
        writer.write(COLON);
        if (isDst) {
            writer.write(ICAL_DAYLIGHT);
        } else {
            writer.write(ICAL_STANDARD);
        }
        writer.write(NEWLINE);

        // TZOFFSETTO
        writer.write(ICAL_TZOFFSETTO);
        writer.write(COLON);
        writer.write(millisToOffset(toOffset));
        writer.write(NEWLINE);

        // TZOFFSETFROM
        writer.write(ICAL_TZOFFSETFROM);
        writer.write(COLON);
        writer.write(millisToOffset(fromOffset));
        writer.write(NEWLINE);

        // TZNAME
        writer.write(ICAL_TZNAME);
        writer.write(COLON);
        writer.write(tzname);
        writer.write(NEWLINE);

        // DTSTART
        writer.write(ICAL_DTSTART);
        writer.write(COLON);
        writer.write(getDateTimeString(startTime + fromOffset));
        writer.write(NEWLINE);
    }

    /*
     * Writes the closing section of zone properties
     */
    private static void endZoneProps(Writer writer, boolean isDst) throws IOException{
        // END:STANDARD or END:DAYLIGHT
        writer.write(ICAL_END);
        writer.write(COLON);
        if (isDst) {
            writer.write(ICAL_DAYLIGHT);
        } else {
            writer.write(ICAL_STANDARD);
        }
        writer.write(NEWLINE);
    }

    /*
     * Write the beginning part of RRULE line
     */
    private static void beginRRULE(Writer writer, int month) throws IOException {
        writer.write(ICAL_RRULE);
        writer.write(COLON);
        writer.write(ICAL_FREQ);
        writer.write(EQUALS_SIGN);
        writer.write(ICAL_YEARLY);
        writer.write(SEMICOLON);
        writer.write(ICAL_BYMONTH);
        writer.write(EQUALS_SIGN);
        writer.write(Integer.toString(month + 1));
        writer.write(SEMICOLON);
    }

    /*
     * Append the UNTIL attribute after RRULE line
     */
    private static void appendUNTIL(Writer writer, String until) throws IOException {
        if (until != null) {
            writer.write(SEMICOLON);
            writer.write(ICAL_UNTIL);
            writer.write(EQUALS_SIGN);
            writer.write(until);
        }
    }

    /*
     * Write the opening section of the VTIMEZONE block
     */
    private void writeHeader(Writer writer)throws IOException {
        writer.write(ICAL_BEGIN);
        writer.write(COLON);
        writer.write(ICAL_VTIMEZONE);
        writer.write(NEWLINE);
        writer.write(ICAL_TZID);
        writer.write(COLON);
        writer.write(tz.getID());
        writer.write(NEWLINE);
        if (tzurl != null) {
            writer.write(ICAL_TZURL);
            writer.write(COLON);
            writer.write(tzurl);
            writer.write(NEWLINE);
        }
        if (lastmod != null) {
            writer.write(ICAL_LASTMOD);
            writer.write(COLON);
            writer.write(getUTCDateTimeString(lastmod.getTime()));
            writer.write(NEWLINE);
        }
    }

    /*
     * Write the closing section of the VTIMEZONE definition block
     */
    private static void writeFooter(Writer writer) throws IOException {
        writer.write(ICAL_END);
        writer.write(COLON);
        writer.write(ICAL_VTIMEZONE);
        writer.write(NEWLINE);
    }

    /*
     * Convert date/time to RFC2445 Date-Time form #1 DATE WITH LOCAL TIME
     */
    private static String getDateTimeString(long time) {
        int[] fields = Grego.timeToFields(time, null);
        StringBuilder sb = new StringBuilder(15);
        sb.append(numToString(fields[0], 4));
        sb.append(numToString(fields[1] + 1, 2));
        sb.append(numToString(fields[2], 2));
        sb.append('T');

        int t = fields[5];
        int hour = t / Grego.MILLIS_PER_HOUR;
        t %= Grego.MILLIS_PER_HOUR;
        int min = t / Grego.MILLIS_PER_MINUTE;
        t %= Grego.MILLIS_PER_MINUTE;
        int sec = t / Grego.MILLIS_PER_SECOND;

        sb.append(numToString(hour, 2));
        sb.append(numToString(min, 2));
        sb.append(numToString(sec, 2));
        return sb.toString();
    }

    /*
     * Convert date/time to RFC2445 Date-Time form #2 DATE WITH UTC TIME
     */
    private static String getUTCDateTimeString(long time) {
        return getDateTimeString(time) + "Z";
    }

    /*
     * Parse RFC2445 Date-Time form #1 DATE WITH LOCAL TIME and
     * #2 DATE WITH UTC TIME
     */
    private static long parseDateTimeString(String str, int offset) {
        int year = 0, month = 0, day = 0, hour = 0, min = 0, sec = 0;
        boolean isUTC = false;
        boolean isValid = false;
        do {
            if (str == null) {
                break;
            }

            int length = str.length();
            if (length != 15 && length != 16) {
                // FORM#1 15 characters, such as "20060317T142115"
                // FORM#2 16 characters, such as "20060317T142115Z"
                break;
            }
            if (str.charAt(8) != 'T') {
                // character "T" must be used for separating date and time
                break;
            }
            if (length == 16) {
                if (str.charAt(15) != 'Z') {
                    // invalid format
                    break;
                }
                isUTC = true;
            }

            try {
                year = Integer.parseInt(str.substring(0, 4));
                month = Integer.parseInt(str.substring(4, 6)) - 1;  // 0-based
                day = Integer.parseInt(str.substring(6, 8));
                hour = Integer.parseInt(str.substring(9, 11));
                min = Integer.parseInt(str.substring(11, 13));
                sec = Integer.parseInt(str.substring(13, 15));
            } catch (NumberFormatException nfe) {
                break;
            }

            // check valid range
            int maxDayOfMonth = Grego.monthLength(year, month);
            if (year < 0 || month < 0 || month > 11 || day < 1 || day > maxDayOfMonth ||
                    hour < 0 || hour >= 24 || min < 0 || min >= 60 || sec < 0 || sec >= 60) {
                break;
            }

            isValid = true;
        } while(false);

        if (!isValid) {
            throw new IllegalArgumentException("Invalid date time string format");
        }
        // Calculate the time
        long time = Grego.fieldsToDay(year, month, day) * Grego.MILLIS_PER_DAY;
        time += (hour*Grego.MILLIS_PER_HOUR + min*Grego.MILLIS_PER_MINUTE + sec*Grego.MILLIS_PER_SECOND);
        if (!isUTC) {
            time -= offset;
        }
        return time;
    }

    /*
     * Convert RFC2445 utc-offset string to milliseconds
     */
    private static int offsetStrToMillis(String str) {
        boolean isValid = false;
        int sign = 0, hour = 0, min = 0, sec = 0;

        do {
            if (str == null) {
                break;
            }
            int length = str.length();
            if (length != 5 && length != 7) {
                // utf-offset must be 5 or 7 characters
                break;
            }
            // sign
            char s = str.charAt(0);
            if (s == '+') {
                sign = 1;
            } else if (s == '-') {
                sign = -1;
            } else {
                // utf-offset must start with "+" or "-"
                break;
            }

            try {
                hour = Integer.parseInt(str.substring(1, 3));
                min = Integer.parseInt(str.substring(3, 5));
                if (length == 7) {
                    sec = Integer.parseInt(str.substring(5, 7));
                }
            } catch (NumberFormatException nfe) {
                break;
            }
            isValid = true;
        } while(false);

        if (!isValid) {
            throw new IllegalArgumentException("Bad offset string");
        }
        int millis = sign * ((hour * 60 + min) * 60 + sec) * 1000;
        return millis;
    }

    /*
     * Convert milliseconds to RFC2445 utc-offset string
     */
    private static String millisToOffset(int millis) {
        StringBuilder sb = new StringBuilder(7);
        if (millis >= 0) {
            sb.append('+');
        } else {
            sb.append('-');
            millis = -millis;
        }
        int hour, min, sec;
        int t = millis / 1000;

        sec = t % 60;
        t = (t - sec) / 60;
        min = t % 60;
        hour = t / 60;

        sb.append(numToString(hour, 2));
        sb.append(numToString(min, 2));
        sb.append(numToString(sec, 2));

        return sb.toString();
    }

    /*
     * Format integer number
     */
    private static String numToString(int num, int width) {
        String str = Integer.toString(num);
        int len = str.length();
        if (len >= width) {
            return str.substring(len - width, len);
        }
        StringBuilder sb = new StringBuilder(width);
        for (int i = len; i < width; i++) {
            sb.append('0');
        }
        sb.append(str);
        return sb.toString();
    }

    // Freezable stuffs
    private volatile transient boolean isFrozen = false;

    /**
     * {@inheritDoc}
     * @stable ICU 49
     */
    @Override
    public boolean isFrozen() {
        return isFrozen;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 49
     */
    @Override
    public TimeZone freeze() {
        isFrozen = true;
        return this;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 49
     */
    @Override
    public TimeZone cloneAsThawed() {
        VTimeZone vtz = (VTimeZone)super.cloneAsThawed();
        vtz.tz = (BasicTimeZone)tz.cloneAsThawed();
        vtz.isFrozen = false;
        return vtz;
    }
}
