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

import com.ibm.icu.impl.Grego;


/**
 * <code>AnnualTimeZoneRule</code> is a class used for representing a time zone
 * rule which takes effect annually.  Years used in this class are
 * all Gregorian calendar years.
 *
 * @stable ICU 3.8
 */
public class AnnualTimeZoneRule extends TimeZoneRule {

    private static final long serialVersionUID = -8870666707791230688L;

    /**
     * The constant representing the maximum year used for designating a rule is permanent.
     * @stable ICU 3.8
     */
    public static final int MAX_YEAR = Integer.MAX_VALUE;

    private final DateTimeRule dateTimeRule;
    private final int startYear;
    private final int endYear;

    /**
     * Constructs a <code>AnnualTimeZoneRule</code> with the name, the GMT offset of its
     * standard time, the amount of daylight saving offset adjustment,
     * the annual start time rule and the start/until years.
     *
     * @param name          The time zone name.
     * @param rawOffset     The GMT offset of its standard time in milliseconds.
     * @param dstSavings    The amount of daylight saving offset adjustment in
     *                      milliseconds.  If this ia a rule for standard time,
     *                      the value of this argument is 0.
     * @param dateTimeRule  The start date/time rule repeated annually.
     * @param startYear     The first year when this rule takes effect.
     * @param endYear       The last year when this rule takes effect.  If this
     *                      rule is effective forever in future, specify MAX_YEAR.
     *
     * @stable ICU 3.8
     */
    public AnnualTimeZoneRule(String name, int rawOffset, int dstSavings,
            DateTimeRule dateTimeRule, int startYear, int endYear) {
        super(name, rawOffset, dstSavings);
        this.dateTimeRule = dateTimeRule;
        this.startYear = startYear;
        this.endYear = endYear;
    }

    /**
     * Gets the start date/time rule associated used by this rule.
     *
     * @return  An <code>AnnualDateTimeRule</code> which represents the start date/time
     *          rule used by this time zone rule.
     *
     * @stable ICU 3.8
     */
    public DateTimeRule getRule() {
        return dateTimeRule;
    }

    /**
     * Gets the first year when this rule takes effect.
     *
     * @return  The start year of this rule.  The year is in Gregorian calendar
     *          with 0 == 1 BCE, -1 == 2 BCE, etc.
     *
     * @stable ICU 3.8
     */
    public int getStartYear() {
        return startYear;
    }

    /**
     * Gets the end year when this rule takes effect.
     *
     * @return  The end year of this rule (inclusive). The year is in Gregorian calendar
     *          with 0 == 1 BCE, -1 == 2 BCE, etc.
     *
     * @stable ICU 3.8
     */
    public int getEndYear() {
        return endYear;
    }

