/**
 * SegmentedButton is a container for a group of {@link Ext.Button}s. Generally a SegmentedButton would be
 * a child of a {@link Ext.Toolbar} and would be used to switch between different views.
 *
 * ## Example usage:
 *
 *     @example
 *     var segmentedButton = Ext.create('Ext.SegmentedButton', {
 *         allowMultiple: true,
 *         items: [
 *             {
 *                 text: 'Option 1'
 *             },
 *             {
 *                 text: 'Option 2',
 *                 pressed: true
 *             },
 *             {
 *                 text: 'Option 3'
 *             }
 *         ],
 *         listeners: {
 *             toggle: function(container, button, pressed){
 *                 alert("User toggled the '" + button.getText() + "' button: " + (pressed ? 'on' : 'off'));
 *             }
 *         }
 *     });
 *     Ext.Viewport.add({ xtype: 'container', padding: 10, items: [segmentedButton] });
 */
Ext.define('Ext.SegmentedButton', {
    extend: 'Ext.Container',
    xtype : 'segmentedbutton',
    requires: ['Ext.Button'],

    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'segmentedbutton',

        /**
         * @cfg {String} pressedCls
         * CSS class when a button is in pressed state.
         * @accessor
         */
        pressedCls: Ext.baseCSSPrefix + 'button-pressed',

        /**
         * @cfg {Boolean} allowMultiple
         * Allow multiple pressed buttons.
         * @accessor
         */
        allowMultiple: false,

        /**
         * @cfg {Boolean} allowDepress
         * Allow toggling the pressed state of each button.
         * Defaults to `true` when {@link #allowMultiple} is `true`.
         * @accessor
         */
        allowDepress: false,

        /**
         * @cfg {Boolean} allowToggle Allow child buttons to be pressed when tapped on. Set to `false` to allow tapping but not toggling of the buttons.
         * @accessor
         */
        allowToggle: true,

        /**
         * @cfg {Array} pressedButtons
         * The pressed buttons for this segmented button.
         *
         * You can remove all pressed buttons by calling {@link #setPressedButtons} with an empty array.
         * @accessor
         */
        pressedButtons: [],

        /**
         * @cfg
         * @inheritdoc
         */
        layout: {
            type : 'hbox',
            align: 'stretch'
        },

        /**
         * @cfg
         * @inheritdoc
         */
        defaultType: 'button'
    },

    /**
     * @event toggle
     * Fires when any child button's pressed state has changed.
     * @param {Ext.SegmentedButton} this
     * @param {Ext.Button} button The toggled button.
     * @param {Boolean} isPressed Boolean to indicate if the button was pressed or not.
     */

    initialize: function() {
        var me = this;

        me.callParent();

        me.on({
            delegate: '> button',
            scope   : me,
            tap: 'onButtonRelease'
        });

        me.onAfter({
            delegate: '> button',
            scope   : me,
            hide: 'onButtonHiddenChange',
            show: 'onButtonHiddenChange'
        });
    },

    updateAllowMultiple: function(allowMultiple) {
        if (!this.initialized && !this.getInitialConfig().hasOwnProperty('allowDepress') && allowMultiple) {
            this.setAllowDepress(true);
        }
    },

    /**
     * We override `initItems` so we can check for the pressed config.
     */
    applyItems: function() {
        var me = this,
            pressedButtons = [],
            ln, i, item, items;

        //call the parent first so the items get converted into a MixedCollection
        me.callParent(arguments);

        items = this.getItems();
        ln = items.length;

        for (i = 0; i < ln; i++) {
            item = items.items[i];
            if (item.getInitialConfig('pressed')) {
                pressedButtons.push(items.items[i]);
            }
        }

        me.updateFirstAndLastCls(items);

        me.setPressedButtons(pressedButtons);
    },

    /**
     * Button sets a timeout of 10ms to remove the {@link #pressedCls} on the release event.
     * We don't want this to happen, so lets return `false` and cancel the event.
     * @private
     */
    onButtonRelease: function(button) {
        if (!this.getAllowToggle()) {
            return;
        }
        var me             = this,
            pressedButtons = me.getPressedButtons() || [],
            buttons        = [],
            alreadyPressed;

        if (!me.getDisabled() && !button.getDisabled()) {
            //if we allow for multiple pressed buttons, use the existing pressed buttons
            if (me.getAllowMultiple()) {
                buttons = pressedButtons.concat(buttons);
            }

            alreadyPressed = (buttons.indexOf(button) !== -1) || (pressedButtons.indexOf(button) !== -1);

            //if we allow for depressing buttons, and the new pressed button is currently pressed, remove it
            if (alreadyPressed && me.getAllowDepress()) {
                Ext.Array.remove(buttons, button);
            } else if (!alreadyPressed || !me.getAllowDepress()) {
                buttons.push(button);
            }

            me.setPressedButtons(buttons);
        }
    },

    onItemAdd: function() {
        this.callParent(arguments);
        this.updateFirstAndLastCls(this.getItems());
    },

    onItemRemove: function() {
        this.callParent(arguments);
        this.updateFirstAndLastCls(this.getItems());
    },

    // @private
    onButtonHiddenChange: function() {
        this.updateFirstAndLastCls(this.getItems());
    },

    // @private
    updateFirstAndLastCls: function(items) {
        var ln = items.length,
            basePrefix = Ext.baseCSSPrefix,
            firstCls = basePrefix + 'first',
            lastCls = basePrefix + 'last',
            item, i;

        //remove all existing classes
        for (i = 0; i < ln; i++) {
            item = items.items[i];
            item.removeCls(firstCls);
            item.removeCls(lastCls);
        }

        //add a first cls to the first non-hidden button
        for (i = 0; i < ln; i++) {
            item = items.items[i];
            if (!item.isHidden()) {
                item.addCls(firstCls);
                break;
            }
        }

        //add a last cls to the last non-hidden button
        for (i = ln - 1; i >= 0; i--) {
            item = items.items[i];
            if (!item.isHidden()) {
                item.addCls(lastCls);
                break;
            }
        }
    },

    /**
     * @private
     */
    applyPressedButtons: function(newButtons) {
        var me    = this,
            array = [],
            button, ln, i;

        if (me.getAllowToggle()) {
            if (Ext.isArray(newButtons)) {
                ln = newButtons.length;
                for (i = 0; i< ln; i++) {
                    button = me.getComponent(newButtons[i]);
                    if (button && array.indexOf(button) === -1) {
                        array.push(button);
                    }
                }
            } else {
                button = me.getComponent(newButtons);
                if (button && array.indexOf(button) === -1) {
                    array.push(button);
                }
            }
        }

        return array;
    },

    /**
     * Updates the pressed buttons.
     * @private
     */
    updatePressedButtons: function(newButtons, oldButtons) {
        var me    = this,
            items = me.getItems(),
            pressedCls = me.getPressedCls(),
            events = [],
            item, button, ln, i, e;

        //loop through existing items and remove the pressed cls from them
        ln = items.length;
        if (oldButtons && oldButtons.length) {
            for (i = 0; i < ln; i++) {
                item = items.items[i];

                if (oldButtons.indexOf(item) != -1 && newButtons.indexOf(item) == -1) {
                    item.removeCls([pressedCls, item.getPressedCls()]);
                    events.push({
                        item: item,
                        toggle: false
                    });
                }
            }
        }

        //loop through the new pressed buttons and add the pressed cls to them
        ln = newButtons.length;
        for (i = 0; i < ln; i++) {
            button = newButtons[i];
            if (!oldButtons || oldButtons.indexOf(button) == -1) {
                button.addCls(pressedCls);
                events.push({
                    item: button,
                    toggle: true
                });
            }
        }

        //loop through each of the events and fire them after a delay
        ln = events.length;
        if (ln && oldButtons !== undefined) {
            Ext.defer(function() {
                for (i = 0; i < ln; i++) {
                    e = events[i];
                    me.fireEvent('toggle', me, e.item, e.toggle);
                }
            }, 50);
        }
    },

    /**
     * Returns `true` if a specified {@link Ext.Button} is pressed.
     * @param {Ext.Button} button The button to check if pressed.
     * @return {Boolean} pressed
     */
    isPressed: function(button) {
        var pressedButtons = this.getPressedButtons();
        return pressedButtons.indexOf(button) != -1;
    },

    /**
     * @private
     */
    doSetDisabled: function(disabled) {
        var me = this;

        me.items.each(function(item) {
            item.setDisabled(disabled);
        }, me);

        me.callParent(arguments);
    }
}, function() {
    //<deprecated product=touch since=2.0>
    var me = this;

    /**
     * Activates a button.
     * @param {Number/String/Ext.Button} button The button to activate.
     * @param {Boolean} pressed If defined, sets the pressed state of the button,
     * otherwise the pressed state is toggled.
     * @param {Boolean} suppressEvents `true` to suppress toggle events during the action.
     * If {@link #allowMultiple} is `true`, then {@link #setPressed} will toggle the button state.
     * @method setPressed
     * @deprecated 2.0.0 Please use {@link #setPressedButtons} instead
     */
    Ext.deprecateClassMethod(me, 'setPressed', 'setPressedButtons');

    /**
     * Gets the currently pressed button(s).
     * @method getPressed
     * @deprecated 2.0.0 Please use {@link #getPressedButtons} instead
     */
    Ext.deprecateClassMethod(me, 'getPressed', 'getPressedButtons');

    //</deprecated>
});


