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.*;
import java.beans.Beans;
import javax.swing.event.DocumentListener;
import javax.swing.event.DocumentEvent;
import javax.swing.text.*;

/**
 * <p>Title: OpenSwing Framework</p>
 * <p>Description: Currency (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 CurrencyControl 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 CurrencyBox currencyBox = new CurrencyBox();

  /** 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;


  /** currency symbol; default value: ClientSettings.getInstance().getResources().getCurrencySymbol() */
  private String currencySymbol = ClientSettings.getInstance().getResources().getCurrencySymbol();

  /** decimal symbol; default value: ClientSettings.getInstance().getResources().getDecimalSymbol() */
  private char decimalSymbol = ClientSettings.getInstance().getResources().getDecimalSymbol();

  /** grouping symbol; default value: ClientSettings.getInstance().getResources().getGroupingSymbol() */
  private char groupingSymbol = ClientSettings.getInstance().getResources().getGroupingSymbol();

  /** flag used to define the default position of currency symbol in currency control/column: on the left or on the right of the numeric value; default value: <code>ClientSettings.CURRENCY_SYMBOL_ON_LEFT</code> i.e. on the left of the numeric value */
  private boolean currencySymbolOnLeft = ClientSettings.CURRENCY_SYMBOL_ON_LEFT;


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


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

