/*
 * WepsTextField.java
 *
 * Created on December 27, 2005, 1:02 PM
 *
 * To change this template, choose Tools | Options and locate the template under
 * the Source Creation and Management node. Right-click the template and choose
 * Open. You can then make changes to the template in the Source Editor.
 */

package usda.weru.util;
import java.awt.event.*;
import javax.swing.*;
import java.text.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger; 
import usda.weru.weps.PercentageInputVerifier;
/**
 * Extends JTextField to add extended features for numeric formatting and bounds 
 * testing.
 * @author Joseph Levin
 */
public class WepsTextField extends JTextField implements FocusListener, ActionListener {
    private static final long serialVersionUID = 1L;

    private static final Logger LOGGER = LogManager.getLogger(WepsTextField.class);
    
    /**
     * Constant indicating that a bound includes the given limit.
     */
    public static final int CHECK_INCLUSIVE = 2;
    /**
     * Constant that indicates the a bound excludes the given limit.
     */
    public static final int CHECK_EXCLUSIVE = 4;
    /**
     * Constant indicating that a check should not be performed.
     */
    public static final int CHECK_DISABLED = 8;
    
    /**
     * Constant indicating that when a given value is non numeric to replace the value
     * with the last known valid value.
     */
    public static final int DEFAULT_LAST = 2;       //Last valid value
    /**
     * Constant indicating that a non numeric entry will result in the value being
     * set to a given number.
     */
    public static final int DEFAULT_NUMERIC = 4;     //Numeric default value to set
    /**
     * Constant indicating that a non numeric entry will result in a string of text 
     * being displayed in the text field. #NAN# is the default.
     */
    public static final int DEFAULT_TEXT = 8;       //Text to display, the text will not be parsed when it is applied.

	/**
	 *
	 */
	public static final String PROP_VALUE = "value";
    
    /**
     * Stores the actual value without formatting.
     */
    private double c_value;
    /**
     * Previous valid value.  Used with invalid entries.
     */
    private double c_oldValue;
    /**
     * String representing the format to use when formating the value for display.
     */
    private String c_formatString = "#0.00";
    /**
     * String representing the format to use when formating the value for editing.  It
     * is typical for this format to show more detail.
     */
    private String c_formatStringEdit;
    /**
     * DecimalFormat object used to format values for display.
     */
    private DecimalFormat c_format;
    /**
     * DecimalFormat object used to format values for editing.
     */
    private DecimalFormat c_formatEdit;
    /**
     * Boolean indicating if the numerics of the entry be checked.
     */
    private boolean c_checkNumerics = true; 
    /**
     * Boolean indicating that the entered value should be checked against a high bound.
     */
    private boolean c_checkHigh = false;
    /**
     * Boolean indicating that the entered value should be checked against a low bound.
     */
    private boolean c_checkLow = false;
    /**
     * Boolean indicating the high bound includes the limit.
     */
    private boolean c_checkHighInclusive = true;
    /**
     * Boolean indicating the low bound includes the limit.
     */
    private boolean c_checkLowInclusive = true;
    /**
     * Stores the bits indicating what to do with non numeric entries.
     */
    private int c_defaultFlags = DEFAULT_LAST;
    /**
     * Default numeric value.
     */
    private double c_defaultValue = 0;
    /**
     * Default text value.
     */
    private String c_defaultText = "#NAN#";
    /**
     * Lower limit of the bounds.
     */
    private double c_low = 0;
    /**
     * Higher limit of the bounds.
     */
    private double c_high = 0;
    /** Creates a new instance of WepsTextField */
    public WepsTextField() {
        super();
        c_formatString = "#0.00";
        c_formatStringEdit = "#0.00###";
        setValueInternal(0);
        super.addFocusListener(this);
        super.addActionListener(this);
        updateText();     
    }      
    
    /**
     * Captures focus being given to the textfield.  This event begins the process of
     * changing the displayed text to the value formatted for editing.  The text is 
     * also preselected for the user.
     * @param event FocusEvent
     */
    @Override
    public void focusGained(FocusEvent event){
        updateText();
        if (isEnabled()){
            setSelectionEnd(getText().length());
            setSelectionStart(0);
        }        
    }
    
    /**
     * Captures the loss of focus and begins the process of checking the entered text
     * for validity.
     * @param event FocusEvent
     */
    @Override
    public void focusLost(FocusEvent event){
        checkNumerics(getText());
    }
    
    /**
     * Captures the user hitting enter while in the text field.  Begins the process of 
     * checking the entered text for validity.
     * @param event ActionEvent
     */
    @Override
    public void actionPerformed(ActionEvent event){
        checkNumerics(getText());
    }
    
