/**
 * An abstract class for fields that have a single trigger which opens a "picker" popup
 * above the field. It provides a base implementation for toggling the picker's
 * visibility when the trigger is tapped.
 *
 * You would not normally use this class directly, but instead use it as the parent
 * class for a specific picker field implementation.
 */
Ext.define('Ext.field.Picker', {
    extend: 'Ext.field.Text',
    xtype: 'pickerfield',
 
    requires: [
        'Ext.field.trigger.Expand'
    ],
 
    mixins: [
        'Ext.mixin.Bufferable'
    ],
 
    bufferableMethods: {
        restoreReadonlyState: 300
    },
 
    config: {
 
        /**
         * @cfg {String/Object} [picker='auto']
         *
         * A string representing the type of picker to use.  Can be one of the following values.
         *
         * - `'edge'` to use the {@link #edgePicker}, generally used on small formfactor devices.
         * - `'floated'` to use the {@link #floatedPicker}, generally used on tablets or desktops.
         * - `'auto'` to allow the framework to select the appropriate picker for the device.
         *
         * Can also be a config object for the picker.
         *
         */
        picker: {
            lazy: true,
            $value: 'auto'
        },
 
        /**
         * A configuration object, containing an {@link cfg#xtype} property which specifies the widget to
         * create if `{@link #cfg!picker}: 'floated'` (or if it's '`auto'` and the app is *not* on a phone)
         * Replaces `defaultTabletPicker`
         * @since 6.5.0
         */
        floatedPicker: {
            lazy: true,
            $value: null
        },
 
        /**
         * A configuration object, containing an {@link cfg#xtype} property which specifies the widget to
         * create if `{@link #cfg!picker}: 'edge'` (or if it's '`auto'` and the app is on a phone)
         * Replaces `defaultPhonePicker`
         * @since 6.5.0
         */
        edgePicker: {
            lazy: true,
            $value: null
        },
 
        clearable: false,
 
        /**
         * @cfg {Boolean} [matchFieldWidth=true]
         * *Only valid when the `{@link #cfg!picker}: 'floated'` is used.
         * Whether the {@link #cfg!floatedPicker}'s width should be explicitly set to match the width of the input element.
         */
        matchFieldWidth: true,
 
        /**
         * @cfg {String} [floatedPickerAlign=tl-bl?]
         * *Only valud when the {@link #cfg!floatedPicker} is used.
         * The {@link Ext.Component#method!showBy} alignment string to use when showing the floated picker
         * by the input field.
         */
        floatedPickerAlign: 'tl-bl?',
 
        /**
         * @cfg {String} pickerSlotAlign 
         * The alignment of text in the picker created by this Select
         * @private
         */
        pickerSlotAlign: 'center',
 
        /**
         * @cfg {Boolean} hideTrigger 
         * `true` to hide the expand {@link #triggers trigger}.
         */
        hideTrigger: false,
 
        triggers: {
            expand: {
                type: 'expand'
            }
        }
    },
 
    /**
     * @cfg {String} alignTarget 
     * The element reference to which the {@link #cfg!picker floated picker} aligns
     * and sizes to. By default, it sizes to the `bodyElement` which encapsulates the
     * input field and triggers.
     *
     * An alternate value which may be useful if using `floated` pickers on phone platforms
     * could be `el`, to align the picker to the field's encapsulating element.
     */
    alignTarget: 'bodyElement',
 
    keyMap: {
        scope: 'this',
        DOWN: 'onDownArrow',
        ESC: 'onEsc'
    },
    keyMapTarget: 'inputElement',
 
    /**
     * @cfg {Boolean} [autoComplete=false]
     * Autocomplete is disabled on Picker fields by default.
     */
    autoComplete: false,
 
    classCls: Ext.baseCSSPrefix + 'pickerfield',
 
    /**
     * @event expand
     * Fires when the field's picker is expanded.
     * @param {Ext.form.field.Picker} field This field instance
     */
 
    /**
     * @event collapse
     * Fires when the field's picker is collapsed.
     * @param {Ext.form.field.Picker} field This field instance
     */
 
    /**
     * @private
     */
    initialize: function () {
        var me = this,
            listeners = {
                click: 'onInputElementClick',
                scope: me
            };
 
        // We don't want the soft keyboard to open on the first tap. 
        // A picker field will be filled by choosing from the picker 
        // 99% of the time, not by typing. 
        // 
        // On a tap when focused, and on focus leave, we revert to our configured 
        // readonly state. 
        if (Ext.supports.TouchEvents) {
            listeners.tap = {
                fn: 'onInputTap'
            };
        }
 
        me.callParent();
 
        me.inputElement.on(listeners);
    },
 
    onFocusLeave: function (e) {
        var me = this;
 
        me.collapse();
        me.callParent([e]);
 
        // Restore our default readonly state. 
        me.updateReadOnly(me._readonly);
    },
 
    /**
     * @private
     */
    onEsc: function (e) {
        if (Ext.isIE) {
            // Stop the esc key from "restoring" the previous value in IE 
            // For example, type "foo". Highlight all the text, hit backspace. 
            // Hit esc, "foo" will be restored. This behaviour doesn't occur 
            // in any other browsers 
            e.preventDefault();
        }
 
        if (this.expanded) {
            this.collapse();
            e.stopEvent();
        }
    },
 
    onDownArrow: function (e) {
        var me = this;
 
        if ((e.time - me.lastDownArrow) > 150) {
            delete me.lastDownArrow;
        }
 
        if (!me.expanded) {
            // Do not let the down arrow event propagate into the picker 
            e.stopEvent();
 
            // Don't call expand() directly as there may be additional processing involved before 
            // expanding, e.g. in the case of a ComboBox query. 
            me.onExpandTap(e);
 
            // Tell setPickerLocation that it's invoked from the keyboard so 
            // that it may set the location regardless of other settings. 
            // For example, ComboBox has autoSelect and autoSelectLast which *may* 
            // be set to false for some applications. This information 
            // can override that. 
            me.setPickerLocation(true);
 
            me.lastDownArrow = e.time;
        }
        else if (!e.stopped && (e.time - me.lastDownArrow) < 150) {
            delete me.lastDownArrow;
        }
    },
 
    /**
     * @template
     * @method
     * @param {Boolean} [fromKeyboard=false] Passed as `true` if the keyboard was used
     * to open the picker. This usually means that picker location should be set.
     *
     * A function which may be implemented in subclasses which moves the focus
     * to the value in the {@link #cfg!picker} which matches this field's value.
     *
     * For example, if you were writing a time picker, this method would be where
     * you synced the picker's value with the field's value.
     */
    setPickerLocation: Ext.emptyFn,
 
    updateHideTrigger: function(hideTrigger) {
        var triggers = this.getTriggers(),
            expand = triggers && triggers.expand;
 
        if (expand) {
            expand.setHidden(hideTrigger);
        }
    },
 
    applyPicker: function (picker) {
        var me = this,
            pickerListeners = {
                show: 'onPickerShow',
                hide: 'onPickerHide',
                scope: me
            },
            type = picker,
            config;
 
        if (!type) {
            type = 'auto';
        }
        else if (Ext.isObject(picker)) {
            type = null;
 
            if (!picker.isWidget && !picker.xtype) {
                config = picker;
                type = 'auto';
            }
        }
 
        if (type) {
            if (type === 'auto') {
                type = Ext.platformTags.phone ? 'edge' : 'floated';
            }
 
            if (type === 'edge') {
                picker = me.createEdgePicker(config);
            }
            //<debug> 
            else if (type !== 'floated') {
                Ext.raise('Picker type must be "edge" or "floated" not "' + type + '"');
            }
            //</debug> 
            else {
                picker = me.createFloatedPicker(config);
                pickerListeners.resize = pickerListeners.hiddenchange = 'realignFloatedPicker';
            }
        }
 
        if (!picker.isWidget) {
            picker.ownerField = me;
 
            // Allow mutation of the picker configuration 
            me.fireEvent('beforepickercreate', me, picker);
 
            picker = Ext.create(picker);
        }
 
        // Detect whether we are using a floated or edge picker. 
        me.pickerType = type || (picker.isViewportMenu ? 'edge' : 'floated');
 
        // Allow configuration of the instantiated picker 
        me.fireEvent('pickercreate', me, picker);
 
        picker.on(pickerListeners);
        return picker;
    },
 
    updatePicker: function (picker) {
        var value = this.getValue();
 
        if (picker && picker.setValue && value != null) {
            if (this.pickerType === 'floated' || picker.isPicker) {
                picker.setValue(value);
            }
        }
    },
 
    onResize: function () {
        // See if the picker has been created 
        var picker = this.getConfig('picker', false, true);
 
        if (picker && picker.isVisible()) {
            this.realignFloatedPicker();
        }
    },
 
    /**
     * @private
     */
    realignFloatedPicker: function (picker) {
        var me = this;
 
        picker = me.getConfig('picker', false, true);
 
        if (picker && picker.isVisible()) {
            if (me.getMatchFieldWidth()) {
                picker.setWidth(me[me.alignTarget].getWidth());
            }
            picker.realign(me[me.alignTarget], me.getFloatedPickerAlign(), {
                minHeight: 100
            });
            me.setPickerLocation();
        }
    },
 
    onInputTap: function (e) {
        var me = this,
            inputEl = me.inputElement;
 
        // Check the event type to avoid responding to mouse events. 
        if (e.pointerType === 'touch') {
 
            // If we already have focus, restore editability. 
            if (me.hasFocus) {
                // In order for the soft keyboard to be displayed, we must *gain* focus 
                // on an editable input field, so we have to toggle it. 
                // Ensure no focus events leak out during this process. 
                inputEl.suspendFocusEvents();
                inputEl.blur();
                me.updateReadOnly(me._readonly);
                inputEl.focus();
                inputEl.resumeFocusEvents();
            }
 
            // If we do not have focus, this is the initial tap, set to readonly so that 
            // we do not get the soft keyboard, and call the expand handler. 
            else {
                me.setInputAttribute('readonly', true);
                me.onExpandTap();
 
                // An application event handler may prevent or move focus in some 
                // way. We need to restore to the configured state, so we do it 
                // on a delay. 
                // 
                // If we have focused by that time, the keyboard will not appear. 
                // In order to appear, we must *gain* focus on an editable input field. 
                // If the app has manipulated the event, or focus in some way, then 
                // we've restored correctness. 
                me.restoreReadonlyState();
            }
        }
    },
 
    doRestoreReadonlyState: function() {
        this.updateReadOnly(this._readonly);
    },
 
    onInputElementClick: function (e) {
        if (e.pointerType === 'mouse' && (!this.getEditable() && !this.getReadOnly())) {
            this[this.expanded ? 'collapse' : 'expand']();
        }
    },
 
    onExpandTap: function () {
        if (this.expanded) {
            this.collapse();
        } else {
            this.expand();
        }
 
        return false;
    },
 
    expand: function () {
        if (!this.expanded && !this.getDisabled()) {
            this.showPicker();
        }
    },
 
    collapse: function () {
        if (this.expanded) {
            this.getPicker().hide();
        }
    },
 
    /**
     * @private
     * Runs on touchstart of doc to check to see if we should collapse the picker.
     */
    collapseIf: function (e) {
        var me = this;
 
        // If what was mousedowned on is outside of this Field, then collapse. 
        if (!me.destroyed && !e.within(me.bodyElement, false, true) && !me.owns(e.target)) {
            me.collapse();
        }
    },
 
    showPicker: function () {
        var me = this,
            alignTarget = me[me.alignTarget],
            picker = me.getPicker(),
            value;
 
        if (me.pickerType === 'floated') {
            if (me.getMatchFieldWidth()) {
                picker.setWidth(alignTarget.getWidth());
            }
            picker.showBy(alignTarget, me.getFloatedPickerAlign(), {
                minHeight: 100
            });
 
            // Collapse on touch outside this component tree. 
            // Because touch platforms do not focus document.body on touch 
            // so no focusleave would occur to trigger a collapse. 
            me.touchListeners = Ext.getDoc().on({
                // Do not translate on non-touch platforms. 
                // mousedown will blur the field. 
                translate: false,
                touchstart: me.collapseIf,
                scope: me,
                delegated: false,
                destroyable: true
            });
        } else {
            if (!picker.getParent()) {
                Ext.Viewport.add(picker);
            }
 
            picker.show();
 
            value = me.getValue();
 
            if (value != null) {
                me.updatePickerValue(picker, value);
            }
        }
    },
 
    updatePickerValue: function (picker, value) {
        var slot = picker.getSlots()[0],
            name = slot.name || slot.getName(),
            pickerValue = {};
 
        pickerValue[name] = value;
 
        picker.setValue(pickerValue);
    },
 
    onPickerShow: function () {
        var me = this;
 
        me.expanded = true;
 
        // Scrolling of anything which causes this field to move should collapse 
        me.scrollListeners = Ext.on({
            scroll: me.onGlobalScroll,
            scope: me,
            destroyable: true
        });
 
        me.fireEvent('expand', me);
    },
 
    onPickerHide: function () {
        var me = this;
 
        me.expanded = false;
        Ext.destroy(me.scrollListeners, me.touchListeners);
        me.fireEvent('collapse', me);
    },
 
    doDestroy: function () {
        var me = this;
 
        Ext.destroy(me.getConfig('picker', false, true), me.scrollListeners);
 
        me.callParent();
    },
 
    privates: {
        onGlobalScroll: function (scroller) {
            if (this.expanded) {
 
                // Edge pickers are modal and anchored. We do not care if other 
                // parts of the app scroll. 
                if (this.pickerType === 'edge') {
                    return;
                }
 
                // Collapse if the scroll is anywhere but inside the picker 
                if (!this.getPicker().owns(scroller.getElement())) {
                    this.collapse();
                }
            }
        }
    }
});