package org.openswing.swing.client;

import java.math.*;
import java.text.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

import org.openswing.swing.logger.client.*;
import org.openswing.swing.util.client.*;


/**
 * <p>Title: OpenSwing Framework</p>
 * <p>Description: Numeric input control.</p>
 * <p>Copyright: Copyright (C) 2006 Mauro Carniel</p>
 *
 * <p> This file is part of OpenSwing Framework.
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the (LGPL) Lesser General Public
 * License as published by the Free Software Foundation;
 *
 *                GNU LESSER GENERAL PUBLIC LICENSE
 *                 Version 2.1, February 1999
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with this library; if not, write to the Free
 * Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 *       The author may be contacted at:
 *           maurocarniel@tin.it</p>
 *
 * @author Mauro Carniel
 * @version 1.0
 */
public class NumericControl extends BaseInputControl implements InputControl {

  /** maximum number of digits */
  private int maxCharacters = 255;

  /** maximum value */
  private double maxValue = Integer.MAX_VALUE;

  /** minimum value; default value: 0 */
  private double minValue = 0;

  /** maximum number of decimals */
  private int decimals = 0;

  /** flag used to set thousands symbol visibility; default value = false */
  private boolean grouping = false;

  /** format for the text */
  private DecimalFormat format = null;

  /** value for an input control value not defined */
  private String nullValue = null;

  /** numeric field */
  private NumBox numBox = new NumBox();

  /** flag used to define whether zero digits (after decimal point) must be hided/showed; default value: <code>ClientSettings.ZERO_SHOWS_AS_ABSENT</code> i.e. show zero digits */
  private boolean hideZeroDigits = ClientSettings.HIDE_ZERO_DIGITS;


  /**
   * Constructor.
   */
  public NumericControl() {
    this(10);
  }


  /**
   * Constructor.
   * @param columns number of visibile characters
   */
  public NumericControl(int columns) {
    numBox.setColumns(columns);
    numBox.setDisabledTextColor(UIManager.getColor("TextField.foreground"));

//    setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
//    this.add(numBox);
    this.setLayout(new GridBagLayout());
    this.add(numBox, new GridBagConstraints(0, 0, 1, 1, 1.0, 1.0, GridBagConstraints.WEST, GridBagConstraints.HORIZONTAL, new Insets(0, 0, 0, 0), 0, 0));
//    this.setLayout(new java.awt.BorderLayout(0,0));
//    this.add(numBox, java.awt.BorderLayout.CENTER);


    StringBuffer s = new StringBuffer(numBox.getColumns()); for(int i=0;i<numBox.getColumns();i++) s.append("0");
    setMinimumSize(new Dimension(
      numBox.getFontMetrics(numBox.getFont()).stringWidth(s.toString()),
      numBox.getPreferredSize().height
    ));


    setFormat();
    addKeyListener();
    addFocusListener();
    initListeners();
  }


  /**
   * @return component inside this whose contains the value
   */
  public JComponent getBindingComponent() {
    return numBox;
  }


  /**
   * Method called to set the numeric format.
   */
  private void setFormat() {

    DecimalFormatSymbols dfs = new DecimalFormatSymbols();
    dfs.setGroupingSeparator(ClientSettings.getInstance().getResources().getGroupingSymbol());
    dfs.setDecimalSeparator(ClientSettings.getInstance().getResources().getDecimalSymbol());

    String zero = "0";
    if (hideZeroDigits)
      zero = "#";

    if (!grouping && decimals==0)
      format = new DecimalFormat("#");
    else if (grouping && decimals==0)
      format = new DecimalFormat("#,###",dfs);
    else if (grouping && decimals>0) {
      String dec = "";
      for(int i=0;i<decimals;i++)
        dec += zero;
      format = new DecimalFormat("#,##0."+dec,dfs);
    } else if (!grouping && decimals>0) {
      String dec = "";
      for(int i=0;i<decimals;i++)
        dec += zero;
      format = new DecimalFormat("0."+dec,dfs);
    }
    format.setGroupingUsed(grouping);
    nullValue = "";
  }


