/**
 * The text field is the basis for most of the input fields. It provides a baseline of shared
 * functionality such as input validation, standard events, state management and look and feel. Typically we create
 * text fields inside a form, like this:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         fullscreen: true,
 *         items: [
 *             {
 *                 xtype: 'fieldset',
 *                 title: 'Enter your name',
 *                 items: [
 *                     {
 *                         xtype: 'textfield',
 *                         label: 'First Name',
 *                         name: 'firstName'
 *                     },
 *                     {
 *                         xtype: 'textfield',
 *                         label: 'Last Name',
 *                         name: 'lastName'
 *                     }
 *                 ]
 *             }
 *         ]
 *     });
 *
 * This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Your Name',
 *         value: 'Ed Spencer'
 *     });
 *
 * ## Configuring
 *
 * Text field offers several configuration options, including {@link #placeHolder}, {@link #maxLength},
 * {@link #autoComplete}, {@link #autoCapitalize} and {@link #autoCorrect}. For example, here is how we would configure
 * a text field to have a maximum length of 10 characters, with placeholder text that disappears when the field is
 * focused:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Username',
 *         maxLength: 10,
 *         placeHolder: 'Enter your username'
 *     });
 *
 * The autoComplete, autoCapitalize and autoCorrect configs simply set those attributes on the text field and allow the
 * native browser to provide those capabilities. For example, to enable auto complete and auto correct, simply
 * configure your text field like this:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Username',
 *         autoComplete: true,
 *         autoCorrect: true
 *     });
 *
 * These configurations will be picked up by the native browser, which will enable the options at the OS level.
 *
 * Text field inherits from {@link Ext.field.Field}, which is the base class for all fields and provides
 * a lot of shared functionality for all fields, including setting values, clearing and basic validation. See the
 * {@link Ext.field.Field} documentation to see how to leverage its capabilities.
 */