/**
 * Simple Select field wrapper. Example usage:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         fullscreen: true,
 *         items: [
 *             {
 *                 xtype: 'fieldset',
 *                 title: 'Select',
 *                 items: [
 *                     {
 *                         xtype: 'selectfield',
 *                         label: 'Choose one',
 *                         options: [
 *                             {text: 'First Option',  value: 'first'},
 *                             {text: 'Second Option', value: 'second'},
 *                             {text: 'Third Option',  value: 'third'}
 *                         ]
 *                     }
 *                 ]
 *             }
 *         ]
 *     });
 *
 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
 */
Ext.define('Ext.field.Select', {
    extend: 'Ext.field.Text',
    xtype: 'selectfield',
    alternateClassName: 'Ext.form.Select',
    requires: [
        'Ext.Panel',
        'Ext.picker.Picker',
        'Ext.data.Store',
        'Ext.data.StoreManager',
        'Ext.dataview.List'
    ],

    /**
     * @event change
     * Fires when an option selection has changed
     * @param {Ext.field.Select} this
     * @param {Mixed} newValue The new value
     * @param {Mixed} oldValue The old value
     */

    /**
     * @event focus
     * Fires when this field receives input focus. This happens both when you tap on the field and when you focus on the field by using
     * 'next' or 'tab' on a keyboard.
     *
     * Please note that this event is not very reliable on Android. For example, if your Select field is second in your form panel,
     * you cannot use the Next button to get to this select field. This functionality works as expected on iOS.
     * @param {Ext.field.Select} this This field
     * @param {Ext.event.Event} e
     */

    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        ui: 'select',

        /**
         * @cfg {Boolean} useClearIcon
         * @hide
         */

        /**
         * @cfg {String/Number} valueField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
         * Select control.
         * @accessor
         */
        valueField: 'value',

        /**
         * @cfg {String/Number} displayField The underlying {@link Ext.data.Field#name data value name} (or numeric Array index) to bind to this
         * Select control. This resolved value is the visibly rendered value of the available selection options.
         * @accessor
         */
        displayField: 'text',

        /**
         * @cfg {Ext.data.Store/Object/String} store The store to provide selection options data.
         * Either a Store instance, configuration object or store ID.
         * @accessor
         */
        store: null,

        /**
         * @cfg {Array} options An array of select options.
         *
         *     [
         *         {text: 'First Option',  value: 'first'},
         *         {text: 'Second Option', value: 'second'},
         *         {text: 'Third Option',  value: 'third'}
         *     ]
         *
         * __Note:__ Option object member names should correspond with defined {@link #valueField valueField} and {@link #displayField displayField} values.
         * This config will be ignored if a {@link #store store} instance is provided.
         * @accessor
         */
        options: null,

        /**
         * @cfg {String} hiddenName Specify a `hiddenName` if you're using the {@link Ext.form.Panel#standardSubmit standardSubmit} option.
         * This name will be used to post the underlying value of the select to the server.
         * @accessor
         */
        hiddenName: null,

        /**
         * @cfg {Object} component
         * @accessor
         * @hide
         */
        component: {
            useMask: true
        },

        /**
         * @cfg {Boolean} clearIcon
         * @hide
         * @accessor
         */
        clearIcon: false,

        /**
         * @cfg {String/Boolean} usePicker
         * `true` if you want this component to always use a {@link Ext.picker.Picker}.
         * `false` if you want it to use a popup overlay {@link Ext.List}.
         * `auto` if you want to show a {@link Ext.picker.Picker} only on phones.
         */
        usePicker: 'auto',

        /**
         * @cfg {Boolean} autoSelect
         * `true` to auto select the first value in the {@link #store} or {@link #options} when they are changed. Only happens when
         * the {@link #value} is set to `null`.
         */
        autoSelect: true,

        /**
         * @cfg {Object} defaultPhonePickerConfig
         * The default configuration for the picker component when you are on a phone.
         */
        defaultPhonePickerConfig: null,

        /**
         * @cfg {Object} defaultTabletPickerConfig
         * The default configuration for the picker component when you are on a tablet.
         */
        defaultTabletPickerConfig: null,

        /**
         * @cfg
         * @inheritdoc
         */
        name: 'picker',

        /**
         * @cfg {String} pickerSlotAlign
         * The alignment of text in the picker created by this Select
         * @private
         */
        pickerSlotAlign: 'center'
    },

    platformConfig: [
        {
            theme: ['Windows'],
            pickerSlotAlign: 'left'
        },
        {
            theme: ['Tizen'],
            usePicker: false
        }
    ],

    // @private
    initialize: function() {
        var me = this,
            component = me.getComponent();

        me.callParent();

        component.on({
            scope: me,
            masktap: 'onMaskTap'
        });

        component.doMaskTap = Ext.emptyFn;

        if (Ext.browser.is.AndroidStock2) {
            component.input.dom.disabled = true;
        }

        if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
            this.label.on({
                scope: me,
                tap: "onFocus"
            });
        }
    },

    getElementConfig: function() {
        if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
                var prefix = Ext.baseCSSPrefix;

                return {
                    reference: 'element',
                    className: 'x-container',
                    children: [
                        {
                            reference: 'innerElement',
                            cls: prefix + 'component-outer',
                            children: [
                                {
                                    reference: 'label',
                                    cls: prefix + 'form-label',
                                    children: [{
                                        reference: 'labelspan',
                                        tag: 'span'
                                    }]
                                }
                            ]
                        }
                    ]
                };
        } else {
            return this.callParent(arguments);
        }
    },

    /**
     * @private
     */
    updateDefaultPhonePickerConfig: function(newConfig) {
        var picker = this.picker;
        if (picker) {
            picker.setConfig(newConfig);
        }
    },

    /**
     * @private
     */
    updateDefaultTabletPickerConfig: function(newConfig) {
        var listPanel = this.listPanel;
        if (listPanel) {
            listPanel.setConfig(newConfig);
        }
    },

    /**
     * @private
     * Checks if the value is `auto`. If it is, it only uses the picker if the current device type
     * is a phone.
     */
    applyUsePicker: function(usePicker) {
        if (usePicker == "auto") {
            usePicker = (Ext.os.deviceType == 'Phone');
        }

        return Boolean(usePicker);
    },

    syncEmptyCls: Ext.emptyFn,

    /**
     * @private
     */
    applyValue: function(value) {
        var record = value,
            index, store;

        //we call this so that the options configruation gets intiailized, so that a store exists, and we can
        //find the correct value
        this.getOptions();

        store = this.getStore();

        if ((value != undefined && !value.isModel) && store) {
            index = store.find(this.getValueField(), value, null, null, null, true);

            if (index == -1) {
                index = store.find(this.getDisplayField(), value, null, null, null, true);
            }

            record = store.getAt(index);
        }

        return record;
    },

    updateValue: function(newValue, oldValue) {
        this.record = newValue;

        this.callParent([(newValue && newValue.isModel) ? newValue.get(this.getDisplayField()) : '']);
    },

    getValue: function() {
        var record = this.record;
        return (record && record.isModel) ? record.get(this.getValueField()) : null;
    },

    /**
     * Returns the current selected {@link Ext.data.Model record} instance selected in this field.
     * @return {Ext.data.Model} the record.
     */
    getRecord: function() {
        return this.record;
    },

    // @private
    getPhonePicker: function() {
        var config = this.getDefaultPhonePickerConfig();

        if (!this.picker) {
            this.picker = Ext.create('Ext.picker.Picker', Ext.apply({
                slots: [
                    {
                        align: this.getPickerSlotAlign(),
                        name: this.getName(),
                        valueField: this.getValueField(),
                        displayField: this.getDisplayField(),
                        value: this.getValue(),
                        store: this.getStore()
                    }
                ],
                listeners: {
                    change: this.onPickerChange,
                    scope: this
                }
            }, config));
        }

        return this.picker;
    },

    // @private
    getTabletPicker: function() {
        var config = this.getDefaultTabletPickerConfig();

        if (!this.listPanel) {
            this.listPanel = Ext.create('Ext.Panel', Ext.apply({
                left: 0,
                top: 0,
                modal: true,
                cls: Ext.baseCSSPrefix + 'select-overlay',
                layout: 'fit',
                hideOnMaskTap: true,
                width: Ext.os.is.Phone ? '14em' : '18em',
                height: (Ext.os.is.BlackBerry && Ext.os.version.getMajor() === 10) ? '12em' : (Ext.os.is.Phone ? '12.5em' : '22em'),
                items: {
                    xtype: 'list',
                    store: this.getStore(),
                    itemTpl: '{' + this.getDisplayField() + ':htmlEncode}',
                    listeners: {
                        select: this.onListSelect,
                        itemtap: this.onListTap,
                        scope: this
                    }
                }
            }, config));
        }

        return this.listPanel;
    },

    // @private
    onMaskTap: function() {
        this.onFocus();

        return false;
    },

    /**
     * Shows the picker for the select field, whether that is a {@link Ext.picker.Picker} or a simple
     * {@link Ext.List list}.
     */
    showPicker: function() {
        var me = this,
            store = me.getStore(),
            value = me.getValue();

        //check if the store is empty, if it is, return
        if (!store || store.getCount() === 0) {
            return;
        }

        if (me.getReadOnly()) {
            return;
        }

        me.isFocused = true;

        if (me.getUsePicker()) {
            var picker = me.getPhonePicker(),
                name = me.getName(),
                pickerValue = {};

            pickerValue[name] = value;
            picker.setValue(pickerValue);

            if (!picker.getParent()) {
                Ext.Viewport.add(picker);
            }

            picker.show();
        } else {
            var listPanel = me.getTabletPicker(),
                list = listPanel.down('list'),
                index, record;

            if (!listPanel.getParent()) {
                Ext.Viewport.add(listPanel);
            }

            listPanel.showBy(me.getComponent(), null);

            if (value || me.getAutoSelect()) {
                store = list.getStore();
                index = store.find(me.getValueField(), value, null, null, null, true);
                record = store.getAt(index);

                if (record) {
                    list.select(record, null, true);
                }
            }
        }
    },

    // @private
    onListSelect: function(item, record) {
        var me = this;
        if (record) {
            me.setValue(record);
        }
    },

    onListTap: function() {
        this.listPanel.hide({
            type: 'fade',
            out: true,
            scope: this
        });
    },

    // @private
    onPickerChange: function(picker, value) {
        var me = this,
            newValue = value[me.getName()],
            store = me.getStore(),
            index = store.find(me.getValueField(), newValue, null, null, null, true),
            record = store.getAt(index);

        me.setValue(record);
    },

    onChange: function(component, newValue, oldValue) {
        var me = this,
            store = me.getStore(),
            index = (store) ? store.find(me.getDisplayField(), oldValue, null, null, null, true) : -1,
            valueField = me.getValueField(),
            record = (store) ? store.getAt(index) : null;

        oldValue = (record) ? record.get(valueField) : null;

        me.fireEvent('change', me, me.getValue(), oldValue);
    },

    /**
     * Updates the underlying `<options>` list with new values.
     *
     * @param {Array} newOptions An array of options configurations to insert or append.
     *
     *     selectBox.setOptions([
     *         {text: 'First Option',  value: 'first'},
     *         {text: 'Second Option', value: 'second'},
     *         {text: 'Third Option',  value: 'third'}
     *     ]).setValue('third');
     *
     * __Note:__ option object member names should correspond with defined {@link #valueField valueField} and
     * {@link #displayField displayField} values.
     *
     * @return {Ext.field.Select} this
     */
    updateOptions: function(newOptions) {
        var store = this.getStore();

        if (!store) {
            this.setStore(true);
            store = this._store;
        }

        if (!newOptions) {
            store.clearData();
        }
        else {
            store.setData(newOptions);
            this.onStoreDataChanged(store);
        }
        return this;
    },

    applyStore: function(store) {
        if (store === true) {
            store = Ext.create('Ext.data.Store', {
                fields: [this.getValueField(), this.getDisplayField()],
                autoDestroy: true
            });
        }

        if (store) {
            store = Ext.data.StoreManager.lookup(store);

            store.on({
                scope: this,
                addrecords: 'onStoreDataChanged',
                removerecords: 'onStoreDataChanged',
                updaterecord: 'onStoreDataChanged',
                refresh: 'onStoreDataChanged'
            });
        }

        return store;
    },

    updateStore: function(newStore) {
        if (newStore) {
            this.onStoreDataChanged(newStore);
        }

        if (this.getUsePicker() && this.picker) {
            this.picker.down('pickerslot').setStore(newStore);
        } else if (this.listPanel) {
            this.listPanel.down('dataview').setStore(newStore);
        }
    },

    /**
     * Called when the internal {@link #store}'s data has changed.
     */
    onStoreDataChanged: function(store) {
        var initialConfig = this.getInitialConfig(),
            value = this.getValue();

        if (value || value == 0) {
            this.updateValue(this.applyValue(value));
        }

        if (this.getValue() === null) {
            if (initialConfig.hasOwnProperty('value')) {
                this.setValue(initialConfig.value);
            }

            if (this.getValue() === null && this.getAutoSelect()) {
                if (store.getCount() > 0) {
                    this.setValue(store.getAt(0));
                }
            }
        }
    },

    /**
     * @private
     */
    doSetDisabled: function(disabled) {
        var component = this.getComponent();
        if (component) {
            component.setDisabled(disabled);
        }
        Ext.Component.prototype.doSetDisabled.apply(this, arguments);
    },

    /**
     * @private
     */
    setDisabled: function() {
        Ext.Component.prototype.setDisabled.apply(this, arguments);
    },

    // @private
    updateLabelWidth: function() {
        if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
            return;
        } else {
            this.callParent(arguments);
        }
    },

    // @private
    updateLabelAlign: function() {
        if (Ext.theme.is.Blackberry || Ext.theme.is.Blackberry103) {
            return;
        } else {
            this.callParent(arguments);
        }
    },

    /**
     * Resets the Select field to the value of the first record in the store.
     * @return {Ext.field.Select} this
     * @chainable
     */
    reset: function() {
        var me = this,
            record;

        if (me.getAutoSelect()) {
            var store = me.getStore();

            record = (me.originalValue) ? me.originalValue : store.getAt(0);
        } else {
            var usePicker = me.getUsePicker(),
                picker = usePicker ? me.picker : me.listPanel;

            if (picker) {
                picker = picker.child(usePicker ? 'pickerslot' : 'dataview');

                picker.deselectAll();
            }

            record = null;
        }

        me.setValue(record);

        return me;
    },

    onFocus: function(e) {
        if (this.getDisabled()) {
            return false;
        }

        var component = this.getComponent();
        this.fireEvent('focus', this, e);

        if (Ext.os.is.Android4) {
            component.input.dom.focus();
        }
        component.input.dom.blur();

        this.isFocused = true;

        this.showPicker();
    },

    destroy: function() {
        this.callParent(arguments);
        var store = this.getStore();

        if (store && store.getAutoDestroy()) {
            Ext.destroy(store);
        }

        Ext.destroy(this.listPanel, this.picker);
    }
});


