/**
 * @author Robert Dougan <rob@sencha.com>
 *
 * NavigationView is basically a {@link Ext.Container} with a {@link Ext.layout.Card card} layout, so only one view
 * can be visible at a time. However, NavigationView also adds extra functionality on top of this to allow
 * you to `push` and `pop` views at any time. When you do this, your NavigationView will automatically animate
 * between your current active view, and the new view you want to `push`, or the previous view you want to `pop`.
 *
 * Using the NavigationView is very simple. Here is a basic example of it in action:
 *
 *     @example
 *     var view = Ext.create('Ext.NavigationView', {
 *         fullscreen: true,
 *
 *         items: [{
 *             title: 'First',
 *             items: [{
 *                 xtype: 'button',
 *                 text: 'Push a new view!',
 *                 handler: function() {
 *                     // use the push() method to push another view. It works much like
 *                     // add() or setActiveItem(). it accepts a view instance, or you can give it
 *                     // a view config.
 *                     view.push({
 *                         title: 'Second',
 *                         html: 'Second view!'
 *                     });
 *                 }
 *             }]
 *         }]
 *     });
 *
 * Now, here comes the fun part: you can push any view/item into the NavigationView, at any time, and it will
 * automatically handle the animations between the two views, including adding a back button (if necessary)
 * and showing the new title.
 *
 *     view.push({
 *         title: 'A new view',
 *         html: 'Some new content'
 *     });
 *
 * As you can see, it is as simple as calling the {@link #method-push} method, with a new view (instance or object). Done.
 *
 * You can also `pop` a view at any time. This will remove the top-most view from the NavigationView, and animate back
 * to the previous view. You can do this using the {@link #method-pop} method (which requires no arguments).
 *
 *     view.pop();
 *
 *  Applications that need compatibility with ##Older Android## devices will want to see the {@link #layout} config for details on
 *  disabling navigation view animations as these devices have poor animation support and performance.
 *
 * ###Further Reading
 * [Sencha Touch Navigation View Guide](../../../components/navigation_view.html)
 */
Ext.define('Ext.navigation.View', {
    extend: 'Ext.Container',
    alternateClassName: 'Ext.NavigationView',
    xtype: 'navigationview',
    requires: ['Ext.navigation.Bar'],

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

        /**
         * @cfg {Boolean/Object} navigationBar
         * The NavigationBar used in this navigation view. It defaults to be docked to the top.
         *
         * You can just pass in a normal object if you want to customize the NavigationBar. For example:
         *
         *     navigationBar: {
         *         ui: 'dark',
         *         docked: 'bottom'
         *     }
         *
         * You **cannot** specify a *title* property in this configuration. The title of the navigationBar is taken
         * from the configuration of this views children:
         *
         *     view.push({
         *         title: 'This views title which will be shown in the navigation bar',
         *         html: 'Some HTML'
         *     });
         *
         * @accessor
         */
        navigationBar: {
            docked: 'top'
        },

        /**
         * @cfg {String} defaultBackButtonText
         * The text to be displayed on the back button if:
         *
         * - The previous view does not have a title.
         * - The {@link #useTitleForBackButtonText} configuration is `true`.
         * @accessor
         */
        defaultBackButtonText: 'Back',

        /**
         * @cfg {Boolean} useTitleForBackButtonText
         * Set to `false` if you always want to display the {@link #defaultBackButtonText} as the text
         * on the back button. `true` if you want to use the previous views title.
         * @accessor
         */
        useTitleForBackButtonText: false,

        /**
         * @cfg {Array/Object} items The child items to add to this NavigationView. This is usually an array of Component
         * configurations or instances, for example:
         *
         *     Ext.create('Ext.Container', {
         *         items: [
         *             {
         *                 xtype: 'panel',
         *                 title: 'My title',
         *                 html: 'This is an item'
         *             }
         *         ]
         *     });
         *
         * If you want a title to be displayed in the {@link #navigationBar}, you must specify a `title` configuration in your
         * view, like above.
         *
         * __Note:__ Only one view will be visible at a time. If you want to change to another view, use the {@link #method-push} or
         * {@link #setActiveItem} methods.
         * @accessor
         */

        /**
         * @cfg {Object}
         * Layout used in this navigation view, type must be set to 'card'.
         * **Android NOTE:** Older Android devices have poor animation performance. It is recommended to set the animation to null, for example:
         *
         *      layout: {
         *          type: 'card',
         *          animation: null
         *      }
         *
         * @accessor
         */
        layout: {
            type: 'card',
            animation: {
                duration: 300,
                easing: 'ease-out',
                type: 'slide',
                direction: 'left'
            }
        }
    },

    /**
     * @event push
     * Fires when a view is pushed into this navigation view
     * @param {Ext.navigation.View} this The component instance
     * @param {Mixed} view The view that has been pushed
     */

    /**
     * @event pop
     * Fires when a view is popped from this navigation view
     * @param {Ext.navigation.View} this The component instance
     * @param {Mixed} view The view that has been popped
     */

    /**
     * @event back
     * Fires when the back button in the navigation view was tapped.
     * @param {Ext.navigation.View} this The component instance\
     */

    platformConfig: [{
        theme: ['Blackberry', 'Blackberry103'],
        navigationBar: {
            splitNavigation: true
        }
    }],

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

        //add a listener onto the back button in the navigationbar
        if (navBar) {
            navBar.on({
                back: me.onBackButtonTap,
                scope: me
            });

            me.relayEvents(navBar, 'rightbuttontap');

            me.relayEvents(me, {
                add: 'push',
                remove: 'pop'
            });
        }

        //<debug>
        var layout = me.getLayout();
        if (layout && !layout.isCard) {
            Ext.Logger.error('The base layout for a NavigationView must always be a Card Layout');
        }
        //</debug>
    },

    /**
     * @private
     */
    applyLayout: function(config) {
        config = config || {};

        return config;
    },

    /**
     * @private
     * Called when the user taps on the back button
     */
    onBackButtonTap: function() {
        this.pop();
        this.fireEvent('back', this);
    },

    /**
     * Pushes a new view into this navigation view using the default animation that this view has.
     * @param {Object} view The view to push.
     * @return {Ext.Component} The new item you just pushed.
     */
    push: function(view) {
        return this.add(view);
    },

    /**
     * Removes the current active view from the stack and sets the previous view using the default animation
     * of this view. You can also pass a {@link Ext.ComponentQuery} selector to target what inner item to pop to.
     * @param {Number/String/Object} count If a Number, the number of views you want to pop. If a String, the pops to a matching
     * component query. If an Object, the pops to a matching view instance.
     * @return {Ext.Component} The new active item
     */
    pop: function(count) {
        if (this.beforePop(count)) {
            return this.doPop();
        }
    },

    /**
     * @private
     * Calculates whether it needs to remove any items from the stack when you are popping more than 1
     * item. If it does, it removes those views from the stack and returns `true`.
     * @return {Boolean} `true` if it has removed views.
     */
    beforePop: function(count) {
        var me = this,
            innerItems = me.getInnerItems();

        if (Ext.isString(count) || Ext.isObject(count)) {
            var last = innerItems.length - 1,
                i;

            for (i = last; i >= 0; i--) {
                if ((Ext.isString(count) && Ext.ComponentQuery.is(innerItems[i], count)) || (Ext.isObject(count) && count == innerItems[i])) {
                    count = last - i;
                    break;
                }
            }

            if (!Ext.isNumber(count)) {
                return false;
            }
        }

        var ln = innerItems.length,
            toRemove;

        //default to 1 pop
        if (!Ext.isNumber(count) || count < 1) {
            count = 1;
        }

        //check if we are trying to remove more items than we have
        count = Math.min(count, ln - 1);

        if (count) {
            //we need to reset the backButtonStack in the navigation bar
            me.getNavigationBar().beforePop(count);

            //get the items we need to remove from the view and remove theme
            toRemove = innerItems.splice(-count, count - 1);
            for (i = 0; i < toRemove.length; i++) {
                this.remove(toRemove[i]);
            }

            return true;
        }

        return false;
    },

    /**
     * @private
     */
    doPop: function() {
        var me = this,
            innerItems = this.getInnerItems();

        //set the new active item to be the new last item of the stack
        me.remove(innerItems[innerItems.length - 1]);

        // Hide the backButton
        if (innerItems.length < 3 && this.$backButton) {
            this.$backButton.hide();
        }

        // Update the title container
        if (this.$titleContainer) {
            //<debug>
            if (!this.$titleContainer.setTitle) {
                Ext.Logger.error('You have selected to display a title in a component that does not \
                    support titles in NavigationView. Please remove the `title` configuration from your \
                    NavigationView item, or change it to a component that has a `setTitle` method.');
            }
            //</debug>

            var item = innerItems[innerItems.length - 2];
            this.$titleContainer.setTitle((item.getTitle) ? item.getTitle() : item.config.title);
        }

        return this.getActiveItem();
    },

    /**
     * Returns the previous item, if one exists.
     * @return {Mixed} The previous view
     */
    getPreviousItem: function() {
        var innerItems = this.getInnerItems();
        return innerItems[innerItems.length - 2];
    },

    /**
     * Updates the backbutton text accordingly in the {@link #navigationBar}
     * @private
     */
    updateUseTitleForBackButtonText: function(useTitleForBackButtonText) {
        var navigationBar = this.getNavigationBar();
        if (navigationBar) {
            navigationBar.setUseTitleForBackButtonText(useTitleForBackButtonText);
        }
    },

    /**
     * Updates the backbutton text accordingly in the {@link #navigationBar}
     * @private
     */
    updateDefaultBackButtonText: function(defaultBackButtonText) {
        var navigationBar = this.getNavigationBar();
        if (navigationBar) {
            navigationBar.setDefaultBackButtonText(defaultBackButtonText);
        }
    },

    /**
     * This is called when an Item is added to the BackButtonContainer of a SplitNavigation View
     * @private
     *
     * @param toolbar
     * @param item
     */
    onBackButtonContainerAdd: function(toolbar, item) {
        item.on({
            scope: this,
            show: this.refreshBackButtonContainer,
            hide: this.refreshBackButtonContainer
        });
        this.refreshBackButtonContainer();
    },

    /**
     * This is called when an Item is removed from the BackButtonContainer of a SplitNavigation View
     * @private
     *
     * @param toolbar
     * @param item
     */
    onBackButtonContainerRemove: function(toolbar, item) {
        item.un({
            scope: this,
            show: this.refreshBackButtonContainer,
            hide: this.refreshBackButtonContainer
        });
        this.refreshBackButtonContainer();
    },

    /**
     * This is used for Blackberry SplitNavigation to monitor the state of child items in the bottom toolbar.
     * if no visible children exist the toolbar will be hidden
     * @private
     */
    refreshBackButtonContainer: function() {
        if (!this.$backButtonContainer) {
            return;
        }
        var i = 0,
            backButtonContainer = this.$backButtonContainer,
            items = backButtonContainer.items,
            item;

        for(;i < items.length; i++) {
            item = items.get(i);
            if(!item.isHidden()) {
                this.$backButtonContainer.show();
                return;
            }
        }

        this.$backButtonContainer.hide();
    },

    // @private
    applyNavigationBar: function(config) {
        var me = this;
        if (!config) {
            config = {
                hidden: true,
                docked: 'top'
            };
        }

        if (config.title) {
            delete config.title;
            //<debug>
            Ext.Logger.warn("Ext.navigation.View: The 'navigationBar' configuration does not accept a 'title' property. You " +
                "set the title of the navigationBar by giving this navigation view's children a 'title' property.");
            //</debug>
        }

        config.view = this;
        config.useTitleForBackButtonText = this.getUseTitleForBackButtonText();

        // Blackberry specific nav setup where title is on the top title bar and the bottom toolbar is used for buttons and BACK
        if (config.splitNavigation) {
            this.$titleContainer = this.add({
                docked: 'top',
                xtype: 'titlebar',
                ui: 'light',
                title: this.$currentTitle || ''
            });

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

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

            // Any item that is added to the BackButtonContainer should be monitored for visibility
            // this will allow the toolbar to be hidden when no items exist in it.
            this.$backButtonContainer.on ({
                scope: me,
                add: me.onBackButtonContainerAdd,
                remove: me.onBackButtonContainerRemove
            });

            this.$backButton = this.$backButtonContainer.add({
                xtype: 'button',
                text: 'Back',
                hidden: true,
                ui: 'back'
            });

            // Default config items go into the bottom bar
            if(config.items) {
                this.$backButtonContainer.add(config.items);
            }

            // If the user provided items and splitNav items, default items go into the bottom bar, split nav items go into the top
            if(containerConfig.items) {
                this.$titleContainer.add(containerConfig.items);
            }

            this.$backButton.on({
                scope: this,
                tap: this.onBackButtonTap
            });

            config = {
                hidden: true,
                docked: 'top'
            };
        }

        return Ext.factory(config, Ext.navigation.Bar, this.getNavigationBar());
    },

    // @private
    updateNavigationBar: function(newNavigationBar, oldNavigationBar) {
        if (oldNavigationBar) {
            this.remove(oldNavigationBar, true);
        }

        if (newNavigationBar) {
            this.add(newNavigationBar);
        }
    },

    /**
     * @private
     */
    applyActiveItem: function(activeItem, currentActiveItem) {
        var me = this,
            innerItems = me.getInnerItems();

        // Make sure the items are already initialized
        me.getItems();

        // If we are not initialzed yet, we should set the active item to the last item in the stack
        if (!me.initialized) {
            activeItem = innerItems.length - 1;
        }

        return this.callParent([activeItem, currentActiveItem]);
    },

    doResetActiveItem: function(innerIndex) {
        var me = this,
            innerItems = me.getInnerItems(),
            animation = me.getLayout().getAnimation();

        if (innerIndex > 0) {
            if (animation && animation.isAnimation) {
                animation.setReverse(true);
            }
            me.setActiveItem(innerIndex - 1);
            me.getNavigationBar().onViewRemove(me, innerItems[innerIndex], innerIndex);
        }
    },

    /**
     * @private
     */
    doRemove: function() {
        var animation = this.getLayout().getAnimation();

        if (animation && animation.isAnimation) {
            animation.setReverse(false);
        }

        this.callParent(arguments);
    },

    /**
     * @private
     */
    onItemAdd: function(item, index) {

        // Check for title configuration
        if (item && item.getDocked() && item.config.title === true) {
            this.$titleContainer = item;
        }

        this.doItemLayoutAdd(item, index);

        var navigaitonBar = this.getInitialConfig().navigationBar;

        if (!this.isItemsInitializing && item.isInnerItem()) {
            this.setActiveItem(item);

            // Update the navigationBar
            if (navigaitonBar) {
                this.getNavigationBar().onViewAdd(this, item, index);
            }

            // Update the custom backButton
            if (this.$backButtonContainer) {
                this.$backButton.show();
            }
        }

        if (item && item.isInnerItem()) {
            // Update the title container title
            this.updateTitleContainerTitle((item.getTitle) ? item.getTitle() : item.config.title);
        }

        if (this.initialized) {
            this.fireEvent('add', this, item, index);
        }
    },

    /**
     * @private
     * Updates the title of the titleContainer, if it exists
     */
    updateTitleContainerTitle: function(title) {
        if (this.$titleContainer) {
            //<debug>
            if (!this.$titleContainer.setTitle) {
                Ext.Logger.error('You have selected to display a title in a component that does not \
                    support titles in NavigationView. Please remove the `title` configuration from your \
                    NavigationView item, or change it to a component that has a `setTitle` method.');
            }
            //</debug>

            this.$titleContainer.setTitle(title);
        }
        else {
            this.$currentTitle = title;
        }
    },

    /**
     * Resets the view by removing all items between the first and last item.
     * @return {Ext.Component} The view that is now active
     */
    reset: function() {
        return this.pop(this.getInnerItems().length);
    }
});