    /**
     * Updates the stored numeric value and updates the displayed text to reflect the
     * new value.
     * @param value Numeric value.
     */
    public void setValueInternal(double value){
        c_oldValue = getValueInternal();
        c_value = value;
        updateText();
        firePropertyChange(PROP_VALUE, c_oldValue, value);
    }

    /**
     *
     * @param value
     */
    public void setValue(double value) {
        c_oldValue = getValueInternal();
        c_value = value;
        if (value != c_oldValue) {
            updateText();
        }
        firePropertyChange(PROP_VALUE, c_oldValue, value);
    }
    
    /**
     * Updates the displayed text without triggering numeric parsing methods.
     * @param text Overiding text.
     */
    @Override
    public void setText(String text){
        if (c_checkHigh){
            int a = 1;
        }
        boolean cachedCheck = c_checkNumerics;
        c_checkNumerics = false;
        super.setText(text);
        c_checkNumerics = cachedCheck;
    }
    
    /**
     * Set to true so the numeric validity of the user entry is tested.
     * @param check set to true to test
     */
    public void setCheckNumerics(boolean check){
        c_checkNumerics = check;
    }
    
    /**
     * Set the numeric bounds of valid user entry.  THe flag variables use the CHECK_ 
     * bits declared at the top of this class.  These bits indicate if the bound 
     * should be tested and if the limit is inclusive or exlusive.
     * @param low Low limit
     * @param lowFlags CHECK_ flags for the low limit
     * @param high High limit
     * @param highFlags CHECK_ flags for the high limit
     */
    public void setChecks(double low, int lowFlags, double high, int highFlags){
        if (lowFlags != CHECK_DISABLED){
            c_checkLow = true;
            c_low = low;
            if(lowFlags == CHECK_INCLUSIVE){
                c_checkLowInclusive = true;
            }
            else{
                c_checkLowInclusive = false;
            }
        }
        else{
            c_checkLow = false;
        }
        
        if (highFlags != CHECK_DISABLED){
            c_checkHigh = true;
            c_high = high;
            if(highFlags == CHECK_INCLUSIVE){
                c_checkHighInclusive = true;
            }
            else{
                c_checkHighInclusive = false;
            }
        }
        else{
            c_checkHigh = false;
        }
    }
    
    
    /**
     * Getter method for the boolean indicating if this textfield is testing user input.
     * @return True if the text field is testing user input for numeric validity
     */
    public boolean getCheckNumerics(){
        return c_checkNumerics;               
    }

    /**
     * Getter method for the unformatted numeric value.
     * @return Unformatted numeric value.
     */
    public double getValue(){
        if (hasFocus()){
            checkNumerics(getText());
        }
        return getValueInternal();
    }
    
        private double getValueInternal(){        
            return c_value;
        }
    
    
    /**
     * Setter method for the display format.
     * @param formatString String representing the numeric format to use when not editing the value.
     */
    public void setFormat(String formatString){
        c_format = null;
        c_formatString = formatString;
        updateText();
    }
    
    /**
     * Getter method for the display format.
     * @return String representation of the display format.
     */
    public String getFormat(){
        return c_formatString;
    }
    
    /**
     * Setter method for the editing display format.
     * @param formatStringEdit String representing the format to use when editing the value.
     */
    public void setFormatEdit(String formatStringEdit){
        c_formatEdit = null;
        c_formatStringEdit = formatStringEdit;
        c_formatEdit = new DecimalFormat(c_formatStringEdit);
        updateText();
    }
    
    /**
     * Getter method for the editing format.
     * @return String representing the format used when editing the value.
     */
    public String getFormatEdit(){
        return c_formatStringEdit;
    }
    //1 = too big, 0 == okay, -1 = too small
    /**
     * Checks the value against the numeric bounds.  Will update the stored value if 
     * setValue is true.
     * @param value Numeric value to test.
     * @param setValue Pass true when the stored value should be updating.
     * @return 0 = within bounds
     * 1 = value was too high
     * -1 value was too low
     */
    private int checkLimits(double value, boolean setValue){
        if (c_checkLow){
            this.setInputVerifier(new PercentageInputVerifier());
                if (c_checkLowInclusive){
                    if (value < c_low){                
                        //Too low
                        if (setValue){
                            setValueInternal(c_low);
                        }
                        return -1;
                    }
                }
                else{
                    if (value <= c_low){
                        //Too low
                        if (setValue){
                            setValueInternal(c_low + getAdjustment());
                        }
                        return -1;
                    }
                }
            }
            
            if (c_checkHigh){
                this.setInputVerifier(new PercentageInputVerifier());
                if (c_checkHighInclusive){
                    if (value > c_high){
                        //Too high
                        if (setValue){
                            setValueInternal(c_high);
                        }
                        return 1;
                    }
                }
                else{
                    if (value >= c_high){
                        //Too low
                        if (setValue) {
                            setValueInternal(c_high - getAdjustment());
                        }                        
                        return 1;
                    }
                }
            }
            if (setValue){
                setValueInternal(value);
            }
            return 0;
    }
    