  /**
   * @return BigDecimal value
   */
  public final BigDecimal getBigDecimal() {
    if (numBox.getText().length()==0 || numBox.getText().equals(nullValue))
      return null;
    try {
      BigDecimal num = new BigDecimal(format.parse(numBox.getText()).doubleValue());
//      if (String.valueOf(num.doubleValue()-(double)num.intValue()).length()>10)
        num = num.setScale(decimals,BigDecimal.ROUND_HALF_UP);
      return num;
    }
    catch (ParseException ex) {
      Logger.error(this.getClass().getName(),"getBigDecimal","Error while creating BigDecimal object",ex);
      return null;
    }
  }


  /**
   * @return Double value
   */
  public final Double getDouble() {
    if (numBox.getText().length()==0 || numBox.getText().equals(nullValue))
      return null;
    try {
      return new Double(format.parse(numBox.getText()).doubleValue());
    }
    catch (ParseException ex) {
      Logger.error(this.getClass().getName(),"getDouble","Error while creating BigDecimal object",ex);
      return null;
    }
  }


  /**
   * @return BigDecimal value
   */
  public final Object getValue() {
    return getBigDecimal();
  }


  /**
   * @return numeric value (without any currency symbol)
   */
  public final String getText() {
    BigDecimal value = null;
    try {
      if (numBox.getText().length()!=0 && !numBox.getText().equals(nullValue) && !"-".equals(numBox.getText()))
        value = new BigDecimal(format.parse(numBox.getText()).doubleValue());
      if ("-".equals(numBox.getText()))
        value = new BigDecimal(0).negate();
    }
    catch (ParseException ex) {
      Logger.error(this.getClass().getName(),"getText","Error while creating BigDecimal object",ex);
      return null;
    }

    if (value==null)
      return null;
    else
      return String.valueOf(value.doubleValue());
  }


  /**
   * Set maximum number of characters.
   * @param maxCharacters maximum number of characters
   */
  public void setMaxCharacters(int maxCharacters) {
    this.maxCharacters = maxCharacters;
  }


  /**
   * @return maximum number of decimals
   */
  public int getDecimals() {
    return decimals;
  }


  /**
   * Set maximum number of decimals.
   * @param decimals maximum number of decimals
   */
  public void setDecimals(int decimals) {
    this.decimals = decimals;
    setFormat();
  }


  /**
   * @return maximum value
   */
  public double getMaxValue() {
    return maxValue;
  }


  /**
   * Set maximum value.
   * @param maxValue maximum value
   */
  public void setMaxValue(double maxValue) {
    this.maxValue = maxValue;
  }


  /**
   * @return minimum value
   */
  public double getMinValue() {
    return minValue;
  }


  /**
   * Set minimum value.
   * @param minValue minimum value
   */
  public void setMinValue(double minValue) {
    this.minValue = minValue;
  }


  /**
   * @return maximum number of digits
   */
  public int getMaxCharacters() {
    return maxCharacters;
  }


  /**
   * Set thousands symbol visibility.
   * @param grouping thousands symbol visibility
   */
  public final void setGrouping(boolean grouping) {
    this.grouping = grouping;
    setFormat();
  }


  /**
   * @return boolean thousands symbol visibility
   */
  public final boolean isGrouping() {
    return grouping;
  }


  /**
   * Create a key listener to correcly set the content of the control.
   */
  private void addKeyListener() {
    numBox.addKeyListener(new KeyAdapter() {

      public final void keyReleased(KeyEvent e) {
        e.consume();
      }

      public final void keyTyped(KeyEvent e) {
        if (NumericControl.this.getText()==null ||
            NumericControl.this.getText().length()==0)
          NumericControl.this.setText(nullValue);

        if (e.getKeyChar()==ClientSettings.getInstance().getResources().getDecimalSymbol() &&
            decimals>0 &&
            numBox.getText()!=null &&
            numBox.getText().indexOf(ClientSettings.getInstance().getResources().getDecimalSymbol())!=-1) {
          e.consume();
          return;
        }

        if (e.getKeyChar()==ClientSettings.getInstance().getResources().getDecimalSymbol() && decimals>0)
          return;
        else if (e.getKeyChar()==ClientSettings.getInstance().getResources().getGroupingSymbol() && grouping)
          return;
        else if (e.getKeyChar()=='\b')
          return;
        else if (e.getKeyChar()=='-' && minValue<0 && numBox.getCaretPosition()==0 && numBox.getText()!=null && (numBox.getText().length()==0 || numBox.getText().charAt(0)!='-'))
          return;
        else if (e.getKeyChar()<'0' || e.getKeyChar()>'9')
          e.consume();

        if (numBox.getText()!=null &&
            numBox.getText().length()>=maxCharacters) {
          e.consume();
        }


      }

      public void keyPressed(KeyEvent e) {
        if (e.getKeyCode()==e.VK_ENTER ||
            e.getKeyCode()==e.VK_TAB ||
            e.getKeyCode()==e.VK_DELETE ||
            e.getKeyCode()==e.VK_BACK_SPACE ||
            e.getKeyCode()==e.VK_LEFT ||
            e.getKeyCode()==e.VK_RIGHT)
          return;
        e.consume();
      }

    });
  }