const _ = require('lodash')
const check = require('check-types')
const pgEscape = require('pg-escape')
const { getEntitiesOrderedByRelations } = require('@dataplug/dataplug-flatters')
const Dialects = require('./dialects')
const SqlSerializer = require('./sqlSerializer')

/**
 * Converts JSON schema to SQL schema
 *
 * @param {string} dbDialect Database dialect to use
 * @param {SchemaFlatter~Metadata} metadata Metadata
 * @param {string} [prefix=] Table names prefix
 * @return {Array} Array of queries
 */
function generateSqlSchema (dbDialect, metadata, prefix = undefined) {
  check.assert.nonEmptyString(dbDialect)
  check.assert.object(metadata)
  check.assert.maybe.string(prefix)

  if (!Dialects.isSupported(dbDialect)) {
    throw new Error(`Dialect "${dbDialect}" not supported`)
  }

  prefix = prefix || ''

  const sqlSerializer = new SqlSerializer(dbDialect)
  const orderedEntities = getEntitiesOrderedByRelations(metadata)

  let queries = []
  _.forEach(orderedEntities, (entityName) => {
    const entity = metadata[entityName]
    const tableName = pgEscape.ident(`${prefix}${entityName}`)

    let createQueries = []
    _.forOwn(entity.fields, (field, fieldName) => {
      const columnName = pgEscape.ident(fieldName)

      let columnQuery = ''

      columnQuery += columnName
      let columnConstraints = []

      const columnType = SqlSerializer.resolveType(field.type)
      columnQuery += ` ${columnType}`

      if (field.enum) {
        const enumValues = field.enum
          .map((value) => sqlSerializer.serializeValue(field.type, value))
          .join(', ')
        const expression = columnType.match(/(\[\d*\])+/g)
          ? `<@ ARRAY[${enumValues}]`
          : `IN (${enumValues})`
        columnConstraints.push(`CHECK (${columnName} ${expression})`)
      }

      // SQL doesn't have an "undefined" value, thus no-default would result a nullable column
      columnConstraints.push((field.nullable || (field.default === undefined && !field.identity))
        ? 'NULL'
        : 'NOT NULL')

      if (field.default !== undefined) {
        const value = sqlSerializer.serializeValue(field.type, field.default)
        columnConstraints.push(`DEFAULT ${value}`)
      }

      if (columnConstraints.length > 0) {
        columnQuery += ` ${columnConstraints.join(' ')}`
      }

      createQueries.push(`\n\t${columnQuery}`)
    })

    const identity = _.chain(entity.fields)
      .pickBy((field) => field.identity)
      .map((field, fieldName) => pgEscape.ident(fieldName))
      .value()
    if (identity.length > 0) {
      const primaryKeyName = pgEscape.ident(`${prefix}${entityName}_primary`)
      const uniqueName = pgEscape.ident(`${prefix}${entityName}_unique`)

      const columns = identity.join(', ')
      createQueries.push(`\n\tCONSTRAINT ${primaryKeyName} PRIMARY KEY (${columns})`)
      createQueries.push(`\n\tCONSTRAINT ${uniqueName} UNIQUE (${columns})`)
    }

    let referencesCount = 0
    const references = _.chain(entity.fields)
      .pickBy((field) => field.relation)
      .toPairs()
      .groupBy((pair) => pair[1].relation.entity)
      .value()
    _.forOwn(references, (pairs, otherEntityName) => {
      const otherTableName = pgEscape.ident(`${prefix}${otherEntityName}`)
      const constraintName = pgEscape.ident(`${prefix}${entityName}_ref${referencesCount++}`)

      pairs = _.zip(...pairs)
      const fieldsList = pairs[0]
        .map((fieldName) => pgEscape.ident(fieldName))
        .join(', ')
      const otherEntityFieldsList = pairs[1]
        .map((field) => pgEscape.ident(field.relation.field))
        .join(', ')

      let foreignConstraint = ''
      foreignConstraint += `\n\tCONSTRAINT ${constraintName} FOREIGN KEY (${fieldsList})`
      foreignConstraint += `\n\t\tREFERENCES ${otherTableName} (${otherEntityFieldsList})`
      foreignConstraint += '\n\t\tON DELETE CASCADE'
      foreignConstraint += '\n\t\tON UPDATE CASCADE'

      createQueries.push(foreignConstraint)
    })

    let createQuery = ''
    createQuery += `CREATE TABLE ${tableName} (`
    createQuery += createQueries.join(',')
    createQuery += createQueries.length > 0 ? '\n)' : ')'

    queries.push(createQuery)
  })

  return queries
}

