/**
 * A numeric text field that provides automatic keystroke filtering to disallow non-numeric characters,
 * and numeric validation to limit the value to a range of valid numbers. The range of acceptable number
 * values can be controlled by setting the {@link #minValue} and {@link #maxValue} configs, and fractional
 * decimals can be disallowed by setting {@link #allowDecimals} to `false`.
 *
 * By default, the number field is also rendered with a set of up/down spinner buttons and has
 * up/down arrow key and mouse wheel event listeners attached for incrementing/decrementing the value by the
 * {@link #step} value. To hide the spinner buttons set `{@link #hideTrigger hideTrigger}:true`; to disable
 * the arrow key and mouse wheel handlers set `{@link #keyNavEnabled keyNavEnabled}:false` and
 * `{@link #mouseWheelEnabled mouseWheelEnabled}:false`. See the example below.
 *
 * # Example usage
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'On The Wall',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'bottles',
 *             fieldLabel: 'Bottles of Beer',
 *             value: 99,
 *             maxValue: 99,
 *             minValue: 0
 *         }],
 *         buttons: [{
 *             text: 'Take one down, pass it around',
 *             handler: function() {
 *                 this.up('form').down('[name=bottles]').spinDown();
 *             }
 *         }]
 *     });
 *
 * # Removing UI Enhancements
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         title: 'Personal Info',
 *         width: 300,
 *         bodyPadding: 10,
 *         renderTo: Ext.getBody(),
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'age',
 *             fieldLabel: 'Age',
 *             minValue: 0, //prevents negative numbers
 *
 *             // Remove spinner buttons, and arrow key and mouse wheel listeners
 *             hideTrigger: true,
 *             keyNavEnabled: false,
 *             mouseWheelEnabled: false
 *         }]
 *     });
 *
 * # Using Step
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         renderTo: Ext.getBody(),
 *         title: 'Step',
 *         width: 300,
 *         bodyPadding: 10,
 *         items: [{
 *             xtype: 'numberfield',
 *             anchor: '100%',
 *             name: 'evens',
 *             fieldLabel: 'Even Numbers',
 *
 *             // Set step so it skips every other number
 *             step: 2,
 *             value: 0,
 *
 *             // Add change handler to force user-entered numbers to evens
 *             listeners: {
 *                 change: function(field, value) {
 *                     value = parseInt(value, 10);
 *                     field.setValue(value + value % 2);
 *                 }
 *             }
 *         }]
 *     });
 */