  /**
   * Method that overrides the super class setText method.
   * @param text number to set
   */
  public final void setText(String text) {
    try {
      BigDecimal number = null;
      if (text != null && text.length() > 0) {
        number = new BigDecimal(text);
      }
      if (number!=null)
        numBox.setText(format.format(number));
      else
        numBox.setText(nullValue);
    }
    catch (Exception ex) {
      numBox.setText(nullValue);
    }
  }


  /**
   * @param value number to set
   */
  public final void setValue(Object value) {
    try {
      if (value==null)
        numBox.setText(nullValue);
      else
        numBox.setText(format.format(value));
    }
    catch (Exception ex) {
      numBox.setText(nullValue);
    }
  }


  /**
   * Create a focus listener to correcly set the content of the control.
   */
  private void addFocusListener() {
    numBox.addFocusListener(new FocusAdapter() {

      public void focusLost(FocusEvent e) {

        if (numBox.isEnabled() && numBox.isEditable()) {
          try {
            Number number = format.parse(numBox.getText());
            if (number!=null) {
              if (number.doubleValue()>maxValue) {
                setText(String.valueOf(maxValue));
                number = new BigDecimal(maxValue);
              }
              else if (number.doubleValue()<minValue) {
                setText(String.valueOf(minValue));
                number = new BigDecimal(minValue);
              }
              else
                setText(String.valueOf(number));

              // test number of decimals...
              java.math.BigDecimal d = new java.math.BigDecimal(number.doubleValue());
              d = d.setScale(decimals,java.math.BigDecimal.ROUND_HALF_UP);
              setText(d.toString());

            }

          }
          catch (ParseException ex) {
            setText(nullValue);
          }
          if (numBox.getText().length()>maxCharacters)
            NumericControl.this.setText(numBox.getText().substring(0,maxCharacters));
        }

      }

    });
  }


  /**
   * @return flag used to define whether zero digits (after decimal point) must be hided/showed
   */
  public final boolean isHideZeroDigits() {
    return hideZeroDigits;
  }


  /**
   * Define whether zero digits (after decimal point) must be hided/showed; default value: <code>false</code> i.e. show zero digits.
   * @param hideZeroDigits flag used to define whether zero digits (after decimal point) must be hided/showed
   */
  public final void setHideZeroDigits(boolean hideZeroDigits) {
    this.hideZeroDigits = hideZeroDigits;
  }


  /**
   * Replace enabled setting with editable setting (this allow tab swithing).
   * @param enabled flag used to set abilitation of control
   */
  public void setEnabled(boolean enabled) {
    numBox.setEditable(enabled);
    numBox.setFocusable(enabled || ClientSettings.DISABLED_INPUT_CONTROLS_FOCUSABLE);
    if (!enabled)
      numBox.setBackground((Color)UIManager.get("TextField.inactiveBackground"));
  }


  /**
   * @return current input control abilitation
   */
  public final boolean isEnabled() {
    try {
      return numBox.isEditable();
    }
    catch (Exception ex) {
      return false;
    }
  }


  /**
   * Set input control visible characters.
   * @param columns visible characters
   */
  public final void setColumns(int columns) {
    numBox.setColumns(columns);

    StringBuffer s = new StringBuffer(numBox.getColumns()); for(int i=0;i<numBox.getColumns();i++) s.append("0");
    setMinimumSize(new Dimension(
      numBox.getFontMetrics(numBox.getFont()).stringWidth(s.toString()),
      numBox.getPreferredSize().height
    ));
  }