module.exports = generateSqlSchema


"use strict";

var fs = require('fs');

var rMemberName = new RegExp('[a-z]+[_0-9a-z]*');
var rExportTypeString = new RegExp('[A-Z]+[_0-9a-zA-Z]*');

function getMemberName(str) {
    return rMemberName.exec(str.toLowerCase());
}

function isExportTypeString(str) {
    return rExportTypeString.exec(str) == str;
}

function isBaseType(str) {
    if (str == 'int') {
        return true;
    }
    else if (str == 'string') {
        return true;
    }
    else if (str == 'time') {
        return true;
    }
    else if (str == 'bool') {
        return true;
    }
    else if (str == 'bytes') {
        return true;
    }
    else if (str == 'float') {
        return true;
    }

    return false;
}

function isType(str, root) {
    if (isBaseType(str)) {
        return true;
    }

    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'type' || root[i].type == 'struct' || root[i].type == 'enum' || root[i].type == 'static' || root[i].type == 'message') {
                return true;
            }
        }
    }

    return false;
}

function getRealType_Enum(str, root) {
    if (isBaseType(str)) {
        return str;
    }

    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'enum') {
                return 'int';
            }

            if (root[i].type == 'struct' || root[i].type == 'message' || root[i].type == 'static') {
                return root[i].name;
            }

            if (root[i].type == 'type') {
                return getRealType_Enum(root[i].val);
            }
        }
    }

    return '[ERR]';
}

function getRealType(str, root) {
    if (isBaseType(str)) {
        return str;
    }

    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'enum') {
                return root[i].name;
            }

            if (root[i].type == 'struct' || root[i].type == 'message' || root[i].type == 'static') {
                return root[i].name;
            }

            if (root[i].type == 'type') {
                return getRealType(root[i].val);
            }
        }
    }

    return '[ERR]';
}

// 根据名字判断是否是枚举
function isEnum(str, root) {
    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'enum') {
                return true;
            }
        }
    }

    return false;
}

function isStruct(str, root) {
    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'struct') {
                return true;
            }
        }
    }

    return false;
}

function isStatic(str, root) {
    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            if (root[i].type == 'static') {
                return true;
            }
        }
    }

    return false;
}

function hasMember2(str, obj, root) {
    var ii = str.indexOf('.');
    if (ii < 0) {
        return hasMember(getNoUnderscoreName(str), obj, root);
    }

    if (ii == 0) {
        return false;
    }

    var curtype = undefined;
    var cur = getNoUnderscoreName(str.slice(0, ii));
    forEachStruct(obj.name, obj, root, function (structname, cobj, root) {
        if (cur == getNoUnderscoreName(cobj.name.name)) {
            curtype = cobj.type;
        }
    });

    if (curtype != undefined) {
        return hasMember2(str.slice(ii + 1), getGlobalObj(curtype, root), root);
    }

    return false;
}

function hasMember(str, obj, root) {
    for (var i = 0; i < obj.val.length; ++i) {
        if (obj.val[i].type2 == 'expand' && !obj.val[i].hasOwnProperty('expand') && isStruct(obj.val[i].type, root)) {
            var cobj = getGlobalObj(obj.val[i].type, root);
            if (cobj != undefined) {
                if (hasMember(str, cobj, root)) {
                    return true;
                }
            }
        }
        else if (getNoUnderscoreName(obj.val[i].name.name) == str) {
            return true;
        }
    }

    return false;
}

function countMember(str, obj, root) {
    var nums = 0;

    for (var i = 0; i < obj.val.length; ++i) {
        if (obj.val[i].type2 == 'expand' && !obj.val[i].hasOwnProperty('expand') && isStruct(obj.val[i].type, root)) {
            var cobj = getGlobalObj(obj.val[i].type, root);
            if (cobj != undefined) {
                nums += hasMember(str, cobj, root);
            }
        }
        else if (obj.val[i].name.name == str) {
            nums += 1;
        }
    }

    return nums;
}

function hasMemberEx(str, noexpand, obj, root) {
    for (var i = 0; i < obj.val.length; ++i) {
        if (obj.val[i].type2 == 'expand' && !obj.val[i].hasOwnProperty('expand') && obj.val[i].type != noexpand && isStruct(obj.val[i].type, root)) {
            var cobj = getGlobalObj(obj.val[i].type, root);
            if (cobj != undefined) {
                if (hasMember(str, cobj, root)) {
                    return true;
                }
            }
        }
        else if (obj.val[i].name.name == str) {
            return true;
        }
    }

    return false;
}

// 根据名字获取全局对象
function getGlobalObj(str, root) {
    for (var i = 0; i < root.length; ++i) {
        if (root[i].name == str) {
            return root[i];
        }
    }

    return undefined;
}

function getEnumMemberRealName(str, enumname) {
    return str.slice(enumname.length + 1).toLowerCase();
}

// callback(structname, obj, root)
function forEachStruct(structname, obj, root, callback, noexline) {
    if (noexline == undefined) {
        noexline = false;
    }

    for (var i = 0; i < obj.val.length; ++i) {
        if (noexline && obj.val[i].name.name.indexOf('_') == 0) {
            continue ;
        }

        if (obj.val[i].hasOwnProperty('type2') && obj.val[i].type2 == 'expand') {
            if (!obj.val[i].hasOwnProperty('expand')) {
                forEachStruct(structname, getGlobalObj(obj.val[i].type, root), root, callback);
            }
            else {
                var expandobj = getGlobalObj(obj.val[i].expand, root);
                for (var eoi = 0; eoi < expandobj.val.length; ++eoi) {
                    callback(structname, {
                        name: {name: getEnumMemberRealName(expandobj.val[eoi].name, expandobj.name)},
                        type: obj.val[i].type,
                        comment: expandobj.val[eoi].comment}, root);
                }
            }
        }
        else {
            if (obj.name != structname && obj.val[i].hasOwnProperty('type2') && obj.val[i].type2 == 'primary') {
                callback(structname, {
                    name: obj.val[i].name,
                    val: obj.val[i].val,
                    type: obj.val[i].type,
                    type2: 'unique'
                }, root);
            }
            else {
                callback(structname, obj.val[i], root);
            }
        }
    }
}

