/**
 * @private
 */
Ext.define('Ext.dataview.element.Container', {
    extend: 'Ext.Component',

    /**
     * @event itemtouchstart
     * Fires whenever an item is touched
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item touched
     * @param {Number} index The index of the item touched
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemtouchmove
     * Fires whenever an item is moved
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item moved
     * @param {Number} index The index of the item moved
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemtouchend
     * Fires whenever an item is touched
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item touched
     * @param {Number} index The index of the item touched
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemtap
     * Fires whenever an item is tapped
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item tapped
     * @param {Number} index The index of the item tapped
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemtaphold
     * Fires whenever an item is tapped
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item tapped
     * @param {Number} index The index of the item tapped
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemsingletap
     * Fires whenever an item is singletapped
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item singletapped
     * @param {Number} index The index of the item singletapped
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemdoubletap
     * Fires whenever an item is doubletapped
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item doubletapped
     * @param {Number} index The index of the item doubletapped
     * @param {Ext.EventObject} e The event object
     */

    /**
     * @event itemswipe
     * Fires whenever an item is swiped
     * @param {Ext.dataview.element.Container} this
     * @param {Ext.dom.Element} item The item swiped
     * @param {Number} index The index of the item swiped
     * @param {Ext.EventObject} e The event object
     */

    doInitialize: function() {
        this.element.on({
            touchstart: 'onItemTouchStart',
            touchend: 'onItemTouchEnd',
            tap: 'onItemTap',
            taphold: 'onItemTapHold',
            touchmove: 'onItemTouchMove',
            singletap: 'onItemSingleTap',
            doubletap: 'onItemDoubleTap',
            swipe: 'onItemSwipe',
            delegate: '> div',
            scope: this
        });
    },

    //@private
    initialize: function() {
        this.callParent();
        this.doInitialize();
    },

    updateBaseCls: function(newBaseCls, oldBaseCls) {
        var me = this;

        me.callParent([newBaseCls + '-container', oldBaseCls]);
    },

    onItemTouchStart: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        Ext.get(target).on({
            touchmove: 'onItemTouchMove',
            scope   : me,
            single: true
        });

        me.fireEvent('itemtouchstart', me, Ext.get(target), index, e);
    },

    onItemTouchEnd: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        Ext.get(target).un({
            touchmove: 'onItemTouchMove',
            scope   : me
        });

        me.fireEvent('itemtouchend', me, Ext.get(target), index, e);
    },

    onItemTouchMove: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        me.fireEvent('itemtouchmove', me, Ext.get(target), index, e);
    },

    onItemTap: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        me.fireEvent('itemtap', me, Ext.get(target), index, e);
    },

    onItemTapHold: function(e) {
        var me     = this,
            target = e.getTarget(),
            index  = me.getViewItems().indexOf(target);

        me.fireEvent('itemtaphold', me, Ext.get(target), index, e);
    },

    onItemDoubleTap: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        me.fireEvent('itemdoubletap', me, Ext.get(target), index, e);
    },

    onItemSingleTap: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        me.fireEvent('itemsingletap', me, Ext.get(target), index, e);
    },

    onItemSwipe: function(e) {
        var me = this,
            target = e.getTarget(),
            index = me.getViewItems().indexOf(target);

        me.fireEvent('itemswipe', me,  Ext.get(target), index, e);
    },

    updateListItem: function(record, item) {
        var me       = this,
            dataview = me.dataview,
            store    = dataview.getStore(),
            index    = store.indexOf(record),
            data     = dataview.prepareData(record.getData(true), index, record);

        data.xcount = store.getCount();
        data.xindex = typeof data.xindex === 'number' ? data.xindex : index;

        item.innerHTML = dataview.getItemTpl().apply(data);
    },

    addListItem: function(index, record) {
        var me         = this,
            dataview   = me.dataview,
            store      = dataview.getStore(),
            data       = dataview.prepareData(record.getData(true), index, record),
            element    = me.element,
            childNodes = element.dom.childNodes,
            ln         = childNodes.length,
            wrapElement;

        data.xcount = typeof data.xcount === 'number' ? data.xcount : store.getCount();
        data.xindex = typeof data.xindex === 'number' ? data.xindex : index;

        wrapElement = Ext.Element.create(this.getItemElementConfig(index, data));

        if (!ln || index == ln) {
            wrapElement.appendTo(element);
        } else {
            wrapElement.insertBefore(childNodes[index]);
        }
    },

    getItemElementConfig: function(index, data) {
        var dataview = this.dataview,
            itemCls = dataview.getItemCls(),
            cls = dataview.getBaseCls() + '-item';

        if (itemCls) {
            cls += ' ' + itemCls;
        }
        return {
            cls: cls,
            html: dataview.getItemTpl().apply(data)
        };
    },

    doRemoveItemCls: function(cls) {
        var elements = this.getViewItems(),
            ln = elements.length,
            i = 0;

        for (; i < ln; i++) {
            Ext.fly(elements[i]).removeCls(cls);
        }
    },

    doAddItemCls: function(cls) {
        var elements = this.getViewItems(),
            ln = elements.length,
            i = 0;

        for (; i < ln; i++) {
            Ext.fly(elements[i]).addCls(cls);
        }
    },

    // Remove
    moveItemsToCache: function(from, to) {
        var me = this,
            items = me.getViewItems(),
            i = to - from,
            item;

        for (; i >= 0; i--) {
            item = items[from + i];
            Ext.get(item).destroy();
        }
        if (me.getViewItems().length == 0) {
            this.dataview.showEmptyText();
        }
    },

    // Add
    moveItemsFromCache: function(records) {
        var me = this,
            dataview = me.dataview,
            store = dataview.getStore(),
            ln = records.length,
            i, record;

        if (ln) {
            dataview.hideEmptyText();
        }

        for (i = 0; i < ln; i++) {
            records[i]._tmpIndex = store.indexOf(records[i]);
        }

        Ext.Array.sort(records, function(record1, record2) {
            return record1._tmpIndex > record2._tmpIndex ? 1 : -1;
        });

        for (i = 0; i < ln; i++) {
            record = records[i];
            me.addListItem(record._tmpIndex, record);
            delete record._tmpIndex;
        }
    },

    // Transform ChildNodes into a proper Array so we can do indexOf...
    getViewItems: function() {
        return Array.prototype.slice.call(this.element.dom.childNodes);
    },

    updateAtNewIndex: function(oldIndex, newIndex, record) {
        this.moveItemsToCache(oldIndex, oldIndex);
        this.moveItemsFromCache([record]);
    },

    destroy: function() {
        var elements = this.getViewItems(),
            ln = elements.length,
            i = 0;

        for (; i < ln; i++) {
            Ext.get(elements[i]).destroy();
        }
        this.callParent();
    }
});


/**
 * NestedList provides a miller column interface to navigate between nested sets
 * and provide a clean interface with limited screen real-estate.
 *
 *     @example miniphone preview
 *      var data = {
 *          text: 'Groceries',
 *          items: [{
 *              text: 'Drinks',
 *              items: [{
 *                  text: 'Water',
 *                  items: [{
 *                      text: 'Sparkling',
 *                      leaf: true
 *                  }, {
 *                      text: 'Still',
 *                      leaf: true
 *                  }]
 *              }, {
 *                  text: 'Coffee',
 *                  leaf: true
 *              }, {
 *                  text: 'Espresso',
 *                  leaf: true
 *              }, {
 *                  text: 'Redbull',
 *                  leaf: true
 *              }, {
 *                  text: 'Coke',
 *                  leaf: true
 *              }, {
 *                  text: 'Diet Coke',
 *                  leaf: true
 *              }]
 *          }, {
 *              text: 'Fruit',
 *              items: [{
 *                  text: 'Bananas',
 *                  leaf: true
 *              }, {
 *                  text: 'Lemon',
 *                  leaf: true
 *              }]
 *          }, {
 *              text: 'Snacks',
 *              items: [{
 *                  text: 'Nuts',
 *                  leaf: true
 *              }, {
 *                  text: 'Pretzels',
 *                  leaf: true
 *              }, {
 *                  text: 'Wasabi Peas',
 *                  leaf: true
 *              }]
 *          }]
 *      };
 *
 *      Ext.define('ListItem', {
 *          extend: 'Ext.data.Model',
 *          config: {
 *              fields: [{
 *                  name: 'text',
 *                  type: 'string'
 *              }]
 *          }
 *      });
 *
 *      var store = Ext.create('Ext.data.TreeStore', {
 *          model: 'ListItem',
 *          defaultRootProperty: 'items',
 *          root: data
 *      });
 *
 *      var nestedList = Ext.create('Ext.NestedList', {
 *          fullscreen: true,
 *          title: 'Groceries',
 *          displayField: 'text',
 *          store: store
 *      });
 *
 *
 * ###Further Reading
 * [Sencha Touch Nested List Guide](../../../components/nested_list.html)
 */