  /**
   * @return input control visibile characters
   */
  public final int getColumns() {
    return numBox.getColumns();
  }


  /**
   * Adds the specified focus listener to receive focus events from
   * this component when this component gains input focus.
   * If listener <code>l</code> is <code>null</code>,
   * no exception is thrown and no action is performed.
   *
   * @param    l   the focus listener
   * @see      java.awt.event.FocusEvent
   * @see      java.awt.event.FocusListener
   * @see      #removeFocusListener
   * @see      #getFocusListeners
   * @since    JDK1.1
   */
  public final void addFocusListener(FocusListener listener) {
    try {
      numBox.addFocusListener(listener);
    }
    catch (Exception ex) {
    }
  }


  /**
   * Removes the specified focus listener so that it no longer
   * receives focus events from this component. This method performs
   * no function, nor does it throw an exception, if the listener
   * specified by the argument was not previously added to this component.
   * If listener <code>l</code> is <code>null</code>,
   * no exception is thrown and no action is performed.
   *
   * @param    l   the focus listener
   * @see      java.awt.event.FocusEvent
   * @see      java.awt.event.FocusListener
   * @see      #addFocusListener
   * @see      #getFocusListeners
   * @since    JDK1.1
   */
  public final void removeFocusListener(FocusListener listener) {
    try {
      numBox.removeFocusListener(listener);
    }
    catch (Exception ex) {
    }
  }


  /**
   * Adds the specified action listener to receive
   * action events from this textfield.
   *
   * @param l the action listener to be added
   */
  public final void addActionListener(ActionListener listener) {
    try {
      numBox.addActionListener(listener);
    }
    catch (Exception ex) {
    }
  }


  /**
   * Adds the specified key listener to receive
   * action events from this field.
   *
   * @param l the key listener to be added
   */
  public final void addKeyListener(KeyListener listener) {
    try {
      numBox.addKeyListener(listener);
    }
    catch (Exception ex) {
    }
  }



  public void processKeyEvent(KeyEvent e) {
    numBox.processKeyEvent(e);
  }


  /**
   * Removes the specified action listener so that it no longer
   * receives action events from this field.
   *
   * @param l the action listener to be removed
   */
  public final void removeActionListener(ActionListener listener) {
    try {
      numBox.removeActionListener(listener);
    }
    catch (Exception ex) {
    }
  }


  /**
   * Removes the specified key listener so that it no longer
   * receives action events from this field.
   *
   * @param l the key listener to be removed
   */
  public final void removeKeyListener(KeyListener listener) {
    try {
      numBox.removeKeyListener(listener);
    }
    catch (Exception ex) {
    }
  }


  /**
   * Selects the text between the specified start and end positions.
   * <p>
   * This method sets the start and end positions of the
   * selected text, enforcing the restriction that the start position
   * must be greater than or equal to zero.  The end position must be
   * greater than or equal to the start position, and less than or
   * equal to the length of the text component's text.
   * <p>
   * If the caller supplies values that are inconsistent or out of
   * bounds, the method enforces these constraints silently, and
   * without failure. Specifically, if the start position or end
   * position is greater than the length of the text, it is reset to
   * equal the text length. If the start position is less than zero,
   * it is reset to zero, and if the end position is less than the
   * start position, it is reset to the start position.
   * <p>
   * This call is provided for backward compatibility.
   * It is routed to a call to <code>setCaretPosition</code>
   * followed by a call to <code>moveCaretPosition</code>.
   * The preferred way to manage selection is by calling
   * those methods directly.
   *
   * @param selectionStart the start position of the text
   * @param selectionEnd the end position of the text
   * @see #setCaretPosition
   * @see #moveCaretPosition
   */
  public final void select(int selectionStart,int selectionEnd) {
    numBox.select(selectionStart,selectionEnd);
  }


}


/**
 * <p>Title: OpenSwing Framework</p>
 * <p>Description: Inner class used to redirect key event to the inner JTextField.</p>
 * <p>Copyright: Copyright (C) 2006 Mauro Carniel</p>
 * @author Mauro Carniel
 * @version 1.0
 */
class NumBox extends JTextField {

    public void processKeyEvent(KeyEvent e) {
      super.processKeyEvent(e);
    }

}
