package nxm.ucl.libg;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import javax.swing.SpinnerDateModel;
import javax.swing.event.ChangeEvent;

import nxm.sys.inc.InternalUseOnly;

/**
  A class that extends (@link SpinnerDateModel) primarily in order to override
  the SpinnerDateModel constructor that ignores any and all time zones.

  NOTE: MJTimeSpinnerModel does NOT currently support Java Serialization, even though it extends classes that support it.

  @author crs
 */
@SuppressWarnings({"rawtypes","unchecked"}) // <-- NOTE: JDK7 has some unchecked & rawtypes Xlint warnings that CANNOT be fixed since we still support JDK6
public class MJTimeSpinnerModel extends SpinnerDateModel implements Serializable {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2011052700292L; // for Serializable interface

  /** Message given when user tries to serialize an MJTimeSPinnerModel object */
  @InternalUseOnly
  public static final String UNSERIALIZABLE_MJTIMESPINNERMODEL_OBJECT_MESSAGE =
      "nxm.ucl.libg.MJTimeSpinnerModel does NOT currently support Java Serialization";

  /** The current date. */
  private Calendar date;

  /** The default time zone (UTC). */
  private TimeZone defTimeZone = TimeZone.getTimeZone("UTC");

  /** A constraint on the start or earliest permitted date (<code>null</code> for no minimum). */
  private transient Comparable start;

  /** A constraint on the end or latest permitted date (<code>null</code> for no maximum). */
  private transient Comparable end;

  /** The calendar field used to calculate the previous or next date. */
  private int calendarField;

  /**
    Constructs a <code>SpinnerDateModel</code> using the current date,
    no start or end limit, and {@link Calendar#DAY_OF_MONTH} as the calendar field.
   */
  public MJTimeSpinnerModel () {
    this(new Date(), TimeZone.getTimeZone("UTC"), null, null, Calendar.DAY_OF_MONTH);
  }

  /** MJTimeSpinnerModel Constructor
   *  @param value         the target date
   *  @param timeZone      the time zone
   *  @param start         the earliest permitted date
   *  @param end           the latest permitted date
   *  @param calendarField Sets the date field to change when calculating the next and previous values.
   */
  public MJTimeSpinnerModel (Date value, TimeZone timeZone, Comparable start, Comparable end, int calendarField) {
    if (value == null)
      throw new IllegalArgumentException("Null 'value' argument.");
    if (start != null && start.compareTo(value) > 0)
      throw new IllegalArgumentException("Require value on or after start.");
    if (end != null && end.compareTo(value) < 0)
      throw new IllegalArgumentException("Require value on or before end.");

    date = Calendar.getInstance(timeZone);
    // date = Calendar.getInstance();
    date.setTime(value);
    this.start = start;
    this.end = end;
    setCalendarField(calendarField);
  }

  /** Returns the {@link Calendar} field used to calculate the previous and next dates in the sequence.
      @return The date field code.
   */
  public int getCalendarField ()
  {
    return calendarField;
  }

  /** Returns the current date/time.
      @return The current date/time (never <code>null</code>).
      @see #getValue()
   */
  public Date getDate () {
    return date.getTime();
  }

  /** Returns the lower limit on the date/time value, or <code>null</code> if there is no minimum date/time.
      @return The lower limit.
      @see #setStart(Comparable)
   */
  public Comparable getStart () {
    return start;
  }

  /**
    Returns the upper limit on the date/time value, or <code>null</code> if
    there is no maximum date/time.
    @return The upper limit.
    @see #setEnd(Comparable)
   */
  public Comparable getEnd () {
    return end;
  }

  /** Returns the current date in the sequence (this method returns the same as {@link #getDate()}).
      @return The current date (never <code>null</code>).
   */
  public Object getValue () {
    return date.getTime();
  }

  /**
    Returns the next date in the sequence, or <code>null</code> if the next date
    is past the upper limit (if one is specified).  The current date is not changed.
    @return The next date, or <code>null</code> if the current value is
            the latest date represented by the model.
    @see #getEnd()
   */
  public Object getNextValue () {
    Calendar nextCal = Calendar.getInstance();
    nextCal.setTime(date.getTime());
    nextCal.roll(calendarField, true);
    Date nextDate = nextCal.getTime();
    if (end != null)
      if (end.compareTo(nextDate) < 0)
        return null;
    return nextDate;
  }