Ext.define('Ext.dataview.NestedList', {
    alternateClassName: 'Ext.NestedList',
    extend: 'Ext.Container',
    xtype: 'nestedlist',
    requires: [
        'Ext.dataview.List',
        'Ext.TitleBar',
        'Ext.Button',
        'Ext.XTemplate',
        'Ext.data.StoreManager',
        'Ext.data.NodeStore',
        'Ext.data.TreeStore'
    ],

    config: {
        /**
         * @cfg
         * @inheritdoc
         */
        baseCls: Ext.baseCSSPrefix + 'nested-list',

        /**
         * @cfg {String/Object/Boolean} cardSwitchAnimation
         * Animation to be used during transitions of cards.
         * @removed 2.0.0 please use {@link Ext.layout.Card#animation}
         */

        /**
         * @cfg {String} backText
         * The label to display for the back button.
         * @accessor
         */
        backText: 'Back',

        /**
         * @cfg {Boolean} useTitleAsBackText
         * `true` to use title as a label for back button.
         * @accessor
         */
        useTitleAsBackText: true,

        /**
         * @cfg {Boolean} updateTitleText
         * Update the title with the currently selected category.
         * @accessor
         */
        updateTitleText: true,

        /**
         * @cfg {String} displayField
         * Display field to use when setting item text and title.
         * This configuration is ignored when overriding {@link #getItemTextTpl} or
         * {@link #getTitleTextTpl} for the item text or title.
         * @accessor
         */
        displayField: 'text',

        /**
         * @cfg {String} loadingText
         * Loading text to display when a subtree is loading.
         * @accessor
         */
        loadingText: 'Loading...',

        /**
         * @cfg {String} emptyText
         * Empty text to display when a subtree is empty.
         * @accessor
         */
        emptyText: 'No items available.',

        /**
         * @cfg {Boolean/Function} onItemDisclosure
         * Maps to the {@link Ext.List#onItemDisclosure} configuration for individual lists.
         * @accessor
         */
        onItemDisclosure: false,

        /**
         * @cfg {Boolean} allowDeselect
         * Set to `true` to allow the user to deselect leaf items via interaction.
         * @accessor
         */
        allowDeselect: false,

        /**
         * @deprecated 2.0.0 Please set the {@link #toolbar} configuration to `false` instead
         * @cfg {Boolean} useToolbar `true` to show the header toolbar.
         * @accessor
         */
        useToolbar: null,

        /**
         * @cfg {Ext.Toolbar/Object/Boolean} toolbar
         * The configuration to be used for the toolbar displayed in this nested list.
         * @accessor
         */
        toolbar: {
            docked: 'top',
            xtype: 'titlebar',
            ui: 'light',
            inline: true
        },

        /**
         * @cfg {String} title The title of the toolbar
         * @accessor
         */
        title: '',

        /**
         * @cfg {String} layout
         * @hide
         * @accessor
         */
        layout: {
            type: 'card',
            animation: {
                type: 'slide',
                duration: 250,
                direction: 'left'
            }
        },

        /**
         * @cfg {Ext.data.TreeStore/String} store The tree store to be used for this nested list.
         */
        store: null,

        /**
         * @cfg {Ext.Container} detailContainer The container of the `detailCard`.
         * A detailContainer is a reference to the container where a detail card
         * displays.
         *
         * See [Sencha Touch Nested List Guide](../../../components/nested_list.html)
         * and http://en.wikipedia.org/wiki/Miller_columns
         *
         * The two possible values for a detailContainer are undefined (default),
         * which indicates that a detailCard appear in the same container, or you
         * can specify a new container location. The default condition uses the
         * current List container.
         *
         * The following example shows creating a location for a detailContainer:
         *
         * var detailContainer = Ext.create('Ext.Container', {
         *     layout: 'card'
         * });
         *
         * var nestedList = Ext.create('Ext.NestedList', {
         *     store: treeStore,
         *     detailCard: true,
         *     detailContainer: detailContainer
         * });
         *
         * The default value is typically used for phone devices in portrait mode
         * where the small screen size dictates that the detailCard replace the
         * current container.
         * @accessor
         */
        detailContainer: undefined,

        /**
         * @cfg {Ext.Component} detailCard provides the information for a leaf
         * in a Miller column list. In a Miller column, users follow a
         * hierarchial tree structure to a leaf, which provides information
         * about the item in the list. The detailCard lists the information at
         * the leaf.
         *
         * See [Sencha Touch Nested List Guide](../../../components/nested_list.html)
         * and http://en.wikipedia.org/wiki/Miller_columns
         *
         * @accessor
         */
        detailCard: null,

        /**
         * @cfg {Object} backButton The configuration for the back button used in the nested list.
         */
        backButton: {
            ui: 'back',
            hidden: true
        },

        /**
         * @cfg {Object} listConfig An optional config object which is merged with the default
         * configuration used to create each nested list.
         */
        listConfig: null,

        /**
         * @cfg {Boolean} useSimpleItems
         * Set this to false if you want the lists in this NestedList to create complex container list items.
         */
        useSimpleItems: true,

        /**
         * @cfg {Number} itemHeight
         * This allows you to set the default item height and is used to roughly calculate the amount
         * of items needed to fill the list. By default items are around 50px high. If you set this
         * configuration in combination with setting the {@link #variableHeights} to false you
         * can improve the scrolling speed
         */
        itemHeight: 47,

        /**
         * @cfg {Boolean} variableHeights
         * This configuration allows you optimize the picker by not having it read the DOM heights of list items.
         * Instead it will assume (and set) the height to be the {@link #itemHeight}.
         */
        variableHeights: false,

        // @private
        lastNode: null,

        // @private
        lastActiveList: null,

        ui: null,

        clearSelectionOnListChange: true
    },

    platformConfig: [
        {
            theme: ['Windows'],
            itemHeight: 42
        },
        {
            theme: ['Cupertino'],
            itemHeight: 43,
            useTitleAsBackText: true,
            updateTitleText: false
        },
        {
            theme: ['Blackberry', 'Blackberry103'],
            toolbar: {
                splitNavigation: true
            }
        },
        {
            theme: ['Tizen'],
            backText: ''
        }
    ],

    /**
     * @event itemtap
     * Fires when a node is tapped on.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Number} index The index of the item tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event object.
     */

    /**
     * @event itemdoubletap
     * Fires when a node is double tapped on.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Number} index The index of the item that was tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event object.
     */

    /**
     * @event containertap
     * Fires when a tap occurs and it is not on a template node.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Ext.event.Event} e The raw event object.
     */

    /**
     * @event selectionchange
     * Fires when the selected nodes change.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {Array} selections Array of the selected nodes.
     */

    /**
     * @event beforeselectionchange
     * Fires before a selection is made.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.dataview.List} list The Ext.dataview.List that is currently active.
     * @param {HTMLElement} node The node to be selected.
     * @param {Array} selections Array of currently selected nodes.
     * @deprecated 2.0.0 Please listen to the {@link #selectionchange} event with an order of `before` instead.
     */

    /**
     * @event listchange
     * Fires when the user taps a list item.
     * @param {Ext.dataview.NestedList} this
     * @param {Object} listitem The new active list.
     */

    /**
     * @event leafitemtap
     * Fires when the user taps a leaf list item.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.List} list The subList the item is on.
     * @param {Number} index The index of the item tapped.
     * @param {Ext.dom.Element} target The element tapped.
     * @param {Ext.data.Record} record The record tapped.
     * @param {Ext.event.Event} e The event.
     */

    /**
     * @event back
     * @preventable doBack
     * Fires when the user taps Back.
     * @param {Ext.dataview.NestedList} this
     * @param {HTMLElement} node The node to be selected.
     * @param {Ext.dataview.List} lastActiveList The Ext.dataview.List that was last active.
     * @param {Boolean} detailCardActive Flag set if the detail card is currently active.
     */

    /**
     * @event beforeload
     * Fires before a request is made for a new data object.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.data.Store} store The store instance.
     * @param {Ext.data.Operation} operation The Ext.data.Operation object that will be passed to the Proxy to
     * load the Store.
     */

    /**
     * @event load
     * Fires whenever records have been loaded into the store.
     * @param {Ext.dataview.NestedList} this
     * @param {Ext.data.Store} store The store instance.
     * @param {Ext.util.Grouper[]} records An array of records.
     * @param {Boolean} successful `true` if the operation was successful.
     * @param {Ext.data.Operation} operation The associated operation.
     */
    constructor: function (config) {
        if (Ext.isObject(config)) {
            if (config.getTitleTextTpl) {
                this.getTitleTextTpl = config.getTitleTextTpl;
            }
            if (config.getItemTextTpl) {
                this.getItemTextTpl = config.getItemTextTpl;
            }
        }
        this.callParent(arguments);
    },

    onItemInteraction: function () {
        if (this.isGoingTo) {
            return false;
        }
    },

    applyDetailContainer: function (config) {
        if (!config) {
            config = this;
        }

        return config;
    },

    updateDetailContainer: function (newContainer, oldContainer) {
        if (newContainer) {
            newContainer.onBefore('activeitemchange', 'onBeforeDetailContainerChange', this);
            newContainer.onAfter('activeitemchange', 'onDetailContainerChange', this);
        }
    },

    onBeforeDetailContainerChange: function () {
        this.isGoingTo = true;
    },

    onDetailContainerChange: function () {
        this.isGoingTo = false;
    },

    /**
     * Called when an list item has been tapped.
     * @param {Ext.List} list The subList the item is on.
     * @param {Number} index The id of the item tapped.
     * @param {Ext.Element} target The list item tapped.
     * @param {Ext.data.Record} record The record which as tapped.
     * @param {Ext.event.Event} e The event.
     */
    onItemTap: function (list, index, target, record, e) {
        var me = this,
            store = list.getStore(),
            node = store.getAt(index);

        me.fireEvent('itemtap', this, list, index, target, record, e);
        if (node.isLeaf()) {
            me.fireEvent('leafitemtap', this, list, index, target, record, e);
            me.goToLeaf(node);
        }
        else {
            this.goToNode(node);
        }
    },

    onBeforeSelect: function () {
        this.fireEvent.apply(this, [].concat('beforeselect', this, Array.prototype.slice.call(arguments)));
    },

    onContainerTap: function () {
        this.fireEvent.apply(this, [].concat('containertap', this, Array.prototype.slice.call(arguments)));
    },

    onSelectionChange: function () {
        this.fireEvent.apply(this, [].concat('selectionchange', this, Array.prototype.slice.call(arguments)));
    },

    onItemDoubleTap: function () {
        this.fireEvent.apply(this, [].concat('itemdoubletap', this, Array.prototype.slice.call(arguments)));
    },

    onStoreBeforeLoad: function () {
        var loadingText = this.getLoadingText(),
            scrollable = this.getScrollable();

        if (loadingText) {
            this.setMasked({
                xtype: 'loadmask',
                message: loadingText
            });

            //disable scrolling while it is masked
            if (scrollable) {
                scrollable.getScroller().setDisabled(true);
            }
        }

        this.fireEvent.apply(this, [].concat('beforeload', this, Array.prototype.slice.call(arguments)));
    },

    onStoreLoad: function (store, records, successful, operation) {
        this.setMasked(false);
        this.fireEvent.apply(this, [].concat('load', this, Array.prototype.slice.call(arguments)));

        if (store.indexOf(this.getLastNode()) === -1) {
            this.goToNode(store.getRoot());
        }
    },

    /**
     * Called when the backButton has been tapped.
     */
    onBackTap: function () {
        var me = this,
            node = me.getLastNode(),
            detailCard = me.getDetailCard(),
            detailCardActive = detailCard && me.getActiveItem() == detailCard,
            lastActiveList = me.getLastActiveList();

        this.fireAction('back', [this, node, lastActiveList, detailCardActive], 'doBack');
    },

    doBack: function (me, node, lastActiveList, detailCardActive) {
        var layout = me.getLayout(),
            animation = (layout) ? layout.getAnimation() : null;

        if (detailCardActive && lastActiveList) {
            if (animation) {
                animation.setReverse(true);
            }
            me.setActiveItem(lastActiveList);
            me.setLastNode(node.parentNode);
            me.syncToolbar();
        }
        else {
            this.goToNode(node.parentNode);
        }
    },

    updateData: function (data) {
        if (!this.getStore()) {
            this.setStore(new Ext.data.TreeStore({
                root: data
            }));
        }
    },

    applyStore: function (store) {
        if (store) {
            if (Ext.isString(store)) {
                // store id
                store = Ext.data.StoreManager.get(store);
            } else {
                // store instance or store config
                if (!(store instanceof Ext.data.TreeStore)) {
                    store = Ext.factory(store, Ext.data.TreeStore, null);
                }
            }

            // <debug>
            if (!store) {
                Ext.Logger.warn("The specified Store cannot be found", this);
            }
            //</debug>
        }

        return store;
    },

    storeListeners: {
        rootchange: 'onStoreRootChange',
        load: 'onStoreLoad',
        beforeload: 'onStoreBeforeLoad'
    },

    updateStore: function (newStore, oldStore) {
        var me = this,
            listeners = this.storeListeners;

        listeners.scope = me;

        if (oldStore && Ext.isObject(oldStore) && oldStore.isStore) {
            if (oldStore.autoDestroy) {
                oldStore.destroy();
            }
            oldStore.un(listeners);
        }

        if (newStore) {
            newStore.on(listeners);
            me.goToNode(newStore.getRoot());
        }
    },

    onStoreRootChange: function (store, node) {
        this.goToNode(node);
    },

    applyBackButton: function (config) {
        return Ext.factory(config, Ext.Button, this.getBackButton());
    },

    applyDetailCard: function (config, oldDetailCard) {
        if (config === null) {
            return Ext.factory(config, Ext.Component, oldDetailCard);
        } else {
            return Ext.factory(config, Ext.Component);
        }
    },

    updateBackButton: function (newButton, oldButton) {
        if (newButton) {
            var me = this;
            newButton.on('tap', me.onBackTap, me);
            newButton.setText(me.getBackText());

            var toolbar = me.getToolbar();
            if (this.$backButtonContainer) {
                this.$backButtonContainer.insert(0, newButton);
            } else {
                toolbar.insert(0, newButton);
            }
        }
        else if (oldButton) {
            oldButton.destroy();
        }
    },

    applyToolbar: function (config) {
        if (config && config.splitNavigation) {
            Ext.apply(config, {
                docked: 'top',
                xtype: 'titlebar',
                ui: 'light'
            });

            var containerConfig = (config.splitNavigation === true) ? {} : config.splitNavigation;

            this.$backButtonContainer = this.add(Ext.apply({
                xtype: 'toolbar',
                docked: 'bottom',
                hidden: true,
                ui: 'dark'
            }, containerConfig));
        }

        return Ext.factory(config, Ext.TitleBar, this.getToolbar());
    },

    updateToolbar: function (newToolbar, oldToolbar) {
        var me = this;
        if (newToolbar) {
            newToolbar.setTitle(me.getTitle());
            if (!newToolbar.getParent()) {
                me.add(newToolbar);
            }
        }
        else if (oldToolbar) {
            oldToolbar.destroy();
        }
    },

    updateUseToolbar: function (newUseToolbar, oldUseToolbar) {
        if (!newUseToolbar) {
            this.setToolbar(false);
        }
    },

    updateTitle: function (newTitle) {
        var me = this,
            toolbar = me.getToolbar();

        if (toolbar) {
            if (me.getUpdateTitleText()) {
                toolbar.setTitle(newTitle);
            }
        }
    },

    /**
     * Override this method to provide custom template rendering of individual
     * nodes. The template will receive all data within the Record and will also
     * receive whether or not it is a leaf node.
     * @param {Ext.data.Record} node
     * @return {String}
     */
    getItemTextTpl: function (node) {
        return '{' + this.getDisplayField() + '}';
    },

    /**
     * Override this method to provide custom template rendering of titles/back
     * buttons when {@link #useTitleAsBackText} is enabled.
     * @param {Ext.data.Record} node
     * @return {String}
     */
    getTitleTextTpl: function (node) {
        return '{' + this.getDisplayField() + '}';
    },

    /**
     * @private
     */
    renderTitleText: function (node, forBackButton) {
        if (!node.titleTpl) {
            node.titleTpl = Ext.create('Ext.XTemplate', this.getTitleTextTpl(node));
        }

        if (node.isRoot()) {
            var initialTitle = this.getInitialConfig('title');
            return (forBackButton && initialTitle === '') ? this.getInitialConfig('backText') : initialTitle;
        }

        return  node.titleTpl.applyTemplate(node.data);
    },

    /**
     * Method to handle going to a specific node within this nested list. Node must be part of the
     * internal {@link #store}.
     * @param {Ext.data.NodeInterface} node The specified node to navigate to.
     */
    goToNode: function (node) {
        if (!node) {
            return;
        }

        var me = this,
            activeItem = me.getActiveItem(),
            detailCard = me.getDetailCard(),
            detailCardActive = detailCard && me.getActiveItem() == detailCard,
            reverse = me.goToNodeReverseAnimation(node),
            firstList = me.firstList,
            secondList = me.secondList,
            layout = me.getLayout(),
            animation = (layout) ? layout.getAnimation() : null,
            list;

        //if the node is a leaf, throw an error
        if (node.isLeaf()) {
            throw new Error('goToNode: passed a node which is a leaf.');
        }

        //if we are currently at the passed node, do nothing.
        if (node == me.getLastNode() && !detailCardActive) {
            return;
        }

        if (detailCardActive) {
            if (animation) {
                animation.setReverse(true);
            }
            list = me.getLastActiveList();
            list.getStore().setNode(node);
            node.expand();
            me.setActiveItem(list);
        }
        else {
            if (animation) {
                animation.setReverse(reverse);
            }

            if (firstList && secondList) {
                //firstList and secondList have both been created
                activeItem = me.getActiveItem();

                me.setLastActiveList(activeItem);
                list = (activeItem == firstList) ? secondList : firstList;

                list.getStore().setNode(node);
                node.expand();

                me.setActiveItem(list);
                if (this.getClearSelectionOnListChange()) {
                    list.deselectAll();
                }
            }
            else if (firstList) {
                //only firstList has been created
                me.setLastActiveList(me.getActiveItem());
                me.setActiveItem(me.getList(node));
                me.secondList = me.getActiveItem();
            }
            else {
                //no lists have been created
                me.setActiveItem(me.getList(node));
                me.firstList = me.getActiveItem();
            }
        }

        me.fireEvent('listchange', this, me.getActiveItem());

        me.setLastNode(node);

        me.syncToolbar();
    },

    /**
     * The leaf you want to navigate to. You should pass a node instance.
     * @param {Ext.data.NodeInterface} node The specified node to navigate to.
     */
    goToLeaf: function (node) {
        if (!node.isLeaf()) {
            throw new Error('goToLeaf: passed a node which is not a leaf.');
        }

        var me = this,
            card = me.getDetailCard(node),
            container = me.getDetailContainer(),
            sharedContainer = container == this,
            layout = me.getLayout(),
            animation = (layout) ? layout.getAnimation() : false;

        if (card) {
            if (container.getItems().indexOf(card) === -1) {
                container.add(card);
            }
            if (sharedContainer) {
                if (me.getActiveItem() instanceof Ext.dataview.List) {
                    me.setLastActiveList(me.getActiveItem());
                }
                me.setLastNode(node);
            }
            if (animation) {
                animation.setReverse(false);
            }
            container.setActiveItem(card);
            me.syncToolbar();
        }
    },

    /**
     * @private
     * Method which updates the {@link #backButton} and {@link #toolbar} with the latest information from
     * the current node.
     */
    syncToolbar: function (forceDetail) {
        var me = this,
            detailCard = me.getDetailCard(),
            node = me.getLastNode(),
            detailActive = forceDetail || (detailCard && (me.getActiveItem() == detailCard)),
            parentNode = (detailActive) ? node : node.parentNode,
            backButton = me.getBackButton();

        //show/hide the backButton, and update the backButton text, if one exists
        if (backButton) {
            var toolbar = this.getToolbar(),
                splitNavigation = toolbar.getInitialConfig("splitNavigation");

            if (splitNavigation) {
                this.$backButtonContainer[parentNode ? 'show' : 'hide']();
            }

            backButton[parentNode ? 'show' : 'hide']();
            if (parentNode && me.getUseTitleAsBackText()) {
                backButton.setText(me.renderTitleText(node.parentNode, true));
            }
        }

        if (node) {
            me.setTitle(me.renderTitleText(node));
        }
    },

    updateBackText: function (newText) {
        this.getBackButton().setText(newText);
    },

    /**
     * @private
     * Returns `true` if the passed node should have a reverse animation from the previous current node.
     * @param {Ext.data.NodeInterface} node
     */
    goToNodeReverseAnimation: function (node) {
        var me = this,
            lastNode = me.getLastNode();
        if (!lastNode) {
            return false;
        }

        return (!lastNode.contains(node) && lastNode.isAncestor(node)) ? true : false;
    },

    /**
     * @private
     * Returns the list config for a specified node.
     * @param {HTMLElement} node The node for the list config.
     */
    getList: function (node) {
        var me = this,
            nodeStore = Ext.create('Ext.data.NodeStore', {
                recursive: false,
                node: node,
                rootVisible: false,
                model: me.getStore().getModel()
            });

        node.expand();

        return Ext.Object.merge({
            xtype: 'list',
            useSimpleItems: me.getUseSimpleItems(),
            pressedDelay: 250,
            autoDestroy: true,
            store: nodeStore,
            onItemDisclosure: me.getOnItemDisclosure(),
            allowDeselect: me.getAllowDeselect(),
            itemHeight: me.getItemHeight(),
            variableHeights: me.getVariableHeights(),
            emptyText: me.getEmptyText(),
            listeners: [
                { event: 'itemdoubletap', fn: 'onItemDoubleTap', scope: me },
                { event: 'itemtap', fn: 'onItemInteraction', scope: me, order: 'before'},
                { event: 'itemtouchstart', fn: 'onItemInteraction', scope: me, order: 'before'},
                { event: 'itemtap', fn: 'onItemTap', scope: me },
                { event: 'beforeselectionchange', fn: 'onBeforeSelect', scope: me },
                { event: 'containertap', fn: 'onContainerTap', scope: me },
                { event: 'selectionchange', fn: 'onSelectionChange', order: 'before', scope: me }
            ],
            itemTpl: '<span<tpl if="leaf == true"> class="x-list-item-leaf"</tpl>>' + me.getItemTextTpl(node) + '</span>'
        }, this.getListConfig());
    }

}, function () {
    //<deprecated product=touch since=2.0>

    /**
     * @member Ext.dataview.NestedList
     * @method getSubList
     * Returns the subList for a specified node.
     * @removed 2.0.0
     */
    Ext.deprecateMethod(this, 'getSubList', null, "Ext.dataview.NestedList.getSubList() has been removed");

    /**
     * @member Ext.dataview.NestedList
     * @cfg {Number} clearSelectionDelay
     * Number of milliseconds to show the highlight when going back in a list.
     * @removed 2.0.0
     */
    Ext.deprecateProperty(this, 'clearSelectionDelay', null, "Ext.dataview.NestedList.clearSelectionDelay has been removed");
    //</deprecated>
});


