package nxm.ucl.libg;

import java.awt.BorderLayout;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerModel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Time;

/** A class that is used by GDatePicker to create a series of controls
    enabling the easy selection of a calendar date.  The controls include a
    field label, a read-only text-field for printing the select date/Time value,
    and an icon-based JButton which opens the calendar popup.  The popup includes
    a month and year picker, a calendar-based "day picker", and a time selector.

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

    @author crs
    @since NeXtMidas 2.9.1
 **/
public class MJDatePickerPanel extends JPanel
  implements ActionListener, ChangeListener, PropertyChangeListener {
  /** NOTE: this should be changed if the structure/fields of this class changes.
   *  This is being left to avoid warnings, though this class is not truly serializable.
   *  We are not overwriting readObject and writeObject because serialization will error out on MJTimeSpinnerModel */
  private static final long serialVersionUID = 2012122808331L; // for Serializable interface

  private static final String OK_TEXT     = "APPLY";
  private static final String CANCEL_TEXT = "CANCEL";

  /** Stores date and time */
  private Calendar calendar;
  /** Stores time-only */
  private Calendar timeCalendar;
  /** The Panel which holds the Month and Year spinners */
  private JPanel monthYearPanel;
  /** The Panel which holds the Time spinner */
  private JPanel timePanel;

  /** The GDatePicker (GWidget) parent/owner of this component */
  private transient GDatePicker parent;

  /** The Year picker */
  protected MJYearPicker yearPicker;
  /** The Month picker */
  protected MJMonthPicker monthPicker;
  /** The Day picker */
  protected MJDayPicker dayPicker;
  /** The delimiter between time components */
  protected String delimiter = ":";
  /** true if time is visible */
  protected boolean timeIsVisible = true;
  /** The label for the Time spinner */
  protected JLabel timeLabel;
  /** The Time spinner */
  protected JSpinner timeSpinner;
  /** The Time editor associated with the Time spinner */
  protected JComponent timeEditor;
  /** The spinner model associated with the Time spinner */
  protected transient SpinnerModel timeModel;
  /** Panel to contain OK and CANCEL buttons */
  protected JPanel btnPanel;
  /** OK Button */
  protected JButton okBtn;
  /** CANCEL Button */
  protected JButton cancelBtn;

  /** Flag for whether or not MJDatePickerPanel is initialized */
  private boolean initialized = false;

  /**
     Default constructor that takes in the specified Time object so as to
     store month, year, day, etc.
     @param time The optional start time
     @param parent The GDatePicker (GWidget) parent/owner of this component
     @param mh The parent's MessageHandler object
   */
  public MJDatePickerPanel (Time time, GDatePicker parent, MessageHandler mh) {
    setName("MJDatePickerPanel");

    // The line below would make it easy to set the timeZone on the
    // DateSpinnerModel; however, this will set the time zone throughout
    // the JVM in JDK 1.6, and across the thread in JDK 1.5
//    TimeZone.setDefault(TimeZone.getTimeZone("UTC"));

    // Fixes an issue where the font does not get set upon instantiation
    dayPicker = null;
    monthPicker = null;
    yearPicker = null;

    this.parent = parent;

    calendar = Calendar.getInstance();
    timeCalendar = (Calendar) calendar.clone();

    // Set the current time and UTC timezone
    if (time != null) {
      calendar.setTime(time.toDateObject());
    }

    setLayout(new BorderLayout());

    monthPicker = new MJMonthPicker();
    yearPicker = new MJYearPicker();
    dayPicker = new MJDayPicker();

    // Create the panel to hold Month and Year spinners
    monthYearPanel = new JPanel();
    monthYearPanel.setLayout(new BorderLayout());

    monthYearPanel.add(monthPicker, BorderLayout.WEST);
    monthYearPanel.add(yearPicker, BorderLayout.EAST);
    monthYearPanel.setBorder(BorderFactory.createEmptyBorder());

    // Add listeners
    monthPicker.addPropertyChangeListener(this);
    yearPicker.addPropertyChangeListener(this);
    dayPicker.addPropertyChangeListener(this);

    // Set inter-component references
    yearPicker.setMonthPicker(monthPicker);
    yearPicker.setDayPicker(dayPicker);
    monthPicker.setYearPicker(yearPicker);
    monthPicker.setDayPicker(dayPicker);
    dayPicker.setMonthPicker(monthPicker);
    dayPicker.setYearPicker(yearPicker);

    timePanel = new JPanel();
    timePanel.setSize(120, 35);
    timePanel.setLayout(new BorderLayout());
    timePanel.setBorder(BorderFactory.createEmptyBorder());
    timePanel.addPropertyChangeListener(this);

    // Time spinner component
    timeLabel = new JLabel("Time (UTC):");
//    timeModel = new SpinnerDateModel();
    timeModel = new MJTimeSpinnerModel();
    timeSpinner = new JSpinner(timeModel);

    JSpinner.DateEditor dateEditor = new JSpinner.DateEditor(timeSpinner, "HH:mm:ss.SSS");
    dateEditor.getFormat().setTimeZone(TimeZone.getTimeZone("UTC"));
//    dateEditor.getTextField().setEditable(false);     // Prevent user editing

    timeEditor = dateEditor;
    timeSpinner.setEditor(timeEditor);


    timeSpinner.addChangeListener(this);

    timePanel.add(timeLabel, BorderLayout.WEST);
    timePanel.add(timeSpinner, BorderLayout.EAST);

    // Add the timePanel to dayPicker panel in order to resolve layout issue
    dayPicker.add(timePanel, BorderLayout.SOUTH);

    // Panel to contain OK and CANCEL buttons, displays at button of popup
    btnPanel = new JPanel();
    okBtn = new JButton(OK_TEXT);
    okBtn.setActionCommand(OK_TEXT);
    cancelBtn = new JButton(CANCEL_TEXT);
    cancelBtn.setActionCommand(CANCEL_TEXT);
    btnPanel.setLayout(new BorderLayout());
    btnPanel.setBorder(BorderFactory.createEmptyBorder());
    btnPanel.add(okBtn, BorderLayout.WEST);
    btnPanel.add(cancelBtn, BorderLayout.EAST);

    okBtn.addActionListener(this);
    okBtn.addActionListener(parent);
    okBtn.addPropertyChangeListener(this);
    cancelBtn.addActionListener(this);
    cancelBtn.addActionListener(parent);
    cancelBtn.addPropertyChangeListener(this);
    btnPanel.addPropertyChangeListener(this);
    btnPanel.addPropertyChangeListener(this);


    add(monthYearPanel, BorderLayout.NORTH);
    add(dayPicker, BorderLayout.CENTER);
    add(btnPanel, BorderLayout.SOUTH);

    initialized = true;
    setCalendar(calendar, true);
  }

  /**
    Updates the current calendar reference with a flag to update GUI value.
    @param c The new Calendar reference
    @param update Whether or not to update the text field value of the selected date/time
   */
  private void setCalendar (Calendar c, boolean update) {

    if (c == null) {
      return;
    }

    Calendar oldCalendar = calendar;

    // Update the primary calendar reference to the new Calendar
    calendar = c;

    // Update the primary calendar
    if (update) {
      yearPicker.setYear(c.get(Calendar.YEAR));
      monthPicker.setMonth(c.get(Calendar.MONTH));
      dayPicker.setDay(c.get(Calendar.DATE));

      timeSpinner.setValue(c.getTime());
    }

    firePropertyChange("calendar", oldCalendar, calendar);
  }

  /**
    Returns this panel's Calendar object.
    @return The current Calendar object.
   */
  public Calendar getCalendar () {
    return calendar;
  }

  /**
    Sets a new Time object value on this calendar by calling setCalendar().
    This method is primarily used by GDatePicker to update the Calendar/
    popup after a manual setValue() is called via NeXtMidas.
    @param time The new Time object
   */
  public void setTime (Time time) {

    Calendar newCal = Calendar.getInstance();
    if (time != null) {
      newCal.setTime(time.toDateObject());
    }

    setCalendar(newCal, true);
  }

  /**
    Sets the Date of this component and fires the "date" property change event.
    @param date new date to use
   */
  public void setDate (Date date) {
    Date oldDate = calendar.getTime();
    Calendar oldCalendar = (Calendar) calendar.clone();
    calendar.setTime(date);

    // Add the time to the current Calendar
    setHMSS((Date)timeSpinner.getValue(), true);

    // Fire property change including date and time
    firePropertyChange("date", oldCalendar, calendar);
  }

  /**
    Returns the current Date and Time.  Should be used in place of
    getCalendar().getTime(), as the latter will only return the Date value
    without the updated time value.
    @return The currently selected Date, including time.
   */
  public Date getDate () {
    Calendar dayCal = (Calendar) calendar.clone();
    Calendar timeCal = (Calendar) timeCalendar.clone();

    dayCal.set(Calendar.HOUR_OF_DAY, timeCal.get(Calendar.HOUR_OF_DAY));
    dayCal.set(Calendar.MINUTE, timeCal.get(Calendar.MINUTE));
    dayCal.set(Calendar.SECOND, timeCal.get(Calendar.SECOND));
    dayCal.set(Calendar.MILLISECOND, timeCal.get(Calendar.MILLISECOND));

    return dayCal.getTime();
  }

  /**
    Sets the hours, minutes, seconds, and milliseconds value to the
    current Calendar object.
    TODO: Remove the redundancy between this method and getDate().
    @param date The new time in Date format
   */
  private void setHMSS (Date date, boolean update) {
    Calendar calClone = (Calendar) calendar.clone();
    Calendar newCal = Calendar.getInstance();
    newCal.setTime(date);

//     Set hour, minutes, seconds, and milliseconds to the Calendar clone
    calClone.set(Calendar.HOUR_OF_DAY, newCal.get(Calendar.HOUR_OF_DAY));
    calClone.set(Calendar.MINUTE, newCal.get(Calendar.MINUTE));
    calClone.set(Calendar.SECOND, newCal.get(Calendar.SECOND));
    calClone.set(Calendar.MILLISECOND, newCal.get(Calendar.MILLISECOND));

    // When updating, the entire Date/Time combination will be set
    // and GUI set accordingly
    if (calClone != null && update)
    {
      // Save this as the last known "good" time
      // prevTimeCalendar = (Calendar) timeCalendar.clone();

      // Save the instance copy of the time-only value
      timeCalendar.setTime(calClone.getTime());

      // Save the entire date/time combination
      calendar = (Calendar) calClone.clone();
    }
    else {
      // Only set the new time-only Calendar as this value has not yet been "saved"
      timeCalendar = calClone;
    }
  }

  /**
    In the event the Cancel button is pressed, the Time Spinner will reset
    its value to the last known "good" time-value.
    The last known "good" time-value is stored in "prevTimeCalendar".
    This method is not currently implemented..
   */
  private void resetDateTime () {
//    timeSpinner.setValue(prevTimeCalendar.getTime());
//    calendar = (Calendar) prevCalendar.clone();
//    dayPicker.setDay(calendar.get(Calendar.DAY_OF_WEEK));
  }

  /**
    Returns the value of OK_TEXT, which is used for button
    action commands and the text of the button.
    @return A String value from this classes's instance variable OK_TEXT.
   */
  public String getOkText () {
    return OK_TEXT;
  }

  /**
    Returns the value of CANCEL_TEXT.
    @return A String from this classes's instance variable CANCEL_TEXT.
   */
  public String getCancelText () {
    return CANCEL_TEXT;
  }

  /** Handle action events from the OK and CANCEL buttons. */
  @Override
  public void actionPerformed (ActionEvent evt) {

    if (evt.getActionCommand().equals(OK_TEXT)) {
      // "OK" button sets the Date/Time combination
      setDate(getDate());
    }

    else if (evt.getActionCommand().equals(CANCEL_TEXT)) {
      // "CANCEL" button resets the day picker and time spinner
//      resetDateTime();
    }
  }

  /** Handles property change events sent from components that this class is listening to. */
  @Override
  public void propertyChange (PropertyChangeEvent evt) {

    if (calendar != null) {

      Calendar calClone = (Calendar) calendar.clone();

      // Handle a property change event from the YearPicker
      if (evt.getPropertyName().equals("year")) {
        calClone.set(Calendar.YEAR, ((Integer) evt.getNewValue()).intValue());

        // Set and update primary calendar
        setCalendar(calClone, false);

      }
      else if (evt.getPropertyName().equals("month")) {

        calClone.set(Calendar.MONTH, ((Integer) evt.getNewValue()).intValue());

        // Set and update primary calendar
        setCalendar(calClone, false);

      }
      else if (evt.getPropertyName().equals("day")) {
        // Set calendar with new day
        calClone.set(Calendar.DAY_OF_MONTH, ((Integer) evt.getNewValue()).intValue());

        // Set and update primary calendar
        setCalendar(calClone, false);
      }
      else if (evt.getPropertyName().equals("date)")) {
        calClone.setTime((Date) evt.getNewValue());
        setCalendar(calClone, true);
      }
      else if (evt.getPropertyName().equals("calendar")) {
      }
      else if (evt.getPropertyName().equals("time")) {
        // Do nothing
      }
    }
  }

  /** Handles state changes that occur on components this class is listening to. */
  @Override
  public void stateChanged (ChangeEvent evt) {

    JSpinner spinner = (JSpinner) evt.getSource();
    Date oldTime = (Date) spinner.getPreviousValue();
    Date newTime = (Date) spinner.getValue();

    // Set hours, minutes, seconds, and milliseconds to the time-only calendar
    setHMSS(newTime, false);
  }

  /**
    Sets the font property.
    @param font the new font
   */
  public void setFont (Font font) {
    super.setFont(font);

    if (dayPicker != null) {
      dayPicker.setFont(font);
      monthPicker.setFont(font);
      yearPicker.setFont(font);
      timeSpinner.setFont(font);
    }
  }

}