/* Copyright (C) 1998, 1999  Cygnus Solutions

   This file is part of libgcj.

This software is copyrighted work licensed under the terms of the
Libgcj License.  Please consult the file "LIBGCJ_LICENSE" for
details.  */

package java.util;

/**
 * @author Per Bothner <bothner@cygnus.com>
 * @date October 24, 1998.
 */

/* Written using "Java Class Libraries", 2nd edition, ISBN 0-201-31002-3,
 * and "The Java Language Specification", ISBN 0-201-63451-1.
 * Status:  "leniency" is not handled, and neither is roll-over in
 *   add and roll.  This is partly because of unclear specification.
 *   hashCode has no spec.
 */

public class GregorianCalendar extends Calendar {
  public static final int BC = 0;
  public static final int AD = 1;

  // The fields are as specified in Sun's "Serialized Form"
  // in the JDK 1.2 beta 4 API specification.
  // Value from a simple test program (getGregorianChange.getTime()).
  long gregorianCutover = -12219292800000L;

  private final static int[] mins = {
    0 /* ERA */,
    1 /* YEAR */,
    0 /* MONTH */,
    0 /* WEEK_OF_YEAR */,
    0 /* WEEK_OF_MONTH */,
    1 /* DATE */,
    1 /* DAY_OF_YEAR */,
    1 /* DAY_OF_WEEK */,
    -1 /* DAY_OF_WEEK_IN_MONTH */,
    0 /* AM_PM */,
    0 /* HOUR */,
    0 /* HOUR_OF_DAY */,
    0 /* MINUTE */,
    0 /* SECOND */,
    0 /* MILLISECOND */,
    -43200000 /* ZONE_OFFSET */,
    0 /* DST_OFFSET */
  };

  private final static int[] maxs = {
    1 /* ERA */,
    5000000 /* YEAR */,
    11 /* MONTH */,
    54 /* WEEK_OF_YEAR */,
    6 /* WEEK_OF_MONTH */,
    31 /* DATE */,
    366 /* DAY_OF_YEAR */,
    7 /* DAY_OF_WEEK */,
    6 /* DAY_OF_WEEK_IN_MONTH */,
    1 /* AM_PM */,
    12 /* HOUR */,
    23 /* HOUR_OF_DAY */,
    59 /* MINUTE */,
    59 /* SECOND */,
    999 /* MILLISECOND */,
    43200000 /* ZONE_OFFSET */,
    3600000 /* DST_OFFSET */
  };

  private final static int[] leastMaximums = {
    1 /* ERA */,
    5000000 /* YEAR */,
    11 /* MONTH */,
    53 /* WEEK_OF_YEAR */,
    6 /* WEEK_OF_MONTH */,
    28 /* DATE */,
    365 /* DAY_OF_YEAR */,
    7 /* DAY_OF_WEEK */,
    4 /* DAY_OF_WEEK_IN_MONTH */,
    1 /* AM_PM */,
    11 /* HOUR */,
    23 /* HOUR_OF_DAY */,
    59 /* MINUTE */,
    59 /* SECOND */,
    999 /* MILLISECOND */,
    43200000 /* ZONE_OFFSET */,
    3600000 /* DST_OFFSET */
  };

  public GregorianCalendar ()
  {
    this(null, null);
  }

  public GregorianCalendar (TimeZone zone)
  {
    this (zone, null);
  }

  public GregorianCalendar (Locale locale)
  {
    this (null, locale);
  }

  public GregorianCalendar (TimeZone zone, Locale locale)
  {
    super (zone, locale);
    setDefaultTime ();
  }

  public GregorianCalendar (int year, int month, int date)
  {
    this((TimeZone) null);
    setDefaultTime ();
    set (year, month, date);
  }

  public GregorianCalendar (int year, int month, int date,
			    int hour, int minute)
  {
    this((TimeZone) null);
    setDefaultTime ();
    set (year, month, date, hour, minute);
  }