//<feature logger>
/**
 * @class Ext.Logger
 * Logs messages to help with debugging.
 *
 * ## Example
 *
 *     Ext.Logger.deprecate('This method is no longer supported.');
 *
 * @singleton
 */
(function() {
var Logger = Ext.define('Ext.log.Logger', {

    extend: 'Ext.log.Base',

    statics: {
        defaultPriority: 'info',

        priorities: {
            /**
             * @method verbose
             * Convenience method for {@link #log} with priority 'verbose'.
             */
            verbose:    0,
            /**
             * @method info
             * Convenience method for {@link #log} with priority 'info'.
             */
            info:       1,
            /**
             * @method deprecate
             * Convenience method for {@link #log} with priority 'deprecate'.
             */
            deprecate:  2,
            /**
             * @method warn
             * Convenience method for {@link #log} with priority 'warn'.
             */
            warn:       3,
            /**
             * @method error
             * Convenience method for {@link #log} with priority 'error'.
             */
            error:      4
        }
    },

    config: {
        enabled: true,
        minPriority: 'deprecate',
        writers: {}
    },

    /**
     * Logs a message to help with debugging.
     * @param {String} message  Message to log.
     * @param {Number} priority Priority of the log message.
     */
    log: function(message, priority, callerId) {
        if (!this.getEnabled()) {
            return this;
        }

        var statics = Logger,
            priorities = statics.priorities,
            priorityValue = priorities[priority],
            caller = this.log.caller,
            callerDisplayName = '',
            writers = this.getWriters(),
            event, i, originalCaller;

        if (!priority) {
            priority = 'info';
        }

        if (priorities[this.getMinPriority()] > priorityValue) {
            return this;
        }

        if (!callerId) {
            callerId = 1;
        }

        if (Ext.isArray(message)) {
            message = message.join(" ");
        }
        else {
            message = String(message);
        }

        if (typeof callerId == 'number') {
            i = callerId;

            do {
                i--;

                caller = caller.caller;

                if (!caller) {
                    break;
                }

                if (!originalCaller) {
                    originalCaller = caller.caller;
                }

                if (i <= 0 && caller.displayName) {
                    break;
                }
            }
            while (caller !== originalCaller);

            callerDisplayName = Ext.getDisplayName(caller);
        }
        else {
            caller = caller.caller;
            callerDisplayName = Ext.getDisplayName(callerId) + '#' + caller.$name;
        }

        event = {
            time: Ext.Date.now(),
            priority: priorityValue,
            priorityName: priority,
            message: message,
            caller: caller,
            callerDisplayName: callerDisplayName
        };

        for (i in writers) {
            if (writers.hasOwnProperty(i)) {
                writers[i].write(Ext.merge({}, event));
            }
        }

        return this;
    }

}, function() {
    Ext.Object.each(this.priorities, function(priority) {
        this.override(priority, function(message, callerId) {
            if (!callerId) {
                callerId = 1;
            }

            if (typeof callerId == 'number') {
                callerId += 1;
            }

            this.log(message, priority, callerId);
        });
    }, this);
});

})();
//</feature>


var fs   = require('fs'),
    _   = require('lodash'),
    path = require('path');

module.exports = {
  writeTemplateFileSync : writeTemplateFileSync,
  templateSync          : templateSync,
  copyFileSync          : copyFileSync,
  copyFilesMatchSync    : copyFilesMatchSync,
  replaceMatchFilesSync : replaceMatchFilesSync,
  unlinkFilesMatchSync  : unlinkFilesMatchSync,
  existsDirectorySync   : existsDirectorySync,
  existsFileSync        : existsFileSync,
  validateDirectorySync : validateDirectorySync,
  validateFileSync      : validateFileSync,
  subdirectoriesWithFile: subDirectoriesWithFile,
  maximisePath          : maximisePath,
  resolveDirMatches     : resolveDirMatches,
  reduceDirectories     : reduceDirectories
};

/**
 * Shortcut to read a template file, apply a context and write it to a new file.
 * //todo refactor template utilities from ../util.js to lib/template.js
 * @param source
 * @param context
 * @param destination
 */
function writeTemplateFileSync(source, context, destination) {
  var templateContent = fs.readFileSync(source, 'utf8');
  var templateResult;
  try {
    templateResult = _.template(templateContent, context);
  } catch(error) {
    console.error('templateFileSync() error with source', source, ' and ', destination);
    console.error(error);
  }
  fs.writeFileSync(destination, templateResult);
}