function countGlobalObj(str, root) {
    var nums = 0;
    for (var i = 0; i < root.length; ++i) {
        if (getNoUnderscoreName(root[i].name) == getNoUnderscoreName(str)) {
            nums++;
        }
    }

    return nums;
}

function getNoUnderscoreName(name) {
    if (name.indexOf('_') == 0) {
        return getNoUnderscoreName(name.slice(1));
    }

    return name;
}

function loadJson(filename) {
    if (fs.existsSync(filename)) {
        var str = fs.readFileSync(filename, 'utf-8');
        return JSON.parse(str);
    }

    return undefined;
}

//function isStaticStruct(name, root) {
//    var obj = getGlobalObj(name, root);
//    if (obj != undefined) {
//        if (obj.type == 'static') {
//            return true;
//        }
//
//        if (obj.type == 'struct') {
//            for (var i = 0; i < obj.val.length; ++i) {
//                if (isBaseType(obj.val[i].type)) {
//                    if (!(obj.val[i].hasOwnProperty('type2') && obj.val[i].type2 == 'primary')) {
//                        return false;
//                    }
//                }
//                else {
//                    if (!isStaticStruct(obj.val[i].type, root)) {
//                        return false;
//                    }
//                }
//            }
//
//            return true;
//        }
//    }
//
//    return false;
//}

function getStructMemberType(membername, structname, root) {
    var obj = getGlobalObj(structname, root);
    if (obj != undefined) {
        if (obj.type == 'message' || obj.type == 'struct' || obj.type == 'static') {
            for (var i = 0; i < obj.val.length; ++i) {
                if (obj.val[i].name.name == membername) {
                    return obj.val[i].type;
                }
            }
        }
    }

    return undefined;
}

function isResMsg(name) {
    return name.indexOf('Res_') == 0;
}

function isReqMsg(name) {
    return name.indexOf('Req_') == 0;
}

function getRealMsgName(name) {
    if (isResMsg(name)) {
        return name.slice(4);
    }
    else if (isReqMsg(name)) {
        return name.slice(4);
    }

    return name;
}

function buildEnumReqMemberName(name) {
    return 'MSGID_REQ_' + name.slice(4).toUpperCase();
}

function buildEnumResMemberName(name) {
    return 'MSGID_RES_' + name.slice(4).toUpperCase();
}

function getMemberDefaultVal(obj, member, root) {
    var curtype = getRealType(member.type, root);
    if (isBaseType(curtype)) {
        if (member.hasOwnProperty('val')) {
            if ('AUTOINC' == member.val.val) {
                return 0;
            }
            else if ('NOW' == member.val.val) {
                return 'Date.now()';
            }
        }

        if ('string' == curtype) {
            return '""';
        }

        return 0;
    }

    if (member.hasOwnProperty('val')) {
        return member.val.name;
    }

    return ;
}

// 根据名字取到成员
function getMember(obj, name, root) {
    var ii = str.indexOf('.');
    if (ii < 0) {
        var curobj = undefined;
        forEachStruct(obj.name, obj, root, function (structname, cobj, root) {
            if (name == cobj.name.name) {
                curobj = cobj;
            }
        });

        return curobj;
    }

    if (ii == 0) {
        return ;
    }

    var curtype = undefined;
    var cur = str.slice(0, ii);
    forEachStruct(obj.name, obj, root, function (structname, cobj, root) {
        if (cur == cobj.name.name) {
            curtype = cobj.type;
        }
    });

    if (curtype != undefined) {
        return getMember(getGlobalObj(curtype, root), str.slice(ii + 1), root);
    }

    return ;
}

// 为结构增加标识
function setInMessage(obj, root) {
    obj.inmessage = true;

    if (obj.type == 'static' || obj.type == 'struct' || obj.type == 'message') {
        for (var i = 0; i < obj.val.length; ++i) {
            var cval = obj.val[i];
            if (cval.hasOwnProperty('type2') && 'expand' == cval.type2) {
                continue ;
            }

            var mytype = getRealType(cval.type, root);
            if (isBaseType(mytype)) {
                continue ;
            }

            setInMessage(getGlobalObj(mytype, root), root);
        }
    }
}

// 为所有消息内结构增加标识
function procInMessage(root) {
    for (var i = 0; i < root.length; ++i) {
        if ('message' == root[i].type) {
            setInMessage(root[i], root);
        }
    }

    return root;
}

// 获取版本号
function getVer(root) {
    let cur = getGlobalObj('VER', root);
    if (cur == undefined) {
        return '';
    }

    return cur.val.val;
}

exports.isBaseType = isBaseType;
exports.isType = isType;
exports.getRealType = getRealType;
exports.getRealType_Enum = getRealType_Enum;
exports.getMemberName = getMemberName;
exports.isEnum = isEnum;
exports.isStruct = isStruct;
exports.isStatic = isStatic;
exports.hasMember = hasMember;
exports.hasMember2 = hasMember2;
exports.hasMemberEx = hasMemberEx;
exports.getGlobalObj = getGlobalObj;
exports.getEnumMemberRealName = getEnumMemberRealName;
exports.isExportTypeString = isExportTypeString;
exports.countMember = countMember;
exports.forEachStruct = forEachStruct;
exports.countGlobalObj = countGlobalObj;
exports.getNoUnderscoreName = getNoUnderscoreName;
exports.loadJson = loadJson;
//exports.isStaticStruct = isStaticStruct;
exports.getStructMemberType = getStructMemberType;
exports.isResMsg = isResMsg;
exports.getRealMsgName = getRealMsgName;
exports.isReqMsg = isReqMsg;
exports.buildEnumReqMemberName = buildEnumReqMemberName;
exports.buildEnumResMemberName = buildEnumResMemberName;
exports.getMemberDefaultVal = getMemberDefaultVal;
exports.getMember = getMember;
exports.procInMessage = procInMessage;
exports.getVer = getVer;


'use strict'

var inherits = require('util').inherits
var events = require('events')
var assert = require('assert')
var async = require('async')
var constants = require('./constants')
var Contact = require('./contact')
var Message = require('./message')

/**
 * Represents an RPC interface
 * @constructor
 * @param {Contact} contact - Your node's {@link Contact} information
 * @param {Object} options
 * @param {Contact} options.replyto - Optional alternate public contact
 * @param {Logger} options.logger - Logger instance to use
 * @property {Number} readyState - Closed: 0, Transitioning: 1, Open: 2
 */
function RPC (contact, options) {
  assert(this instanceof RPC, 'Invalid instance supplied')
  assert(contact instanceof Contact, 'Invalid contact was supplied')

  events.EventEmitter.call(this)

  options = options || {}

  if (options.replyto) {
    assert(options.replyto instanceof Contact, 'Invalid contact was supplied')
  }

  this._hooks = {
    before: {},
    after: {}
  }
  this._pendingCalls = {}
  this._contact = options.replyto || contact
  this._log = options && options.logger
  this.readyState = 0

  this.open()
}

/**
 * Triggered when the underlying transport is open
 * @event RPC#ready
 */

/**
 * Triggered when a contact is seen
 * @event RPC#CONTACT_SEEN
 * @param {Contact} contact - The contact that was just encountered
 */

/**
 * Triggered when a RPC message times out
 * @event RPC#TIMEOUT
 * @param {Contact} contact - The contact that did not respond
 * @param {Message} message - The message sent that timed out
 */

/**
 * Triggered when a FIND_NODE RPC is received
 * @event RPC#FIND_NODE
 * @param {Message} message - The message received
 */

/**
 * Triggered when a FIND_VALUE RPC message is received
 * @event RPC#FIND_VALUE
 * @param {Message} message - The message received
 */

/**
 * Triggered when a PING RPC message is received
 * @event RPC#PING
 * @param {Message} message - The message received
 */

/**
 * Triggered when a STORE RPC message is received
 * @event RPC#STORE
 * @param {Message} message - The message received
 */

/**
 * Triggered when an invalid RPC message is dropped
 * @event RPC#MESSAGE_DROP
 * @param {Buffer} message - The raw invalid message received
 */

inherits(RPC, events.EventEmitter)

/**
 * Opens the underlying transport
 * @emits RPC#ready
 * @param {RPC~openCallback} callback - Called when the transport is opened
 */