const acorn 		= require('acorn');
const asyncPlugin	= require('acorn-es7-plugin');
const {generate} 	= require('escodegen');
const vm 			= require('vm');
const Module 		= require('./module');
const extractVars	= require('./extract-vars');

const moduleCache = new Map();

const parseOpts = {
	plugins: {
		asyncawait: true
	},
	sourceType: 'module',
	ecmaVersion: 7,
	locations: true
};

const genOpts = {
	sourceMap: true,
	sourceMapWithCode: true
};

const sourceCache = new Map();

function processProgram(ast) {
	const exports = [];
	const imports = new Map();
	const bodies = [];
	const relays = new Map();

	let filtered = [];
	let defaultName = null;

	for (let line of ast.body) {
		if (line.type === 'ImportDeclaration') {
			const importee = line.source.value;
			if (!imports.has(importee)) {
				imports.set(importee, []);
			}
			const importList = imports.get(importee);

			for (let specifier of line.specifiers) {
				if (specifier.type === 'ImportDefaultSpecifier') {
					const {local} = specifier;
					importList.push({
						local: local.name,
						imported: 'default'
					});
					continue;
				}

				if (specifier.type === 'ImportNamespaceSpecifier') {
					const {local} = specifier;
					importList.push({
						local: local.name,
						imported: '*'
					});
					continue;
				}

				if (specifier.type === 'ImportSpecifier') {
					const {local, imported} = specifier;
					importList.push({
						local: local.name,
						imported: imported.name
					});
					continue;
				}

				throw new Error('WTF!');
			}

			continue;
		}

		if (line.type === 'ExportDefaultDeclaration') {
			bodies.push(makeProgram(filtered));
			filtered = [];

			if (line.declaration.type === 'FunctionDeclaration' ||
				line.declaration.type === 'ClassDeclaration') {

				if (line.declaration.id) {
					defaultName = line.declaration.id.name;
					filtered.push(line.declaration);
				} else {
					const expression = Object.assign({}, line.declaration);
					expression.type = expression.type
						.replace('Declaration','Expression');
					filtered.push(makeStatement(expression));
				}
			} else {
				filtered.push(makeStatement(line.declaration));
			}

			bodies.push(makeProgram(filtered));
			filtered = [];
			continue;
		}

		if (line.type === 'ExportNamedDeclaration') {
			if (line.declaration) {
				if (line.declaration.type === 'VariableDeclaration') {
					for (let declarator of line.declaration.declarations) {
						for (let name of extractVars(declarator.id)) {
							exports.push({
								local: name,
								exported: name
							});
						}
					}
				} else {
					const name = line.declaration.id.name;
					exports.push({
						local: name,
						exported: name
					});
				}
				filtered.push(line.declaration);
			} else {

				if (line.source) {
					const source = line.source.value;
					if (!imports.has(source)) {
						imports.set(source, []);
					}

					if (!relays.has(source)) {
						relays.set(source, {
							all: false,
							exportees: []
						});
					}

					const relay = relays.get(source);
					if (relay.all)
						throw new Error('Already exporting all!');

					for (let {local: imported, exported} of line.specifiers) {
						relay.exportees.push({
							imported: imported.name,
							exported: exported.name
						});
					}
				} else {
					for (let {local, exported} of line.specifiers) {
						exports.push({
							local: local.name,
							exported: exported.name
						});
					}
				}
			}

			continue;
		}

		if (line.type === 'ExportAllDeclaration') {
			const source = line.source.value;
			if (!imports.has(source)) {
				imports.set(source, []);
			}

			relays.set(source, {all: true})

			continue;
		}

		filtered.push(line);
	}

	bodies.push(makeProgram(filtered));

	return {exports, imports, bodies, relays, defaultName};
}