/**
 * Shortcut to apply a template context and return the result
 * //todo refactor template utilities from ../util.js to lib/template.js
 * @param content
 * @param context
 */
function templateSync(content, context) {
  var templateResult;
  try {
    templateResult = _.template(content, context);
  } catch(error) {
    console.error('templateSync() error with source', content, 'or content', context);
    console.error(error);
  }
  return templateResult;
}

/**
 * Shortcut to copy a file Synchronously
 * @param source
 * @param destination
 */
function copyFileSync(source, destination) {
  if (validateFileSync(source)) {
    fs.writeFileSync(destination, fs.readFileSync(source));
  }
}

/**
 * If there are any files in the source folder that are a match copy them to the destination.
 * @param match
 * @param source
 * @param destination
 */
function copyFilesMatchSync(match, source, destination) {
  fs.readdirSync(source).forEach(function eachTemplate(filename) {
    var sourceFile = path.join(source, filename);
    var destinationFile = path.join(destination, filename);
    if (match.test(path.basename(filename))) {
      fs.writeFileSync(destinationFile, fs.readFileSync(sourceFile));
    }
  });
}

/**
 * Copy any number files that are a match to a regex to a given destination.
 * If there are any files in the destination that are also a match replace them.
 * @param match
 * @param source
 * @param destination
 */
function replaceMatchFilesSync(match, source, destination) {
  unlinkFilesMatchSync(match, destination);
  copyFilesMatchSync(match, source, destination);
}

/**
 * If there are any files in the location folder that are a match remove them.
 * @param match
 * @param location
 */
function unlinkFilesMatchSync(match, location) {
  fs.readdirSync(location).forEach(function eachTemplate(filename) {
    if (match.test(path.basename(filename))) {
      var filePath = path.join(location, filename);
      if (validateFileSync(filePath)) {
        fs.unlinkSync(filePath);
      }
    }
  });
}

/**
 * Shortcut to validate the existence of a directory path.
 * @param location
 */
function existsDirectorySync(path) {
  return fs.existsSync(path) && fs.statSync(path).isDirectory();
}

/**
 * Shortcut to validate the existence of a file path.
 * @param location
 */
function existsFileSync(path) {
  return fs.existsSync(path) && fs.statSync(path).isFile();
}

/**
 * Validate if a given path has a valid directory, is it doesn't console.error with an optional custom message.
 * @param path
 * @param errorMessage
 * @returns {Boolean}
 */
function validateDirectorySync(path, errorMessage) {
  errorMessage = errorMessage || 'Error validateDirectorySync() the directory path is not valid ' + path;
  var isValid = existsDirectorySync(path);
  if (!isValid) {
    console.error(errorMessage);
  }
  return isValid;
}

/**
 * Validate if a given path has a valid file, is it doesn't console.error with an optional custom message.
 * @param path
 * @param errorMessage
 * @returns {Boolean}
 */
function validateFileSync(path, errorMessage) {
  errorMessage = errorMessage || 'Error validateFileSync() the file path is not valid ' + path;
  var isValid = existsFileSync(path);
  if (!isValid) {
    console.error(errorMessage);
  }
  return isValid;
}

/**
 * Find all subdirectories of the base, recursively.
 * @param base The base directory to start in
 * @param filename A filename that needs to be found for the path to be added
 * @return all subdirectories of the base, split by path separator
 */
function subDirectoriesWithFile(base, filename) {
  var result = [];
  if (fs.existsSync(base) && fs.statSync(base).isDirectory()) {
    if (fs.existsSync(path.join(base, filename))) {
      result.push(base);
    }
    fs.readdirSync(base)
      .forEach(function (subdir) {
        result.push.apply(result, subDirectoriesWithFile(path.join(base, subdir), filename));
      });
  }
  return result;
}

/**
 * Match the path defined by path elements, where some may be RegExp.
 * When there is more than one candidate, prefer the one with greatest interger value.
 * @param {...string|RegExp} elements Any number of path elements
 * @returns {string|boolean} A true concatenated path where found, else false
 */
function maximisePath() {
  // Ensure each element in the path exists,
  // where it is a regex, match it and replace the element with a string
  var elements = Array.prototype.slice.call(arguments);

  for (var i = 1; i < elements.length; i++) {
    // the directory is elements 0 .. i-1 joined
    var directory = path.resolve(path.join.apply(path, elements.slice(0, i)));

    // no directory implies failure
    if (!fs.existsSync(directory)) {
      return null;
    }
    // regex element is matched
    else if ((typeof elements[i] !== 'string') && ('test' in elements[i])) {

      var matches = resolveDirMatches(directory, elements[i]);

      // no match implies failure, else use the item with the highest numeric index
      if (matches.length === 0) {
        return null;
      } else {
        elements[i] = matches[0];
      }
    }
    // anything else is cast to string
    else {
      elements[i] = String(elements[i]);
    }
  }

  // now join them all together
  // do a final check to make sure it exists
  var result = path.resolve(elements.join(path.sep));
  return fs.existsSync(result) && result;
}

/**
 * For a given directory find all files that match, the result will be sorted with highest numeric index first.
 * @param directory
 * @param match
 * @returns {T[]}
 */
function resolveDirMatches(directory, match) {
  if (validateDirectorySync(directory)) {
    return fs.readdirSync(directory)
      .filter(function eachDirectoryItem(item) {
        var resolved = path.resolve(path.join(directory, item));
        return match.test(item) && fs.statSync(resolved).isDirectory();
      })
      .sort(compareHigher);
  } else {
    return [];
  }
}

/**
 * Rank a vs b based on any numeric component in their string.
 */
function compareHigher(a, b) {
  var numA = parseFloat(/[\d\.]+$/.exec(a)[0]);
  var numB = parseFloat(/[\d\.]+$/.exec(b)[0]);
  if (isNaN(numA) || (numB > numA)) {
    return +1;
  } else if (isNaN(numB) || (numA > numB)) {
    return -1;
  } else {
    return 0;
  }
}

/**
 * Pick the first existing directory from any of the given arguments.
 * @param {...string} candidates Any number of possible directories
 * @returns {string} The first valid directory or else undefined
 */
function reduceDirectories() {
  return Array.prototype.slice.call(arguments)
    .map(function (candidate) {
      return path.normalize(candidate);
    })
    .filter(function (candidate) {
      return fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();
    })
    .shift();
}


/**
 * @private
 * Base class for iOS and Android viewports.
 */