RPC.prototype.open = function (callback) {
  var self = this

  if (callback) {
    self.once('ready', callback)
  }

  self.readyState = 1

  self._trigger('before:open', [], function () {
    self._open(function () {
      self.readyState = 2
      self.emit('ready')
      self._trigger('after:open')
    })

    self._expirator = setInterval(
      self._expireCalls.bind(self),
      constants.T_RESPONSETIMEOUT + 5
    )
  })
}
/**
 * This callback is called upon completion of {@link RPC#open}
 * @callback RPC~openCallback
 */

/**
 * Closes the underlying transport
 * @param {RPC~closeCallback} callback - Called when the transport is closed
 */
RPC.prototype.close = function (callback) {
  var self = this

  self.readyState = 1

  self._trigger('before:close', [], function () {
    self._close()
    self.readyState = 0
    self._trigger('after:close')
    clearInterval(self._expirator)

    if (callback) {
      callback()
    }
  })
}
/**
 * This callback is called upon completion of {@link RPC#open}
 * @callback RPC~closeCallback
 */

/**
 * Send a RPC to the given contact
 * @param {Contact} contact - Delivery target for message
 * @param {Message} message - Message to send to target
 * @param {RPC~sendCallback} callback - Response handler function
 */
RPC.prototype.send = function (contact, message, callback) {
  var self = this

  contact = this._createContact(contact)

  assert(contact instanceof Contact, 'Invalid contact supplied')
  assert(message instanceof Message, 'Invalid message supplied')

  if (Message.isRequest(message)) {
    this._log.info('sending message', {
      method: message.method,
      contact: contact
    })
  } else {
    this._log.info('replying to message', {
      id: message.id
    })
  }

  this._trigger('before:serialize', [message], function () {
    var serialized = message.serialize()

    self._trigger('after:serialize')
    self._trigger('before:send', [serialized, contact], function () {
      if (Message.isRequest(message) && typeof callback === 'function') {
        self._log.debug('queuing callback for reponse', {
          id: message.id
        })

        self._pendingCalls[message.id] = {
          timestamp: Date.now(),
          callback: callback,
          contact: contact,
          message: message
        }
      } else {
        self._log.debug('not waiting on callback for message', {
          id: message.id
        })
      }

      self._send(message.serialize(), contact)
      self._trigger('after:send')
    })
  })
}
/**
 * This callback is called upon receipt of a response from {@link RPC#send}
 * @callback RPC~sendCallback
 * @param {Error} err - The error object, if any
 * @param {Message} message - The received response {@link Message}
 */

/**
 * Handle incoming messages
 * @param {Buffer} buffer - Raw binary data to be processed by the RPC handler
 */
RPC.prototype.receive = function (buffer) {
  var self = this,
    message, contact

  function deserialize () {
    message = Message.fromBuffer(buffer)

    self._trigger('after:deserialize')

    if (Message.isRequest(message)) {
      contact = self._createContact(message.params.contact)
    } else {
      contact = self._createContact(message.result.contact)
    }

    self._log.info('received valid message', {
      contact: contact
    })
  }

  if (!buffer) {
    self._log.warn('missing or empty reply from contact')
    return self.emit('MESSAGE_DROP')
  }

  this._trigger('before:deserialize', [buffer], function () {
    try {
      deserialize()
    } catch (err) {
      self._log.error('failed to handle message', {
        error: err.message
      })
      return self.emit('MESSAGE_DROP', buffer)
    }

    self.emit('CONTACT_SEEN', contact)
    self._trigger('before:receive', [message, contact], function () {
      self._execPendingCallback(message, contact)
    })
  })
}

/**
 * Registers a "before" hook
 * @param {String} event - Name of the event to catch
 * @param {Function} handler - Event handler to register
 */
RPC.prototype.before = function (event, handler) {
  return this._register('before', event, handler)
}

/**
 * Registers an "after" hook
 * @param {String} event - Name of the event to catch
 * @param {Function} handler - Event handler to register
 */
RPC.prototype.after = function (event, handler) {
  return this._register('after', event, handler)
}

/**
 * Registers a middleware or "hook" in a set
 * @private
 * @param {String} time - One of "before" or "after"
 * @param {String} event - Name of the event to catch
 * @param {Function} handler - Event handler to register
 */
RPC.prototype._register = function (time, event, handler) {
  assert(Object.keys(this._hooks).indexOf(time) !== -1, 'Invalid hook')
  assert(typeof event === 'string', 'Invalid event supplied')
  assert(typeof handler === 'function', 'Invalid handler supplied')

  if (!this._hooks[time][event]) {
    this._hooks[time][event] = []
  }

  this._hooks[time][event].push(handler)

  return this
}

/**
 * Triggers a middleware or "hook" set
 * @private
 * @param {String} event -  Name of the event to trigger
 * @param {Array} args - Arguments to pass to event handlers
 * @param {Function} callback - Fired after all events are triggered
 */
RPC.prototype._trigger = function (event, args, complete) {
  var self = this
  var hook = event.split(':')[0]
  var name = event.split(':')[1]
  var callback = complete || function () {}

  if (!this._hooks[hook][name]) {
    return callback()
  }

  var stack = this._hooks[hook][name].map(function (fn) {
    return fn.bind.apply(fn, [self].concat(args || []))
  })

  async.series(stack, function (err) {
    if (err) {
      return self.emit('error', err)
    }

    callback()
  })
}

/**
 * Create a contact object from the supplied contact information
 * @private
 * @param {Object} options - Data to be passed to the transport's
 * {@link Contact} constructor
 */
RPC.prototype._createContact = function (options) {
  return new this._contact.constructor(options)
}

/**
 * Executes the pending callback for a given message
 * @private
 * @param {Message} message - Message to handle any callbacks for
 * @param {Contact} contact - Contact who sent the message
 */
RPC.prototype._execPendingCallback = function (message) {
  var pendingCall = this._pendingCalls[message.id]

  this._log.debug('checking pending rpc callback stack', {
    id: message.id
  })

  if (Message.isResponse(message) && pendingCall) {
    delete this._pendingCalls[message.id]
    pendingCall.callback(null, message)
  } else if (Message.isRequest(message)) {
    if (constants.MESSAGE_TYPES.indexOf(message.method) === -1) {
      this.emit('MESSAGE_DROP', message.serialize())
      this._log.warn(
        'message references unsupported method', {
          method: message.method
        }
      )
    } else {
      this.emit(message.method, message)
    }
  } else {
    this.emit('MESSAGE_DROP', message.serialize())
    this._log.warn('dropping received late response', {
      id: message.id
    })
  }

  this._trigger('after:receive', [])
}

/**
 * Expire RPCs that have not received a reply
 * @private
 */
RPC.prototype._expireCalls = function () {
  this._log.debug('checking pending rpc callbacks for expirations')

  for (var rpcID in this._pendingCalls) {
    var pendingCall = this._pendingCalls[rpcID]
    var timePassed = Date.now() - pendingCall.timestamp

    if (timePassed > constants.T_RESPONSETIMEOUT) {
      this._log.warn('rpc call timed out', {
        id: rpcID
      })
      this.emit('TIMEOUT', pendingCall.contact, pendingCall.message)
      pendingCall.callback(new Error('RPC with ID `' + rpcID + '` timed out'))
      delete this._pendingCalls[rpcID]
    }
  }
}

/**
 * Unimplemented stub, called on close()
 * @abstract
 */
RPC.prototype._close = function () {
  throw new Error('Method not implemented')
}

/**
 * Unimplemented stub, called on send()
 * @abstract
 * @param {Buffer} data - Raw binary data to send
 * @param {Contact} contact - Target peer to send data
 */
RPC.prototype._send = function () {
  throw new Error('Method not implemented')
}

/**
 * Unimplemented stub, called on constructor
 * @abstract
 */
RPC.prototype._open = function (done) {
  async.setImmediate(done)
}

module.exports = RPC


//@tag dom,core
//@define Ext.DomQuery
//@define Ext.core.DomQuery
//@require Ext.env.Feature