function makeStatement(expression) {
	return {
		type: 'ExpressionStatement',
		expression
	}
}

function makeProgram(lines) {
	return {
		type	: 'Program',
		body	: lines
	}
}

// bring all function declarations and exports containing them to top
// of the program
function hoistFunctions(program) {
	var functions = [];
	var body = [];
	for (let line of program.body) {
		if (line.type === 'ExportDefaultDeclaration') {
			if (line.declaration.type === 'FunctionDeclaration') {
				functions.push(line);
			} else {
				body.push(line);
			}

			continue;
		}

		if (line.type === 'ExportNamedDeclaration') {
			if (!!line.declaration &&
				line.declaration.type === 'FunctionDeclaration') {

				functions.push(line);
			} else {
				body.push(line);
			}

			continue;
		}

		if (line.type === 'FunctionDeclaration') {
			functions.push(line);
		} else {
			body.push(line);
		}
	}

	return makeProgram([...functions, ...body]);
}

// get ast from loaded id (if applicable)
function getAST(load) {

	if (load.ast) {
		return load.ast;
	} else {
		if (load.source) {
			const opts = Object.assign({sourceFile: load.sourceFile}, parseOpts);
			return acorn.parse(load.source, opts);
		} else {
			throw new Error('Cannot get AST!');
		}
	}
}