Ext.define('Ext.viewport.Default', {
    extend: 'Ext.Container',

    xtype: 'viewport',

    PORTRAIT: 'portrait',

    LANDSCAPE: 'landscape',

    requires: [
        'Ext.LoadMask',
        'Ext.layout.Card',
        'Ext.util.InputBlocker'
    ],

    /**
     * @event ready
     * Fires when the Viewport is in the DOM and ready.
     * @param {Ext.Viewport} this
     */

    /**
     * @event maximize
     * Fires when the Viewport is maximized.
     * @param {Ext.Viewport} this
     */

    /**
     * @event orientationchange
     * Fires when the Viewport orientation has changed.
     * @param {Ext.Viewport} this
     * @param {String} newOrientation The new orientation.
     * @param {Number} width The width of the Viewport.
     * @param {Number} height The height of the Viewport.
     */

    config: {
        /**
         * @private
         */
        autoMaximize: false,

        /**
         * @private
         */
        autoBlurInput: true,

        /**
         * @cfg {Boolean} preventPanning
         * Whether or not to always prevent default panning behavior of the
         * browser's viewport.
         * @accessor
         */
        preventPanning: true,

        /**
         * @cfg {Boolean} preventZooming
         * `true` to attempt to stop zooming when you double tap on the screen on mobile devices,
         * typically HTC devices with HTC Sense UI.
         * @accessor
         */
        preventZooming: false,

        /**
         * @cfg
         * @private
         */
        autoRender: true,

        /**
         * @cfg {Object/String} layout Configuration for this Container's layout. Example:
         *
         *     Ext.create('Ext.Container', {
         *         layout: {
         *             type: 'hbox',
         *             align: 'middle'
         *         },
         *         items: [
         *             {
         *                 xtype: 'panel',
         *                 flex: 1,
         *                 style: 'background-color: red;'
         *             },
         *             {
         *                 xtype: 'panel',
         *                 flex: 2,
         *                 style: 'background-color: green'
         *             }
         *         ]
         *     });
         *
         * See the [layouts guide](../../../core_concepts/layouts.html) for more information.
         *
         * @accessor
         */
        layout: 'card',

        /**
         * @cfg
         * @private
         */
        width: '100%',

        /**
         * @cfg
         * @private
         */
        height: '100%',

        useBodyElement: true,

        /**
         * An object of all the menus on this viewport.
         * @private
         */
        menus: {},

        /**
         * @private
         */
        orientation: null
    },

    getElementConfig: function() {
        var cfg = this.callParent(arguments);

        // Used in legacy browser that do not support matchMedia. Hidden element is used for checking of orientation
        if (!Ext.feature.has.MatchMedia) {
            cfg.children.unshift({reference: 'orientationElement', className: 'x-orientation-inspector'});
        }
        return cfg;
    },

    /**
     * @property {Boolean} isReady
     * `true` if the DOM is ready.
     */
    isReady: false,

    isViewport: true,

    isMaximizing: false,

    id: 'ext-viewport',

    isInputRegex: /^(input|textarea|select|a)$/i,

    isInteractiveWebComponentRegEx: /^(audio|video)$/i,

    focusedElement: null,

    /**
     * @private
     */
    fullscreenItemCls: Ext.baseCSSPrefix + 'fullscreen',

    constructor: function(config) {
        var bind = Ext.Function.bind;

        this.doPreventPanning = bind(this.doPreventPanning, this);
        this.doPreventZooming = bind(this.doPreventZooming, this);
        this.doBlurInput = bind(this.doBlurInput, this);

        this.maximizeOnEvents = [
          'ready',
          'orientationchange'
        ];

      // set default devicePixelRatio if it is not explicitly defined
        window.devicePixelRatio = window.devicePixelRatio || 1;

        this.callSuper([config]);

        this.windowWidth = this.getWindowWidth();
        this.windowHeight = this.getWindowHeight();
        this.windowOuterHeight = this.getWindowOuterHeight();

        if (!this.stretchHeights) {
            this.stretchHeights = {};
        }

        if(Ext.feature.has.OrientationChange) {
            this.addWindowListener('orientationchange', bind(this.onOrientationChange, this));
        } else {
            this.addWindowListener('resize', bind(this.onResize, this));
        }

        document.addEventListener('focus', bind(this.onElementFocus, this), true);
        document.addEventListener('blur', bind(this.onElementBlur, this), true);

        Ext.onDocumentReady(this.onDomReady, this);

        this.on('ready', this.onReady, this, {single: true});
        this.getEventDispatcher().addListener('component', '*', 'fullscreen', 'onItemFullscreenChange', this);
        return this;
    },

    onDomReady: function() {
        this.isReady = true;
        this.updateSize();
        this.fireEvent('ready', this);
    },

    onReady: function() {
        if (this.getAutoRender()) {
            this.render();
        }
        if (Ext.browser.name == 'ChromeiOS') {
            this.setHeight('-webkit-calc(100% - ' + ((window.outerHeight - window.innerHeight) / 2) + 'px)');
        }
    },

    onElementFocus: function(e) {
        this.focusedElement = e.target;
    },

    onElementBlur: function() {
        this.focusedElement = null;
    },

    render: function() {
        if (!this.rendered) {
            var body = Ext.getBody(),
                clsPrefix = Ext.baseCSSPrefix,
                classList = [],
                osEnv = Ext.os,
                osName = osEnv.name.toLowerCase(),
                browserName = Ext.browser.name.toLowerCase(),
                osMajorVersion = osEnv.version.getMajor();

            this.renderTo(body);

            classList.push(clsPrefix + osEnv.deviceType.toLowerCase());

            if (osEnv.is.iPad) {
                classList.push(clsPrefix + 'ipad');
            }

            classList.push(clsPrefix + osName);
            classList.push(clsPrefix + browserName);

            if (osMajorVersion) {
                classList.push(clsPrefix + osName + '-' + osMajorVersion);
            }

            if (osEnv.is.BlackBerry) {
                classList.push(clsPrefix + 'bb');
                if (Ext.browser.userAgent.match(/Kbd/gi)) {
                    classList.push(clsPrefix + 'bb-keyboard');
                }
            }

            if (Ext.browser.is.WebKit) {
                classList.push(clsPrefix + 'webkit');
            }

            if (Ext.browser.is.Standalone) {
                classList.push(clsPrefix + 'standalone');
            }

            if (Ext.browser.is.AndroidStock) {
                classList.push(clsPrefix + 'android-stock');
            }

            if (Ext.browser.is.GoogleGlass) {
                classList.push(clsPrefix + 'google-glass');
            }

            this.setOrientation(this.determineOrientation());
            classList.push(clsPrefix + this.getOrientation());
            body.addCls(classList);
        }
    },

    applyAutoBlurInput: function(autoBlurInput) {
        var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';

        if (autoBlurInput) {
            this.addWindowListener(touchstart, this.doBlurInput, false);
        }
        else {
            this.removeWindowListener(touchstart, this.doBlurInput, false);
        }

        return autoBlurInput;
    },

    applyAutoMaximize: function(autoMaximize) {
        if (Ext.browser.is.WebView) {
            autoMaximize = false;
        }
        if (autoMaximize) {
            this.on('ready', 'doAutoMaximizeOnReady', this, { single: true });
            this.on('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
        }
        else {
            this.un('ready', 'doAutoMaximizeOnReady', this);
            this.un('orientationchange', 'doAutoMaximizeOnOrientationChange', this);
        }

        return autoMaximize;
    },

    applyPreventPanning: function(preventPanning) {
        if (preventPanning) {
            this.addWindowListener('touchmove', this.doPreventPanning, false);
        }
        else {
            this.removeWindowListener('touchmove', this.doPreventPanning, false);
        }

        return preventPanning;
    },

    applyPreventZooming: function(preventZooming) {
        var touchstart = (Ext.feature.has.Touch) ? 'touchstart' : 'mousedown';

        if (preventZooming) {
            this.addWindowListener(touchstart, this.doPreventZooming, false);
        }
        else {
            this.removeWindowListener(touchstart, this.doPreventZooming, false);
        }

        return preventZooming;
    },

    doAutoMaximizeOnReady: function() {
        var controller = arguments[arguments.length - 1];

        controller.pause();

        this.isMaximizing = true;

        this.on('maximize', function() {
            this.isMaximizing = false;

            this.updateSize();

            controller.resume();

            this.fireEvent('ready', this);
        }, this, { single: true });

        this.maximize();
    },

    doAutoMaximizeOnOrientationChange: function() {
        var controller = arguments[arguments.length - 1],
            firingArguments = controller.firingArguments;

        controller.pause();

        this.isMaximizing = true;

        this.on('maximize', function() {
            this.isMaximizing = false;

            this.updateSize();

            firingArguments[2] = this.windowWidth;
            firingArguments[3] = this.windowHeight;

            controller.resume();
        }, this, { single: true });

        this.maximize();
    },

    doBlurInput: function(e) {
        var target = e.target,
            focusedElement = this.focusedElement;
        //In IE9/10 browser window loses focus and becomes inactive if focused element is <body>. So we shouldn't call blur for <body>
        if (focusedElement && focusedElement.nodeName.toUpperCase() != 'BODY' && !this.isInputRegex.test(target.tagName)) {
            delete this.focusedElement;
            focusedElement.blur();
        }
    },

    doPreventPanning: function(e) {
        var target = e.target, touch;

        // If we have an interaction on a WebComponent we need to check the actual shadow dom element selected
        // to determine if it is an input before preventing default behavior
        // Side effect to this is if the shadow input does not do anything with 'touchmove' the user could pan
        // the screen.
        if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
            touch = e.touches[0];
            if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
                return;
            }
        }

        if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
            e.preventDefault();
        }
    },

    doPreventZooming: function(e) {
        // Don't prevent right mouse event
        if ('button' in e && e.button !== 0) {
            return;
        }

        var target = e.target, touch;
        if (this.isInteractiveWebComponentRegEx.test(target.tagName) && e.touches && e.touches.length > 0) {
            touch = e.touches[0];
            if (touch && touch.target && this.isInputRegex.test(touch.target.tagName)) {
                return;
            }
        }

        if (target && target.nodeType === 1 && !this.isInputRegex.test(target.tagName)) {
            e.preventDefault();
        }
    },

    addWindowListener: function(eventName, fn, capturing) {
        window.addEventListener(eventName, fn, Boolean(capturing));
    },

    removeWindowListener: function(eventName, fn, capturing) {
        window.removeEventListener(eventName, fn, Boolean(capturing));
    },

    doAddListener: function(eventName, fn, scope, options) {
        if (eventName === 'ready' && this.isReady && !this.isMaximizing) {
            fn.call(scope);
            return this;
        }

        return this.callSuper(arguments);
    },

    determineOrientation: function() {
        // First attempt will be to use Native Orientation information
        if (Ext.feature.has.Orientation) {
            var nativeOrientation= this.getWindowOrientation();
            // 90 || -90 || 270 is landscape
            if (Math.abs(nativeOrientation) === 90 || nativeOrientation === 270) {
                return this.LANDSCAPE;
            } else {
                return this.PORTRAIT;
            }
        // Second attempt will be to use MatchMedia and a media query
        } else if (Ext.feature.has.MatchMedia) {
            return window.matchMedia('(orientation : landscape)').matches ? this.LANDSCAPE : this.PORTRAIT;
        // Fall back on hidden element with media query attached to it (media query in Base Theme)
        } else if (this.orientationElement) {
            return this.orientationElement.getStyle('content');
        }
    },

    updateOrientation: function(newValue, oldValue) {
        if (oldValue) {
            this.fireOrientationChangeEvent(newValue, oldValue);
        }
    },

    /**
     * Listener for Orientation Change in environments that support orientationchange events
     * @private
     */
    onOrientationChange: function() {
        this.setOrientation(this.determineOrientation());
    },

    /**
     * Listener for Orientation Change in environments that do no support orientationchange events
     * @private
     */
    onResize: function() {
        this.updateSize();
        this.setOrientation(this.determineOrientation());
    },

    fireOrientationChangeEvent: function(newOrientation, oldOrientation) {
        var clsPrefix = Ext.baseCSSPrefix;
        Ext.getBody().replaceCls(clsPrefix + oldOrientation, clsPrefix + newOrientation);

        this.updateSize();
        this.fireEvent('orientationchange', this, newOrientation, this.windowWidth, this.windowHeight);
    },

    updateSize: function(width, height) {
        this.windowWidth = width !== undefined ? width : this.getWindowWidth();
        this.windowHeight = height !== undefined ? height : this.getWindowHeight();

        return this;
    },

    maximize: function() {
        this.fireMaximizeEvent();
    },

    fireMaximizeEvent: function() {
        this.updateSize();
        this.fireEvent('maximize', this);
    },

    doSetHeight: function(height) {
        Ext.getBody().setHeight(height);

        this.callParent(arguments);
    },

    doSetWidth: function(width) {
        Ext.getBody().setWidth(width);

        this.callParent(arguments);
    },

    scrollToTop: function() {
        window.scrollTo(0, -1);
    },

    /**
     * Retrieves the document width.
     * @return {Number} width in pixels.
     */
    getWindowWidth: function() {
        return window.innerWidth;
    },

    /**
     * Retrieves the document height.
     * @return {Number} height in pixels.
     */
    getWindowHeight: function() {
        return window.innerHeight;
    },

    getWindowOuterHeight: function() {
        return window.outerHeight;
    },

    getWindowOrientation: function() {
        return window.orientation;
    },

    getSize: function() {
        return {
            width: this.windowWidth,
            height: this.windowHeight
        };
    },

    onItemFullscreenChange: function(item) {
        item.addCls(this.fullscreenItemCls);
        this.add(item);
    },
    waitUntil: function(condition, onSatisfied, onTimeout, delay, timeoutDuration) {
        if (!delay) {
            delay = 50;
        }

        if (!timeoutDuration) {
            timeoutDuration = 2000;
        }

        var scope = this,
            elapse = 0;

        setTimeout(function repeat() {
            elapse += delay;

            if (condition.call(scope) === true) {
                if (onSatisfied) {
                    onSatisfied.call(scope);
                }
            }
            else {
                if (elapse >= timeoutDuration) {
                    if (onTimeout) {
                        onTimeout.call(scope);
                    }
                }
                else {
                    setTimeout(repeat, delay);
                }
            }
        }, delay);
    },

    /**
     * Sets a menu for a given side of the Viewport.
     *
     * Adds functionality to show the menu by swiping from the side of the screen from the given side.
     *
     * If a menu is already set for a given side, it will be removed.
     *
     * Available sides are: `left`, `right`, `top`, and `bottom`.
     *
     * @param {Ext.Menu} menu The menu to assign to the viewport
     * @param {Object} config The configuration for the menu.
     * @param {String} config.side The side to put the menu on.
     * @param {Boolean} config.cover True to cover the viewport content. Defaults to `true`.
     */
    setMenu: function(menu, config) {
        var me = this;
        config = config || {};

        // Temporary workaround for body shifting issue
        if (Ext.os.is.iOS && !this.hasiOSOrientationFix) {
            this.hasiOSOrientationFix = true;
            this.on('orientationchange', function() {
                window.scrollTo(0, 0);
            }, this);
        }

        if (!menu) {
            //<debug error>
            Ext.Logger.error("You must specify a side to dock the menu.");
            //</debug>
            return;
        }

        if (!config.side) {
            //<debug error>
            Ext.Logger.error("You must specify a side to dock the menu.");
            //</debug>
            return;
        }

        if (['left', 'right', 'top', 'bottom'].indexOf(config.side) == -1) {
            //<debug error>
            Ext.Logger.error("You must specify a valid side (left, right, top or botom) to dock the menu.");
            //</debug>
            return;
        }

        var menus = me.getMenus();

        if (!menus) {
            menus = {};
        }

        // Add a listener to show this menu on swipe
        if (!me.addedSwipeListener) {
            me.addedSwipeListener = true;

            me.element.on({
                tap: me.onTap,
                swipestart: me.onSwipeStart,
                edgeswipestart: me.onEdgeSwipeStart,
                edgeswipe: me.onEdgeSwipe,
                edgeswipeend: me.onEdgeSwipeEnd,
                scope: me
            });

            // Add BB10 webworks API for swipe down.
            if (window.blackberry) {
                var toggleMenu = function() {
                    var menus = me.getMenus(),
                        menu = menus['top'];

                    if (!menu) {
                        return;
                    }

                    if (menu.isHidden()) {
                        me.showMenu('top');
                    } else {
                        me.hideMenu('top');
                    }
                };

                if (blackberry.app && blackberry.app.event && blackberry.app.event.onSwipeDown) {
                    blackberry.app.event.onSwipeDown(toggleMenu); // PlayBook
                }
                else if (blackberry.event && blackberry.event.addEventListener) {
                    blackberry.event.addEventListener("swipedown", toggleMenu); // BB10
                }
            }
        }

        menus[config.side] = menu;
        menu.$reveal = Boolean(config.reveal);
        menu.$cover = config.cover !== false && !menu.$reveal;
        menu.$side = config.side;

        me.fixMenuSize(menu, config.side);

        if (config.side == 'left') {
            menu.setLeft(0);
            menu.setRight(null);
            menu.setTop(0);
            menu.setBottom(0);
        }
        else if (config.side == 'right') {
            menu.setLeft(null);
            menu.setRight(0);
            menu.setTop(0);
            menu.setBottom(0);
        }
        else if (config.side == 'top') {
            menu.setLeft(0);
            menu.setRight(0);
            menu.setTop(0);
            menu.setBottom(null);
        }
        else if (config.side == 'bottom') {
            menu.setLeft(0);
            menu.setRight(0);
            menu.setTop(null);
            menu.setBottom(0);
        }

        me.setMenus(menus);
    },

    /**
     * Removes a menu from a specified side.
     * @param {String} side The side to remove the menu from
     */
    removeMenu: function(side) {
        var menus = this.getMenus() || {},
            menu = menus[side];

        if(menu) this.hideMenu(side);
        delete menus[side];
        this.setMenus(menus);
    },

    /**
     * @private
     * Changes the sizing of the specified menu so that it displays correctly when shown.
     */
    fixMenuSize: function(menu, side) {
        if (side == 'top' || side == 'bottom') {
            menu.setWidth('100%');
        }
        else if (side == 'left' || side == 'right') {
            menu.setHeight('100%');
        }
    },

    /**
     * Shows a menu specified by the menu's side.
     * @param {String} side The side which the menu is placed.
     */
    showMenu: function(side) {
        var menus = this.getMenus(),
            menu = menus[side],
            before, after,
            viewportBefore, viewportAfter;

        if (!menu || menu.isAnimating) {
            return;
        }

        this.hideOtherMenus(side);

        before = {
            translateX: 0,
            translateY: 0
        };

        after = {
            translateX: 0,
            translateY: 0
        };

        viewportBefore = {
            translateX: 0,
            translateY: 0
        };

        viewportAfter = {
            translateX: 0,
            translateY: 0
        };

        if (menu.$reveal) {
            Ext.getBody().insertFirst(menu.element);
        }
        else {
            Ext.Viewport.add(menu);
        }

        menu.show();
        menu.addCls('x-' + side);

        var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();

        if (side == 'left') {
            before.translateX = -size;
            viewportAfter.translateX = size;
        }
        else if (side == 'right') {
            before.translateX = size;
            viewportAfter.translateX = -size;
        }
        else if (side == 'top') {
            before.translateY = -size;
            viewportAfter.translateY = size;
        }
        else if (side == 'bottom') {
            before.translateY = size;
            viewportAfter.translateY = -size;
        }

        if (menu.$reveal) {
            if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
                menu.translate(0, 0);
            }
        }
        else {
            menu.translate(before.translateX, before.translateY);
        }

        if (menu.$cover) {
            menu.getTranslatable().on('animationend', function() {
                menu.isAnimating = false;
            }, this, {
                single: true
            });

            menu.translate(after.translateX, after.translateY, {
                preserveEndState: true,
                duration: 200
            });

        }
        else {
            this.translate(viewportBefore.translateX, viewportBefore.translateY);

            this.getTranslatable().on('animationend', function() {
                menu.isAnimating = false;
            }, this, {
                single: true
            });

            this.translate(viewportAfter.translateX, viewportAfter.translateY, {
                preserveEndState: true,
                duration: 200
            });
        }

        // Make the menu as animating
        menu.isAnimating = true;
    },

    /**
     * Hides a menu specified by the menu's side.
     * @param {String} side The side which the menu is placed.
     */
    hideMenu: function(side, animate) {
        var menus = this.getMenus(),
            menu = menus[side],
            after, viewportAfter,
            size;

        animate = (animate === false) ? false : true;

        if (!menu || (menu.isHidden() || menu.isAnimating)) {
            return;
        }

        after = {
            translateX: 0,
            translateY: 0
        };

        viewportAfter = {
            translateX: 0,
            translateY: 0
        };

        size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight();

        if (side == 'left') {
            after.translateX = -size;
        }
        else if (side == 'right') {
            after.translateX = size;
        }
        else if (side == 'top') {
            after.translateY = -size;
        }
        else if (side == 'bottom') {
            after.translateY = size;
        }

        if (menu.$cover) {
            if (animate) {
                menu.getTranslatable().on('animationend', function() {
                    menu.isAnimating = false;
                    menu.hide();
                }, this, {
                    single: true
                });

                menu.translate(after.translateX, after.translateY, {
                    preserveEndState: true,
                    duration: 200
                });
            }
            else {
                menu.translate(after.translateX, after.translateY);
                menu.hide()
            }
        }
        else {
            if (animate) {
                this.getTranslatable().on('animationend', function() {
                    menu.isAnimating = false;
                    menu.hide();
                }, this, {
                    single: true
                });

                this.translate(viewportAfter.translateX, viewportAfter.translateY, {
                    preserveEndState: true,
                    duration: 200
                });
            }
            else {
                this.translate(viewportAfter.translateX, viewportAfter.translateY);
                menu.hide();
            }
        }
    },

    /**
     * Hides all visible menus.
     */
    hideAllMenus: function(animation) {
        var menus = this.getMenus();

        for (var side in menus) {
            this.hideMenu(side, animation);
        }
    },

    /**
     * Hides all menus except for the side specified
     * @param {String} side         Side(s) not to hide
     * @param {String} animation    Animation to hide with
     */
    hideOtherMenus: function(side, animation){
        var menus = this.getMenus();

        for (var menu in menus) {
            if (side != menu) {
                this.hideMenu(menu, animation);
            }
        }
    },

    /**
     * Toggles the menu specified by side
     * @param {String} side The side which the menu is placed.
     */
    toggleMenu: function(side) {
        var menus = this.getMenus(), menu;
        if (menus[side]) {
            menu = menus[side];
            if (menu.isHidden()) {
                this.showMenu(side);
            } else {
                this.hideMenu(side);
            }
        }
    },

    /**
     * @private
     */
    sideForDirection: function(direction) {
        if (direction == 'left') {
            return 'right';
        }
        else if (direction == 'right') {
            return 'left';
        }
        else if (direction == 'up') {
            return 'bottom';
        }
        else if (direction == 'down') {
            return 'top';
        }
    },

    /**
     * @private
     */
    sideForSwipeDirection: function(direction) {
        if (direction == "up") {
            return  "top";
        }
        else if (direction == "down") {
            return "bottom";
        }
        return direction;
    },

    /**
     * @private
     */
    onTap: function(e) {
        // this.hideAllMenus();
    },

    /**
     * @private
     */
    onSwipeStart: function(e) {
        var side = this.sideForSwipeDirection(e.direction);
        this.hideMenu(side);
    },

    /**
     * @private
     */
    onEdgeSwipeStart: function(e) {
        var side = this.sideForDirection(e.direction),
            menus = this.getMenus(),
            menu = menus[side],
            menuSide, checkMenu;

        if (!menu || !menu.isHidden()) {
            return;
        }

        for (menuSide in menus) {
            checkMenu = menus[menuSide];
            if (checkMenu.isHidden() !== false) {
                return;
            }
        }

        this.$swiping = true;

        this.hideAllMenus(false);

        // show the menu first so we can calculate the size
        if (menu.$reveal) {
            Ext.getBody().insertFirst(menu.element);
        }
        else {
            Ext.Viewport.add(menu);
        }
        menu.show();

        var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
            after, viewportAfter;

        after = {
            translateX: 0,
            translateY: 0
        };

        viewportAfter = {
            translateX: 0,
            translateY: 0
        };

        if (side == 'left') {
            after.translateX = -size;
        }
        else if (side == 'right') {
            after.translateX = size;
        }
        else if (side == 'top') {
            after.translateY = -size;
        }
        else if (side == 'bottom') {
            after.translateY = size;
        }

        var transformStyleName = 'webkitTransform' in document.createElement('div').style ? 'webkitTransform' : 'transform',
            setTransform = menu.element.dom.style[transformStyleName];

        if (setTransform) {
            menu.element.dom.style[transformStyleName] = '';
        }

        if (menu.$reveal) {
            if (Ext.browser.getPreferredTranslationMethod() != 'scrollposition') {
                menu.translate(0, 0);
            }
        }
        else {
            menu.translate(after.translateX, after.translateY);
        }

        if (!menu.$cover) {
            if (setTransform) {
                this.innerElement.dom.style[transformStyleName] = '';
            }

            this.translate(viewportAfter.translateX, viewportAfter.translateY);
        }
    },

    /**
     * @private
     */
    onEdgeSwipe: function(e) {
        var side = this.sideForDirection(e.direction),
            menu = this.getMenus()[side];

        if (!menu || !this.$swiping) {
            return;
        }

        var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
            after, viewportAfter,
            movement = Math.min(e.distance - size, 0),
            viewportMovement = Math.min(e.distance, size);

        after = {
            translateX: 0,
            translateY: 0
        };

        viewportAfter = {
            translateX: 0,
            translateY: 0
        };

        if (side == 'left') {
            after.translateX = movement;
            viewportAfter.translateX = viewportMovement;
        }
        else if (side == 'right') {
            after.translateX = -movement;
            viewportAfter.translateX = -viewportMovement;
        }
        else if (side == 'top') {
            after.translateY = movement;
            viewportAfter.translateY = viewportMovement;
        }
        else if (side == 'bottom') {
            after.translateY = -movement;
            viewportAfter.translateY = -viewportMovement;
        }

        if (menu.$cover) {
            menu.translate(after.translateX, after.translateY);
        }
        else {
            this.translate(viewportAfter.translateX, viewportAfter.translateY);
        }
    },

    /**
     * @private
     */
    onEdgeSwipeEnd: function(e) {
        var side = this.sideForDirection(e.direction),
            menu = this.getMenus()[side],
            shouldRevert = false;

        if (!menu) {
            return;
        }

        var size = (side == 'left' || side == 'right') ? menu.element.getWidth() : menu.element.getHeight(),
            velocity = (e.flick) ? e.flick.velocity : 0;

        // check if continuing in the right direction
        if (side == 'right') {
            if (velocity.x > 0) {
                shouldRevert = true;
            }
        }
        else if (side == 'left') {
            if (velocity.x < 0) {
                shouldRevert = true;
            }
        }
        else if (side == 'top') {
            if (velocity.y < 0) {
                shouldRevert = true;
            }
        }
        else if (side == 'bottom') {
            if (velocity.y > 0) {
                shouldRevert = true;
            }
        }

        var movement = (shouldRevert) ? size : 0,
            viewportMovement = (shouldRevert) ? 0 : -size,
            after, viewportAfter;

        after = {
            translateX: 0,
            translateY: 0
        };

        viewportAfter = {
            translateX: 0,
            translateY: 0
        };

        if (side == 'left') {
            after.translateX = -movement;
            viewportAfter.translateX = -viewportMovement;
        }
        else if (side == 'right') {
            after.translateX = movement;
            viewportAfter.translateX = viewportMovement;
        }
        else if (side == 'top') {
            after.translateY = -movement;
            viewportAfter.translateY = -viewportMovement;
        }
        else if (side == 'bottom') {
            after.translateY = movement;
            viewportAfter.translateY = viewportMovement;
        }

        // Move the viewport if cover is not enabled
        if (menu.$cover) {
            menu.getTranslatable().on('animationend', function() {
                if (shouldRevert) {
                    menu.hide();
                }
            }, this, {
                single: true
            });

            menu.translate(after.translateX, after.translateY, {
                preserveEndState: true,
                duration: 200
            });

        }
        else {
            this.getTranslatable().on('animationend', function() {
                if (shouldRevert) {
                    menu.hide();
                }
            }, this, {
                single: true
            });

            this.translate(viewportAfter.translateX, viewportAfter.translateY, {
                preserveEndState: true,
                duration: 200
            });
        }

        this.$swiping = false;
    }
});