/**
 * @class Ext.DomQuery
 * @alternateClassName Ext.core.DomQuery
 * @extend Ext.dom.Query
 * @singleton
 *
 * Provides functionality to select elements on the page based on a CSS selector. Delegates to
 * document.querySelectorAll. More information can be found at
 * [http://www.w3.org/TR/css3-selectors/](http://www.w3.org/TR/css3-selectors/)
 *
 * All selectors, attribute filters and pseudos below can be combined infinitely in any order. For example
 * `div.foo:nth-child(odd)[@foo=bar].bar:first` would be a perfectly valid selector.
 *
 * ## Element Selectors:
 *
 * * \* any element
 * * E an element with the tag E
 * * E F All descendant elements of E that have the tag F
 * * E > F or E/F all direct children elements of E that have the tag F
 * * E + F all elements with the tag F that are immediately preceded by an element with the tag E
 * * E ~ F all elements with the tag F that are preceded by a sibling element with the tag E
 *
 * ## Attribute Selectors:
 *
 * The use of @ and quotes are optional. For example, div[@foo='bar'] is also a valid attribute selector.
 *
 * * E[foo] has an attribute "foo"
 * * E[foo=bar] has an attribute "foo" that equals "bar"
 * * E[foo^=bar] has an attribute "foo" that starts with "bar"
 * * E[foo$=bar] has an attribute "foo" that ends with "bar"
 * * E[foo*=bar] has an attribute "foo" that contains the substring "bar"
 * * E[foo%=2] has an attribute "foo" that is evenly divisible by 2
 * * E[foo!=bar] has an attribute "foo" that does not equal "bar"
 *
 * ## Pseudo Classes:
 *
 * * E:first-child E is the first child of its parent
 * * E:last-child E is the last child of its parent
 * * E:nth-child(n) E is the nth child of its parent (1 based as per the spec)
 * * E:nth-child(odd) E is an odd child of its parent
 * * E:nth-child(even) E is an even child of its parent
 * * E:only-child E is the only child of its parent
 * * E:checked E is an element that is has a checked attribute that is true (e.g. a radio or checkbox)
 * * E:first the first E in the resultset
 * * E:last the last E in the resultset
 * * E:nth(n) the nth E in the resultset (1 based)
 * * E:odd shortcut for :nth-child(odd)
 * * E:even shortcut for :nth-child(even)
 * * E:not(S) an E element that does not match simple selector S
 * * E:has(S) an E element that has a descendant that matches simple selector S
 * * E:next(S) an E element whose next sibling matches simple selector S
 * * E:prev(S) an E element whose previous sibling matches simple selector S
 * * E:any(S1|S2|S2) an E element which matches any of the simple selectors S1, S2 or S3//\\
 *
 * ## CSS Value Selectors:
 *
 * * E{display=none} CSS value "display" that equals "none"
 * * E{display^=none} CSS value "display" that starts with "none"
 * * E{display$=none} CSS value "display" that ends with "none"
 * * E{display*=none} CSS value "display" that contains the substring "none"
 * * E{display%=2} CSS value "display" that is evenly divisible by 2
 * * E{display!=none} CSS value "display" that does not equal "none"
 */

/**
 * See {@link Ext.DomQuery} which is the singleton instance of this
 * class.  You most likely don't need to instantiate Ext.dom.Query by
 * yourself.
 * @private
 */
Ext.define('Ext.dom.Query', {
    /**
     * Selects a group of elements.
     * @param {String} selector The selector/xpath query (can be a comma separated list of selectors)
     * @param {HTMLElement/String} [root] The start of the query (defaults to document).
     * @return {HTMLElement[]} An Array of DOM elements which match the selector. If there are
     * no matches, and empty Array is returned.
     */
    select: function(q, root) {
        var results = [],
            nodes, i, j, qlen, nlen;

        root = root || document;

        if (typeof root == 'string') {
            root = document.getElementById(root);
        }

        q = q.split(",");

        for (i = 0,qlen = q.length; i < qlen; i++) {
            if (typeof q[i] == 'string') {

                //support for node attribute selection
                if (q[i][0] == '@') {
                    nodes = root.getAttributeNode(q[i].substring(1));
                    results.push(nodes);
                }
                else {
                    nodes = root.querySelectorAll(q[i]);

                    for (j = 0,nlen = nodes.length; j < nlen; j++) {
                        results.push(nodes[j]);
                    }
                }
            }
        }

        return results;
    },

    /**
     * Selects a single element.
     * @param {String} selector The selector/xpath query
     * @param {HTMLElement/String} [root] The start of the query (defaults to document).
     * @return {HTMLElement} The DOM element which matched the selector.
     */
    selectNode: function(q, root) {
        return this.select(q, root)[0];
    },

    /**
     * Returns true if the passed element(s) match the passed simple selector (e.g. div.some-class or span:first-child)
     * @param {String/HTMLElement/Array} el An element id, element or array of elements
     * @param {String} selector The simple selector to test
     * @return {Boolean}
     */
    is: function (el, q) {
        var root, is, i, ln;

        if (typeof el == "string") {
            el = document.getElementById(el);
        }

        if (Ext.isArray(el)) {
            is = true;
            ln = el.length;
            for (i = 0; i < ln; i++) {
                if (!this.is(el[i], q)) {
                    is = false;
                    break;
                }
            }
        }
        else {
            root = el.parentNode;

            if (!root) {
                root = document.createDocumentFragment();
                root.appendChild(el);
                is = this.select(q, root).indexOf(el) !== -1;
                root.removeChild(el);
                root = null;
            } else {
                is = this.select(q, root).indexOf(el) !== -1;
            }
        }
        return is;
    },

    isXml: function(el) {
        var docEl = (el ? el.ownerDocument || el : 0).documentElement;
        return docEl ? docEl.nodeName !== "HTML" : false;
    }

}, function() {
    Ext.ns('Ext.core');
    Ext.core.DomQuery = Ext.DomQuery = new this();
    /**
     * Shorthand of {@link Ext.dom.Query#select}
     * @member Ext
     * @method query
     * @inheritdoc Ext.dom.Query#select
     */
    Ext.query = Ext.Function.alias(Ext.DomQuery, 'select');
});


var util = require('util')
  , events = require('events')
//  , Exchange = require('./exchange')
  ;

/**
 * An Channel is where messages are sent to and receive
 *
 * @param {string} id
 * @param {Exchange} exchange
 */

function Channel (id, exchange) {
  var self = this;
  events.EventEmitter.call(this);
  this.id = id;
  this.exchange = exchange;
  this.exchange.on(this.id, function (message) {
    self.emit('message', message);
  });
  this.setMaxListeners(0);
}

util.inherits(Channel, events.EventEmitter);

/**
 * Makes a new channgel
 *
 * @param {string} id
 * @param {Exchange} exchange
 * @return Channel
 */

Channel.make = function (id, exchange) {
  return new Channel(id, exchange);
};

/**
 * publishes a message onto this channel
 *
 * @param {string} message
 * @return Channel
 */

Channel.prototype.publish = function (message) {
  this.exchange.publish(message, this.id);
  return this;
};

module.exports = Channel;


/**
 * @class kad-telemetry/metrics/availability
 */

'use strict'

var Message = require('../../kad/index.js').Message
var inherits = require('util').inherits
var Metric = require('../metric')

/**
 * Defines a metric that measures availability by number of requests/responses
 * @constructor
 */
function AvailabilityMetric () {
  if (!(this instanceof AvailabilityMetric)) {
    return new AvailabilityMetric()
  }

  Metric.call(this)

  this.key = 'availability'
  this.default = [0, 0] // [requests,responses]
  this.hooks = [{
    trigger: 'before',
    event: 'send',
    handler: this._start
  }, {
    trigger: 'before',
    event: 'receive',
    handler: this._stop
  }]
}

inherits(AvailabilityMetric, Metric)

/**
 * Begins the Availability test
 * #_start
 * @param {telemetry.metrics.AvailabilityMetric} self
 * @param {telemetry.Persistence} profiles
 * @returns {Function}
 */
AvailabilityMetric.prototype._start = function (self, profiles) {
  return function (buffer, contact, next) {
    var message = Message.fromBuffer(buffer)
    var cb = function (err, profile) {
      var metric = self.getMetric(profile)

      if (Message.isRequest(message)) {
        metric[0]++
        self.setMetric(profile, metric)
        profiles.setProfile(contact, profile, function () {
          next()
        })
      } else {
        next()
      }
    }
    profiles.getProfile(contact, cb)
  }
}

/**
 * Ends the Availability test
 * #_stop
 * @param {telemetry.metrics.AvailabilityMetric} self
 * @param {telemetry.Persistence} profiles
 * @returns {Function}
 */
AvailabilityMetric.prototype._stop = function (self, profiles) {
  return function (message, contact, next) {
    var cb = function (err, profile) {
      var metric = self.getMetric(profile)

      if (Message.isResponse(message)) {
        metric[1]++
        self.setMetric(profile, metric)
        profiles.setProfile(contact, profile, function () {
          next()
        })
      } else {
        next()
      }
    }
    profiles.getProfile(contact, cb)
  }
}

/**
 * Returns the score for a given metric value
 * #score
 * @param {Array} value
 * @returns {Number}
 */
AvailabilityMetric.score = function (value) {
  return value[1] / value[0]
}

module.exports = AvailabilityMetric