  public GregorianCalendar (int year, int month, int date,
			    int hour, int minute, int second)
  {
    this((TimeZone) null);
    setDefaultTime ();
    set (year, month, date, hour, minute, second);
  }

  private final void setDefaultTime ()
  {
    setTimeInMillis (System.currentTimeMillis());
  }

  public int getMinimum(int calfield) { return mins[calfield]; }
  public int getGreatestMinimum(int calfield) { return mins[calfield]; }
  public int getMaximum(int calfield) { return maxs[calfield]; }
  public int getLeastMaximum(int calfield) { return leastMaximums[calfield]; }

  protected native void computeFields();

  protected native void computeTime();

  public void add (int fld, int amount)
  {
    if (fld >= ZONE_OFFSET)
      throw new IllegalArgumentException("bad field to add");
    fields[fld] += amount;
    adjust(fld);
  }

  public void roll (int fld, boolean up)
  {
    if (fld >= ZONE_OFFSET)
      throw new IllegalArgumentException("bad field to roll");

    int old = fields[fld];
    if (up)
      {
	fields[fld] = old == getMaximum(fld) ? getMinimum(fld)
	  : old + 1;
      }
    else
      {
	fields[fld] = old == getMinimum(fld) ? getMaximum(fld)
	  : old - 1;
      }
  }

  private void adjust (int fld)
  {
    int value = fields[fld];
    int radix = maxs[fld] + 1;
    switch (fld)
      {
      case MONTH:
      case SECOND:
      case MILLISECOND:
	if (value >= radix)
	  {
	    int next = value / radix;
	    fields[fld] = value - radix * next;
	    fields[fld - 1] += next;
	    adjust(fld - 1);
	  }
	else if (value < 0) // min[fld]
	  {
	    int next = (value - radix - 1) / radix;
	    fields[fld] = value - radix * next;
	    fields[fld - 1] += next;
            adjust(fld - 1);
	  }
	break;
      }
  }

  public final Date getGregorianChange() { return new Date(gregorianCutover); }
  public void setGregorianChange (Date date)
  { gregorianCutover = date.getTime(); }

  public boolean isLeapYear(int year)
  {
    if ((year % 4) != 0)
      return false;
    if ((year % 100) != 0 || (year % 400) == 0)
      return true;
    // year divisible by 100 but not 400.
    GregorianCalendar date = new GregorianCalendar(year, FEBRUARY, 28);
    return gregorianCutover < date.getTimeInMillis();
  }

  public boolean after (Object cal)
  {
    return cal instanceof Calendar
      && getTimeInMillis() > ((Calendar) cal).getTimeInMillis();
  }

  public boolean before (Object cal)
  {
    return cal instanceof Calendar
      && getTimeInMillis() < ((Calendar) cal).getTimeInMillis();
  }

  public boolean equals (Object obj)
  {
    if (obj == null || ! (obj instanceof GregorianCalendar))
      return false;
    GregorianCalendar other = (GregorianCalendar) obj;

    for (int i = FIELD_COUNT;  --i >= 0; )
      {
	boolean set = isSet[i];
	if (set != other.isSet[i]
	    || (set && fields[i] != other.fields[i]))
	  return false;
      }
    if (areFieldsSet != other.areFieldsSet
	|| isTimeSet != other.isTimeSet
	|| (isTimeSet && time != other.time)
	|| getFirstDayOfWeek() != other.getFirstDayOfWeek()
	|| getMinimalDaysInFirstWeek() != other.getMinimalDaysInFirstWeek()
	|| isLenient() != other.isLenient()
	|| ! getTimeZone().equals(other.getTimeZone()))
      return false;
    return true;
  }

  public int hashCode ()
  {
    int hashcode = 0;
    for (int i = FIELD_COUNT;  --i >= 0; )
      {
	if (isSet[i])
	  hashcode += 37 * fields[i];
      }
    if (isTimeSet)
      hashcode += 89 * time;
    return hashcode;
  }
}