"use strict";

exports.library = function library(_){

    // TODO: add transports, parents
    /*
    measurer.prototype.transport = function(measurement){
        var self = this;

        _.each(this.transportsSync(), function(transport){ transport.call(self, measurement); });
        _.nextTick(function(){ _.each(self.transportsAsync(), function(transport){ transport.call(self, measurement); }); });

        if(this.parent()){ this.parent().transport(measurement); }
    };
    */

    function timer(options){
        if(_.isFunction(options)){ options = { out: options }; }
        options = options || {};

        this._out = options.out || _.stderr;
        this._times = {};
    }

    timer.prototype.time = function(event_category, event_name){
        var self = this;

        if(!event_name){
            event_name = event_category;
            event_category = "uncategorized";

            if(event_name){
                var event_split = event_name.split(".");
                if(event_split.length > 1){
                    event_category = event_split[0];
                    event_name = event_split[1];
                }
            }
        }

        var start_time = (new Date()).valueOf();
        var elapsed_time = null;

        return(function(log_str){
            if(elapsed_time === null){

                var end_time = (new Date()).valueOf();
                elapsed_time = end_time - start_time;

                if(event_category && event_name){
                    if(!self._times[event_category]){ self._times[event_category] = {}; }
                    if(!self._times[event_category][event_name]){ self._times[event_category][event_name] = []; }
                    self._times[event_category][event_name].push(elapsed_time);
                };
            }

            if(log_str){ self._out(log_str + ": ", elapsed_time + "ms"); }

            return(elapsed_time);
        });
    }

    timer.prototype.last = function(category_name){
        var last = {};
        if(!this._times[category_name]){ return last; }

        _.each(this._times[category_name], function(times, key){
            last[key] = _.last(times);
        });

        return(last);
    };

    timer.prototype.print_last = function(category_name){
        var self = this;
        _.each(self.last(category_name), function(time, key){
            self._out(key + ": " + time + "ms");
        });
    };

    timer.prototype.times = function(event_category, event_name){

        if(!event_category){ return(this._times); }

        var event_split = event_category.split(".");
        if(event_split.length > 1){
            event_category = event_split[0];
            event_name = event_split[1];
        }

        else if(!this._times[event_category]){ return({}); }

        if(!event_name){ return(this._times[event_category]); }
        else if(!this._times[event_category][event_name]){ return([]); }

        return(this._times[event_category][event_name]);
    };

    timer.prototype.make = function(options){ return(new timer(options)); };

    timer.prototype.library = library;
    timer.prototype.class = timer;

    return(new timer());
};


