package nxm.ucl.libg;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

import javax.swing.BorderFactory;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.JTextField;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingConstants;
import javax.swing.UIManager;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

/** A class that extends JPanel and implements a modified "spinner" date model.
    This class is used by GDatePicker, specifically for month and year spinners.
    Some of the source code contained below was taken and modified from the
    JCalendar package, an open-source, freely licensed Swing Calendar.
    @author crs
    @since NeXtMidas 2.9.1
 **/
public class MJSpinField extends JPanel implements ChangeListener, CaretListener, ActionListener, FocusListener {
  /** NOTE: this should be changed if the structure/fields of this class changes. */
  private static final long serialVersionUID = 2012122807331L; // for Serializable interface

  /** The main Spinner component */
  protected JSpinner spinner;
  /** The input text field */
  protected JTextField textField;
  /** The minimum value */
  protected int min;
  /** The maximum value */
  protected int max;
  /** The selected value */
  protected int value;
  /** The foreground Color used when a valid value is entered */
  protected Color darkGreen;

  /**
    Default constructor that instantiates this class with
    minimum and maximum values from the Integer class.
   */
  public MJSpinField () {
    this(Integer.MIN_VALUE, Integer.MAX_VALUE);
  }

  /**
    Instantiates this class with the given minimum and maximum values.
    @param min The minimum value represented in this spinner
    @param max The maximum value represented in this spinner
   */
  public MJSpinField (int min, int max) {

    super();
    setName("MJSpinField");

    this.min = min;
    if (max < min)
      max = min;

    this.max = max;

    value = 0;
    if (value < min)
      value = min;
    if (value > max)
      value = max;

    darkGreen = new Color(0, 150, 0);
    setLayout(new BorderLayout());
    textField = new JTextField();
    textField.addCaretListener(this);
    textField.addActionListener(this);
    textField.setHorizontalAlignment(SwingConstants.RIGHT);
    textField.setBorder(BorderFactory.createEmptyBorder());
    textField.setText(Integer.toString(value));
    textField.addFocusListener(this);
    spinner = new JSpinner() {
      private static final long serialVersionUID = -6287709243342021172L;
      private JTextField textField = new JTextField();

      public Dimension getPreferredSize () {
        Dimension size = super.getPreferredSize();
        return new Dimension(size.width, textField.getPreferredSize().height);
      }
    };
    spinner.setEditor(textField);
    spinner.addChangeListener(this);
    add(spinner, BorderLayout.CENTER);

  }

  /**
    Adjusts the spinner to the appropriate size given the length of the value.
   */
  public void adjustWidthToMaximumValue () {
    JTextField testTextField = new JTextField(Integer.toString(max));
    int width = testTextField.getPreferredSize().width;
    int height = testTextField.getPreferredSize().height;
    textField.setPreferredSize(new Dimension(width, height));
    textField.revalidate();
  }

  /**
    Is invoked when the spinner model changes
    @param e the ChangeEvent
   */
  public void stateChanged (ChangeEvent e) {
    SpinnerNumberModel model = (SpinnerNumberModel) spinner.getModel();
    int value = model.getNumber().intValue();
    setValue(value);
  }

  /**
    Sets the value attribute of the JSpinField object.
    @param newValue The new value
    @param updateTextField true if text field should be updated
   */
  protected void setValue (int newValue, boolean updateTextField,
          boolean firePropertyChange) {
    int oldValue = value;
    if (newValue < min) {
      value = min;
    } else if (newValue > max) {
      value = max;
    } else {
      value = newValue;
    }

    if (updateTextField) {
      textField.setText(Integer.toString(value));
    }

    if (firePropertyChange) {
      firePropertyChange("value", oldValue, value);
    }
  }

  /**
    Sets the value.
    @param newValue the new value
    @see #getValue
   */
  public void setValue (int newValue) {
    setValue(newValue, true, true);
    spinner.setValue(Integer.valueOf(value)); // Updated in 3.9.0 to use valueOf instead of new instance
  }

  /**
    Returns the value.
    @return the value value
   */
  public int getValue () {
    return value;
  }

  /**
    Sets the minimum value.
    @param newMinimum the new minimum value
    @see #getMinimum
   */
  public void setMinimum (int newMinimum) {
    min = newMinimum;
  }

  /**
    Returns the minimum value.
    @return the minimum value
   */
  public int getMinimum () {
    return min;
  }

  /**
    Sets the maximum value and adjusts the preferred width.
    @param newMaximum the new maximum value
    @see #getMaximum
   */
  public void setMaximum (int newMaximum) {
    max = newMaximum;
  }

  /**
    Sets the horizontal alignment of the displayed value.
    @param alignment the horizontal alignment
   */
  public void setHorizontalAlignment (int alignment) {
    textField.setHorizontalAlignment(alignment);
  }

  /**
    Returns the maximum value.
    @return the maximum value
   */
  public int getMaximum () {
    return max;
  }

  /**
    Sets the font property.
    @param font the new font
   */
  public void setFont (Font font) {
    if (textField != null) {
      textField.setFont(font);
    }
  }

  /**
    Sets the foreground
    @param fg the foreground color
   */
  public void setForeground (Color fg) {
    if (textField != null) {
      textField.setForeground(fg);
    }
  }

  /**
    After any user input, the value of the textfield is proofed. Depending on
    being an integer, the value is colored green or red.
    @param e the caret event
   */
  public void caretUpdate (CaretEvent e) {
      try {
        int testValue = Integer.valueOf(textField.getText()).intValue();

        if ((testValue >= min) && (testValue <= max)) {
          textField.setForeground(darkGreen);
          setValue(testValue, false, true);
        } else {
          textField.setForeground(Color.red);
        }
      } catch (Exception ex) {
        if (ex instanceof NumberFormatException) {
          textField.setForeground(Color.red);
        }

        // Ignore all other exceptions, e.g. illegal state exception
      }

      textField.repaint();
  }

  /**
    After any user input, the value of the textfield is proofed. Depending on
    being an integer, the value is colored green or red. If the textfield is
    green, the enter key is accepted and the new value is set.
    @param e The action event
   */
  public void actionPerformed (ActionEvent e) {
    if (textField.getForeground().equals(darkGreen)) {
      setValue(Integer.valueOf(textField.getText()).intValue());
    }
  }

  /**
    Enable or disable the JSpinField.
    @param enabled The new enabled value
   */
  public void setEnabled (boolean enabled) {
    super.setEnabled(enabled);
    spinner.setEnabled(enabled);
    textField.setEnabled(enabled);

    if (!enabled) {
      textField.setBackground(UIManager.getColor("TextField.inactiveBackground"));
    }
  }

  /**
    Returns the year picker's spinner (which allow the focus to be set to it).
    @return Component the spinner or null, if the month chooser has no spinner
   */
  public Component getSpinner () {
    return spinner;
  }

  /*
    (non-Javadoc)
    @see java.awt.event.FocusListener#focusGained(java.awt.event.FocusEvent)
   */
  public void focusGained (FocusEvent e) {
  }

  /**
    The value of the text field is checked against a valid (green) value. If
    valid, the value is set and a property change is fired.
   */
  public void focusLost (FocusEvent e) {
    actionPerformed(null);
  }

}