    /**
     * Calculates the adjustment to use by looking at the editing format.  The 
     * adjustment value will be the lowest value possible and still be within the
     * editing format.  
     * 
     * Example: If the editing format is #0.00#, the adjustment value will be 0.001.
     * @return Adjustment value to use.
     */
    private double getAdjustment(){
        if (c_formatEdit == null) return 1;
        String adjustment = "";
        for (int i = 1; i < c_formatEdit.getMaximumFractionDigits(); i++){
            if (adjustment.length() == 0){
                adjustment = "0.0";
            }
            else{
                adjustment += "0";
            }                            
        }
        adjustment += "1";
        try{
            return Double.parseDouble(adjustment);
        }
        catch(NumberFormatException nfe){
            return 0;
        }        
    }
    
    
    /**
     * Performs the required default operations as set by the c_defaultFlags bits.
     */
    private void applyDefaults(){
        if ((c_defaultFlags & DEFAULT_LAST) == DEFAULT_LAST){
            setValueInternal(c_oldValue);
        }
        if ((c_defaultFlags & DEFAULT_NUMERIC) == DEFAULT_NUMERIC){
            setValueInternal(c_defaultValue);
        }
        if ((c_defaultFlags & DEFAULT_TEXT) == DEFAULT_TEXT){
            setText(c_defaultText);
        }
    }
    
    /**
     * Setter method for defaults. Use the DEFAULT_ bits as specified at the top of 
     * this class.
     * @param value Numeric value to default to.
     * @param text Text value to display when setting defaults.
     * @param flags Flags to indicate which defaults should be used.
     */
    public void setDefaults(double value, String text, int flags){
        c_defaultValue = value;
        if (text != null) c_defaultText = text;
        c_defaultFlags = flags;
    }
    
    /**
     * Checks the validity for user input and saves the value.
     * @param valueString String inputted by the user.
     */
    private void checkNumerics(String valueString){
        if (!c_checkNumerics) return;        
        c_oldValue = getValueInternal();
        try{            
            double newValue = Double.parseDouble(valueString);
            checkLimits(newValue, true);            
        }
        catch(NumberFormatException nfe){
            setValueInternal(c_oldValue);
            applyDefaults();
        }
        updateText();
    }
    /**
     * Helper method for testing if the user is editing the textfield.
     * @return true if isEnabled() and hasFocus() return true
     */
    private boolean isEditing(){
        return (hasFocus() && isEnabled());
    }
    
    /**
     * Entry point for the display updates.  Determines which format should be used and
     * then calls updateFormatted or updateUnFormatted when editing.
     */
    private void updateText(){
        if ((c_defaultFlags & DEFAULT_TEXT) == DEFAULT_TEXT && getText().trim().equals(c_defaultText)) {
            return;
        }
        if (isEditing()){
            //Update the display to the unformatted value
            updateUnFormatted();
        }
        else{
            //Update the display to the formatted value
            updateFormatted();
        }
    }
    
    /**
     * Displays the value in it's display format.  The formatted value is tested for
     * rounding error so an exlusive bound won't cause the display value to equal the 
     * limit.
     */
    private void updateFormatted(){
        if(c_format == null) {
            c_format = new DecimalFormat(c_formatString);            
        }
        if (Double.isNaN(getValueInternal())){
            setText("");
            return;
        }
        String firstPass = c_format.format(getValueInternal());
        double value1 = Double.NaN;
            try{
                value1 = Double.parseDouble(firstPass);
            }
            catch(NumberFormatException nfe){
                LOGGER.error("Error parsing value:", nfe);
            }
        double value2 = value1;
        int result = checkLimits(value2, false);
        while (result != 0){
            value1 -= getAdjustment() * result;
            firstPass = c_format.format(value1);
            value2 = Double.parseDouble(firstPass);
            result = checkLimits(value2, false);
        }

        String secondPass = c_format.format(value1);
        setText(secondPass);
    }
    
    /**
     * Displays the value in the editing format.
     */
    private void updateUnFormatted(){
        if(c_formatEdit == null && c_formatStringEdit != null){
            c_formatEdit = new DecimalFormat(c_formatStringEdit);
        }
        if(c_formatEdit == null){
            setText(Double.toString(getValueInternal()));
        }
        else{
            setText(c_formatEdit.format(getValueInternal()));
        }

    }
    



}
