package nxm.ucl.libg;

import java.awt.ComponentOrientation;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.net.URL;
import java.util.Date;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.border.BevelBorder;
import javax.swing.border.Border;
import javax.swing.event.ChangeListener;

import nxm.sys.inc.MessageHandler;
import nxm.sys.lib.Message;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.Table;
import nxm.sys.lib.Time;
import nxm.sys.libg.BeanWidget;
import nxm.sys.libg.GWidget;
import nxm.sys.libg.MBox;
import nxm.sys.libg.MJTextField;
import nxm.sys.libg.Theme;

/** A class to display a a popup-based calendar date and time selector.
    This component was created with a Web-based reservation system in mind and
    aims to enhance the current GCalendar functionality.
    This class is a custom control meant to be used along with the GCONTROL BEAN
    widget.
    Example: gcontrol bean myDatePicker "My Date" P1=nxm.ucl.libg.GDatePicker
    Example: gcontrol bean myDatePicker "My Date" P1=nxm.ucl.libg.GDatePicker
             TABLE={StartTime=^myTimeObject}

    Requirements for BEAN GCONTROL Custom Widgets:

    The custom control's class should be added to the UCL/libg folder or your own option tree.
    The class must extend <code>GWidget</code> and implement the <code>BeanWidget</code>
    interface.  As of NeXtMidas 2.9.1, the <code>BeanWidget</code> interface
    requires any implementing class to define the functionality for the
    <code>setupBean()</code> method.  In addition, the class must contain an
    empty constructor.  Both the empty constructor and the <code>setupBean()</code>
    method are required in order for your custom control to invoked correctly.

    @author crs
    @since NeXtMidas 2.9.1
 */
public class GDatePicker extends GWidget implements ActionListener, BeanWidget, PropertyChangeListener {

  /** Static constants. **/
  private final int     DEBUG_ON          = 0;
  private static final String CLASS_CAST_ERROR_MSG =
          "Unable to cast STARTIME argument to Time object.";
  private static final String DEFAULT_SEED = "Date/Time:"; // used for empty labels
  private static final String DEFAULT_FORMAT_STR = "yyyy:MM:DD::HH:mm:ss.SSS";

  private int       defaultWidth      = 330;
  private int       defaultHeight     = 50;
  private int       popupWidth        = 220;
  private int       popupHeight       = 220;
  private int       timeFieldCols     = 24;
  private MBox      bp;            // box for prompt
  private MBox      bc;
  private MessageHandler mh;

  /** Swing components. **/
  protected FlowLayout  layout;
  protected JLabel      seedText;
  protected MJTextField timeField;
  protected JButton     calendarButton;
  protected JPopupMenu  popup;
  protected ChangeListener changeListener;

  protected MJDatePickerPanel calendarPanel;

  /** Instance values. **/
  private String        seed;
  private Time          value;
  protected boolean     dateSelected;
  protected Time        lastSelectedTime;
  /** Time the JPopupMenu was set to be visible
   *  This is used as part of a RHEL 8 and 9 workaround.
   *  An extra event is coming through only about 30 milliseconds
   *  after it is made visible, setting visible to false.
   */
  private double        timeJPopupMenuMillis;
  /** 50 milliseconds is too quick for a user to make a selection,
   *  but the extra RHEL 8 and 9 event comes after about 30 milliseconds */
  private static final int TOO_QUICK_FOR_USER = 50;

  /** Empty constructor used for bean lookup via reflection. */
  public GDatePicker () {}

  /**
    Default constructor that takes only a name for the GDatePicker.
    @param parent The parent Midas object of this control.
    @param name The name or ID of the control.
    @param flags The flags of this GWidget.
    @param mh The Midas message handler reference.
   */
  public GDatePicker (Object parent, String name, int flags, MessageHandler mh) {

    super("GDatePicker", parent, name, flags, mh);
    open(Time.currentTime());
    addTo(parent);
  }

  /** Instantiates the bean with a label and a specified start time.
      @param parent The parent Midas object of this control.
      @param name The name or ID of the control.
      @param startTime The start time object which auto-populates the control.
      @param flags The flags of this GWidget.
      @param mh The Midas message handler reference.
   */
  public GDatePicker (Object parent, String name, Time startTime, int flags, MessageHandler mh) {

    super("GDatePicker", parent, name, flags, mh);
    open(startTime);
    addTo(parent);
  }

  /** The method that is called in lieu of a constructor when this control
      is instantiated through reflection.  This method is required by the
      <code>BeanWidget</code> interface.  The method provides access to the
      Midas arguments through the <code>argTable</code> parameter.
      @param name The name of the control or widget.
      @param parent The parent component.
      @param title The Midas title for the control.
      @param flags Any applicable flags.
      @param mh The MessageHandler reference. @see MessageHandler
      @param argTable The table of Midas arguments.
      @see BeanWidget
   */
  public void setupBean (String name, Object parent, String title, int flags, MessageHandler mh, Table argTable) {

    double startTimeMs = 0;

    // Individual label has been removed
    // String seed = new String(DEFAULT_SEED);

    this.mh = mh;

    // Must call super.setupBean in order to accept messages
    super.setupBean(name, parent, title, flags, mh, argTable);

    // Parse arguments table
    if (argTable != null) {

      try {
        // Argument "P1" is already used for the class name
        if (argTable.size() > 0)
          startTimeMs = argTable.getD("STARTTIME");
      }
      catch (ClassCastException cce) {
        throw new MidasException(CLASS_CAST_ERROR_MSG, cce);
      }
    }

    if (startTimeMs > 0)  open(new Time(startTimeMs));
    else                  open(Time.currentTime());

    addTo(parent);
  }