Ext.define('Ext.field.Text', {
    extend: 'Ext.field.Field',
    xtype: 'textfield',
    alternateClassName: 'Ext.form.Text',
 
    requires: [
        'Ext.field.trigger.Clear'
    ],
 
    /**
     * @event focus
     * Fires when this field receives input focus
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event blur
     * Fires when this field loses input focus
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event paste
     * Fires when this field is pasted.
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event mousedown
     * Fires when this field receives a mousedown
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event keyup
     * @preventable
     * Fires when a key is released on the input element
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event clearicontap
     * @preventable
     * Fires when the clear icon is tapped
     * @param {Ext.field.Text} this This field
     * @param {Ext.field.Input} input The field's input component.
     * @param {Ext.event.Event} e
     */
 
    /**
     * @event change
     * Fires when the value has changed.
     * @param {Ext.field.Text} this This field
     * @param {String} newValue The new value
     * @param {String} oldValue The original value
     */
 
    /**
     * @event action
     * @preventable
     * Fires whenever the return key or go is pressed. FormPanel listeners
     * for this event, and submits itself whenever it fires. Also note
     * that this event bubbles up to parent containers.
     * @param {Ext.field.Text} this This field
     * @param {Mixed} e The key event object
     */
 
    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        clearIcon: true,
 
        /**
         * @cfg {'top'/'left'/'bottom'/'right'/'placeholder'} labelAlign
         * When value is `'placeholder'`, the label text will be rendered as placeholder
         * text inside the empty input and will animated to "top" alignment when the input
         * is focused or contains text.
         * @inheritdoc
         * @accessor
         */
 
        /**
         * @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
         * @accessor
         */
        placeHolder: null,
 
        /**
         * @cfg {Number} maxLength The maximum number of permitted input characters.
         * @accessor
         */
        maxLength: null,
 
        /**
         * True to set the field's DOM element autocomplete attribute to "on", false to set to "off".
         * @cfg {Boolean} autoComplete 
         * @accessor
         */
        autoComplete: null,
 
        /**
         * True to set the field's DOM element autocapitalize attribute to "on", false to set to "off".
         * @cfg {Boolean} autoCapitalize 
         * @accessor
         */
        autoCapitalize: null,
 
        /**
         * True to set the field DOM element autocorrect attribute to "on", false to set to "off".
         * @cfg {Boolean} autoCorrect 
         * @accessor
         */
        autoCorrect: null,
 
        /**
         * True to set the field DOM element readonly attribute to true.
         * @cfg {Boolean} readOnly 
         * @accessor
         */
        readOnly: null,
 
        /**
         * @cfg {Object} component The inner component for this field, which defaults to an input text.
         * @accessor
         */
        component: {
            xtype: 'textinput'
        },
 
        // @cmd-auto-dependency {aliasPrefix: "trigger.", isKeyedObject: true} 
        /**
         * @cfg {Object} triggers 
         * {@link Ext.field.trigger.Trigger Triggers} to use in this field.  The keys in
         * this object are unique identifiers for the triggers. The values in this object
         * are {@link Ext.field.trigger.Trigger Trigger} configuration objects.
         *
         *     Ext.create('Ext.field.Text', {
         *         label: 'My Custom Field',
         *         triggers: {
         *             foo: {
         *                 cls: 'my-foo-trigger',
         *                 handler: function() {
         *                     console.log('foo trigger clicked');
         *                 }
         *             },
         *             bar: {
         *                 cls: 'my-bar-trigger',
         *                 handler: function() {
         *                     console.log('bar trigger clicked');
         *                 }
         *             }
         *         }
         *     });
         *
         * The weight value may be a negative value in order to position custom triggers
         * ahead of default triggers like that of a DatePicker field.
         *
         *     Ext.create('Ext.form.DatePicker', {
         *         label: 'Pick a Date',
         *         triggers: {
         *             foo: {
         *                 cls: 'my-foo-trigger',
         *                 weight: -2, // negative to place before default triggers
         *                 handler: function() {
         *                     console.log('foo trigger clicked');
         *                 }
         *             },
         *             bar: {
         *                 cls: 'my-bar-trigger',
         *                 weight: -1,
         *                 handler: function() {
         *                     console.log('bar trigger clicked');
         *                 }
         *             }
         *         }
         *     });
         */
        triggers: {
            clear: {
                type: 'clear'
            }
        },
 
        bubbleEvents: ['action'],
 
        bodyAlign: 'stretch',
 
        /**
          * @cfg {'left'/'center'/'right'} [textAlign='left'].
          * The text alignment of this field.
          */
        textAlign: null
    },
 
    defaultBindProperty: 'value',
    twoWayBindable: {
        value: 1
    },
    
    publishes: {
        value: 1
    },
 
    classCls: Ext.baseCSSPrefix + 'textfield',
    focusedCls: Ext.baseCSSPrefix + 'focused',
    emptyCls: Ext.baseCSSPrefix + 'empty',
 
    /**
     * @private
     */
    initialize: function() {
        var me = this;
 
        me.callParent();
 
        me.getComponent().on({
            keyup: 'onKeyUp',
            input: 'onInput',
            focus: 'onFocus',
            blur: 'onBlur',
            paste: 'onPaste',
            mousedown: 'onMouseDown',
            scope: this
        });
 
        // set the originalValue of the textfield, if one exists 
        me.originalValue = me.getValue() || "";
        me.getComponent().originalValue = me.originalValue;
 
        me.syncEmptyCls();
    },
 
    applyValue: function(value) {
        return Ext.isEmpty(value) ? '' : value;
    },
 
    /**
     * @private
     */
    updateValue: function(value, oldValue) {
        var me = this,
            component  = me.getComponent(),
            // allows value to be zero but not undefined or null (other falsey values) 
            valueValid = value !== undefined && value !== null && value !== '';
 
        if (component) {
            component.setValue(value);
        }
 
        me.toggleClearTrigger(valueValid && me.isDirty());
 
        me.syncEmptyCls();
 
        if (me.initialized) {
            me.fireEvent('change', me, value, oldValue);
        }
 
        // When configured with labelAlign placeholder, we should animate the label back to the 
        // placeholder or placeholder back to label if the field's value was cleared or set programatically. 
        if (!me.isConfiguring && !me.isFocused && me.getLabelAlign() === 'placeholder') {
            me[!value ? 'animateLabelToPlaceholder' : 'animatePlaceholderToLabel']();
        }
    },
 
    updateLabel: function (newLabel, oldLabel) {
        var me = this;
 
        me.callParent([newLabel, oldLabel]);
 
        if (me.getLabelAlign() === 'placeholder') {
            if (me.getValue()) {
                me.labelElement.setStyle('opacity', 1);
            } else {
                me.setPlaceHolder(newLabel);
            }
        }
    },
 
    updateLabelAlign: function(labelAlign, oldLabelAlign) {
 
        this.callParent([labelAlign, oldLabelAlign]);
    },
 
    updateTextAlign: function(newAlign, oldAlign) {
        var element = this.element;
 
        if (oldAlign) {
            element.removeCls(Ext.baseCSSPrefix + 'text-align-' + oldAlign);
        }
 
        if (newAlign) {
            element.addCls(Ext.baseCSSPrefix + 'text-align-' + newAlign);
        }
    },
 
    /**
     * @private
     */
    updatePlaceHolder: function(newPlaceHolder) {
        var me = this,
            label = me.getLabel();
 
        //<debug> 
        if ((me.getLabelAlign() === 'placeholder') && newPlaceHolder !== label) {
            Ext.log.warn('PlaceHolder should not be set when using "labelAlign: \'placeholder\'"');
        }
        //</debug> 
 
        me.getComponent().setPlaceHolder(newPlaceHolder);
    },
 
    /**
     * @private
     */
    updateMaxLength: function(newMaxLength) {
        this.getComponent().setMaxLength(newMaxLength);
    },
 
    /**
     * @private
     */
    updateAutoComplete: function(newAutoComplete) {
        this.getComponent().setAutoComplete(newAutoComplete);
    },
 
    /**
     * @private
     */
    updateAutoCapitalize: function(newAutoCapitalize) {
        this.getComponent().setAutoCapitalize(newAutoCapitalize);
    },
 
    /**
     * @private
     */
    updateAutoCorrect: function(newAutoCorrect) {
        this.getComponent().setAutoCorrect(newAutoCorrect);
    },
 
    /**
     * @private
     */
    updateReadOnly: function(newReadOnly) {
        this.toggleClearTrigger(!newReadOnly);
        this.getComponent().setReadOnly(newReadOnly);
    },
 
    /**
     * @private
     */
    updateInputType: function(newInputType) {
        var component = this.getComponent();
        if (component) {
            component.setType(newInputType);
        }
    },
 
    /**
     * @private
     */
    updateName: function(newName) {
        var component = this.getComponent();
        if (component) {
            component.setName(newName);
        }
    },
 
    /**
     * @private
     */
    updateTabIndex: function(newTabIndex) {
        var component = this.getComponent();
        if (component) {
            component.setTabIndex(newTabIndex);
        }
    },
 
    /**
     * Updates the {@link #inputCls} configuration on this fields {@link #component}
     * @private
     */
    updateInputCls: function(newInputCls, oldInputCls) {
        var component = this.getComponent();
        if (component) {
            component.replaceCls(oldInputCls, newInputCls);
        }
    },
 
    updateDisabled: function(disabled, oldDisabled) {
        this.callParent([disabled, oldDisabled]);
        this.toggleClearTrigger(!disabled);
    },
 
    updateClearIcon: function(clearIcon, oldClearIcon) {
        var me = this,
            triggers, clearTrigger;
 
        if (!me.isConfiguring) {
            triggers = me.getTriggers();
            clearTrigger = triggers && triggers.clear;
 
            if (clearIcon) {
                if (!clearTrigger) {
                    me.addTrigger('clear', 'clear');
                }
            } else if (clearTrigger) {
                me.removeTrigger('clear');
            }
        }
    },
 
    applyTriggers: function(triggers, oldTriggers) {
        var me = this,
            instances = oldTriggers || {},
            clearable = me.getClearIcon(),
            name, trigger, oldTrigger;
 
        for (name in triggers) {
            trigger = triggers[name];
            oldTrigger = instances[name];
 
            // Any key that exists on the incoming object should cause destruction of 
            // the existing trigger for that key, if one exists.  This is true for both 
            // truthy values (triggers and trigger configs) and falsy values. Falsy values 
            // cause destruction of the existing trigger without replacement. 
            if (oldTrigger) {
                oldTrigger.destroy();
            }
 
            if (trigger) {
                if (!clearable && (name === 'clear')) {
                    continue;
                }
 
                instances[name] = me.createTrigger(name, trigger);
            }
        }
 
        return instances;
    },
 
    updateTriggers: function(triggers, oldTriggers) {
        this.syncTriggers();
    },
 
    /**
     * Adds a trigger to this text field.
     * @param {String} name Unique name (within this field) for the trigger.  Cannot be the
     * same as the name of an existing trigger for this field.
     * @param {Ext.field.trigger.Trigger/Object} trigger The trigger instance or a config
     * object for a trigger to add
     * @return {Ext.field.trigger.Trigger} The trigger that was added
     */
    addTrigger: function(name, trigger) {
        var me = this,
            triggers = me.getTriggers(),
            triggerConfig;
 
        //<debug> 
        if (triggers && triggers[name]) {
            Ext.raise('Trigger with name "' + name + '" already exists.');
        }
        if (typeof name !== 'string') {
            Ext.raise('Cannot add trigger. Key must be a string.');
        }
        if (typeof trigger !== 'string' && !Ext.isObject(trigger)) {
            Ext.raise('Cannot add trigger "' + name + '". A trigger config or instance is required.');
        }
        //</debug> 
 
        trigger = me.createTrigger(name, trigger);
 
        if (triggers) {
            triggers[name] = trigger;
            me.syncTriggers();
        } else {
            triggerConfig = {};
            triggerConfig[name] = trigger;
            me.setTriggers(triggerConfig);
        }
 
        return trigger;
    },
 
    /**
     * Removes a trigger from this text field.
     * @param {String/Ext.field.trigger.Trigger} trigger The name of the trigger to remove,
     * or a trigger reference.
     * @param {Boolean} [destroy=true] False to prevent the trigger from being destroyed
     * on removal.  Only use this option if you want to reuse the trigger instance.
     * @return {Ext.field.trigger.Trigger} The trigger that was removed
     */
    removeTrigger: function(trigger, destroy) {
        var me = this,
            triggers = me.getTriggers(),
            name = trigger,
            triggerEl;
 
        if (name.isTrigger) {
            name = trigger.getName();
        } else {
            trigger = triggers[name];
        }
 
        //<debug> 
        if (!name) {
            Ext.raise('Trigger not found.');
        } else if (!triggers[name]) {
            Ext.raise('Cannot remove trigger. Trigger with name "' + name + '" not found.');
        }
        //</debug> 
 
        delete triggers[name];
 
        if (destroy !== false) {
            trigger.destroy();
        } else {
            triggerEl = trigger.el.dom;
            triggerEl.parentNode.removeChild(triggerEl);
        }
 
        this.syncTriggers();
 
        return trigger;
    },
 
    /**
     * @private
     */
    showClearTrigger: function() {
        var me = this,
            value = me.getValue(),
            // allows value to be zero but not undefined or null (other falsey values) 
            valueValid = value !== undefined && value !== null && value !== "",
            triggers, clearTrigger;
 
        if (me.getClearIcon() && !me.getDisabled() && !me.getReadOnly() && valueValid) {
            triggers = me.getTriggers();
            clearTrigger = triggers && triggers.clear;
 
            if (clearTrigger) {
                clearTrigger.show();
            }
        }
 
        return me;
    },
 
    /**
     * @private
     */
    hideClearTrigger: function() {
        var triggers = this.getTriggers(),
            clearTrigger = triggers && triggers.clear;
 
        if (clearTrigger) {
            clearTrigger.hide();
        }
    },
 
    onKeyUp: function(e) {
        this.fireAction('keyup', [this, e], 'doKeyUp');
    },
 
    /**
     * Called when a key has been pressed in the `<input>`
     * @private
     */
    doKeyUp: function(me, e) {
        // getValue to ensure that we are in sync with the dom 
        var value      = me.getValue(),
            // allows value to be zero but not undefined or null (other falsey values) 
            valueValid = value !== undefined && value !== null && value !== '';
 
        me.toggleClearTrigger(valueValid);
 
        if (e.browserEvent.keyCode === 13) {
            me.fireAction('action', [me, e], 'doAction');
        }
    },
 
    doAction: function() {
        this.blur();
    },
 
    onClearIconTap: function(input, e) {
        this.fireAction('clearicontap', [this, input, e], 'doClearIconTap');
 
        //focus the field after cleartap happens, but only on android. 
        //this is to stop the keyboard from hiding. TOUCH-2064 
        if (Ext.os.is.Android) {
            this.getComponent().focus();
        }
    },
 
    /**
     * @private
     */
    doClearIconTap: function(me, e) {
        me.setValue('');
    },
 
    onInput: function(component, value) {
        this.setValue(value);
    },
 
    onFocus: function(e) {
        var me = this;
 
        me.addCls(me.focusedCls);
        me.isFocused = true;
 
        if (me.getLabelAlign() === 'placeholder' && !me.getValue()) {
            me.animatePlaceholderToLabel();
        }
 
        me.fireEvent('focus', me, e);
    },
 
    onBlur: function(e) {
        var me = this;
 
        me.removeCls(me.focusedCls);
        me.isFocused = false;
 
        if (me.getLabelAlign() === 'placeholder' && !me.getValue()) {
            me.animateLabelToPlaceholder();
        }
 
        me.fireEvent('blur', me, e);
 
        Ext.defer(function() {
            me.isFocused = false;
        }, 50);
    },
 
    onPaste: function(e) {
        this.fireEvent('paste', this, e);
    },
 
    onMouseDown: function(e) {
        this.fireEvent('mousedown', this, e);
    },
 
    /**
     * Attempts to set the field as the active input focus.
     * @return {Ext.field.Text} This field
     */
    focus: function() {
        this.getComponent().focus();
        return this;
    },
 
    /**
     * Attempts to forcefully blur input focus for the field.
     * @return {Ext.field.Text} This field
     */
    blur: function() {
        this.getComponent().blur();
        return this;
    },
 
    /**
     * Attempts to forcefully select all the contents of the input field.
     * @return {Ext.field.Text} this
     */
    select: function() {
        this.getComponent().select();
        return this;
    },
 
    resetOriginalValue: function() {
        var me = this,
            component;
 
        me.callParent();
        component = me.getComponent();
        if(component && component.hasOwnProperty("originalValue")) {
            me.getComponent().originalValue = me.originalValue;
        }
        me.reset();
    },
 
    reset: function() {
        var me = this;
        me.getComponent().reset();
 
        //we need to call this to sync the input with this field 
        this.callParent();
 
        me.toggleClearTrigger(me.isDirty());
    },
 
    isDirty: function() {
        var component = this.getComponent();
        if (component) {
            return component.isDirty();
        }
        return false;
    },
 
    doDestroy: function() {
        var me = this,
            triggers = me.getTriggers(),
            triggerGroups = me.triggerGroups,
            name;
 
        if (triggerGroups) {
            for (name in triggerGroups) {
                triggerGroups[name].destroy();
            }
            me.triggerGroups = null;
        }
 
        for (name in triggers) {
            triggers[name].destroy();
        }
 
        me.setTriggers(null);
 
        me.callParent();
    },
 
    privates: {
        animateLabelToPlaceholder: function() {
            var me = this,
                animInfo = me.getPlaceholderAnimInfo();
 
            me.labelElement.animate({
                from: {
                    left: 0,
                    top: 0,
                    opacity: 1
                },
                to: animInfo,
                preserveEndState: true,
                duration: 250,
                easing: 'ease-out',
                callback: function() {
                    me.setPlaceHolder(me.getLabel());
                }
            });
 
            me.lastPlaceholderAnimInfo = animInfo;
        },
 
        animatePlaceholderToLabel: function() {
            var me = this;
 
            me.labelElement.animate({
                from: me.lastPlaceholderAnimInfo || me.getPlaceholderAnimInfo(),
                to: {
                    left: 0,
                    top: 0,
                    opacity: 1
                },
                easing: 'ease-out',
                preserveEndState: true,
                duration: 250
            });
 
            me.setPlaceHolder(null);
 
            me.lastPlaceholderAnimInfo = null;
        },
 
        /**
         * @private
         */
        createTrigger: function(name, trigger) {
            if (!trigger.isTrigger) {
                if (trigger === true) {
                    trigger = {
                        type: name
                    };
                } else if (typeof trigger === 'string') {
                    trigger = {
                        type: trigger
                    };
                }
 
                trigger = Ext.apply({
                    name: name,
                    field: this
                }, trigger);
 
                trigger = trigger.xtype ? Ext.create(trigger) : Ext.Factory.trigger(trigger);
            }
 
            return trigger;
        },
 
        getPlaceholderAnimInfo: function() {
            var me = this,
                element = me.element,
                labelElement = me.labelElement,
                inputElement = me.getComponent().inputElement,
                labelOffsets = labelElement.getOffsetsTo(element),
                inputOffsets = inputElement.getOffsetsTo(element),
                labelPadding = labelElement.getPadding('l'),
                inputPadding = inputElement.getPadding('l'),
                translateX = inputOffsets[0] - labelOffsets[0] + (inputPadding - labelPadding),
                translateY = inputOffsets[1] - labelOffsets[1];
 
            return {
                left: translateX,
                top: translateY,
                opacity: 0
            };
        },
 
        syncEmptyCls: function() {
            this.toggleCls(this.emptyCls, !this.getValue());
        },
 
        /**
         * Synchronizes the DOM to match the triggers' configured weight, side, and grouping
         * @private
         */
        syncTriggers: function() {
            var me = this,
                triggers = me.getTriggers(),
                input = me.getComponent(),
                triggerGroups = me.triggerGroups || (me.triggerGroups = {}),
                beforeTriggers = [],
                afterTriggers = [],
                triggersByGroup = {},
                Trigger = Ext.field.trigger['Trigger'],
                name, trigger, groupName, triggerGroup, i, ln;
 
            for (name in triggers) {
                trigger = triggers[name];
 
                groupName = trigger.getGroup();
 
                if (groupName) {
                    (triggersByGroup[groupName] || (triggersByGroup[groupName] = [])).push(trigger);
                } else if (trigger.getSide() === 'left') {
                    beforeTriggers.push(trigger);
                } else {
                    afterTriggers.push(trigger);
                }
            }
 
            for (groupName in triggersByGroup) {
                triggerGroup = triggerGroups[groupName];
 
                if (!triggerGroup) {
                    triggerGroup = triggers[groupName]; // just in case the user configured a group trigger 
 
                    if (!triggerGroup) {
                        triggerGroup = new Trigger();
                    }
 
                    triggerGroups[groupName] = triggerGroup;
                }
 
                triggerGroup.setTriggers(Trigger.sort(triggersByGroup[groupName]));
 
                if (triggerGroup.getSide() === 'left') {
                    beforeTriggers.push(triggerGroup);
                } else {
                    afterTriggers.push(triggerGroup);
                }
            }
 
            Trigger.sort(beforeTriggers);
            Trigger.sort(afterTriggers);
 
            for (= 0, ln = beforeTriggers.length; i < ln; i++) {
                input.beforeElement.appendChild(beforeTriggers[i].element);
            }
 
            for (= 0, ln = afterTriggers.length; i < ln; i++) {
                input.afterElement.appendChild(afterTriggers[i].element);
            }
 
            for (groupName in triggerGroups) {
                if (!(groupName in triggersByGroup)) {
                    // group no longer has any triggers. it can be removed. 
                    triggerGroup = triggerGroups[groupName];
                    triggerGroup.setTriggers(null);
                    triggerGroup.destroy();
                    delete triggerGroups[groupName];
                }
            }
        },
 
        toggleClearTrigger: function(state) {
            if (state) {
                this.showClearTrigger();
            } else {
                this.hideClearTrigger();
            }
        }
    }
});