/**
 * Library to interface with Puppet Hiera.
 *
 * @author rajkissu <rajkissu@gmail.com>
 */

/* jslint node: true */
'use strict';

var path, async, yaml, fs;

path  = require('path');
async = require('async');
yaml  = require('js-yaml');

/**
 * Initializes the Hiera module.
 *
 * @param {string} adapter - which adapter to use
 * @param {string} configFile - the path to `hiera.yaml`.
 *
 * @example init('/path/to/hiera.yaml');
 */
function init(adapter, config) {
  if (!fs) {
    var Adapter = require('./adapters/' + adapter);

    fs = new Adapter(config);
  }
}

/**
 * Retrieves Hiera configuration.
 *
 * @param {Function} cb - callback to invoke.
 *
 * @example getConfig(cb);
 */
function getConfig(cb) {
  fs.readConfig(cb);
}

/**
 * Saves Hiera configuration to file.
 *
 * @param {Object} data - an object loaded with hiera configuration.
 * @param {Function} cb - callback to invoke.
 *
 * @example saveConfig(hieraConfig, cb);
 */
function saveConfig(data, cb) {
  fs.saveConfig(data, cb);
}

/**
 * Gets the Hiera hierarchy.
 *
 * @param {Function} cb - callback to invoke.
 *
 * @example getHierarchy(cb);
 */
function getHierarchy(cb) {
  getConfig(function (err, config) {
    if (err) {
      cb(err);
      return;
    }

    cb(null, config[':hierarchy']);
  });
}

/**
 * Retrieves all Hiera backend configurations.
 *
 * @param {Function} cb - callback to invoke.
 *
 * @example getBackends(cb);
 */
function getBackends(cb) {
  getConfig(function (err, config) {
    if (err) {
      cb(err);
      return;
    }

    cb(null, config[':backends']);
  });
}

/**
 * Gets configuration for a specific backend.
 *
 * @param {string} backend - the backend to load.
 * @param {Function} cb - callback to invoke.
 *
 * @example getBackendConfig('yaml', cb);
 */
function getBackendConfig(backend, cb) {
  getConfig(function (err, config) {
    if (err) {
      cb(err);
      return;
    }

    cb(null, config[':' + backend]);
  });
}

/**
 * Retrieves data from a Hiera file.
 *
 * @param {string} backend - the backend to load.
 * @param {string} file - the Hiera file to load.
 * @param {Function} cb - callback to invoke.
 *
 * @example getFile('yaml', 'defaults.yaml', function (err, data) { ... });
 */
function getFile(backend, file, cb) {
  getBackendConfig(backend, function (err, config) {
    file = [ config[':datadir'], '/', file ].join('');

    fs.readFile(file, cb);
  });
}