  /** A typical GWdiget open() method to configure this component.
      @param startTime The optional start time which pre-populates the calendar;
                       if no start time is provided, the calendar pre-populates to "now"
   */
  public void open (Time startTime) {
    super.open();  // added in 3.1.3 to ensure size is set
    setDate(startTime.toDateObject());    // Also sets value, or Time
    setupComponent();
  }

  /**
   *  @deprecated Since NeXtMidas 3.9.0 use {@link #paintComponent(Graphics)}, but just changed in 3.9.3
   */
  @Deprecated
  public void paint () {
//    old deprecated paint method - logic moved to paintComponent (Graphics) - otherwise Realtime Graphics does not work
//    if (g == null) return;
//    super.paint();
//    updateUI();
    refresh();
  }

  /** Deprecated paintComponent method
   *  needed for backwards compatibility
   */
  public void paintComponent () { }

  @Override
  public void paintComponent (Graphics g) {
    super.paintComponent(g);
    updateUI();
  }

  /** Sets up the base component, i.e. the GWidget. */
  @SuppressWarnings("serial")
  private void setupComponent () {

    layout = new FlowLayout();

    pos.w = defaultWidth;
    pos.h = defaultHeight;

    timeField = new MJTextField("", 24);
    calendarPanel = new MJDatePickerPanel(getValue(), this, this.mh);

    panel.setSize(pos.w, pos.h);
    setupTimeField();
    setupCalendarButton();

    panel.setLayout(layout);
    panel.add(timeField);
    panel.add(calendarButton);
    panel.setComponentOrientation(ComponentOrientation.LEFT_TO_RIGHT);

    popup = new JPopupMenu() {
      public void setVisible(boolean b) {
        if (b) timeJPopupMenuMillis = System.currentTimeMillis(); // NXM-4012 workaround
        if (b || (System.currentTimeMillis() > (timeJPopupMenuMillis + TOO_QUICK_FOR_USER))) {
          Boolean isCanceled = (Boolean) getClientProperty("JPopupMenu.firePopupMenuCanceled");

          if (b || (!b && dateSelected)
                || ((isCanceled != null) && !b && isCanceled.booleanValue())) {
            super.setVisible(b);
          }
        }
      }
    };

    popup.setLightWeightPopupEnabled(false);
    calendarPanel.setPreferredSize(new Dimension(popupHeight, popupWidth)); // Fixes the cutoff of the bottom

    popup.add(calendarPanel);

    calendarPanel.addPropertyChangeListener(this);
    this.addListeners();
  }

  /** Configures the text field ({@link #timeField} to display time and date. */
  private void setupTimeField () {

    timeField.setFont(font);
    timeField.setForeground(theme.cwfg);
    timeField.setBackground(theme.cwbg);
    timeField.setEditable(false);
    timeField.setVisible(true);
    timeField.setColumns(timeFieldCols);
  }

  /** Configures the date picker calendar and icon. */
  private void setupCalendarButton () {
    // Display a calendar button with an icon
    URL iconURL = getClass().getResource("/nxm/ucl/dat/GDatePickerIconNew.gif");
    ImageIcon icon = new ImageIcon(iconURL);

    calendarButton = new JButton(icon) {
        private static final long serialVersionUID = -1913767779079949668L;

        public boolean isFocusable () {
          return false;
        }
      };
    calendarButton.setSize(32, 32);
    calendarButton.setBorder(BorderFactory.createBevelBorder(BevelBorder.RAISED));
    calendarButton.setRolloverEnabled(true);
    calendarButton.setVisible(true);
    calendarButton.addActionListener(this);
  }

  // ***** Purely Graphical Stuff. ******

  /** Configures the border on the given component.
      @param jComp The JComponent on which to set the border.
   */
  public void setBorder (JComponent jComp) {
    Border outer = BorderFactory.createEtchedBorder(Theme.global().cwbh,Theme.global().cwbs );
    Border inner = BorderFactory.createEtchedBorder(Theme.global().cwbh,Theme.global().cwbs );
    jComp.setBorder(BorderFactory.createCompoundBorder(outer, inner));
  }

  // ***** Swing Events *****