Ext.define('Ext.form.field.Number', {
    extend:'Ext.form.field.Spinner',
    alias: 'widget.numberfield',
    alternateClassName: ['Ext.form.NumberField', 'Ext.form.Number'],
 
    /**
     * @cfg {RegExp} stripCharsRe 
     * @private
     */
    /**
     * @cfg {RegExp} maskRe 
     * @private
     */
     
    /**
     * @cfg {Boolean} [allowExponential=true]
     * Set to `false` to disallow Exponential number notation
     */
    allowExponential: true,
 
    /**
     * @cfg {Boolean} [allowDecimals=true]
     * False to disallow decimal values
     */
    allowDecimals : true,
 
    /**
     * @cfg {String} decimalSeparator 
     * Character(s) to allow as the decimal separator. Defaults to {@link Ext.util.Format#decimalSeparator decimalSeparator}.
     * @locale
     */
    decimalSeparator : null,
 
    /**
     * @cfg {Boolean} [submitLocaleSeparator=true]
     * False to ensure that the {@link #getSubmitValue} method strips
     * always uses `.` as the separator, regardless of the {@link #decimalSeparator}
     * configuration.
     * @locale
     */
    submitLocaleSeparator: true,
 
    /**
     * @cfg {Number} decimalPrecision 
     * The maximum precision to display after the decimal separator
     * @locale
     */
    decimalPrecision : 2,
 
    /**
     * @cfg {Number} minValue 
     * The minimum allowed value. Will be used by the field's validation logic,
     * and for {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the down spinner button}.
     *
     * Defaults to Number.NEGATIVE_INFINITY.
     */
    minValue: Number.NEGATIVE_INFINITY,
 
    /**
     * @cfg {Number} maxValue 
     * The maximum allowed value. Will be used by the field's validation logic, and for
     * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the up spinner button}.
     *
     * Defaults to Number.MAX_VALUE.
     */
    maxValue: Number.MAX_VALUE,
 
    /**
     * @cfg {Number} step 
     * Specifies a numeric interval by which the field's value will be incremented or decremented when the user invokes
     * the spinner.
     */
    step: 1,
 
    /**
     * @cfg {String} minText 
     * Error text to display if the minimum value validation fails.
     * @locale
     */
    minText : 'The minimum value for this field is {0}',
 
    /**
     * @cfg {String} maxText 
     * Error text to display if the maximum value validation fails.
     * @locale
     */
    maxText : 'The maximum value for this field is {0}',
 
    /**
     * @cfg {String} nanText 
     * Error text to display if the value is not a valid number. For example, this can happen if a valid character like
     * '.' or '-' is left in the field with no number.
     * @locale
     */
    nanText : '{0} is not a valid number',
 
    /**
     * @cfg {String} negativeText 
     * Error text to display if the value is negative and {@link #minValue} is set to 0. This is used instead of the
     * {@link #minText} in that circumstance only.
     * @locale
     */
    negativeText : 'The value cannot be negative',
 
    /**
     * @cfg {String} baseChars 
     * The base set of characters to evaluate as valid numbers.
     */
    baseChars : '0123456789',
 
    /**
     * @cfg {Boolean} autoStripChars 
     * True to automatically strip not allowed characters from the field.
     */
    autoStripChars: false,
 
    initComponent: function() {
        var me = this;
        if (me.decimalSeparator === null) {
            me.decimalSeparator = Ext.util.Format.decimalSeparator;
        }
        me.callParent();
 
        me.setMinValue(me.minValue);
        me.setMaxValue(me.maxValue);
    },
    
    getSubTplData: function(fieldData) {
        var me = this,
            min = me.minValue,
            max = me.maxValue,
            data, inputElAttr, value;
        
        data = me.callParent([fieldData]);
        
        inputElAttr = data.inputElAriaAttributes;
        
        if (inputElAttr) {
            // The checks are to skip the default min and max values, 
            // in which case we don't want to set corresponding ARIA 
            // attributes at all 
            if (min > Number.NEGATIVE_INFINITY) {
                inputElAttr['aria-valuemin'] = min;
            }
            
            if (max < Number.MAX_VALUE) {
                inputElAttr['aria-valuemax'] = max;
            }
            
            value = me.getValue();
            
            if (value != null && value >= min && value <= max) {
                inputElAttr['aria-valuenow'] = value;
            }
        }
        
        return data;
    },
 
    setValue: function(value) {
        var me = this,
            bind, valueBind;
 
        // This portion of the code is to prevent a binding from stomping over 
        // the typed value. Say we have decimalPrecision 4 and the user types 
        // 1.23456. The value of the field will be set as 1.2346 and published to 
        // the viewmodel, which will trigger the binding to fire and setValue to 
        // be called on the field, which would then set the value (and rawValue) to 
        // 1.2346. Instead, if we have focus and the value is the same, just leave 
        // the rawValue alone 
        if (me.hasFocus) {
            bind = me.getBind();
            valueBind = bind && bind.value;
            if (valueBind && valueBind.syncing && value === me.value) {
                return me;
            }
        }
        return me.callParent([value]);
    },
 
    /**
     * Runs all of Number's validations and returns an array of any errors. Note that this first runs Text's
     * validations, so the returned array is an amalgamation of all field errors. The additional validations run test
     * that the value is a number, and that it is within the configured min and max values.
     * @param {Object} [value] The value to get errors for (defaults to the current field value)
     * @return {String[]} All validation errors for this field
     */
    getErrors: function(value) {
        value = arguments.length > 0 ? value : this.processRawValue(this.getRawValue());
 
        var me = this,
            errors = me.callParent([value]),
            format = Ext.String.format,
            num;
 
        if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid 
            return errors;
        }
 
        value = String(value).replace(me.decimalSeparator, '.');
 
        if(isNaN(value)){
            errors.push(format(me.nanText, value));
        }
 
        num = me.parseValue(value);
 
        if (me.minValue === 0 && num < 0) {
            errors.push(this.negativeText);
        }
        else if (num < me.minValue) {
            errors.push(format(me.minText, me.minValue));
        }
 
        if (num > me.maxValue) {
            errors.push(format(me.maxText, me.maxValue));
        }
 
 
        return errors;
    },
 
    rawToValue: function(rawValue) {
        var value = this.fixPrecision(this.parseValue(rawValue));
        if (value === null) {
            value = rawValue || null;
        }
        return  value;
    },
 
    valueToRaw: function(value) {
        var me = this,
            decimalSeparator = me.decimalSeparator;
        value = me.parseValue(value);
        value = me.fixPrecision(value);
        value = Ext.isNumber(value) ? value : parseFloat(String(value).replace(decimalSeparator, '.'));
        value = isNaN(value) ? '' : String(value).replace('.', decimalSeparator);
        return value;
    },
    
    getSubmitValue: function() {
        var me = this,
            value = me.callParent();
            
        if (!me.submitLocaleSeparator) {
            value = value.replace(me.decimalSeparator, '.');
        }  
        return value;
    },
 
    onChange: function(newValue) {
        var ariaDom = this.ariaEl.dom;
        
        this.toggleSpinners();
        this.callParent(arguments);
        
        if (ariaDom) {
            if (Ext.isNumber(newValue) && isFinite(newValue)) {
                ariaDom.setAttribute('aria-valuenow', newValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuenow');
            }
        }
        
    },
    
    toggleSpinners: function(){
        var me = this,
            value = me.getValue(),
            valueIsNull = value === null,
            enabled;
        
        // If it's disabled, only allow it to be re-enabled if we are 
        // the ones who are disabling it. 
        if (me.spinUpEnabled || me.spinUpDisabledByToggle) {
            enabled = valueIsNull || value < me.maxValue;
            me.setSpinUpEnabled(enabled, true);
        }
        
        
        if (me.spinDownEnabled || me.spinDownDisabledByToggle) {
            enabled = valueIsNull || value > me.minValue;
            me.setSpinDownEnabled(enabled, true);
        }
    },
 
    /**
     * Replaces any existing {@link #minValue} with the new value.
     * @param {Number} value The minimum value
     */
    setMinValue: function(value) {
        var me = this,
            ariaDom = me.ariaEl.dom,
            minValue, allowed;
        
        me.minValue = minValue = Ext.Number.from(value, Number.NEGATIVE_INFINITY);
        me.toggleSpinners();
        
        // May not be rendered yet 
        if (ariaDom) {
            if (minValue > Number.NEGATIVE_INFINITY) {
                ariaDom.setAttribute('aria-valuemin', minValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuemin');
            }
        }
        
        // Build regexes for masking and stripping based on the configured options 
        if (me.disableKeyFilter !== true) {
            allowed = me.baseChars + '';
            
            if (me.allowExponential) {
                allowed += me.decimalSeparator + 'e+-';
            }
            else {
                if (me.allowDecimals) {
                    allowed += me.decimalSeparator;
                }
                if (me.minValue < 0) {
                    allowed += '-';
                }
            }
            
            allowed = Ext.String.escapeRegex(allowed);
            me.maskRe = new RegExp('[' + allowed + ']');
            if (me.autoStripChars) {
                me.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi');
            }
        }
    },
 
    /**
     * Replaces any existing {@link #maxValue} with the new value.
     * @param {Number} value The maximum value
     */
    setMaxValue: function(value) {
        var ariaDom = this.ariaEl.dom,
            maxValue;
        
        this.maxValue = maxValue = Ext.Number.from(value, Number.MAX_VALUE);
        
        // May not be rendered yet 
        if (ariaDom) {
            if (maxValue < Number.MAX_VALUE) {
                ariaDom.setAttribute('aria-valuemax', maxValue);
            }
            else {
                ariaDom.removeAttribute('aria-valuemax');
            }
        }
        
        this.toggleSpinners();
    },
 
    /**
     * @private
     */
    parseValue : function(value) {
        value = parseFloat(String(value).replace(this.decimalSeparator, '.'));
        return isNaN(value) ? null : value;
    },
 
    /**
     * @private
     */
    fixPrecision : function(value) {
        var me = this,
            nan = isNaN(value),
            precision = me.decimalPrecision;
 
        if (nan || !value) {
            return nan ? '' : value;
        } else if (!me.allowDecimals || precision <= 0) {
            precision = 0;
        }
 
        return parseFloat(Ext.Number.toFixed(parseFloat(value), precision));
    },
 
    onBlur : function(e) {
        var me = this,
            v = me.rawToValue(me.getRawValue());
 
        if (!Ext.isEmpty(v)) {
            me.setValue(v);
        }
        me.callParent([e]);
    },
 
    setSpinUpEnabled: function(enabled, internal){
        this.callParent(arguments);
        if (!internal) {
            delete this.spinUpDisabledByToggle;
        } else {
            this.spinUpDisabledByToggle = !enabled;
        }
    },
 
    onSpinUp: function() {
        var me = this;
            
        if (!me.readOnly) {
            me.setSpinValue(Ext.Number.constrain(me.getValue() + me.step, me.minValue, me.maxValue));
        }
    },
 
    setSpinDownEnabled: function(enabled, internal){
        this.callParent(arguments);
        if (!internal) {
            delete this.spinDownDisabledByToggle;
        } else {
            this.spinDownDisabledByToggle = !enabled;
        }   
    },
 
    onSpinDown: function() {
        var me = this;
        
        if (!me.readOnly) {
            me.setSpinValue(Ext.Number.constrain(me.getValue() - me.step, me.minValue, me.maxValue));
        }
    },
    
    setSpinValue: function(value) {
        var me = this;
            
        if (me.enforceMaxLength) {
            // We need to round the value here, otherwise we could end up with a 
            // very long number (think 0.1 + 0.2) 
            if (me.fixPrecision(value).toString().length > me.maxLength) {
                return;
            }
        }
        me.setValue(value);
    }
});