/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (c) 2003, 2004, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.util.calendar;

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

/**
 * The <code>AbstractCalendar</code> class provides a framework for
 * implementing a concrete calendar system.
 *
 * <p><a name="fixed_date"></a><B>Fixed Date</B><br>
 *
 * For implementing a concrete calendar system, each calendar must
 * have the common date numbering, starting from midnight the onset of
 * Monday, January 1, 1 (Gregorian). It is called a <I>fixed date</I>
 * in this class. January 1, 1 (Gregorian) is fixed date 1. (See
 * Nachum Dershowitz and Edward M. Reingold, <I>CALENDRICAL
 * CALCULATION The Millennium Edition</I>, Section 1.2 for details.)
 *
 * @author Masayoshi Okutsu
 * @since 1.5
 */

public abstract class AbstractCalendar extends CalendarSystem {

    // The constants assume no leap seconds support.
    static final int SECOND_IN_MILLIS = 1000;
    static final int MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
    static final int HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
    static final int DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;

    // The number of days between January 1, 1 and January 1, 1970 (Gregorian)
    static final int EPOCH_OFFSET = 719163;

    private Era[] eras;

    protected AbstractCalendar() {
    }

    public Era getEra(String eraName) {
        if (eras != null) {
            for (int i = 0; i < eras.length; i++) {
                if (eras[i].equals(eraName)) {
                    return eras[i];
                }
            }
        }
        return null;
    }

    public Era[] getEras() {
        Era[] e = null;
        if (eras != null) {
            e = new Era[eras.length];
            System.arraycopy(eras, 0, e, 0, eras.length);
        }
        return e;
    }

    public void setEra(CalendarDate date, String eraName) {
        if (eras == null) {
            return; // should report an error???
        }
        for (int i = 0; i < eras.length; i++) {
            Era e = eras[i];
            if (e != null && e.getName().equals(eraName)) {
                date.setEra(e);
                return;
            }
        }
        throw new IllegalArgumentException("unknown era name: " + eraName);
    }

    protected void setEras(Era[] eras) {
        this.eras = eras;
    }

    public CalendarDate getCalendarDate() {
        return getCalendarDate(System.currentTimeMillis(), newCalendarDate());
    }

    public CalendarDate getCalendarDate(long millis) {
        return getCalendarDate(millis, newCalendarDate());
    }

    public CalendarDate getCalendarDate(long millis, TimeZone zone) {
        CalendarDate date = newCalendarDate(zone);
        return getCalendarDate(millis, date);
    }

    public CalendarDate getCalendarDate(long millis, CalendarDate date) {
        int ms = 0;             // time of day
        int zoneOffset = 0;
        int saving = 0;
        long days = 0;          // fixed date

        // adjust to local time if `date' has time zone.
        TimeZone zi = date.getZone();
        if (zi != null) {
            int[] offsets = new int[2];
            zoneOffset = zi.getOffset(millis);
            offsets[0] = zi.getRawOffset();
            offsets[1] = zoneOffset - offsets[0];

            // We need to calculate the given millis and time zone
            // offset separately for java.util.GregorianCalendar
            // compatibility. (i.e., millis + zoneOffset could cause
            // overflow or underflow, which must be avoided.) Usually
            // days should be 0 and ms is in the range of -13:00 to
            // +14:00. However, we need to deal with extreme cases.
            days = zoneOffset / DAY_IN_MILLIS;
            ms = zoneOffset % DAY_IN_MILLIS;
            saving = offsets[1];
        }
        date.setZoneOffset(zoneOffset);
        date.setDaylightSaving(saving);

        days += millis / DAY_IN_MILLIS;
        ms += (int) (millis % DAY_IN_MILLIS);
        if (ms >= DAY_IN_MILLIS) {
            // at most ms is (DAY_IN_MILLIS - 1) * 2.
            ms -= DAY_IN_MILLIS;
            ++days;
        } else {
            // at most ms is (1 - DAY_IN_MILLIS) * 2. Adding one
            // DAY_IN_MILLIS results in still negative.
            while (ms < 0) {
                ms += DAY_IN_MILLIS;
                --days;
            }
        }

        // convert to fixed date (offset from Jan. 1, 1 (Gregorian))
        days += EPOCH_OFFSET;

        // calculate date fields from the fixed date
        getCalendarDateFromFixedDate(date, days);

        // calculate time fields from the time of day
        setTimeOfDay(date, ms);
        date.setLeapYear(isLeapYear(date));
        date.setNormalized(true);
        return date;
    }