/**
 * Saves data to a Hiera file.
 *
 * @param {string} backend - the backend to load.
 * @param {string} file - the Hiera file to save to.
 * @param {Object} data - contents to save to the Hiera file.
 * @param {Function} cb - callback to invoke once file saving completes.
 *
 * @example saveFile(
 *  'gpg', '/path/to/file.gpg', fileData,
 *  function (err) { ... }
 * );
 */
function saveFile(backend, file, data, cb) {
  cb = typeof(cb) === 'function' ? cb : function () {};

  getBackendConfig(backend, function (err, config) {
    var datadir = config[':datadir'];
    file = path.join(datadir, file);

    fs.writeFile(file, data, cb);
  });
}

/**
 * Check for hierarchy overrides for a given file.
 *
 * @param {string} backend - the backend to load.
 * @param {string} file - the Hiera file to check overrides for.
 * @param {Function} cb - callback to invoke once override checking completes.
 *
 * @example getOverrides(
 *  'gpg', '/path/to/file.gpg', function (err) { ... }
 * );
 */
function getOverrides(backend, file, cb) {
  async.parallel([
    function hierarchy(cb) {
      getHierarchy(cb);
    },

    function backendConfig(cb) {
      getBackendConfig(backend, cb);
    }
  ], function (err, results) {
    var hierarchy, datadir, filename, tasks,
        pos, searchHierarchy, tasks;

    hierarchy = results[0];
    datadir   = results[1][':datadir'];
    filename  = file.remove('.' + backend);
    tasks     = [];

    // remove the file's matching hierarchy
    pos = hierarchy.findIndex(filename);
    searchHierarchy = hierarchy.to(pos);

    getFile(backend, file, function (err, data) {
      var sourceData;

      if (err) {
        cb(err);
        return;
      }

      sourceData = yaml.safeLoad(data);

      // setup hierarchy search tasks
      _.each(searchHierarchy, function (hierarchy) {
        tasks.push(hierarchy + '.' + backend);
      });

      async.map(tasks, function (f, cb) {
        // get data for each file in the hierarchy
        // TODO: support magic hiera vars
        getFile(backend, f, function (err, data) {
          cb(null, {
            file : f,
            data : yaml.safeLoad(data)
          });
        });
      }, function (err, comparisonData) {
        var list = {};

        if (err) {
          cb(err);
          return;
        }

        _.each(sourceData, function (key, value) {
          _.each(comparisonData, function (set) {
            _.each(set.data, function (cKey, cValue) {
              if (cKey === key) {
                list[cKey] = {
                  file  : set.file,
                  value : cValue
                };
              }
            });

            if (list[key]) {
              // already exists
              return false;
            }
          });
        });

        cb(null, list);
      });
    });
  });
}

module.exports = {
  init             : init,
  getConfig        : getConfig,
  saveConfig       : saveConfig,
  getHierarchy     : getHierarchy,
  getBackends      : getBackends,
  getBackendConfig : getBackendConfig,
  getFile          : getFile,
  saveFile         : saveFile,
  getOverrides     : getOverrides
};


/**
 * The text field is the basis for most of the input fields in Sencha Touch. It provides a baseline of shared
 * functionality such as input validation, standard events, state management and look and feel. Typically we create
 * text fields inside a form, like this:
 *
 *     @example
 *     Ext.create('Ext.form.Panel', {
 *         fullscreen: true,
 *         items: [
 *             {
 *                 xtype: 'fieldset',
 *                 title: 'Enter your name',
 *                 items: [
 *                     {
 *                         xtype: 'textfield',
 *                         label: 'First Name',
 *                         name: 'firstName'
 *                     },
 *                     {
 *                         xtype: 'textfield',
 *                         label: 'Last Name',
 *                         name: 'lastName'
 *                     }
 *                 ]
 *             }
 *         ]
 *     });
 *
 * This creates two text fields inside a form. Text Fields can also be created outside of a Form, like this:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Your Name',
 *         value: 'Ed Spencer'
 *     });
 *
 * ## Configuring
 *
 * Text field offers several configuration options, including {@link #placeHolder}, {@link #maxLength},
 * {@link #autoComplete}, {@link #autoCapitalize} and {@link #autoCorrect}. For example, here is how we would configure
 * a text field to have a maximum length of 10 characters, with placeholder text that disappears when the field is
 * focused:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Username',
 *         maxLength: 10,
 *         placeHolder: 'Enter your username'
 *     });
 *
 * The autoComplete, autoCapitalize and autoCorrect configs simply set those attributes on the text field and allow the
 * native browser to provide those capabilities. For example, to enable auto complete and auto correct, simply
 * configure your text field like this:
 *
 *     Ext.create('Ext.field.Text', {
 *         label: 'Username',
 *         autoComplete: true,
 *         autoCorrect: true
 *     });
 *
 * These configurations will be picked up by the native browser, which will enable the options at the OS level.
 *
 * Text field inherits from {@link Ext.field.Field}, which is the base class for all fields in Sencha Touch and provides
 * a lot of shared functionality for all fields, including setting values, clearing and basic validation. See the
 * {@link Ext.field.Field} documentation to see how to leverage its capabilities.
 *
 * For more information regarding forms and fields, please review [Using Forms in Sencha Touch Guide](../../../components/forms.html)
 */
Ext.define('Ext.field.Text', {
    extend: 'Ext.field.Field',
    xtype: 'textfield',
    alternateClassName: 'Ext.form.Text',

    /**
     * @event focus
     * Fires when this field receives input focus
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */

    /**
     * @event blur
     * Fires when this field loses input focus
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */

    /**
     * @event paste
     * Fires when this field is pasted.
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */

    /**
     * @event mousedown
     * Fires when this field receives a mousedown
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */

    /**
     * @event keyup
     * @preventable doKeyUp
     * Fires when a key is released on the input element
     * @param {Ext.field.Text} this This field
     * @param {Ext.event.Event} e
     */

    /**
     * @event clearicontap
     * @preventable doClearIconTap
     * Fires when the clear icon is tapped
     * @param {Ext.field.Text} this This field
     * @param {Ext.field.Input} input The field's input component.
     * @param {Ext.event.Event} e
     */

    /**
     * @event change
     * Fires just before the field blurs if the field value has changed
     * @param {Ext.field.Text} this This field
     * @param {Mixed} newValue The new value
     * @param {Mixed} oldValue The original value
     */

    /**
     * @event action
     * @preventable doAction
     * Fires whenever the return key or go is pressed. FormPanel listeners
     * for this event, and submits itself whenever it fires. Also note
     * that this event bubbles up to parent containers.
     * @param {Ext.field.Text} this This field
     * @param {Mixed} e The key event object
     */

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

        /**
         * @cfg
         * @inheritdoc
         */
        clearIcon: true,

        /**
         * @cfg {String} placeHolder A string value displayed in the input (if supported) when the control is empty.
         * @accessor
         */
        placeHolder: null,

        /**
         * @cfg {Number} maxLength The maximum number of permitted input characters.
         * @accessor
         */
        maxLength: null,

        /**
         * True to set the field's DOM element autocomplete attribute to "on", false to set to "off".
         * @cfg {Boolean} autoComplete
         * @accessor
         */
        autoComplete: null,

        /**
         * True to set the field's DOM element autocapitalize attribute to "on", false to set to "off".
         * @cfg {Boolean} autoCapitalize
         * @accessor
         */
        autoCapitalize: null,

        /**
         * True to set the field DOM element autocorrect attribute to "on", false to set to "off".
         * @cfg {Boolean} autoCorrect
         * @accessor
         */
        autoCorrect: null,

        /**
         * True to set the field DOM element readonly attribute to true.
         * @cfg {Boolean} readOnly
         * @accessor
         */
        readOnly: null,

        /**
         * @cfg {Object} component The inner component for this field, which defaults to an input text.
         * @accessor
         */
        component: {
            xtype: 'input',
            type: 'text',
            fastFocus: true
        },

        bubbleEvents: ['action']
    },

    // @private
    initialize: function() {
        var me = this;

        me.callParent();

        me.getComponent().on({
            scope: this,

            keyup       : 'onKeyUp',
            change      : 'onChange',
            focus       : 'onFocus',
            blur        : 'onBlur',
            paste       : 'onPaste',
            mousedown   : 'onMouseDown',
            clearicontap: 'onClearIconTap'
        });

        // set the originalValue of the textfield, if one exists
        me.originalValue = me.getValue() || "";
        me.getComponent().originalValue = me.originalValue;

        me.syncEmptyCls();
    },

    syncEmptyCls: function() {
        var empty = (this._value) ? this._value.length : false,
            cls = Ext.baseCSSPrefix + 'empty';

        if (empty) {
            this.removeCls(cls);
        } else {
            this.addCls(cls);
        }
    },

    // @private
    updateValue: function(newValue) {
        var component  = this.getComponent(),
            // allows newValue to be zero but not undefined or null (other falsey values)
            valueValid = newValue !== undefined && newValue !== null && newValue !== "";

        if (component) {
            component.setValue(newValue);
        }

        this[valueValid && this.isDirty() ? 'showClearIcon' : 'hideClearIcon']();

        this.syncEmptyCls();
    },

    getValue: function() {
        var me = this;

        me._value = me.getComponent().getValue();

        me.syncEmptyCls();

        return me._value;
    },

    // @private
    updatePlaceHolder: function(newPlaceHolder) {
        this.getComponent().setPlaceHolder(newPlaceHolder);
    },

    // @private
    updateMaxLength: function(newMaxLength) {
        this.getComponent().setMaxLength(newMaxLength);
    },

    // @private
    updateAutoComplete: function(newAutoComplete) {
        this.getComponent().setAutoComplete(newAutoComplete);
    },

    // @private
    updateAutoCapitalize: function(newAutoCapitalize) {
        this.getComponent().setAutoCapitalize(newAutoCapitalize);
    },

    // @private
    updateAutoCorrect: function(newAutoCorrect) {
        this.getComponent().setAutoCorrect(newAutoCorrect);
    },

    // @private
    updateReadOnly: function(newReadOnly) {
        if (newReadOnly) {
            this.hideClearIcon();
        } else {
            this.showClearIcon();
        }

        this.getComponent().setReadOnly(newReadOnly);
    },

    // @private
    updateInputType: function(newInputType) {
        var component = this.getComponent();
        if (component) {
            component.setType(newInputType);
        }
    },

    // @private
    updateName: function(newName) {
        var component = this.getComponent();
        if (component) {
            component.setName(newName);
        }
    },

    // @private
    updateTabIndex: function(newTabIndex) {
        var component = this.getComponent();
        if (component) {
            component.setTabIndex(newTabIndex);
        }
    },

    /**
     * Updates the {@link #inputCls} configuration on this fields {@link #component}
     * @private
     */
    updateInputCls: function(newInputCls, oldInputCls) {
        var component = this.getComponent();
        if (component) {
            component.replaceCls(oldInputCls, newInputCls);
        }
    },

    doSetDisabled: function(disabled) {
        var me = this;

        me.callParent(arguments);

        var component = me.getComponent();
        if (component) {
            component.setDisabled(disabled);
        }

        if (disabled) {
            me.hideClearIcon();
        } else {
            me.showClearIcon();
        }
    },

    // @private
    showClearIcon: function() {
        var me         = this,
            value      = me.getValue(),
            // allows value to be zero but not undefined or null (other falsey values)
            valueValid = value !== undefined && value !== null && value !== "";

        if (me.getClearIcon() && !me.getDisabled() && !me.getReadOnly() && valueValid) {
            me.element.addCls(Ext.baseCSSPrefix + 'field-clearable');
        }

        return me;
    },

    // @private
    hideClearIcon: function() {
        if (this.getClearIcon()) {
            this.element.removeCls(Ext.baseCSSPrefix + 'field-clearable');
        }
    },

    onKeyUp: function(e) {
        this.fireAction('keyup', [this, e], 'doKeyUp');
    },

    /**
     * Called when a key has been pressed in the `<input>`
     * @private
     */
    doKeyUp: function(me, e) {
        // getValue to ensure that we are in sync with the dom
        var value      = me.getValue(),
            // allows value to be zero but not undefined or null (other falsey values)
            valueValid = value !== undefined && value !== null && value !== "";

        this[valueValid ? 'showClearIcon' : 'hideClearIcon']();

        if (e.browserEvent.keyCode === 13) {
            me.fireAction('action', [me, e], 'doAction');
        }
    },

    doAction: function() {
        this.blur();
    },

    onClearIconTap: function(input, e) {
        this.fireAction('clearicontap', [this, input, e], 'doClearIconTap');
    },

    // @private
    doClearIconTap: function(me, e) {
        me.setValue('');

        //sync with the input
        me.getValue();
    },

    onChange: function(me, value, startValue) {
        me.fireEvent('change', this, value, startValue);
    },

    onFocus: function(e) {
        this.addCls(Ext.baseCSSPrefix + 'field-focused');
        this.isFocused = true;
        this.fireEvent('focus', this, e);
    },

    onBlur: function(e) {
        var me = this;

        this.removeCls(Ext.baseCSSPrefix + 'field-focused');
        this.isFocused = false;

        me.fireEvent('blur', me, e);

        setTimeout(function() {
            me.isFocused = false;
        }, 50);
    },

    onPaste: function(e) {
        this.fireEvent('paste', this, e);
    },

    onMouseDown: function(e) {
        this.fireEvent('mousedown', this, e);
    },

    /**
     * Attempts to set the field as the active input focus.
     * @return {Ext.field.Text} This field
     */
    focus: function() {
        this.getComponent().focus();
        return this;
    },

    /**
     * Attempts to forcefully blur input focus for the field.
     * @return {Ext.field.Text} This field
     */
    blur: function() {
        this.getComponent().blur();
        return this;
    },

    /**
     * Attempts to forcefully select all the contents of the input field.
     * @return {Ext.field.Text} this
     */
    select: function() {
        this.getComponent().select();
        return this;
    },

    resetOriginalValue: function() {
        this.callParent();
        var component = this.getComponent();
        if(component && component.hasOwnProperty("originalValue")) {
            this.getComponent().originalValue = this.originalValue;
        }
//        this.reset();
    },

    reset: function() {
        this.getComponent().reset();

        //we need to call this to sync the input with this field
        this.getValue();

        this[this.isDirty() ? 'showClearIcon' : 'hideClearIcon']();
    },

    isDirty: function() {
        var component = this.getComponent();
        if (component) {
            return component.isDirty();
        }
        return false;
    }
});