  /**
    Returns the previous date in the sequence, or <code>null</code> if the
    previous date is prior to the lower limit (if one is specified).  The
    current date is not changed.

    @return The previous date, or <code>null</code> if the current value is
            the earliest date represented by the model.
    @see #getStart()
   */
  public Object getPreviousValue () {
    Calendar prevCal = Calendar.getInstance();
    prevCal.setTime(date.getTime());
    prevCal.roll(calendarField, false);
    Date prevDate = prevCal.getTime();
    if (start != null)
      if (start.compareTo(prevDate) > 0)
        return null;
    return prevDate;
  }

  /**
    Sets the date field to change when calculating the next and previous
    values. It must be a valid {@link Calendar} field, excluding
    {@link Calendar#ZONE_OFFSET} and {@link Calendar#DST_OFFSET}.

    @param calendarField  the calendar field to set.
    @throws IllegalArgumentException if <code>calendarField</code> is not a valid code.
   */
  public void setCalendarField (int calendarField) {
    if (calendarField < 0 || calendarField >= Calendar.FIELD_COUNT
        || calendarField == Calendar.ZONE_OFFSET
        || calendarField == Calendar.DST_OFFSET)
      throw new IllegalArgumentException("Illegal calendarField");

    if (this.calendarField != calendarField)
      {
        this.calendarField = calendarField;
        fireStateChanged();
      }
  }

  /**
    Sets the lower limit for the date/time value and, if the new limit is
    different to the old limit, sends a {@link ChangeEvent} to all registered
    listeners.  A <code>null</code> value is interpreted as "no lower limit".
    No check is made to ensure that the current date/time is on or after the
    new lower limit - the caller is responsible for ensuring that this
    relationship holds.  In addition, the caller should ensure that
    <code>start</code> is {@link Serializable}.

    @param start  the new lower limit for the date/time value (<code>null</code> permitted).
   */
  public void setStart (Comparable start) {
    if (this.start != start) {
      this.start = start;
      super.setStart(start);
    }
  }

  /**
    Sets the upper limit for the date/time value and, if the new limit is
    different to the old limit, sends a {@link ChangeEvent} to all registered
    listeners.  A <code>null</code> value is interpreted as "no upper limit".
    No check is made to ensure that the current date/time is on or before the
    new upper limit - the caller is responsible for ensuring that this
    relationship holds.  In addition, the caller should ensure that
    <code>end</code> is {@link Serializable}.

    @param end  the new upper limit for the date/time value (<code>null</code> permitted).
   */
  public void setEnd (Comparable end) {
    if (this.end != end) {
      this.end = end;
      super.setEnd(end);
    }
  }

  /**
    Sets the current date and, if the new value is different to the old
    value, sends a {@link ChangeEvent} to all registered listeners.
    @param value  the new date (<code>null</code> not permitted, must be an instance of <code>Date</code>).
    @throws IllegalArgumentException if <code>value</code> is not an instance of <code>Date</code>.
   */
  public void setValue (Object value) {
    if (! (value instanceof Date) || value == null)
      throw new IllegalArgumentException("Value not a date.");

    if (!date.getTime().equals(value)) {
      // In @link SpinnerDateModel, this Object is instantiated as a Date
      // and therefore loses
      Date newDate = (Date) value;
      date.setTime((Date) value);
      fireStateChanged();
    }
  }

  /** Overwrite default serialization for writeObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param oos Object Output Stream
   *  @throws IOException Exception thrown by writeObject
   */
  private void writeObject (ObjectOutputStream oos) throws IOException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_MJTIMESPINNERMODEL_OBJECT_MESSAGE);
  }

  /** Overwrite default deserialization for readObject
   *  to throw an UnsupportedOperationException since Java Serialization currently not supported
   *
   *  @param ois Object Input Stream
   *  @throws ClassNotFoundException Exception thrown by readObject
   *  @throws IOException Exception thrown by readObject
   */
  private void readObject (ObjectInputStream ois) throws IOException, ClassNotFoundException {
    throw new UnsupportedOperationException(UNSERIALIZABLE_MJTIMESPINNERMODEL_OBJECT_MESSAGE);
  }
}