    public long getTime(CalendarDate date) {
        long gd = getFixedDate(date);
        long ms = (gd - EPOCH_OFFSET) * DAY_IN_MILLIS + getTimeOfDay(date);
        int zoneOffset = 0;
        TimeZone zi = date.getZone();
        if (zi != null) {
            if (date.isNormalized()) {
                return ms - date.getZoneOffset();
            }
            // adjust time zone and daylight saving
            int[] offsets = new int[2];
            if (date.isStandardTime()) {
                // 1) 2:30am during starting-DST transition is
                //    intrepreted as 2:30am ST
                // 2) 5:00pm during DST is still interpreted as 5:00pm ST
                // 3) 1:30am during ending-DST transition is interpreted
                //    as 1:30am ST (after transition)
                zoneOffset = zi.getOffset(ms - zi.getRawOffset());
            } else {
                // 1) 2:30am during starting-DST transition is
                //    intrepreted as 3:30am DT
                // 2) 5:00pm during DST is intrepreted as 5:00pm DT
                // 3) 1:30am during ending-DST transition is interpreted
                //    as 1:30am DT/0:30am ST (before transition)
                zoneOffset = zi.getOffset(ms - zi.getRawOffset());
            }
        }
        ms -= zoneOffset;
        getCalendarDate(ms, date);
        return ms;
    }

    protected long getTimeOfDay(CalendarDate date) {
        long fraction = date.getTimeOfDay();
        if (fraction != CalendarDate.TIME_UNDEFINED) {
            return fraction;
        }
        fraction = getTimeOfDayValue(date);
        date.setTimeOfDay(fraction);
        return fraction;
    }

    public long getTimeOfDayValue(CalendarDate date) {
        long fraction = date.getHours();
        fraction *= 60;
        fraction += date.getMinutes();
        fraction *= 60;
        fraction += date.getSeconds();
        fraction *= 1000;
        fraction += date.getMillis();
        return fraction;
    }

    public CalendarDate setTimeOfDay(CalendarDate cdate, int fraction) {
        if (fraction < 0) {
            throw new IllegalArgumentException();
        }
        boolean normalizedState = cdate.isNormalized();
        int time = fraction;
        int hours = time / HOUR_IN_MILLIS;
        time %= HOUR_IN_MILLIS;
        int minutes = time / MINUTE_IN_MILLIS;
        time %= MINUTE_IN_MILLIS;
        int seconds = time / SECOND_IN_MILLIS;
        time %= SECOND_IN_MILLIS;
        cdate.setHours(hours);
        cdate.setMinutes(minutes);
        cdate.setSeconds(seconds);
        cdate.setMillis(time);
        cdate.setTimeOfDay(fraction);
        if (hours < 24 && normalizedState) {
            // If this time of day setting doesn't affect the date,
            // then restore the normalized state.
            cdate.setNormalized(normalizedState);
        }
        return cdate;
    }

    /**
     * Returns 7 in this default implementation.
     *
     * @return 7
     */
    public int getWeekLength() {
        return 7;
    }

    protected abstract boolean isLeapYear(CalendarDate date);