  /** Handle Swing events from this class and other classes that GDatePicker
      listens to. In particular, this method listens to the click-action on the
      "OK" and "CANCEL" buttons.
      If the "OK" button is clicked, the popup panel is closed and the selected
      date/time is set on this class.
      If the "CANCEL" button is clicked, the popup panel is removed and whatever
      date/time that was selected in this open/close session is discarded.
      @param evt The incoming event.
   */
  @Override
  public void actionPerformed (ActionEvent evt) {

    if (evt.getActionCommand().equals(calendarPanel.getOkText())) {
      dateSelected = true;
      setDate(calendarPanel.getDate());
      if (popup.isVisible()) popup.setVisible(false);
      updateUI();
      sendWidgetMessage(label, 0, getValue());
      refresh();
    }
    else if(evt.getActionCommand().equals(calendarPanel.getCancelText())) {
      dateSelected = true;
      // do not update the text field
      if (popup.isVisible()) popup.setVisible(false);
    }
    else {
      int x = calendarButton.getWidth() -
            (int) popup.getPreferredSize().getWidth();
      int y = calendarButton.getY() + calendarButton.getHeight();

      popup.show(calendarButton, x, y);
      dateSelected = false;
    }
  }

  /** Listens for a "date" property change or a "day" property change event
      from the MJDatePickerPanel.  Updates the date text field.
      If the property change event is "done", also sets the date
      text field in addition to closing the popup.  If the property event is
      "cancel", no change is made and the popup is closed.
      @param evt The incoming property change event
   */
  @Override
  public void propertyChange (PropertyChangeEvent evt) {
    if (evt.getPropertyName().equals("day")) {
      dateSelected = true;
      setDate(calendarPanel.getDate());
    }
    else if (evt.getPropertyName().equals("date")) {
      Date newDate = calendarPanel.getDate();
      setDate(newDate);
      updateUI();
      // calendarPanel.firePropertyChange("date", oldValue, newValue);
    }
  }

  // ****** Midas Event Processing *****

  @Override
  protected int processEventMessage (String name, int info, Object data) {
    if (button==3 && !is(PERM)) { // right mouse button (close if NOT permanent)
      close();
      return 1;
    }
    else {
//    System.out.println("DEBUG: Process Event Message: " + name + " info " + info + " data  " + data);
    }
    return -1;
  }

  /** Set the widget theme for this text widget and the MJTextArea.
      @param o The theme in String, Table or Object form
  */
  public void setWidgetTheme (Object o) {
    super.setWidgetTheme(o);
    calendarPanel.setForeground(theme.getWidgetForeground());
    calendarPanel.setBackground(theme.getWidgetBackground());
  }

  @Override
  public int processMessage (Message msg) {
    return super.processMessage(msg);
  }

  /** Updates the Swing interface. */
  public void updateUI () {
    if (calendarPanel != null) {
      SwingUtilities.updateComponentTreeUI(popup);
      calendarPanel.updateUI();
    }

    // Update the timeField text value
    if (this.getValue() != null) {
      // Format the date string to look "nice"
      timeField.setText(getPrettyTime());
    }
  }

  /** Date/Calendar/Time Manipulation Methods **/

  /** Takes a Date object and converts it to NeXtMidas time, then sets the
      instance value of this control to reflect the conversion.
      If the value is updated via shell or macro, this method will also refresh the UI.
      @param date
   */
  private void setDate (Date date) {
    double time = Time.fromDateObject(date);
    Time newTime = new Time(time);
    setValue(newTime);
    super.refresh();
  }

  /** Returns the currently stored Time/Date value.  Calls getValue().toDateObject(). */
  private Date getDate () {
    return getValue().toDateObject();
  }

  // ***** Primary Accessory Methods. *****

  /** Returns the current value (Time) of this DatePicker.
      @return The current time as an Object.
   */
  public Time getValue () {
    return value;
  }

  /** Sets the current Time value of this DatePicker.
   * @param value new time value
   */
  public void setValue (Time value) {
    this.value = value;

    if (calendarPanel != null)
      calendarPanel.setTime(value);

    super.refresh();
  }

  /** Sets the Start Time on this control.
      @param startTime the new Start Time to set
   */
  public void setStartTime (Time startTime) {
    setValue(startTime);
  }

  /** Returns the current Start Time.
      @return The start time as a Time object
   */
  public Time getStartTime () {
    return getValue();
  }

  /** Return the current selected date/time in a user-friendly format.
      The current output format is VAX.
      @return A String representing the current date/time in a nice format.
   */
  private String getPrettyTime () {
    Time currentTime = this.getValue();
    StringBuffer prettyOutput = new StringBuffer();
    prettyOutput.append(Time.format(currentTime.getSec(), Time.VAX));

    return prettyOutput.toString();
  }

  // ***** Secondary Accessory Methods. *****

  /** get seed text from {@link #seedText}.*/
  private String getSeedText () {
    return seedText.getText();
  }

  /** Updates the seed/label value of the text field and attempts to adjust
      the height of the label appropriately.
      @param seed The text value to set
   */
  private void setSeedText (String seed) {
    this.seed = seed;
    this.seedText.setText(seed);

    if (this.seedText != null) {
      if (this.seedText.getText() != null) {
        int strLen = this.seedText.getText().length();
        this.seedText.setSize(strLen * 2, fixHeight);
      }
    }
  }
}