// fetches Module objects given a specific id and resolver
exports.getModuleFromId = getModuleFromId;
function getModuleFromId(id, resolver) {
	if (!moduleCache.has(id)) {
		const load = resolver.load(id);
		if (load.module) {
			moduleCache.set(id, load.module);
			return load.module;
		} else {
			const ast = hoistFunctions(getAST(load));
			const {exports, imports, relays, bodies, defaultName}
				= processProgram(ast);
			const ctxt = Object.assign({}, load.context);
			const module = new Module(exports, id, ctxt);
			let defaultValue;

			moduleCache.set(id, module);

			for (let [source, importees] of imports) {
				const importedId = resolver.resolveId(source, id);
				const importedModule = getModuleFromId(importedId, resolver);

				// relays, aka
				// export ... from "some source"
				// declarations are handled here
				if (relays.has(source)) {
					const {all, exportees} = relays.get(source);
					if (all) {
						for (let exportName of importedModule.exports()) {
							module.addExportRelay(importedModule, exportName);
						}
					} else {
						for (let {imported, exported} of exportees) {
							module.addExportRelay(importedModule, imported, exported);
						}
					}
				}

				for (let {local, imported} of importees) {
					if (imported === '*') {
						importedModule.addNamespaceBinding(ctxt, local);
						continue;
					}

					if (imported === 'default') {
						importedModule.addDefaultBinding(ctxt, local);
						continue;
					}

					// if not default or namespace ...
					importedModule.addBinding(ctxt, imported, local);
				}
			}

			// if module has no default export
			if (bodies.length === 1) {
				const {code, map} = generate(bodies[0], genOpts);
				const filename = `${id}+0`;
				sourceCache.set(filename, {code, map});
				vm.runInNewContext(code, ctxt, {filename});

			// if module has a default export (length === 3)
			} else {
				const tuples = bodies.map(ast => generate(ast, genOpts));
				const codes = tuples.map(({code}) => code);
				tuples.forEach((tuple, i) => {
					sourceCache.set(`${id}-${i}`, tuple);
				});
				vm.runInNewContext(codes[0], ctxt, {filename: `${id}+0`});
				defaultValue = vm.runInNewContext(
					codes[1], ctxt, {filename: `${id}+1`});
				if (defaultName) {
					defaultValue = vm.runInNewContext(
						defaultName, ctxt, {filename: `${id}+1`});
				}

				vm.runInNewContext(codes[2], ctxt, {filename: `${id}+2`});
				module.default = defaultValue;
			}

			module.lock();

			return module;
		}
	} else {
		return moduleCache.get(id);
	}
}

// runs a given ID, and throws error if ID represents module (it exports)
exports.runId = runId;
function runId(id, resolver) {
	const load = resolver.load(id, true);
	const ast = hoistFunctions(getAST(load));
	const {exports, imports, bodies, relays, defaultName}
		= processProgram(ast);
	const ctxt = (!!load.context) ? Object.assign({}, load.context) : {};

	if (exports.length > 0 || relays.length > 0 || bodies.length > 1) {
		throw new Error(`File "${id}" cannot be a module!`);
	} else {
		const {code, map} = generate(bodies[0], genOpts);
		sourceCache.set(`${id}+0`, {code, map});
		for (let [source, importees] of imports) {
			const importedId = resolver.resolveId(source, id);
			const importedModule = getModuleFromId(importedId, resolver);

			for (let {local, imported} of importees) {
				if (imported === '*') {
					importedModule.addNamespaceBinding(ctxt, local);
					continue;
				}

				if (imported === 'default') {
					importedModule.addDefaultBinding(ctxt, local);
					continue;
				}

				// if not default or namespace ...
				importedModule.addBinding(ctxt, imported, local);
			}
		}

		return vm.runInNewContext(code, ctxt, {filename: `${id}+0`});
	}
}