    public CalendarDate getNthDayOfWeek(int nth, int dayOfWeek, CalendarDate date) {
        CalendarDate ndate = (CalendarDate) date.clone();
        normalize(ndate);
        long fd = getFixedDate(ndate);
        long nfd;
        if (nth > 0) {
            nfd = 7 * nth + getDayOfWeekDateBefore(fd, dayOfWeek);
        } else {
            nfd = 7 * nth + getDayOfWeekDateAfter(fd, dayOfWeek);
        }
        getCalendarDateFromFixedDate(ndate, nfd);
        return ndate;
    }

    /**
     * Returns a date of the given day of week before the given fixed
     * date.
     *
     * @param fixedDate the fixed date
     * @param dayOfWeek the day of week
     * @return the calculated date
     */
    static long getDayOfWeekDateBefore(long fixedDate, int dayOfWeek) {
        return getDayOfWeekDateOnOrBefore(fixedDate - 1, dayOfWeek);
    }

    /**
     * Returns a date of the given day of week that is closest to and
     * after the given fixed date.
     *
     * @param fixedDate the fixed date
     * @param dayOfWeek the day of week
     * @return the calculated date
     */
    static long getDayOfWeekDateAfter(long fixedDate, int dayOfWeek) {
        return getDayOfWeekDateOnOrBefore(fixedDate + 7, dayOfWeek);
    }

    /**
     * Returns a date of the given day of week on or before the given fixed
     * date.
     *
     * @param fixedDate the fixed date
     * @param dayOfWeek the day of week
     * @return the calculated date
     */
    // public for java.util.GregorianCalendar
    public static long getDayOfWeekDateOnOrBefore(long fixedDate, int dayOfWeek) {
        long fd = fixedDate - (dayOfWeek - 1);
        if (fd >= 0) {
            return fixedDate - (fd % 7);
        }
        return fixedDate - CalendarUtils.mod(fd, 7);
    }

    /**
     * Returns the fixed date calculated with the specified calendar
     * date. If the specified date is not normalized, its date fields
     * are normalized.
     *
     * @param date a <code>CalendarDate</code> with which the fixed
     * date is calculated
     * @return the calculated fixed date
     * @see AbstractCalendar.html#fixed_date
     */
    protected abstract long getFixedDate(CalendarDate date);

    /**
     * Calculates calendar fields from the specified fixed date. This
     * method stores the calculated calendar field values in the specified
     * <code>CalendarDate</code>.
     *
     * @param date a <code>CalendarDate</code> to stored the
     * calculated calendar fields.
     * @param fixedDate a fixed date to calculate calendar fields
     * @see AbstractCalendar.html#fixed_date
     */
    protected abstract void getCalendarDateFromFixedDate(CalendarDate date,
                                                         long fixedDate);

    public boolean validateTime(CalendarDate date) {
        int t = date.getHours();
        if (t < 0 || t >= 24) {
            return false;
        }
        t = date.getMinutes();
        if (t < 0 || t >= 60) {
            return false;
        }
        t = date.getSeconds();
        // TODO: Leap second support.
        if (t < 0 || t >= 60) {
            return false;
        }
        t = date.getMillis();
        if (t < 0 || t >= 1000) {
            return false;
        }
        return true;
    }


    int normalizeTime(CalendarDate date) {
        long fraction = getTimeOfDay(date);
        long days = 0;

        if (fraction >= DAY_IN_MILLIS) {
            days = fraction / DAY_IN_MILLIS;
            fraction %= DAY_IN_MILLIS;
        } else if (fraction < 0) {
            days = CalendarUtils.floorDivide(fraction, DAY_IN_MILLIS);
            if (days != 0) {
                fraction -= DAY_IN_MILLIS * days; // mod(fraction, DAY_IN_MILLIS)
            }
        }
        if (days != 0) {
            date.setTimeOfDay(fraction);
        }
        date.setMillis((int)(fraction % 1000));
        fraction /= 1000;
        date.setSeconds((int)(fraction % 60));
        fraction /= 60;
        date.setMinutes((int)(fraction % 60));
        date.setHours((int)(fraction / 60));
        return (int)days;
    }
}