// lib/jam.js - Main JAM entrypoint
module.exports = (function() {

  var assert = require('assert'), tick = null;

  // Find out if we have setImmediate and fallback to setTimeout if necessary.
  tick = typeof setImmediate === 'function' ?
    setImmediate :
    function(func) { setTimeout(func, 0); };

  // # INTERNAL HELPERS

  // Function and arguments helpers.
  var toArgs = function(args) { return Array.prototype.slice.call(args); };

  function replaceHead(args, newHead) {
    args = toArgs(args);
    if (!args.length) return [newHead];

    args[0] = newHead;
    return args;
  }

  function bind(func, context) {
    return function() { return func.apply(context, arguments); };
  }

  // Common assertions
  function ensureFunc(func, argName) {
    assert(typeof func === 'function', argName + ' argument missing or not a function');
  };

  function ensureNum(num, argName) {
    assert(typeof num === 'number', argName + ' argument missing or not a number');
  };

  function ensureArray(arr, argName) {
    assert(arr && typeof arr === 'object' && typeof arr.length === 'number',
      argName + ' argument missing or does not looks like an array');
  };

  // ---

  // # HELPERS

  // Additional functions that adds to the original jam function.
  function includeHelpers(func) {

    // ## jam.identity()

    // Simple function that passes the values it receives to the next function.
    // Useful if you need a `process.nextTick` inserted in-between your call chain.
    func.identity = function(next) {
      function _identity(next) {
        var args = arguments;
        tick(function() {
          next.apply(this, replaceHead(args, null));
        });
      }

      // This function can also be passed to jam verbatim.
      return (typeof next === 'function') ?
        _identity.apply(this, arguments) :
        _identity;
    };

    // ## jam.nextTick()

    // Alias for `.identity`. Use when you need a `process.nextTick` inserted in-between
    // your call chain.
    func.nextTick = func.identity

    // ## jam.return( [args...] )

    // Returns a set of values to the next function in the chain. Useful when you want to
    // pass in the next function verbatim without wrapping it in a `function() { }` just
    // to pass values into it.
    func.return = function() {
      var args = toArgs(arguments);
      return function(next) {
        args.unshift(null);
        next.apply(this, args);
      };
    };

    // ## jam.null()

    // Similar to `.identity` but absorbs all arguments that has been passed to it and
    // forward nothing to the next function. Effectively nullifying any arguments passed
    // from previous jam call.
    //
    // Like `jam.identity`, this function can be passed to the jam chain verbatim.
    func.null = function(next) {
      function _null(next) { next(); }

      return (typeof next === 'function') ? _null.call(this, next) : _null;
    };

    // ## jam.call( function, [args...] )

    // Convenience for calling functions that accepts arguments in standard node.js
    // convention. Since jam insert `next` as first argument, most functions cannot be
    // passed directly into the jam chain, thus this helper function.
    //
    // If no `args` is given, this function passes arguments given to `next()` call from
    // previous function directly to the function (with proper callback) placement).
    //
    // Use this in combination with `jam.return` or `jam.null` if you want to control the
    // arguments that are passed to the function.
    func.call = function(func) {
      ensureFunc(func, 'function');

      var args = toArgs(arguments);
      args.shift(); // func

      if (args.length) { // use provided arguments
        return function(next) {
          args.push(next);
          func.apply(this, args);
        };

      } else { // use passed-in arguments during chain resolution
        return function(next) {
          args = toArgs(arguments);
          args.shift(); // move next to last position
          args.push(next);

          func.apply(this, args);
        };
      }
    };

    // ## jam.each( array, iterator( next, element, index ) )

    // Execute the given `iterator` function for each element given in the `array`. The
    // iterator is given a `next` function and the element to act on. The next step in the
    // chain will receive the original array passed verbatim so you can chain multiple
    // `.each` calls to act on the same array.
    //
    // You can also pass `arguments` and `"strings"` as an array or you can omit the array
    // entirely, in which case this method will assume that the previous chain step
    // returns something that looks like an array as its first result.
    //
    // Under the hood, a JAM step is added for each element. So the iterator will be
    // called serially, one after another finish. A parallel version maybe added in the
    // future.
    func.each = function(array, iterator) {
      if (typeof array === 'function') {
        iterator = array;
        array = null
      } else {
        ensureArray(array, 'array');
      }

      ensureFunc(iterator, 'iterator');

      return function(next, array_) {
        var arr = array || array_;

        // Builds another JAM chain internally
        var chain = jam(jam.identity)
          , count = arr.length;

        for (var i = 0; i < count; i++) (function(element, i) {
          chain = chain(function(next) { iterator(next, element, i); });
        })(arr[i], i);

        chain = chain(function(next) { next(null, arr); });
        return chain(next);
      };
    };

    // ## jam.map( array, iterator( next, element, index ) )

    // Works exactly like the `each` helper but if a value is passed to the iterator's
    // `next` function, it is collected into a new array which will be passed to the next
    // function in the JAM chain after `map`.
    //
    // Like with `each`, you can omit the `array` input, in which case this method will
    // assume that the previous chain step returns something that looks like an array as
    // its first result.
    func.map = function(array, iterator) {
      if (typeof array === 'function') {
        iterator = array;
        array = null;
      } else {
        ensureArray(array, 'array');
      }

      ensureFunc(iterator, 'iterator');

      return function(next, array_) {
        var arr = array || array_;

        // Builds another JAM chain internally and collect results.
        // TODO: Dry with .each?
        var chain = jam(jam.identity)
          , count = arr.length
          , result = [];

        for (var i = 0; i < count; i++) (function(element, i) {
          chain = chain(function(next, previous) {
            result.push(previous);
            iterator(next, element, i);
          });
        })(arr[i], i);

        chain = chain(function(next, last) {
          result.push(last);
          result.shift(); // discard first undefined element
          next(null, result);
        });

        return chain(next);
      };
    };

    // ## jam.timeout( timeout )

    // Pauses the chain for the specified `timeout` using `setTimeout`. Useful for
    // inserting a delay in-between a long jam chain.
    func.timeout = function(timeout) {
      ensureNum(timeout, 'timeout');

      return function(next) {
        var args = replaceHead(arguments, null);
        setTimeout(function() { next.apply(this, args); }, timeout);
      };
    };

    // ## jam.promise( [chain] )

    // Returns a JAM promise, useful when you are starting an asynchronous call outside of
    // the JAM chain itself but wants the callback to call into the chain. In other words,
    // this allow you to put a 'waiting point' (aka promise?) into existing JAM chain that
    // waits for the initial call to finish and also pass any arguments passed to the
    // callback to the next step in the JAM chain as well.
    //
    // This function will returns a callback that automatically bridges into the JAM
    // chain. You can pass the returned callback to any asynchronous function and the JAM
    // chain (at the point of calling .promise()) will wait for that asynchronous function
    // to finish effectively creating a 'waiting point'.
    //
    // Additionally, any arguments passed to the callback are forwarded to the next call
    // in the JAM chain as well. If errors are passed, then it is fast-forwarded to the
    // last handler normally like normal JAM steps.
    func.promise = function(chain) {
      chain = typeof chain === 'function' ? chain : // chain is supplied
        typeof this === 'function' ? this : // called from the chain variable
        ensureFunc(chain, 'chain'); // fails

      if (typeof chain === 'undefined' && typeof this === 'function') {
        chain = this;
      }

      var args = null, next = null;

      chain(function(next_) {
        if (args) return next_.apply(this, args); // callback already called
        next = next_; // wait for callback
      });

      return function() {
        if (next) return next.apply(this, arguments); // chain promise already called
        args = arguments; // wait for chain to call the promise
      };
    };

    // TODO: noError() ? or absorbError()
    return func;
  };

  // ---

  // # JAM function

  // Exported function starts the asynchronous call chain.
  function jam(func, context) {
    ensureFunc(func, 'function');

    var steps = [];

    // ##### Chain resolver.

    // The resolver will execute all functions passed to the chain as soon as `nextTick`.
    // Thus jam will not works across async context where the chain is not built all at
    // once in a single event loop, which is not really a problem from my personal
    // experience.
    tick(function resolve(e) {
      var args = Array.prototype.slice.call(arguments);

      // Any errors passed to next() are (fast-)forwarded to the last function in the
      // chain skipping any functions that's left to be executed.
      if (e) return steps[steps.length - 1].apply(this, args);

      // Any parameters given to next() are passed as arguments to the next function in
      // the chain (except for errors, of course.)
      var next = steps.shift()
        , args = Array.prototype.slice.call(arguments)

      if (steps.length) {
        args.shift(); // error arg
        args.unshift(resolve); // next() function
      }

      return next.apply(this, args);
    });

    // ##### Chain context continuation.

    // Subsequent invocation of the function returned from the `jam` function simply adds
    // the given function to the chain.
    function continuable(func, context) {
      ensureFunc(func, 'function');

      if (context) { // TODO: Handle falsy things?
        func = bind(func, context);
      }

      steps.push(func);
      return continuable;
    };

    return includeHelpers(continuable(func, context));
  };

  // ---

  return includeHelpers(jam);

})();