exports.sourceCache = sourceCache;

asyncPlugin(acorn);


'use strict';

const request = require('request-promise');
const cheerio = require('cheerio');
const translations = require('./translations.json');

const APP_BASE_URL = 'http://aplikace.policie.cz/patrani-vozidla/';

/** The police website provides dates in czech human readable form,
    like 7.ledna 2015 (ledna == january).
    Let's transform them to D.M.YYYY format.
*/
const getStandardizedDateStr = function(date) {
  return date
    .replace('ledna', '1.')
    .replace('února', '2.')
    .replace('března', '3.')
    .replace('dubna', '4.')
    .replace('května', '5.')
    .replace('června', '6.')
    .replace('července', '7.')
    .replace('srpna', '8.')
    .replace('září', '9.')
    .replace('října', '10.')
    .replace('listopadu', '11.')
    .replace('prosince', '12.')
    .replace(/\s/g, '');
};

/** create search url on the police website.
    It contains all the MS ASP trash (__EVENTTARGET, __VIEWSTATE, ...) .
    The only two interesting params are txtSPZ and txtVIN. We set both of them
    to the search query, ignoring, if the query itself is VIN or reg.no.
*/
function constructSearchUrl(query) {
  return APP_BASE_URL + 'default.aspx?__EVENTTARGET=&__EVENTARGUMENT=&__VIEWSTATE=%2FwEPDwULLTEzNzIzMjY0MDMPZBYCZg9kFgICBw9kFgICAQ9kFgoCBA8PZBYCHgpvbmtleXByZXNzBTZyZXR1cm4gb25LZXlwcmVzcyhldmVudCwnY3RsMDBfQXBwbGljYXRpb25fY21kSGxlZGVqJylkAgYPD2QWAh8ABTZyZXR1cm4gb25LZXlwcmVzcyhldmVudCwnY3RsMDBfQXBwbGljYXRpb25fY21kSGxlZGVqJylkAhQPDxYEHgRUZXh0BRxDZWxrb3bDvSBwb8SNZXQgesOhem5hbcWvOiAxHgdWaXNpYmxlZ2RkAhUPDxYCHwJoZGQCGQ8PFgIfAQU%2FRGF0YWLDoXplIGJ5bGEgbmFwb3NsZWR5IGFrdHVhbGl6b3bDoW5hIDxiPjYuIHByb3NpbmNlIDIwMTI8L2I%2BZGRkE2qlXWNJcxoc8%2FLZOQEi5oKrGzs%3D&__EVENTVALIDATION=%2FwEWBQL80qOCBwLQsb3%2BBgK9peeFDwLv%2BPyjBAL4oIjjDVvi8FJppOBh8gjuF1u%2Ft7viEDtA&ctl00%24Application%24txtSPZ='
        + query + '&ctl00%24Application%24txtVIN=' + query
        + '&ctl00%24Application%24cmdHledej=Vyhledat&ctl00%24Application%24CurrentPage=1';
}

/** Parse vehicle ID from the search results table */
function parseVehicleID($, item) {
  let fields = $(item).find('td');
  let id = $(fields[1]).find('a').attr('href').replace('Detail.aspx?id=', '');
  return id;
}

/** Add current timestamp and results count to the output object */
function formatDetails(details) {
  let result = {
    results: details,
    count: details.length,
    time: new Date().toISOString()
  };
  return result;
}

/** Read all possible information about desired vehicle. Loads vehicle details page */
function getByID(id) {
  var url = `${APP_BASE_URL}Detail.aspx?id=${id}`;
  return request(url)
    .then(function(body) {
      let $ = cheerio.load(body);

      let info = {
        url: url,
        id: id
      };

      $('table#searchTableResults tr').each((i, item) => {
        let span = $(item).find('span');
        let key = $(span).attr('id').replace('ctl00_Application_lbl', '').toLowerCase();
        let value = $(span).text().trim();
        info[translations[key] || key] = value;
      });
      info.stolendate = getStandardizedDateStr(info.stolendate);
      return info;
    });
}

/** The main and only method of the client, search. It takes searchQuery,
    either VIN or registration number. Returns object in the following form:
    {"results":[...],"count":1,"time":"2016-03-03T07:10:55.213Z"}
*/
const search = function(searchQuery) {
  return request(constructSearchUrl(searchQuery))
    .then(function(body){
      let $ = cheerio.load(body);
      let rows = $('table#celacr tr');
      let promises = rows
      .filter(idx => idx >= 1) // skip header row (index=0)
      .map((idx, el) => parseVehicleID($, el))
      .get()  // convert cheerio object to plain array
      .map(getByID);
      return Promise.all(promises);
    })
    .then(formatDetails);
};

module.exports = {
  search: search
};


/**
 * @private
 */