//<deprecated product=touch since=2.0>
/**
 * @property startValue
 * @type String/Number
 * Used to contain the previous value of the field before the edit
 * @removed 2.0.0
 * @member Ext.field.Text
 */
//</deprecated>


'use strict';

// MODULES //

var isNumber = require( 'validate.io-number-primitive' ),
	isArrayLike = require( 'validate.io-array-like' ),
	isTypedArrayLike = require( 'validate.io-typed-array-like' ),
	isMatrixLike = require( 'validate.io-matrix-like' ),
	ctors = require( 'compute-array-constructors' ),
	matrix = require( 'dstructs-matrix' ),
	validate = require( './validate.js' );

// FUNCTIONS //

var pdf1 = require( './number.js' ),
	pdf2 = require( './array.js' ),
	pdf3 = require( './accessor.js' ),
	pdf4 = require( './deepset.js' ),
	pdf5 = require( './matrix.js' ),
	pdf6 = require( './typedarray.js' );

// PDF //

/**
* FUNCTION: pdf( x[, opts] )
*	Evaluates the probability density function (PDF) for a Normal distribution.
*
* @param {Number|Number[]|Array|Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|Matrix} x - input value
* @param {Object} [opts] - function options
* @param {Number} [opts.mu=0] - mean
* @param {Number} [opts.sigma=1] - standard deviation
* @param {Boolean} [opts.copy=true] - boolean indicating if the function should return a new data structure
* @param {Function} [opts.accessor] - accessor function for accessing array values
* @param {String} [opts.path] - deep get/set key path
* @param {String} [opts.sep="."] - deep get/set key path separator
* @param {String} [opts.dtype="float64"] - output data type
* @returns {Number|Number[]|Array|Int8Array|Uint8Array|Uint8ClampedArray|Int16Array|Uint16Array|Int32Array|Uint32Array|Float32Array|Float64Array|Matrix} evaluated PDF
*/
function pdf( x, options ) {
	/* jshint newcap:false */
	var opts = {},
		ctor,
		err,
		out,
		dt,
		d;

	if ( arguments.length > 1 ) {
		err = validate( opts, options );
		if ( err ) {
			throw err;
		}
	}
	opts.mu = typeof opts.mu !== 'undefined' ? opts.mu : 0;
	opts.sigma = typeof opts.sigma !== 'undefined' ? opts.sigma : 1;

	if ( isNumber( x ) ) {
		return pdf1( x, opts.mu, opts.sigma );
	}
	if ( isMatrixLike( x ) ) {
		if ( opts.copy !== false ) {
			dt = opts.dtype || 'float64';
			ctor = ctors( dt );
			if ( ctor === null ) {
				throw new Error( 'pdf()::invalid option. Data type option does not have a corresponding array constructor. Option: `' + dt + '`.' );
			}
			// Create an output matrix:
			d = new ctor( x.length );
			out = matrix( d, x.shape, dt );
		} else {
			out = x;
		}
		return pdf5( out, x, opts.mu, opts.sigma );
	}
	if ( isTypedArrayLike( x ) ) {
		if ( opts.copy === false ) {
			out = x;
		} else {
			dt = opts.dtype || 'float64';
			ctor = ctors( dt );
			if ( ctor === null ) {
				throw new Error( 'pdf()::invalid option. Data type option does not have a corresponding array constructor. Option: `' + dt + '`.' );
			}
			out = new ctor( x.length );
		}
		return pdf6( out, x, opts.mu, opts.sigma );
	}
	if ( isArrayLike( x ) ) {
		// Handle deepset first...
		if ( opts.path ) {
			opts.sep = opts.sep || '.';
			return pdf4( x, opts.mu, opts.sigma, opts.path, opts.sep );
		}
		// Handle regular and accessor arrays next...
		if ( opts.copy === false ) {
			out = x;
		}
		else if ( opts.dtype ) {
			ctor = ctors( opts.dtype );
			if ( ctor === null ) {
				throw new TypeError( 'pdf()::invalid option. Data type option does not have a corresponding array constructor. Option: `' + opts.dtype + '`.' );
			}
			out = new ctor( x.length );
		}
		else {
			out = new Array( x.length );
		}
		if ( opts.accessor ) {
			return pdf3( out, x, opts.mu, opts.sigma, opts.accessor );
		}
		return pdf2( out, x, opts.mu, opts.sigma );
	}
	return NaN;
} // end FUNCTION pdf()

// EXPORTS //

module.exports = pdf;


/**
 * Async Utils
 */
'use strict'

const _ = require('lodash')
const async = require('async')

// async additions
const _ASYNC = {
  "done": done,
  "fail": fail,
  "cb": cb,
  "notcb": notcb,
  "acb": acb
}

_.unset(async, 'default')

// export _ASNYC extras
exports = module.exports = _ASYNC

// mixin extra async methods directly to _ object -> ie. _.cb(fn)
_.mixin(_ASYNC)

// assign async to _ object for convenience -> ie. _.async.map
_.assign(_, { async: async })

// wrap async success callback
function done(result, callback) {
  if (!_.isFunction(callback)) return
  process.nextTick(function () { callback(null, result) })
}

// wrap async error callback
function fail(err, callback) {
  if (!_.isFunction(callback)) return
  let uError = new Error('Unknown Error')
  err = err ? _.isError(err) ? err : _.isString(err) ? new Error(err) : uError : uError
  process.nextTick(function () { callback(err) })
}

// wrap callback
function cb(callback) {
  callback = _.isArguments(callback) ? acb(callback) : callback
  return function (err, result) {
    if (err) return fail(err, callback)
    done(result, callback)
  }
}

// make sure src is not typeof function, otherwise return def (default)
function notcb(src, def) { return !_.isFunction(src) ? src : def || null }

// reverse scans args for [Function] || null
// -> used for grabbing callback from dynamic args
// returns first arg that is typeof function from right to left
function acb(args) {
  args = _.toArray(args)
  return _.find(_.reverse(args), function (arg) { return _.isFunction(arg) }) || function () { }
}