    /**
     * Gets the time when this rule takes effect in the given year.
     *
     * @param year              The Gregorian year, with 0 == 1 BCE, -1 == 2 BCE, etc.
     * @param prevRawOffset     The standard time offset from UTC before this rule
     *                          takes effect in milliseconds.
     * @param prevDSTSavings    The amount of daylight saving offset from the
     *                          standard time.
     *
     * @return  The time when this rule takes effect in the year, or
     *          null if this rule is not applicable in the year.
     *
     * @stable ICU 3.8
     */
    public Date getStartInYear(int year, int prevRawOffset, int prevDSTSavings) {
        if (year < startYear || year > endYear) {
            return null;
        }

        long ruleDay;
        int type = dateTimeRule.getDateRuleType();

        if (type == DateTimeRule.DOM) {
            ruleDay = Grego.fieldsToDay(year, dateTimeRule.getRuleMonth(), dateTimeRule.getRuleDayOfMonth());
        } else {
            boolean after = true;
            if (type == DateTimeRule.DOW) {
                int weeks = dateTimeRule.getRuleWeekInMonth();
                if (weeks > 0) {
                    ruleDay = Grego.fieldsToDay(year, dateTimeRule.getRuleMonth(), 1);
                    ruleDay += 7 * (weeks - 1);
                } else {
                    after = false;
                    ruleDay = Grego.fieldsToDay(year, dateTimeRule.getRuleMonth(),
                            Grego.monthLength(year, dateTimeRule.getRuleMonth()));
                    ruleDay += 7 * (weeks + 1);
                }
            } else {
                int month = dateTimeRule.getRuleMonth();
                int dom = dateTimeRule.getRuleDayOfMonth();
                if (type == DateTimeRule.DOW_LEQ_DOM) {
                    after = false;
                    // Handle Feb <=29
                    if (month == Calendar.FEBRUARY && dom == 29 && !Grego.isLeapYear(year)) {
                        dom--;
                    }
                }
                ruleDay = Grego.fieldsToDay(year, month, dom);
            }

            int dow = Grego.dayOfWeek(ruleDay);
            int delta = dateTimeRule.getRuleDayOfWeek() - dow;
            if (after) {
                delta = delta < 0 ? delta + 7 : delta;
            } else {
                delta = delta > 0 ? delta - 7 : delta;
            }
            ruleDay += delta;
        }

        long ruleTime = ruleDay * Grego.MILLIS_PER_DAY + dateTimeRule.getRuleMillisInDay();
        if (dateTimeRule.getTimeRuleType() != DateTimeRule.UTC_TIME) {
            ruleTime -= prevRawOffset;
        }
        if (dateTimeRule.getTimeRuleType() == DateTimeRule.WALL_TIME) {
            ruleTime -= prevDSTSavings;
        }
        return new Date(ruleTime);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public Date getFirstStart(int prevRawOffset, int prevDSTSavings) {
        return getStartInYear(startYear, prevRawOffset, prevDSTSavings);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public Date getFinalStart(int prevRawOffset, int prevDSTSavings) {
        if (endYear == MAX_YEAR) {
            return null;
        }
        return getStartInYear(endYear, prevRawOffset, prevDSTSavings);
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public Date getNextStart(long base, int prevRawOffset, int prevDSTSavings, boolean inclusive) {
        int[] fields = Grego.timeToFields(base, null);
        int year = fields[0];
        if (year < startYear) {
            return getFirstStart(prevRawOffset, prevDSTSavings);
        }
        Date d = getStartInYear(year, prevRawOffset, prevDSTSavings);
        if (d != null && (d.getTime() < base || (!inclusive && (d.getTime() == base)))) {
            d = getStartInYear(year + 1, prevRawOffset, prevDSTSavings);
        }
        return d;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public Date getPreviousStart(long base, int prevRawOffset, int prevDSTSavings, boolean inclusive) {
        int[] fields = Grego.timeToFields(base, null);
        int year = fields[0];
        if (year > endYear) {
            return getFinalStart(prevRawOffset, prevDSTSavings);
        }
        Date d = getStartInYear(year, prevRawOffset, prevDSTSavings);
        if (d != null && (d.getTime() > base || (!inclusive && (d.getTime() == base)))) {
            d = getStartInYear(year - 1, prevRawOffset, prevDSTSavings);
        }
        return d;
    }

    /**
     * {@inheritDoc}
     * @stable ICU 3.8
     */
    @Override
    public boolean isEquivalentTo(TimeZoneRule other) {
        if (!(other instanceof AnnualTimeZoneRule)) {
            return false;
        }
        AnnualTimeZoneRule otherRule = (AnnualTimeZoneRule)other;
        if (startYear == otherRule.startYear
                && endYear == otherRule.endYear
                && dateTimeRule.equals(otherRule.dateTimeRule)) {
            return super.isEquivalentTo(other);
        }
        return false;
    }

    /**
     * {@inheritDoc}<br><br>
     * Note: This method in <code>AnnualTimeZoneRule</code> always returns true.
     * @stable ICU 3.8
     */
    @Override
    public boolean isTransitionRule() {
        return true;
    }

    /**
     * Returns a <code>String</code> representation of this <code>AnnualTimeZoneRule</code> object.
     * This method is used for debugging purpose only.  The string representation can be changed
     * in future version of ICU without any notice.
     *
     * @stable ICU 3.8
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();
        buf.append(super.toString());
        buf.append(", rule={" + dateTimeRule + "}");
        buf.append(", startYear=" + startYear);
        buf.append(", endYear=");
        if (endYear == MAX_YEAR) {
            buf.append("max");
        } else {
            buf.append(endYear);
        }
        return buf.toString();
    }
}