Ext.define('Ext.device.purchases.Sencha', {
    requires: [
        'Ext.data.Store'
    ],

    /**
     * Checks if the current user is able to make payments.
     *
     * ## Example
     *
     *     Ext.device.Purchases.canMakePayments({
     *         callback: function(flag) {
     *             console.log(flag ? 'Yup! :)' : 'Nope! :(');
     *         }
     *     });
     *
     * @param {Object} config
     * @param {Function} config.callback
     * @param {Boolean} config.callback.flag whether current user is able to make payments.
     * @param {Object} config.scope
     */
    canMakePayments: function(config) {
        if (!config.callback) {
            Ext.Logger.error('You must specify a `callback` for `#canMakePayments` to work.');
            return false;
        }

        Ext.device.Communicator.send({
            command: 'Purchase#canMakePayments',
            callbacks: {
                callback: function(flag) {
                    config.callback.call(config.scope || this, flag);
                }
            },
            scope: config.scope || this
        });
    },

    /**
     * Returns a {@link Ext.data.Store} instance of all products available to purchase.
     *
     * ## Example
     *
     *     Ext.device.Purchases.getProducts({
     *         success: function(store) {
     *             console.log('Got the store! You have ' + store.getCount() + ' products.');
     *         },
     *         failure: function() {
     *             console.log('Oops. Looks like something went wrong.');
     *         }
     *     });
     *
     * @param {Object} config
     * @param {Object[]} config.productInfos An array of all products productInfos
     * @param {Function} config.success
     * @param {Ext.data.Store} config.success.store A store of all products available to purchase.
     * @param {Function} config.failure
     * @param {Object} config.scope
     */
    getProducts: function(config) {
        if (!config.success) {
            Ext.Logger.error('You must specify a `success` callback for `#getProducts` to work.');
            return false;
        }

        if (!config.failure) {
            Ext.Logger.error('You must specify a `failure` callback for `#getProducts` to work.');
            return false;
        }

        Ext.device.Communicator.send({
            command: 'Purchase#getProducts',
            productInfos: JSON.stringify(config.productInfos),
            callbacks: {
                success: function(products) {
                    var store = Ext.create('Ext.data.Store', {
                        model: 'Ext.device.Purchases.Product',
                        data: products
                    });

                    config.success.call(config.scope || this, store);
                },
                failure: config.failure
            },
            scope: config.scope || this
        });
    },

    /**
     * Returns a {@link Ext.data.Store} instance of all purchases delivered to the current user.
     *
     * @param {Object} config
     * @param {Function} config.callback
     * @param {Ext.data.Store} config.callback.store A store of all purchases delivered to the current user.
     * @param {Object} config.scope
     */
    getCompletedPurchases: function(config) {
        if (!config.callback) {
            Ext.Logger.error('You must specify a `callback` for `#getCompletedPurchases` to work.');
            return false;
        }

        Ext.device.Communicator.send({
            command: 'Purchase#getCompletedPurchases',
            callbacks: {
                callback: function(purchases) {
                    var ln = purchases.length,
                        i;

                    for (i = 0; i < ln; i++) {
                        purchases[i].state = 'completed';
                    }

                    var store = Ext.create('Ext.data.Store', {
                        model: 'Ext.device.purchases.Purchase',
                        data: purchases
                    });

                    config.callback.call(config.scope || this, store);
                }
            },
            scope: config.scope || this
        });
    },

    /**
     * Returns a {@link Ext.data.Store} instance of all purchases the current user has been charged.
     *
     * @param {Object} config
     * @param {Function} config.callback
     * @param {Ext.data.Store} config.callback.store  A store of all purchases the current user has been charged.
     * @param {Object} config.scope
     */
    getPurchases: function(config) {
        if (!config.callback) {
            Ext.Logger.error('You must specify a `callback` for `#getPurchases` to work.');
            return false;
        }

        Ext.device.Communicator.send({
            command: 'Purchase#getPurchases',
            callbacks: {
                callback: function(purchases) {
                    var ln = purchases.length,
                        i;

                    for (i = 0; i < ln; i++) {
                        purchases[i].state = 'charged';
                    }

                    var store = Ext.create('Ext.data.Store', {
                        model: 'Ext.device.purchases.Purchase',
                        data: purchases
                    });

                    config.callback.call(config.scope || this, store);
                }
            },
            scope: config.scope || this
        });
    }
}, function() {
    /**
     * The product model class which is used when fetching available products using {@link Ext.device.Purchases#getProducts}.
     */
    Ext.define('Ext.device.Purchases.Product', {
        extend: 'Ext.data.Model',

        config: {
            fields: [
                'productIdentifier',
                'localizedTitle',
                'price',
                'localizedDescription'
            ]
        },

        /**
         * Will attempt to purchase this product.
         *
         * ## Example
         *
         *     product.purchase({
         *         success: function() {
         *             console.log(product.get('localizedTitle') + ' purchased!');
         *         },
         *         failure: function() {
         *             console.log('Something went wrong while trying to purchase ' + product.get('localizedTitle'));
         *         }
         *     });
         *
         * @param {Object} config
         * @param {Ext.data.Model/String} config.product
         * @param {Function} config.success
         * @param {Function} config.failure
         */
        purchase: function(config) {
            if (!config.success) {
                Ext.Logger.error('You must specify a `success` callback for `#purchase` to work.');
                return false;
            }

            if (!config.failure) {
                Ext.Logger.error('You must specify a `failure` callback for `#purchase` to work.');
                return false;
            }

            Ext.device.Communicator.send({
                command: 'Purchase#purchase',
                identifier: this.get('productIdentifier'),
                callbacks: {
                    success: config.success,
                    failure: config.failure
                },
                scope: config.scope || this
            });
        }
    });

    /**
     *
     */
    Ext.define('Ext.device.purchases.Purchase', {
        extend: 'Ext.data.Model',

        config: {
            fields: [
                'productIdentifier',
                'transactionIdentifier',
                'state'
            ]
        },

        /**
         * Attempts to mark this purchase as complete
         * @param {Object} config
         * @param {Function} config.success
         * @param {Function} config.failure
         * @param {Object} config.scope
         */
        complete: function(config) {
            var me = this;

            if (!config.success) {
                Ext.Logger.error('You must specify a `success` callback for `#complete` to work.');
                return false;
            }

            if (!config.failure) {
                Ext.Logger.error('You must specify a `failure` callback for `#complete` to work.');
                return false;
            }

            if (this.get('state') != 'charged') {
                config.failure.call(config.scope || this, 'purchase is not charged');
            }

            Ext.device.Communicator.send({
                command: 'Purchase#complete',
                identifier: me.get('transactionIdentifier'),
                callbacks: {
                    success: function() {
                        me.set('state', 'completed');
                        config.success.call(config.scope || this);
                    },
                    failure: function() {
                        me.set('state', 'charged');
                        config.failure.call(config.scope || this);
                    }
                },
                scope: config.scope || this
            });
        }
    });
});


/**
 * @author Ed Spencer
 *
 * In-memory proxy. This proxy simply uses a local variable for data storage/retrieval, so its contents are lost on
 * every page refresh.
 *
 * Usually this Proxy isn't used directly, serving instead as a helper to a {@link Ext.data.Store Store} where a reader
 * is required to load data. For example, say we have a Store for a User model and have some inline data we want to
 * load, but this data isn't in quite the right format: we can use a MemoryProxy with a JsonReader to read it into our
 * Store:
 *
 *     //this is the model we will be using in the store
 *     Ext.define('User', {
 *         extend: 'Ext.data.Model',
 *         config: {
 *             fields: [
 *                 {name: 'id',    type: 'int'},
 *                 {name: 'name',  type: 'string'},
 *                 {name: 'phone', type: 'string', mapping: 'phoneNumber'}
 *             ]
 *         }
 *     });
 *
 *     //this data does not line up to our model fields - the phone field is called phoneNumber
 *     var data = {
 *         users: [
 *             {
 *                 id: 1,
 *                 name: 'Ed Spencer',
 *                 phoneNumber: '555 1234'
 *             },
 *             {
 *                 id: 2,
 *                 name: 'Abe Elias',
 *                 phoneNumber: '666 1234'
 *             }
 *         ]
 *     };
 *
 *     //note how we set the 'root' in the reader to match the data structure above
 *     var store = Ext.create('Ext.data.Store', {
 *         autoLoad: true,
 *         model: 'User',
 *         data : data,
 *         proxy: {
 *             type: 'memory',
 *             reader: {
 *                 type: 'json',
 *                 rootProperty: 'users'
 *             }
 *         }
 *     });
 *
 * ###Further Reading
 * [Sencha Touch Data Overview](../../../core_concepts/data/data_package_overview.html)
 * [Sencha Touch Store Guide](../../../core_concepts/data/stores.html)
 * [Sencha Touch Models Guide](../../../core_concepts/data/models.html)
 * [Sencha Touch Proxy Guide](../../../core_concepts/data/proxies.html)
 */
Ext.define('Ext.data.proxy.Memory', {
    extend: 'Ext.data.proxy.Client',
    alias: 'proxy.memory',
    alternateClassName: 'Ext.data.MemoryProxy',

    isMemoryProxy: true,

    config: {
        /**
         * @cfg {Object} data
         * Optional data to pass to configured Reader.
         */
        data: []
    },

    /**
     * @private
     * Fake processing function to commit the records, set the current operation
     * to successful and call the callback if provided. This function is shared
     * by the create, update and destroy methods to perform the bare minimum
     * processing required for the proxy to register a result from the action.
     */
    finishOperation: function(operation, callback, scope) {
        if (operation) {
            var i = 0,
                recs = operation.getRecords(),
                len = recs.length;

            for (i; i < len; i++) {
                recs[i].commit();
            }

            operation.setCompleted();
            operation.setSuccessful();

            Ext.callback(callback, scope || this, [operation]);
        }
    },

    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
     * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
     * there is no real back end in this case there's not much else to do. This method can be easily overridden to
     * implement more complex logic if needed.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether
     * successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    create: function() {
        this.finishOperation.apply(this, arguments);
    },

    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
     * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
     * there is no real back end in this case there's not much else to do. This method can be easily overridden to
     * implement more complex logic if needed.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether
     * successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    update: function() {
        this.finishOperation.apply(this, arguments);
    },

    /**
     * Currently this is a hard-coded method that simply commits any records and sets the operation to successful,
     * then calls the callback function, if provided. It is essentially mocking a server call in memory, but since
     * there is no real back end in this case there's not much else to do. This method can be easily overridden to
     * implement more complex logic if needed.
     * @param {Ext.data.Operation} operation The Operation to perform
     * @param {Function} callback Callback function to be called when the Operation has completed (whether
     * successful or not)
     * @param {Object} scope Scope to execute the callback function in
     * @method
     */
    destroy: function() {
        this.finishOperation.apply(this, arguments);
    },

    /**
     * Reads data from the configured {@link #data} object. Uses the Proxy's {@link #reader}, if present.
     * @param {Ext.data.Operation} operation The read Operation
     * @param {Function} callback The callback to call when reading has completed
     * @param {Object} scope The scope to call the callback function in
     */
    read: function(operation, callback, scope) {
        var me     = this,
            reader = me.getReader();

        if (operation.process('read', reader.process(me.getData())) === false) {
            this.fireEvent('exception', this, null, operation);
        }

        Ext.callback(callback, scope || me, [operation]);
    },

    clear: Ext.emptyFn
});