//    setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
//    this.add(currencyBox);
    this.setLayout(new GridBagLayout());
    this.add(currencyBox, 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(currencyBox, java.awt.BorderLayout.CENTER);


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


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

    setGrouping(true);
  }


  /**
   * Method called to set the numeric format.
   */
  protected void setFormat() {
    DecimalFormatSymbols dfs = new DecimalFormatSymbols();
    dfs.setGroupingSeparator(groupingSymbol);
    dfs.setDecimalSeparator(decimalSymbol);

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

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

    if (currencySymbolOnLeft)
      nullValue = currencySymbol+" ";
    else
      nullValue = " "+currencySymbol;

    setText("");
  }


  /**
   * @return currency symbol
   */
  public final String getCurrencySymbol() {
    return currencySymbol;
  }


  /**
   * Set the currency symbol.
   * @param currencySymbol currency symbol
   */
  public final void setCurrencySymbol(String currencySymbol) {
    Object val = getValue();
    this.currencySymbol = currencySymbol;
    if (!Beans.isDesignTime()) {
      setFormat();
      setValue(val);
    }
  }


  /**
   * @return decimal symbol
   */
  public final char getDecimalSymbol() {
    return decimalSymbol;
  }


  /**
   * @return grouping symbol
   */
  public final char getGroupingSymbol() {
    return groupingSymbol;
  }


  /**
   * Set decimal symbol.
   * @param decimalSymbol decimal symbol
   */
  public final void setDecimalSymbol(char decimalSymbol) {
    Object val = getValue();
    this.decimalSymbol = decimalSymbol;
    if (!Beans.isDesignTime()) {
      setFormat();
      setValue(val);
    }
  }


  /**
   * Set grouping symbol.
   * @param groupingSymbol grouping symbol
   */
  public final void setGroupingSymbol(char groupingSymbol) {
    Object val = getValue();
    this.groupingSymbol = groupingSymbol;
    if (!Beans.isDesignTime()) {
      setFormat();
      setValue(val);
    }
  }


  /**
   * Set maximum number of decimals.
   * @param decimals maximum number of decimals
   */
  public final void setDecimals(int decimals) {
    Object val = getValue();
    this.decimals = decimals;
    if (!Beans.isDesignTime()) {
      setFormat();
      setValue(val);
    }
  }


  /**
   * @return flag used to define the default position of currency symbol in currency control/column: on the left or on the right of the numeric value
   */
  public final boolean isCurrencySymbolOnLeft() {
    return currencySymbolOnLeft;
  }


  /**
   * Set the flag used to define the default position of currency symbol in currency control/column: on the left or on the right of the numeric value.
   * Default value: <code>ClientSettings.CURRENCY_SYMBOL_ON_LEFT</code> i.e. on the left of the numeric value.
   * @param currencySymbolOnLeft flag used to define the default position of currency symbol in currency control/column: on the left or on the right of the numeric value
   */
  public final void setCurrencySymbolOnLeft(boolean currencySymbolOnLeft) {
    this.currencySymbolOnLeft = currencySymbolOnLeft;
  }




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


  /**
   * @return BigDecimal value
   */
  public final BigDecimal getBigDecimal() {
    if (currencyBox.getText().length()==0 || currencyBox.getText().equals(nullValue))
      return null;
    try {
      BigDecimal num = new BigDecimal(format.parse(currencyBox.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 (currencyBox.getText().length()==0 || currencyBox.getText().equals(nullValue))
      return null;
    try {
      return new Double(format.parse(currencyBox.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 (currencyBox.getText().length()!=0 && !currencyBox.getText().trim().equals(nullValue.trim()) && !"-".equals(currencyBox.getText()))
        value = new BigDecimal(format.parse(currencyBox.getText()).doubleValue());
      if ("-".equals(currencyBox.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;
  }



  /**
   * @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() {
    currencyBox.addKeyListener(new KeyAdapter() {

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

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

        if (e.getKeyChar()==ClientSettings.getInstance().getResources().getDecimalSymbol() &&
            decimals>0 &&
            currencyBox.getText()!=null &&
            currencyBox.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') {
          if (currencySymbolOnLeft && currencyBox.getCaretPosition()<=nullValue.length())
            e.consume();
          else if (!currencySymbolOnLeft && currencyBox.getCaretPosition()>currencyBox.getText().length()-nullValue.length())
            e.consume();

          return;
        }
        else if (e.getKeyChar()=='-' && minValue<0 && currencyBox.getCaretPosition()==0 && currencyBox.getText()!=null && (currencyBox.getText().length()==0 || currencyBox.getText().charAt(0)!='-'))
          return;
        else if (e.getKeyChar()<'0' || e.getKeyChar()>'9')
          e.consume();
      }

      public void keyPressed(KeyEvent e) {
        if (e.getKeyCode()==e.VK_DELETE) {
          if (currencySymbolOnLeft && currencyBox.getCaretPosition()<nullValue.length()) {
            e.consume();
            return;
          }
          else if (!currencySymbolOnLeft && currencyBox.getCaretPosition()>currencyBox.getText().length()-nullValue.length()) {
            e.consume();
            return;
          }
        }
        if (e.getKeyCode()==e.VK_LEFT) {
          if (currencySymbolOnLeft && currencyBox.getCaretPosition()<=nullValue.length()) {
            e.consume();
            return;
          }
        }
        if (e.getKeyCode()==e.VK_RIGHT) {
          if (!currencySymbolOnLeft && currencyBox.getCaretPosition()>=currencyBox.getText().length()-nullValue.length()) {
            e.consume();
            return;
          }
        }
        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)
        currencyBox.setText(format.format(number));
      else
        currencyBox.setText(nullValue);
    }
    catch (Exception ex) {
      currencyBox.setText(nullValue);
    }
  }


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


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

      public void focusLost(FocusEvent e) {

        if (currencyBox.isEnabled() && currencyBox.isEditable()) {
          try {
            Number number = format.parse(currencyBox.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 (currencyBox.getText().length()>maxCharacters)
            CurrencyControl.this.setText(currencyBox.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) {
    currencyBox.setEditable(enabled);
    currencyBox.setFocusable(enabled || ClientSettings.DISABLED_INPUT_CONTROLS_FOCUSABLE);
    if (!enabled)
      currencyBox.setBackground((Color)UIManager.get("TextField.inactiveBackground"));
  }


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


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

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


  /**
   * @return input control visibile characters
   */
  public final int getColumns() {
    return currencyBox.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 {
      currencyBox.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 {
      currencyBox.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 {
      currencyBox.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 {
      currencyBox.addKeyListener(listener);
    }
    catch (Exception ex) {
    }
  }



  public void processKeyEvent(KeyEvent e) {
    currencyBox.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 {
      currencyBox.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 {
      currencyBox.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) {
    currencyBox.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 CurrencyBox extends JTextField {

    public CurrencyBox() {
      this.getDocument().addDocumentListener(new DocumentListener() {

        public void changedUpdate(DocumentEvent e) {
        }

        public void insertUpdate(DocumentEvent e) {
          if (!currencySymbolOnLeft && getCaretPosition()>=getText().length()-nullValue.length()) {
            // currency symbol must be showed on the right of numeric value and
            // user attempts to write on the right of currency symbol
            try {
              final BigDecimal n = new BigDecimal(getText().substring(0,getText().length()-nullValue.length()));
              SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                  setText(n+nullValue);
                  setCaretPosition(getText().length()-nullValue.length());
                }
              });
            }
            catch (Exception ex1) {
            }
          }
          if (!currencySymbolOnLeft && getText().startsWith(nullValue))
            // currency symbol must be showed on the right of numeric value and
            // current numeric value is null and user attempts to write the first digit:
            // without rewriting just editing data, it would be write on the right of the text field and
            // this is not correct...
            try {
              final BigDecimal n = new BigDecimal(getText().substring(nullValue.length()));
              SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                  setText(n+nullValue);
                  setCaretPosition(getText().length()-nullValue.length());
                }
              });
            }
            catch (Exception ex1) {
            }
        }

        public void removeUpdate(DocumentEvent e) {
        }

      });
    }


    public String getText() {
      String t = super.getText();
      if (t!=null &&
          !currencySymbolOnLeft &&
          t.indexOf(" ")>0)
        t = t.substring(0,t.indexOf(" "))+nullValue;
      return t;
    }


    public void setCaretPosition(int pos) {
      if (currencySymbolOnLeft && pos<nullValue.length())
        super.setCaretPosition(nullValue.length());
      else if (!currencySymbolOnLeft && pos>=getText().length()-nullValue.length())
        super.setCaretPosition(getText().length()-nullValue.length());
      else
        super.setCaretPosition(pos);
    }


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

  }


}



