/*
 * Copyright 2003-2004 Sun Microsystems, Inc.  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.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun 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 Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */

package rht.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];
	    if (zi instanceof ZoneInfo) {
		zoneOffset = ((ZoneInfo)zi).getOffsets(millis, offsets);
	    } else {
		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)
		if (zi instanceof ZoneInfo) {
		    ((ZoneInfo)zi).getOffsetsByStandard(ms, offsets);
		    zoneOffset = offsets[0];
		} else {
		    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)
		if (zi instanceof ZoneInfo) {
		    zoneOffset = ((ZoneInfo)zi).getOffsetsByWall(ms, offsets);
		} else {
		    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;
    }
}