'use strict';

// FUNCTIONS //

var exp = Math.exp,
	pow = Math.pow,
	sqrt = Math.sqrt;

// VARIABLES //

var PI = Math.PI;

// PDF //

/**
* FUNCTION: pdf( x, mu, sigma )
*	Evaluates the probability density function (PDF) for a Normal distribution with mean  `mu` and standard deviation `sigma` at a value `x`.
*
* @param {Number} x - input value
* @param {Number} mu - mean
* @param {Number} sigma - standard deviation
* @returns {Number} evaluated PDF
*/
function pdf( x, mu, sigma ) {
	if ( sigma === 0 ) {
		return x === mu ? Number.POSITIVE_INFINITY : 0;
	}
	var s2 = pow( sigma, 2 ),
		A = 1 / ( sqrt( 2 * s2 * PI ) ),
		B = -1 / ( 2 * s2 );
	return A * exp( B * pow( x - mu, 2 ) );
} // end FUNCTION pdf()

// EXPORTS //

module.exports = pdf;


'use strict';

/**
 * 整个项目的配置及初始设置
 * @author Architect.bian
 */

const path = require('path');
const fs = require('fs');
global.project = require('./../resources/config/es6mvc');
require('./modules/log');
var log = Log(__filename);

/*自动require所有的util到global下
 global.FileUtil = require('./utils/FileUtil');*/
function requireAllUtils(dir) {
    if (!fs.existsSync(dir)) {
        return;
    }
    log.verbose(`......begin to require All Utils:`);
    var files = fs.readdirSync(dir).filter((file) => ((file.endsWith("Util.js"))));
    for (var file of files) {
        var fileName = file.substring(0, file.lastIndexOf(".js"));
        global[fileName] = require(path.resolve(dir, file));
        log.verbose(`require util: ${file}`);
    }
}
requireAllUtils(path.resolve(__dirname, './utils'));
requireAllUtils(path.resolve(project.path.main, './utils'));

/*自动require所有的enum */
function requireAllEnum(dir) {
    if (!fs.existsSync(dir)) {
        return;
    }
    log.verbose(`......begin to require All Enums:`);
    var files = fs.readdirSync(dir).filter((file) => ((file.startsWith("E") && file.endsWith(".js"))));
    for (var file of files) {
        var fileName = file.substring(0, file.lastIndexOf(".js"));
        global[fileName] = require(path.resolve(dir, file));
        log.verbose(`require enum: ${file}`);
    }
};
requireAllEnum(path.resolve(__dirname, './enums'));
requireAllEnum(path.resolve(project.path.main, './enums'));

/*自动require interceptors */
function requireAllInterceptor(dir) {
    if (!fs.existsSync(dir)) {
        return;
    }
    log.verbose(`......begin to require Interceptor:`);
    global['BaseInterceptorHandler'] = require(path.resolve(dir, 'BaseInterceptorHandler'));
    log.verbose(`require interceptor: BaseInterceptorHandler`);
};
requireAllInterceptor(path.resolve(__dirname, './interceptors'));

/*自动require models */
function requireAllModels(dir) {
    if (!fs.existsSync(dir)) {
        return;
    }
    log.verbose(`......begin to require models:`);
    global['BaseSchema'] = require(path.resolve(dir, 'BaseSchema'));
    log.verbose(`require model: BaseSchema`);
};
requireAllModels(path.resolve(__dirname, './models'));

/*自动require所有的dao */
function requireAllDao(dir) {
    if (!fs.existsSync(dir)) {
        return;
    }
    log.verbose(`......begin to require All Dao:`);
    var files = fs.readdirSync(dir).filter((file) => ((file.endsWith("Dao.js"))));
    for (var file of files) {
        var fileName = file.substring(0, file.lastIndexOf(".js"));
        global[fileName] = require(path.resolve(dir, file));
        log.verbose(`require dao: ${file}`);
    }
};
requireAllDao(path.resolve(project.path.main, './dao'));


var atob = require("atob");

/**
 * Simple, fast parsing of OpenSSH RSA and DSA keys to programmatically access key type, length, and and multiple key formats
 *
 * @param  {String} key
 * @module OpenSSLKey
 */
var OpenSSLKey = function (key) {
  this.key = key;
  this.keyType = key.split(" ")[0];
  this.rawkey = key.split(" ")[1];
  try{
    this.keyComment = key.split(" ")[2];
  } catch(err){
    this.keyComment = null;
  }

  this.byteArray = this._stringToBytes(atob(this.rawkey));
  this.slicedArray = [];

  this.wordLength = 4;
  this._load();
}

/**
 * Load key data
 *
 * @private
 * @module OpenSSLKey
 */
OpenSSLKey.prototype._load = function () {
  var loadingArray = this.byteArray,
    length, capture;

  while (loadingArray.length > 0) {
    length = loadingArray.slice(0, this.wordLength);
    capture = loadingArray.slice(this.wordLength, this._byteArrayToLong(length) + this.wordLength);
    this.slicedArray.push(capture);

    loadingArray = loadingArray.slice(this._byteArrayToLong(length) + this.wordLength);
  }

  if (this.keyType === "ssh-rsa") {
    this.modulusLength = this.getSlicedByteArray()[2].length * 8 - 8;
  } else if (this.keyType === "ssh-dss" || this.keyType === "ssh-dsa") {
    this.modulusLength = this.getSlicedByteArray()[1].length * 8 - 8;
  } else{
    this.modulusLength = null;
  }
};

/**
 * Return the original input as a string
 *
 * @return {String}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getKey = function () {
  return this.key;
};

/**
 * Return the key type as a string
 *
 * @return {String}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getKeyType = function () {
  return this.keyType;
};

/**
 * Return the key data as a string
 *
 * @return {String}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getData = function () {
  return this.rawkey;
};

/**
 * Return the key (modulus) length as a number
 *
 * @return {Number}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getKeyLength = function () {
  return this.modulusLength;
};

/**
 * Return the key's comment, if it has one
 *
 * @return {String|Null}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getComment = function () {
  return this.keyComment;
};

/**
 * Return the key as an array of bytes
 *
 * @return {Number|Array}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getByteArray = function () {
  return this.byteArray;
};

/**
 * Return the sliced key as an array of arrays of bytes, broken on data boundaries
 *
 * @return {Number|Array|Array}
 * @module OpenSSLKey
 */
OpenSSLKey.prototype.getSlicedByteArray = function () {
  return this.slicedArray;
};

/**
 * Returns an array of bytes that represet a string
 *
 * @return {Number|Array}
 * @param  {String} str
 * @module OpenSSLKey
 * @private
 */
OpenSSLKey.prototype._stringToBytes = function (str) {
  var character,
      stack,
      byteArray = [],
      i;
  for (i = 0; i < str.length; i++) {
    character = str.charCodeAt(i);
    stack = [];
    do {
      stack.push(character & 0xFF);
      character >>= 8;
    }
    while (character);
    byteArray = byteArray.concat(stack.reverse());
  }
  return byteArray;
};

/**
 * Returns a string representation of a byte array
 *
 * @return {String}
 * @param  {Number|Array} array
 * @module OpenSSLKey
 * @private
 */
OpenSSLKey.prototype._bytesToString = function (array) {
  var result = "",
      i;
  for (i = 0; i < array.length; i++) {
    result += String.fromCharCode(parseInt(array[i]));
  }
  return result;
};

/**
 * Returns an array of bytes that represet a string
 *
 * @return {Number}
 * @param  {Number|Array} byteArray
 * @module OpenSSLKey
 * @private
 */
OpenSSLKey.prototype._byteArrayToLong = function (byteArray) {
  var value = 0,
      i;
  for (i = 0; i < byteArray.length; i++) {
    value = (value * 256) + byteArray[i];
  }
  return value;
};

module.exports = OpenSSLKey;