//@tag dom,core
//@define Ext.Element-all
//@define Ext.Element-static
//@require Ext.Element

/**
 * @class Ext.dom.Element
 */
Ext.dom.Element.addStatics({
    numberRe: /\d+$/,
    unitRe: /\d+(px|em|%|en|ex|pt|in|cm|mm|pc)$/i,
    camelRe: /(-[a-z])/gi,
    cssRe: /([a-z0-9-]+)\s*:\s*([^;\s]+(?:\s*[^;\s]+)*);?/gi,
    opacityRe: /alpha\(opacity=(.*)\)/i,
    propertyCache: {},
    defaultUnit: "px",
    borders: {l: 'border-left-width', r: 'border-right-width', t: 'border-top-width', b: 'border-bottom-width'},
    paddings: {l: 'padding-left', r: 'padding-right', t: 'padding-top', b: 'padding-bottom'},
    margins: {l: 'margin-left', r: 'margin-right', t: 'margin-top', b: 'margin-bottom'},

    /**
     * Test if size has a unit, otherwise appends the passed unit string, or the default for this Element.
     * @param {Object} size The size to set.
     * @param {String} units The units to append to a numeric size value.
     * @return {String}
     * @private
     * @static
     */
    addUnits: function(size, units) {
        // Size set to a value which means "auto"
        if (size === "" || size == "auto" || size === undefined || size === null) {
            return size || '';
        }

        // Otherwise, warn if it's not a valid CSS measurement
        if (Ext.isNumber(size) || this.numberRe.test(size)) {
            return size + (units || this.defaultUnit || 'px');
        }
        else if (!this.unitRe.test(size)) {
            //<debug>
            Ext.Logger.warn("Warning, size detected (" + size + ") not a valid property value on Element.addUnits.");
            //</debug>
            return size || '';
        }

        return size;
    },

    /**
     * @static
     * @return {Boolean}
     * @private
     */
    isAncestor: function(p, c) {
        var ret = false;

        p = Ext.getDom(p);
        c = Ext.getDom(c);
        if (p && c) {
            if (p.contains) {
                return p.contains(c);
            } else if (p.compareDocumentPosition) {
                return !!(p.compareDocumentPosition(c) & 16);
            } else {
                while ((c = c.parentNode)) {
                    ret = c == p || ret;
                }
            }
        }
        return ret;
    },

    /**
     * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
     * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
     * @static
     * @param {Number/String} box The encoded margins
     * @return {Object} An object with margin sizes for top, right, bottom and left containing the unit
     */
    parseBox: function(box) {
        if (typeof box != 'string') {
            box = box.toString();
        }

        var parts = box.split(' '),
            ln = parts.length;

        if (ln == 1) {
            parts[1] = parts[2] = parts[3] = parts[0];
        }
        else if (ln == 2) {
            parts[2] = parts[0];
            parts[3] = parts[1];
        }
        else if (ln == 3) {
            parts[3] = parts[1];
        }

        return {
            top: parts[0] || 0,
            right: parts[1] || 0,
            bottom: parts[2] || 0,
            left: parts[3] || 0
        };
    },

    /**
     * Parses a number or string representing margin sizes into an object. Supports CSS-style margin declarations
     * (e.g. 10, "10", "10 10", "10 10 10" and "10 10 10 10" are all valid options and would return the same result)
     * @static
     * @param {Number/String} box The encoded margins
     * @param {String} units The type of units to add
     * @return {String} An string with unitized (px if units is not specified) metrics for top, right, bottom and left
     */
    unitizeBox: function(box, units) {
        var me = this;
        box = me.parseBox(box);

        return me.addUnits(box.top, units) + ' ' +
               me.addUnits(box.right, units) + ' ' +
               me.addUnits(box.bottom, units) + ' ' +
               me.addUnits(box.left, units);
    },

    // @private
    camelReplaceFn: function(m, a) {
        return a.charAt(1).toUpperCase();
    },

    /**
     * Normalizes CSS property keys from dash delimited to camel case JavaScript Syntax.
     * For example:
     *
     * - border-width -> borderWidth
     * - padding-top -> paddingTop
     *
     * @static
     * @param {String} prop The property to normalize
     * @return {String} The normalized string
     */
    normalize: function(prop) {
        // TODO: Mobile optimization?
//        if (prop == 'float') {
//            prop = Ext.supports.Float ? 'cssFloat' : 'styleFloat';
//        }
        return this.propertyCache[prop] || (this.propertyCache[prop] = prop.replace(this.camelRe, this.camelReplaceFn));
    },

    /**
     * Returns the top Element that is located at the passed coordinates
     * @static
     * @param {Number} x The x coordinate
     * @param {Number} y The y coordinate
     * @return {String} The found Element
     */
    fromPoint: function(x, y) {
        return Ext.get(document.elementFromPoint(x, y));
    },

    /**
     * Converts a CSS string into an object with a property for each style.
     *
     * The sample code below would return an object with 2 properties, one
     * for background-color and one for color.
     *
     *     var css = 'background-color: red;color: blue; ';
     *     console.log(Ext.dom.Element.parseStyles(css));
     *
     * @static
     * @param {String} styles A CSS string
     * @return {Object} styles
     */
    parseStyles: function(styles) {
        var out = {},
            cssRe = this.cssRe,
            matches;

        if (styles) {
            // Since we're using the g flag on the regex, we need to set the lastIndex.
            // This automatically happens on some implementations, but not others, see:
            // http://stackoverflow.com/questions/2645273/javascript-regular-expression-literal-persists-between-function-calls
            // http://blog.stevenlevithan.com/archives/fixing-javascript-regexp
            cssRe.lastIndex = 0;
            while ((matches = cssRe.exec(styles))) {
                out[matches[1]] = matches[2];
            }
        }
        return out;
    }
});

//<deprecated product=touch since=2.0>
Ext.dom.Element.addStatics({
    /**
     * Serializes a DOM form into a url encoded string
     * @deprecated 2.0.0 Please see {@link Ext.form.Panel#getValues} instead
     * @param {Object} form The form
     * @return {String} The url encoded form
     */
    serializeForm: function(form) {
        var fElements = form.elements || (document.forms[form] || Ext.getDom(form)).elements,
            hasSubmit = false,
            encoder = encodeURIComponent,
            name,
            data = '',
            type,
            hasValue;

        Ext.each(fElements, function(element) {
            name = element.name;
            type = element.type;

            if (!element.disabled && name) {
                if (/select-(one|multiple)/i.test(type)) {
                    Ext.each(element.options, function(opt) {
                        if (opt.selected) {
                            hasValue = opt.hasAttribute ? opt.hasAttribute('value') : opt.getAttributeNode('value').specified;
                            data += Ext.String.format("{0}={1}&", encoder(name), encoder(hasValue ? opt.value : opt.text));
                        }
                    });
                } else if (!(/file|undefined|reset|button/i.test(type))) {
                    if (!(/radio|checkbox/i.test(type) && !element.checked) && !(type == 'submit' && hasSubmit)) {
                        data += encoder(name) + '=' + encoder(element.value) + '&';
                        hasSubmit = /submit/i.test(type);
                    }
                }
            }
        });

        return data.substr(0, data.length - 1);
    },

    /**
     * Retrieves the document height
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getWindowHeight} instead
     * @static
     * @return {Number} documentHeight
     */
    getDocumentHeight: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getDocumentHeight() is no longer supported. " +
            "Please use Ext.Viewport#getWindowHeight() instead", this);
        //</debug>
        return Math.max(!Ext.isStrict ? document.body.scrollHeight : document.documentElement.scrollHeight, this.getViewportHeight());
    },

    /**
     * Retrieves the document width
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getWindowWidth} instead
     * @static
     * @return {Number} documentWidth
     */
    getDocumentWidth: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getDocumentWidth() is no longer supported. " +
            "Please use Ext.Viewport#getWindowWidth() instead", this);
        //</debug>
        return Math.max(!Ext.isStrict ? document.body.scrollWidth : document.documentElement.scrollWidth, this.getViewportWidth());
    },

    /**
     * Retrieves the viewport height of the window.
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getWindowHeight} instead
     * @static
     * @return {Number} viewportHeight
     */
    getViewportHeight: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getDocumentHeight() is no longer supported. " +
            "Please use Ext.Viewport#getWindowHeight() instead", this);
        //</debug>
        return window.innerHeight;
    },

    /**
     * Retrieves the viewport width of the window.
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getWindowWidth} instead
     * @static
     * @return {Number} viewportWidth
     */
    getViewportWidth: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getDocumentWidth() is no longer supported. " +
            "Please use Ext.Viewport#getWindowWidth() instead", this);
        //</debug>
        return window.innerWidth;
    },

    /**
     * Retrieves the viewport size of the window.
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getSize} instead
     * @static
     * @return {Object} object containing width and height properties
     */
    getViewSize: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getViewSize() is no longer supported. " +
            "Please use Ext.Viewport#getSize() instead", this);
        //</debug>
        return {
            width: window.innerWidth,
            height: window.innerHeight
        };
    },

    /**
     * Retrieves the current orientation of the window. This is calculated by
     * determining if the height is greater than the width.
     * @deprecated 2.0.0 Please use {@link Ext.Viewport#getOrientation} instead
     * @static
     * @return {String} Orientation of window: 'portrait' or 'landscape'
     */
    getOrientation: function() {
        //<debug warn>
        Ext.Logger.deprecate("Ext.Element.getOrientation() is no longer supported. " +
            "Please use Ext.Viewport#getOrientation() instead", this);
        //</debug>
        if (Ext.supports.OrientationChange) {
            return (window.orientation == 0) ? 'portrait' : 'landscape';
        }

        return (window.innerHeight > window.innerWidth) ? 'portrait' : 'landscape';
    }
});
//</deprecated>


/**
 * A mixin which allows a data component to be sorted
 * @ignore
 */
Ext.define('Ext.Sortable', {
    mixins: {
        observable: 'Ext.mixin.Observable'
    },

    requires: ['Ext.util.Draggable'],

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

        /**
         * @cfg {Number} delay
         * How many milliseconds a user must hold the draggable before starting a
         * drag operation.
         * @private
         * @accessor
         */
        delay: 0

        },

    /**
     * @cfg {String} direction
     * Possible values: 'vertical', 'horizontal'.
     */
    direction: 'vertical',

    /**
     * @cfg {String} cancelSelector
     * A simple CSS selector that represents elements within the draggable
     * that should NOT initiate a drag.
     */
    cancelSelector: null,

    // not yet implemented
    //indicator: true,
    //proxy: true,
    //tolerance: null,

    /**
     * @cfg {HTMLElement/Boolean} constrain
     * An Element to constrain the Sortable dragging to.
     * If `true` is specified, the dragging will be constrained to the element
     * of the sortable.
     */
    constrain: window,
    /**
     * @cfg {String} group
     * Draggable and Droppable objects can participate in a group which are
     * capable of interacting.
     */
    group: 'base',

    /**
     * @cfg {Boolean} revert
     * This should NOT be changed.
     * @private
     */
    revert: true,

    /**
     * @cfg {String} itemSelector
     * A simple CSS selector that represents individual items within the Sortable.
     */
    itemSelector: null,

    /**
     * @cfg {String} handleSelector
     * A simple CSS selector to indicate what is the handle to drag the Sortable.
     */
    handleSelector: null,

    /**
     * @cfg {Boolean} disabled
     * Passing in `true` will disable this Sortable.
     */
    disabled: false,

    // Properties

    /**
     * Read-only property that indicates whether a Sortable is currently sorting.
     * @type Boolean
     * @private
     * @readonly
     */
    sorting: false,

    /**
     * Read-only value representing whether the Draggable can be moved vertically.
     * This is automatically calculated by Draggable by the direction configuration.
     * @type Boolean
     * @private
     * @readonly
     */
    vertical: false,

    /**
     * Creates new Sortable.
     * @param {Mixed} el
     * @param {Object} config
     */
    constructor : function(el, config) {
        config = config || {};
        Ext.apply(this, config);

        this.addEvents(
            /**
             * @event sortstart
             * @param {Ext.Sortable} this
             * @param {Ext.event.Event} e
             */
            'sortstart',
            /**
             * @event sortend
             * @param {Ext.Sortable} this
             * @param {Ext.event.Event} e
             */
            'sortend',
            /**
             * @event sortchange
             * @param {Ext.Sortable} this
             * @param {Ext.Element} el The Element being dragged.
             * @param {Number} index The index of the element after the sort change.
             */
            'sortchange'

            // not yet implemented.
            // 'sortupdate',
            // 'sortreceive',
            // 'sortremove',
            // 'sortenter',
            // 'sortleave',
            // 'sortactivate',
            // 'sortdeactivate'
        );

        this.el = Ext.get(el);
        this.callParent();

        this.mixins.observable.constructor.call(this);

        if (this.direction == 'horizontal') {
            this.horizontal = true;
        }
        else if (this.direction == 'vertical') {
            this.vertical = true;
        }
        else {
            this.horizontal = this.vertical = true;
        }

        this.el.addCls(this.baseCls);
        this.startEventName = (this.getDelay() > 0) ? 'taphold' : 'tapstart';
        if (!this.disabled) {
            this.enable();
        }
    },

    // @private
    onStart : function(e, t) {
        if (this.cancelSelector && e.getTarget(this.cancelSelector)) {
            return;
        }
        if (this.handleSelector && !e.getTarget(this.handleSelector)) {
            return;
        }

        if (!this.sorting) {
            this.onSortStart(e, t);
        }
    },

    // @private
    onSortStart : function(e, t) {
        this.sorting = true;
        var draggable = Ext.create('Ext.util.Draggable', t, {
            threshold: 0,
            revert: this.revert,
            direction: this.direction,
            constrain: this.constrain === true ? this.el : this.constrain,
            animationDuration: 100
        });
        draggable.on({
            drag: this.onDrag,
            dragend: this.onDragEnd,
            scope: this
        });

        this.dragEl = t;
        this.calculateBoxes();

        if (!draggable.dragging) {
            draggable.onStart(e);
        }

        this.fireEvent('sortstart', this, e);
    },

    // @private
    calculateBoxes : function() {
        this.items = [];
        var els = this.el.select(this.itemSelector, false),
            ln = els.length, i, item, el, box;

        for (i = 0; i < ln; i++) {
            el = els[i];
            if (el != this.dragEl) {
                item = Ext.fly(el).getPageBox(true);
                item.el = el;
                this.items.push(item);
            }
        }
    },

    // @private
    onDrag : function(draggable, e) {
        var items = this.items,
            ln = items.length,
            region = draggable.region,
            sortChange = false,
            i, intersect, overlap, item;

        for (i = 0; i < ln; i++) {
            item = items[i];
            intersect = region.intersect(item);
            if (intersect) {
                if (this.vertical && Math.abs(intersect.top - intersect.bottom) > (region.bottom - region.top) / 2) {
                    if (region.bottom > item.top && item.top > region.top) {
                        draggable.el.insertAfter(item.el);
                    }
                    else {
                        draggable.el.insertBefore(item.el);
                    }
                    sortChange = true;
                }
                else if (this.horizontal && Math.abs(intersect.left - intersect.right) > (region.right - region.left) / 2) {
                    if (region.right > item.left && item.left > region.left) {
                        draggable.el.insertAfter(item.el);
                    }
                    else {
                        draggable.el.insertBefore(item.el);
                    }
                    sortChange = true;
                }

                if (sortChange) {
                    // We reset the draggable (initializes all the new start values)
                    draggable.reset();

                    // Move the draggable to its current location (since the transform is now
                    // different)
                    draggable.moveTo(region.left, region.top);

                    // Finally lets recalculate all the items boxes
                    this.calculateBoxes();
                    this.fireEvent('sortchange', this, draggable.el, this.el.select(this.itemSelector, false).indexOf(draggable.el.dom));
                    return;
                }
            }
        }
    },

    // @private
    onDragEnd : function(draggable, e) {
        draggable.destroy();
        this.sorting = false;
        this.fireEvent('sortend', this, draggable, e);
    },

    /**
     * Enables sorting for this Sortable.
     * This method is invoked immediately after construction of a Sortable unless
     * the disabled configuration is set to `true`.
     */
    enable : function() {
        this.el.on(this.startEventName, this.onStart, this, {delegate: this.itemSelector, holdThreshold: this.getDelay()});
        this.disabled = false;
    },

    /**
     * Disables sorting for this Sortable.
     */
    disable : function() {
        this.el.un(this.startEventName, this.onStart, this);
        this.disabled = true;
    },

    /**
     * Method to determine whether this Sortable is currently disabled.
     * @return {Boolean} The disabled state of this Sortable.
     */
    isDisabled: function() {
        return this.disabled;
    },

    /**
     * Method to determine whether this Sortable is currently sorting.
     * @return {Boolean} The sorting state of this Sortable.
     */
    isSorting : function() {
        return this.sorting;
    },

    /**
     * Method to determine whether this Sortable is currently disabled.
     * @return {Boolean} The disabled state of this Sortable.
     */
    isVertical : function() {
        return this.vertical;
    },

    /**
     * Method to determine whether this Sortable is currently sorting.
     * @return {Boolean} The sorting state of this Sortable.
     */
    isHorizontal : function() {
        return this.horizontal;
    }
});


/**
*
*	COMPUTE: msum
*
*
*	DESCRIPTION:
*		- Compute a moving sum over an array.
*
*
*	NOTES:
*		[1]
*
*
*	TODO:
*		[1]
*
*
*	LICENSE:
*		MIT
*
*	Copyright (c) 2014-2015. Rebekah Smith.
*
*
*	AUTHOR:
*		Rebekah Smith. rebekahjs17@gmail.com. 2014.
*
*/

'use strict';

// MODULES //

var isArray = require( 'validate.io-array' ),
	isObject = require( 'validate.io-object' ),
	isBoolean = require( 'validate.io-boolean-primitive' ),
	isPositiveInteger = require( 'validate.io-positive-integer' ),
	isFunction = require( 'validate.io-function' );

// MOVING SUM //

/**
* FUNCTION: msum( arr, W[, options] )
*	Computes a moving sum over an array.
*
* @param {Array} arr - input array
* @param {Number} W - size of moving window
* @param {Object} [options] - function options
* @param {Function} [options.accessor] - accessor function for accessing numeric values
* @param {Boolean} [options.copy=true] - boolean indicating whether to return a new array of window sums
* @returns {Array} array of window sum values
*/
function msum( arr, W, options ) {
	var copy = true,
		clbk,
		len,
		sum,
		dropVal,
		out,
		i;
	if ( !isArray( arr ) ) {
		throw new TypeError( 'msum()::invalid input argument. Must provide an array. Value: `' + arr + '`.' );
	}
	if ( !isPositiveInteger( W ) ) {
        throw new TypeError( 'msum()::invalid input argument. Window must be a positive integer. Value: `' + W + '`.' );
    }
	if ( arguments.length > 2 ) {
		if ( !isObject( options ) ) {
			throw new TypeError( 'msum()::invalid input argument. Options must be an object. Value: `' + options + '`.' );
		}
		if ( options.hasOwnProperty( 'accessor' ) ) {
			clbk = options.accessor;
			if ( !isFunction( clbk ) ) {
				throw new TypeError( 'msum()::invalid option. Accessor option must be a function. Value: `' + clbk + '`.' );
			}
		}
		if ( options.hasOwnProperty( 'copy' ) ) {
			copy = options.copy;
			if ( !isBoolean( copy ) ) {
				throw new TypeError( 'msum()::invalid option. Copy option must be a boolean primitive. Value: `' + copy + '`.' );
			}
		}
	}
	if ( W > arr.length ) {
		throw new Error( 'msum()::invalid input argument. Window cannot exceed the array length.' );
	}
	len = arr.length;
	sum = 0;

	if ( copy && !clbk ) {
		// Case 1: numeric array and return a copy...
		len = len - W + 1;
		out = new Array( len );

		// Calculate the sum for the first window...
		for ( i = 0; i < W; i++ ) {
			sum += arr[ i ];
		}
		out[ 0 ] = sum;

		// Calculate sums for the remaining windows...
		for ( i = 0; i < len-1; i++ ){
			sum += arr[ i+W ] - arr[ i ];
			out[ i+1 ] = sum;
		}
		return out;
	}
	else if ( clbk ) {
		if ( copy ) {
			// Case 2: non-numeric array and return a copy...
			out = new Array( len );
			for ( i = 0; i < len; i++ ){
				out[ i ] = clbk( arr[i] );
			}
		} else {
			// Case 3: non-numeric array and mutate the input array...
			out = arr;
			for ( i = 0; i < len; i++ ) {
				out[ i ] = clbk( arr[i] );
			}
		}
	}
	else {
		// Case 4: numeric array and mutate the input array...
		out = arr;
	}
	len = len - W + 1;

	// Calculate the sum for the first window...
	for ( i = 0; i < W; i++ ) {
		sum += out[ i ];
	}
	dropVal = out[ 0 ];
	out[ 0 ] = sum;

	// Calculate sums for the remaining windows...
	for ( i = 1; i < len; i++ ){
		sum += out[ i+W-1 ] - dropVal;
		dropVal = out[ i ];
		out[ i ] = sum;
	}
	// Trim the output array:
	out.length = len;
	return out;
} // end FUNCTION msum()

// EXPORTS //

module.exports = msum;


var mime = require('simple-mime')();
var parseRange = require('range-parser');
var parseURL = require('./url').parse;
var unsupported = require('./utils').unsupported;
var requestURL = require('./utils').requestURL;
var get = require('./get').get;

/**
 * Take formatted options and creates an Express style `req` object.
 * Takes an url `String` or an already parsed (via `./lib/url`.parse) object.
 *
 * @param {GhostTrain} ghosttrain
 * @param {Route} route
 * @param {Object|String} url
 * @param {Object} options
 * @return {Request}
 */

function Request (ghosttrain, route, url, options) {
  // Headers info
  this.headers = options.headers || {};

  // Allows us to check protocol of client-side requests,
  // but relative requests won't have a protocol
  var protocol = 'window' in this ? window.location.protocol : '';

  // Expose URL properties
  var parsedURL = url.pathname ? url : parseURL(url, true);
  this.path = parsedURL.pathname;
  this.query = parsedURL.query;
  this.protocol = (parsedURL.protocol || protocol).replace(':', '');
  this.secure = this.protocol === 'https';

  this.route = route;
  this.method = route.method.toUpperCase();
  this.url = this.originalUrl = requestURL(parsedURL);
  this.params = route.params;
  this.body = options.body || {};
  this.headers = options.headers || {};

  this.xhr = 'xmlhttprequest' === (this.get('X-Requested-With') || '').toLowerCase();
}
module.exports = Request;

/**
 *
 * Parse Range header field,
 * capping to the given `size`.
 *
 * Unspecified ranges such as "0-" require
 * knowledge of your resource length. In
 * the case of a byte range this is of course
 * the total number of bytes. If the Range
 * header field is not given `null` is returned,
 * `-1` when unsatisfiable, `-2` when syntactically invalid.
 *
 * NOTE: remember that ranges are inclusive, so
 * for example "Range: users=0-3" should respond
 * with 4 users when available, not 3.
 *
 * @param {Number} size
 * @return {Array}
 */

Request.prototype.range = function(size){
  var range = this.get('Range');
  if (!range) return;
  return parseRange(size, range);
};

/**
 * Check if the incoming request contains the "Content-Type"
 * header field, and it contains the give mime `type`.
 *
 * Examples:
 *
 * // With Content-Type: text/html; charset=utf-8
 * req.is('html');
 * req.is('text/html');
 * req.is('text/*');
 * // => true
 *
 * // When Content-Type is application/json
 * req.is('json');
 * req.is('application/json');
 * req.is('application/*');
 * // => true
 *
 * req.is('html');
 * // => false
 *
 * @param {String} type
 * @return {Boolean}
 */

Request.prototype.is = function (type) {
  var ct = this.get('Content-Type');
  if (!type) return false;
  if (!ct) return false;
  ct = ct.split(';')[0];
  if (!~type.indexOf('/')) type = mime(type);
  if (~type.indexOf('*')) {
    type = type.split('/');
    ct = ct.split('/');
    if ('*' == type[0] && type[1] == ct[1]) return true;
    if ('*' == type[1] && type[0] == ct[0]) return true;
    return false;
  }
  return !!~ct.indexOf(type);
};

/**
 * Return request header `name`.
 *
 * @param {String} name
 * @return {String}
 */

Request.prototype.get = Request.prototype.header = function (name) {
  return this.headers[name];
};

/**
 * Return the value of param `name` when present or `defaultValue`.
 *
 *   - Checks route placeholders, ex: _/user/:id_
 *   - Checks body params, ex: id=12, {"id":12}
 *   - Checks query string params, ex: ?id=12
 *
 * To utilize request bodies, `req.body`
 * should be an object. This can be done by using
 * the `connect.bodyParser()` middleware.
 *
 * @param {String} name
 * @param {Mixed} [defaultValue]
 * @return {String}
 **/

Request.prototype.param = function (name, defaultValue) {
  var params = this.params || {};
  var body = this.body || {};
  var query = this.query || {};
  if (null != params[name] && params.hasOwnProperty(name)) return params[name];
  if (null != body[name]) return body[name];
  if (null != query[name]) return query[name];
  return defaultValue;
};

/**
 * Set up unsupported functions
 */
['accepts', 'acceptsEncoding', 'acceptsCharset', 'acceptsLanguage'].forEach(function (prop) {
  Request.prototype[prop] = unsupported('req.' + prop + '()');
});

/**
 * Set up unsupported getters; on browsers that do not support getters, just
 * ignore and do not throw errors as this is only a "nice to have" feature
 */
['subdomains', 'stale', 'fresh', 'ip', 'ips', 'auth',
   'accepted', 'acceptedEncodings', 'acceptedCharsets',
   'acceptedLanguages'].forEach(function (prop) {
  get(Request.prototype, prop, unsupported('req.' + prop));
});


/** @module patch
 *
 */
 const utils = require('./utils');
 const ops = require('./operations');
 const Options = require('./options');
 const debug = require('debug')('typed-patch~patch');

 'use strict';

/** Patch for objects
 *
 * Functions that should allow us to compare two objects, compute a difference, send the difference over the wire, and apply a patch at the far end
 * to sync a remote object with the changes.
 *
 * For 'patch' to work properly, the patched object (and child objects) should have a static 'fromJSON' method somewhere in its inheritance tree.
 * You may also implement a static 'getAttrProps(name)' object, which may return a variety of options that are applied to
 * child properties with the given name (see {@link DEFAULT_OPTIONS}).
 *
 * Won't work with cyclic data structures.
 */

 /** Compare two Map-like objects.
 */
 function _compareMaps(a,b, options) {

    debug(a,b);

    let alength = a.size === undefined ? a.length : a.size;
    let blength = b.size === undefined ? b.length : b.size;

    if (alength === 0 && blength === 0) return ops.NOP;
    // ops.Map([...b]) and not just ops.Rpl(b) because if b is a map it's not good JSON.
    if (alength === 0 || blength === 0) return new ops.Map([...b]);

    let patch = [];

    if (!options.sorted) {
        a = Array.from(a).sort((a,b) => utils.compare(a,b,options));
        b = Array.from(b).sort((a,b) => utils.compare(a,b,options));
    }

    let ai = 1, bi = 1;
    let ao = a[0], bo = b[0]
    let element_options = options.getArrayElementOptions();

    do {
        let comparison = utils.compare(ao,bo,options);
        debug("comparing items", ao, bo, comparison);
        if (comparison < 0) {
            debug('skip');
            ao = a[ai++];
        } else if (comparison > 0) {
            debug('insert');
            patch.push([options.key(bo), new ops.Ins(options.value(bo))]);
            bo = b[bi++];
        } else {
            if (options.value(ao) !== options.value(bo)) {
                let element_patch = compare(options.value(ao), options.value(bo), element_options)
                if (element_patch != ops.NOP) patch.push([options.key(bo), element_patch]);
            }
            else debug('skip2');
            ao = a[ai++];
            bo = b[bi++];
        }
    } while (ai <= a.length && bi <= b.length);

    while (ai <= a.length) {
        patch.push([options.key(ao), ops.DEL]);
        ao=a[ai++];
    }

    while (bi <= b.length) {
        patch.push([options.key(bo), new ops.Ins(options.value(bo))]);
        bo=b[bi++];
    }

    return new ops.Map(patch);
}

function _compareArrays(a,b,options) {
    debug('_compareArrays %j, %j, options: %j',a,b,options)

    let element_options = options.getArrayElementOptions();
    let result = [];

    utils.diff(a,b,
            (add, index)    => { result.push([index+1, new ops.Ins(add)]); },
            (remove, index) => { result.push([index, ops.DEL]); },
            (a,b, index)   => {
                let patch = compare(a,b,element_options);
                if (patch !== ops.NOP) result.push([index, patch]);
            },
            element_options.identity
        );

    return new ops.Arr(result);
}

function _compareObjects(a,b,options) {

    let data = {};
    let akeys = Object.getOwnPropertyNames(a);
    let bkeys = Object.getOwnPropertyNames(b);

    for (let akey of akeys) {
        if (a[akey] !== b[akey]) data[akey] = compare(a[akey], b[akey], options.getChildOptions(a,akey));
    }
    for (let bkey of bkeys)
        if (a[bkey] === undefined) data[bkey] = compare(undefined, b[bkey], options.getChildOptions(b,bkey));

    return new ops.Mrg(data);
}

/** Compare two object to produce a patch object.
 *
 * @param a First object for comparison
 * @param b Second object for comparison
 * @param options (optional) options to control comparison operation (see {@link DEFAULT_OPTIONS})
 */
 function compare(a,b,options) {
    debug('compare %j,%j options: %j', a, b, options);

    options = Options.addDefaults(options);

    debug('compare - options %j', options);
    if (a === b)
        return ops.NOP;
    if (b === undefined)
        return ops.DEL;
    if (a === undefined)
        return new ops.Rpl(b);

    if (typeof a === 'object' && typeof b === 'object') {
        if (utils.isArrayLike(a) && utils.isArrayLike(b)) {
            if (options.map) {
                return _compareMaps(a,b,options);
            } else  {
                return _compareArrays(a,b,options);
            }
        } else if (a instanceof Map && b instanceof Map) {
            return _compareMaps(a,b,options);
        } else if (a.constructor === b.constructor) { // This isn't quite right, we can merge objects with a common base class
            return _compareObjects(a,b,options);
        }
    }

    return new ops.Rpl(b);
}

/** Convert over-the-wire JSON format back into typed patch object
*/
function fromJSON(object) {
        if (object instanceof ops.Op) return object; // If already patch, return it
        if (object === undefined) return ops.NOP;
        if (object.op) {
            if (object.op === ops.Rpl.name)
                return new ops.Rpl(object.data);
            if (object.op === ops.Ins.name)
                return new ops.Ins(object.data);
            else if (object.op === ops.NOP.name)
                return ops.NOP;
            else if (object.op === ops.DEL.name)
                return ops.DEL;
            else if (object.op === ops.Mrg.name)
                return new ops.Mrg(utils.map(object.data, fromJSON));
            else if (object.op === ops.Map.name)
                return new ops.Map(object.data.map(([key,op]) => [key, fromJSON(op)]));
            else if (object.op === ops.Arr.name)
                return new ops.Arr(object.data.map(([key,op]) => [key, fromJSON(op)]));
            else throw new Error('unknown diff.op ' + object.op);
        } else {
            return new ops.Rpl(object);
        }
}

    /** the public API of this module. */
    module.exports = { compare, fromJSON };


/**
 * Represents a collection of a set of key and value pairs. Each key in the MixedCollection must be unique, the same key
 * cannot exist twice. This collection is ordered, items in the collection can be accessed by index or via the key.
 * Newly added items are added to the end of the collection. This class is similar to {@link Ext.util.HashMap} however
 * it is heavier and provides more functionality. Sample usage:
 *
 *     @example
 *     var coll = new Ext.util.MixedCollection();
 *     coll.add('key1', 'val1');
 *     coll.add('key2', 'val2');
 *     coll.add('key3', 'val3');
 *
 *     alert(coll.get('key1')); // 'val1'
 *     alert(coll.indexOfKey('key3')); // 2
 *
 * The MixedCollection also has support for sorting and filtering of the values in the collection.
 *
 *     @example
 *     var coll = new Ext.util.MixedCollection();
 *     coll.add('key1', 100);
 *     coll.add('key2', -100);
 *     coll.add('key3', 17);
 *     coll.add('key4', 0);
 *     var biggerThanZero = coll.filterBy(function(value){
 *         return value > 0;
 *     });
 *     alert(biggerThanZero.getCount()); // 2
 */
Ext.define('Ext.util.MixedCollection', {
    extend: 'Ext.util.AbstractMixedCollection',
    mixins: {
        sortable: 'Ext.util.Sortable'
    },

    /**
     * @event sort
     * Fires whenever MixedCollection is sorted.
     * @param {Ext.util.MixedCollection} this
     */

    constructor: function() {
        var me = this;
        me.callParent(arguments);
        me.mixins.sortable.initSortable.call(me);
    },

    doSort: function(sorterFn) {
        this.sortBy(sorterFn);
    },

    /**
     * @private
     * Performs the actual sorting based on a direction and a sorting function. Internally,
     * this creates a temporary array of all items in the MixedCollection, sorts it and then writes
     * the sorted array data back into `this.items` and `this.keys`.
     * @param {String} property Property to sort by ('key', 'value', or 'index')
     * @param {String} [dir=ASC] (optional) Direction to sort 'ASC' or 'DESC'.
     * @param {Function} fn (optional) Comparison function that defines the sort order.
     * Defaults to sorting by numeric value.
     */
    _sort: function(property, dir, fn){
        var me = this,
            i, len,
            dsc   = String(dir).toUpperCase() == 'DESC' ? -1 : 1,

            //this is a temporary array used to apply the sorting function
            c     = [],
            keys  = me.keys,
            items = me.items;

        //default to a simple sorter function if one is not provided
        fn = fn || function(a, b) {
            return a - b;
        };

        //copy all the items into a temporary array, which we will sort
        for(i = 0, len = items.length; i < len; i++){
            c[c.length] = {
                key  : keys[i],
                value: items[i],
                index: i
            };
        }

        //sort the temporary array
        Ext.Array.sort(c, function(a, b){
            var v = fn(a[property], b[property]) * dsc;
            if(v === 0){
                v = (a.index < b.index ? -1 : 1);
            }
            return v;
        });

        //copy the temporary array back into the main this.items and this.keys objects
        for(i = 0, len = c.length; i < len; i++){
            items[i] = c[i].value;
            keys[i]  = c[i].key;
        }

        me.fireEvent('sort', me);
    },

    /**
     * Sorts the collection by a single sorter function.
     * @param {Function} sorterFn The function to sort by.
     */
    sortBy: function(sorterFn) {
        var me     = this,
            items  = me.items,
            keys   = me.keys,
            length = items.length,
            temp   = [],
            i;

        //first we create a copy of the items array so that we can sort it
        for (i = 0; i < length; i++) {
            temp[i] = {
                key  : keys[i],
                value: items[i],
                index: i
            };
        }

        Ext.Array.sort(temp, function(a, b) {
            var v = sorterFn(a.value, b.value);
            if (v === 0) {
                v = (a.index < b.index ? -1 : 1);
            }

            return v;
        });

        //copy the temporary array back into the main this.items and this.keys objects
        for (i = 0; i < length; i++) {
            items[i] = temp[i].value;
            keys[i]  = temp[i].key;
        }

        me.fireEvent('sort', me, items, keys);
    },

    /**
     * Reorders each of the items based on a mapping from old index to new index. Internally this just translates into a
     * sort. The `sort` event is fired whenever reordering has occured.
     * @param {Object} mapping Mapping from old item index to new item index.
     */
    reorder: function(mapping) {
        var me = this,
            items = me.items,
            index = 0,
            length = items.length,
            order = [],
            remaining = [],
            oldIndex;

        me.suspendEvents();

        //object of {oldPosition: newPosition} reversed to {newPosition: oldPosition}
        for (oldIndex in mapping) {
            order[mapping[oldIndex]] = items[oldIndex];
        }

        for (index = 0; index < length; index++) {
            if (mapping[index] == undefined) {
                remaining.push(items[index]);
            }
        }

        for (index = 0; index < length; index++) {
            if (order[index] == undefined) {
                order[index] = remaining.shift();
            }
        }

        me.clear();
        me.addAll(order);

        me.resumeEvents();
        me.fireEvent('sort', me);
    },

    /**
     * Sorts this collection by **key**s.
     * @param {String} [dir=ASC] Sorting direction: 'ASC' or 'DESC'.
     * @param {Function} [fn] Comparison function that defines the sort order. Defaults to sorting by case insensitive
     * string.
     */
    sortByKey: function(dir, fn){
        this._sort('key', dir, fn || function(a, b){
            var v1 = String(a).toUpperCase(), v2 = String(b).toUpperCase();
            return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0);
        });
    }
});


'use strict';

// MODULES //

var isMatrixLike = require( 'validate.io-matrix-like' ),
	ctors = require( 'compute-array-constructors' ),
	validate = require( './validate.js' );

// TRANSPOSE //

/**
* FUNCTION: transpose( matrix[, options] )
*	Computes the transpose of a matrix.
*
* @param {Matrix} matrix - input matrix
* @param {Object} [options] - function options
* @param {Boolean} [options.copy=true] - boolean indicating whether to return a new Matrix instance
* @returns {Matrix} matrix transpose
*/
function transpose( mat, options ) {
	/* jshint newcap:false */
	var strides,
		shape,
		opts,
		ctor,
		err,
		tmp,
		d;

	if ( !isMatrixLike( mat ) ) {
		throw new TypeError( 'transpose()::invalid input argument. First argument must be a matrix. Value: `' + mat + '`.' );
	}
	opts = {};
	if ( arguments.length > 1 ) {
		err = validate( opts, options );
		if ( err ) {
			throw err;
		}
	} else {
		opts.copy = true;
	}
	if ( opts.copy ) {
		// Copy the matrix data to a new typed array:
		ctor = ctors( mat.dtype );
		d = new ctor( mat.data );

		// Swap the dimensions:
		shape = [
			mat.shape[ 1 ],
			mat.shape[ 0 ]
		];

		// Swap the strides:
		strides = [
			mat.strides[ 1 ],
			mat.strides[ 0 ]
		];

		// Return a new matrix:
		return new mat.constructor( d, mat.dtype, shape, mat.offset, strides );
	} else {
		// Swap the dimensions...
		tmp = mat.shape[ 0 ];
		mat.shape[ 0 ] = mat.shape[ 1 ];
		mat.shape[ 1 ] = tmp;

		// Swap the strides...
		tmp = mat.strides[ 0 ];
		mat.strides[ 0 ] = mat.strides[ 1 ];
		mat.strides[ 1 ] = tmp;

		// Return the matrix transpose:
		return mat;
	}
} // end FUNCTION transpose()

// EXPORTS //

module.exports = transpose;


var util = require('util')
  , events = require('events')
  , kue = require('kue')
  ;

/*
 * We use a queue to process events when we must handle the events sequentially
 */

function Queue (q) {
  events.EventEmitter.call(this);
  var self = this;

  this.q = q && q.process ? q : kue.createQueue();
  this.q.process('message', function (message, done) {
    self.emit('message', message);
    done();
  });
}

util.inherits(Queue, events.EventEmitter);

/**
 * Makes a Queue
 *
 * @return Queue
 */

Queue.make = function (q) {
  return new Queue(q);
};

/**
 * Sends a message to the queue
 *
 * @param object message
 * @return Queue
 */

Queue.prototype.send = function (message) {
  this.q.create('message', message).save();
  return this;
};

module.exports = Queue;


'use strict'

/**
 * imports.
 */

var array = require('to-array.js')

/**
 * exports.
 */

module.exports = apply

/**
 * Invoke .apply if value is a function, otherwise, return default value.
 *
 * @param  {Function|*} func
 * Function to be invoked.
 *
 * @param  {Array|*} [args]
 * Arguments to apply to function.
 *
 * @param  {*} [self]
 * `this` value.
 *
 * @return {*}
 * Result of applying function or default value.
 */

function apply (func, args, self) {
  return (typeof func === 'function')
    ? func.apply(self, array(args))
    : func
}


/*jslint esversion: 6, -W097, browser: true, strict: implied, -W034 */
/* globals console */
'use strict';

// detecting user device

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.detectDeviceClass = detectDeviceClass;
exports.detectBrowserClass = detectBrowserClass;
exports.supportsTransitions = supportsTransitions;
exports.detectWindowWidth = detectWindowWidth;
var isMobile = exports.isMobile = {
    Android: function Android() {
        return !!(navigator.userAgent.match(/Android/i) && navigator.userAgent.match(/Mobile/i));
    },
    BlackBerry: function BlackBerry() {
        return !!navigator.userAgent.match(/BlackBerry|BB10/i);
    },
    iOS: function iOS() {
        return !!navigator.userAgent.match(/iPhone|iPod/i);
    },
    Opera: function Opera() {
        return !!navigator.userAgent.match(/Opera Mini/i);
    },
    Windows: function Windows() {
        return !!navigator.userAgent.match(/IEMobile/i);
    },
    any: function any() {
        var mobileChecks = [isMobile.Android(), isMobile.BlackBerry(), isMobile.iOS(), isMobile.Opera(), isMobile.Windows()];
        return mobileChecks.some(function (x) {
            return !!x;
        });
    }
};

var isBrowser = exports.isBrowser = {
    Firefox: function Firefox() {
        return !!navigator.userAgent.match(/Firefox/i);
    },
    Webkit: function Webkit() {
        return !!navigator.userAgent.match(/Webkit/i);
    }
};

var isTablet = exports.isTablet = {
    Android: function Android() {
        return !!(navigator.userAgent.match(/Android/i) && !navigator.userAgent.match(/Mobile/i));
    },
    iOS: function iOS() {
        return !!navigator.userAgent.match(/iPad/i);
    },
    any: function any() {
        return !!(isTablet.iOS() || isTablet.Android());
    }
};

// Following functions are not executed on import anymore
function detectDeviceClass() {
    var body = document.body;
    if (isMobile.any()) {
        body.classList.add('mobile');
    }

    if (isMobile.Android()) {
        body.classList.add('android');
    }

    if (isTablet.any()) {
        body.classList.add('tablet');
    }
}

function detectBrowserClass() {
    var body = document.body;
    if (isBrowser.Firefox()) {
        body.classList.add('is-firefox');
    }
}

function supportsTransitions() {
    var doc = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : document;

    var b = doc.body || doc.documentElement;
    var s = b.style;
    var propName = 'transition';
    var capitalizedPropName = propName.charAt(0).toUpperCase() + propName.substr(1);
    var prefixes = ['Moz', 'webkit', 'Webkit', 'Khtml', 'O', 'ms'];

    if (typeof s[propName] === 'string') {
        return true;
    }

    // Tests for vendor specific prop
    for (var i = 0; i < prefixes.length; i++) {
        var prefix = prefixes[i];
        var vendorPropName = prefix + capitalizedPropName;
        if (typeof s[vendorPropName] === 'string') {
            return true;
        }
    }
    return false;
}

// This function can be triggered on window resize
function detectWindowWidth() {
    var mobileWidth = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 730;

    var isMobileWidth = window.innerWidth < mobileWidth;
    var body = document.body;
    if (isMobileWidth) {
        body.classList.add('is-mobile-width');
    } else {
        body.classList.remove('is-mobile-width');
    }
}


Ext.data.CSV = Ext.extend(Ext.data.Config, {

	v: undefined, // array of change stamps

	constructor: function(config,callback,scope) {
		var changed= false;
		if (config) {
			config.config_id= 'csv';
			Ext.data.CSV.superclass.constructor.call(this, config);
			if (config.v) {
				this.v= [];
				this.do_add(config.v);
			}
		}
		if (this.v===undefined) {
			this.v= [];
			changed= true;
		}
		this.writeAndCallback(changed,callback,scope);
	},

	add: function(x,callback,scope) {
		var changed= this.do_add(x);
		this.writeAndCallback(changed,callback,scope);
		return this; // JCM should force use of callback?
	},

	get: function(cs) {
		return this.v[cs.r];
	},

	setReplicaNumber: function(replica_number,callback,scope) {
		this.addReplicaNumbers([replica_number],callback,scope);
	},

	addReplicaNumbers: function(x,callback,scope) {
		var t= [];
		if (x instanceof Array) {
			t= Ext.data.array.collect(x,function(r){return this.do_add(new Ext.data.CS({r:r}));},this);
		} else if (x instanceof Ext.data.CSV) {
			t= Ext.data.array.collect(x.v,function(cs){return this.do_add(new Ext.data.CS({r:cs.r}));},this);
		}
		var changed= Ext.data.array.includes(t,true);
		this.writeAndCallback(changed,callback,scope);
	},

	do_add: function(x) { // CSV, CS, '1-2-3', [x]
		var changed= false;
		if (x instanceof Ext.data.CSV) {
			var t= Ext.data.array.collect(x.v,this.do_add,this);
			changed= Ext.data.array.includes(t,true);
		} else if (x instanceof Ext.data.CS) {
			var r= x.r;
			var t= this.v[r];
			if (!t || x.greaterThan(t)) {
			  this.v[r]= new Ext.data.CS({r:x.r,t:x.t,s:x.s});
				changed= true;
			}
		} else if (typeof x == 'string' || x instanceof String) {
			changed= this.do_add(new Ext.data.CS(x));
		} else if (x instanceof Array) {
			var t= Ext.data.array.collect(x,this.do_add,this);
			changed= Ext.data.array.includes(t,true);
		} else {
			throw "Error - CSV - do_add - Unknown type: "+(typeof x)+": "+x;
		}
		return changed;
	},

	changeReplicaNumber: function(old_replica_number,new_replica_number,callback,scope) {
		var t= this.v[old_replica_number];
		var changed= false;
		if (t) {
			t.r= new_replica_number;
			this.v[old_replica_number]= undefined;
			this.v[new_replica_number]= t;
			changed= true;
		}
		this.writeAndCallback(changed,function(){
			callback.call(scope,this,changed);
		},this);
	},

	isEmpty: function() {
		return this.v.length<1;
	},

	maxChangeStamp: function() {
		if (!this.isEmpty()) {
			var r= new Ext.data.CS();
			this.v.forEach(function(cs){
				var t= new Ext.data.CS({t:cs.t,s:cs.s});
				r= (t.greaterThan(r) ? cs : r);
			},this);
			return r;
		}
	},

	dominates: function(x) {
		return Ext.data.array.any(this.compare(x),function(i){ return i>0; });
	},

	equals: function(x) {
		return Ext.data.array.all(this.compare(x),function(i){ return i===0; });
	},

	compare: function(x) {
        var i, cs, r;
		if (x instanceof Ext.data.CS) {
			cs= this.get(x);
			return [cs ? cs.compare(x) : -1];
		} else if (x instanceof Ext.data.CSV) {
			r= [];
			for(i in this.v) {
				cs= this.v[i];
				if (cs instanceof Ext.data.CS) {
					var cs2= x.get(cs);
					r.push(cs2 ? cs.compare(cs2) : 1);
				}
			}
			return r;
		} else {
			throw "Error - CSV - compare - Unknown type: "+(typeof x)+": "+x;
		}
		return [-1];
	},

	forEach: function(fn,scope) {
		this.v.forEach(fn,scope||this);
	},

	encode: function() { // for the wire
		return Ext.data.array.collect(this.v,function(){
			// JCM can we safely ignore replicas with CS of 0... except for the highest known replica number...
			return this.to_s();
		});
	},

	decode: function(x) { // from the wire
		this.do_add(x);
		return this;
	},

	to_s: function(indent) {
		var r= "CSV: ";
		this.v.forEach(function(cs){
			r+= cs.to_s()+", ";
		},this);
		return r;
	},

	as_data: function() { // for the disk
		var data= {
			v: Ext.data.array.collect(this.v,function(){return this.to_s();})
		};
		data[Ext.data.SyncModel.MODEL]= 'Ext.data.CSV';
		return Ext.data.CSV.superclass.as_data.call(this, data);
	}

});


'use strict';
/*!
 * Dependencies
 * --------------------------------------------------------------------------*/

var Promise  = require('bluebird');
var mongoose = require('mongoose');
var _ = require('lodash');

var findByIdAsync = Promise.promisify(mongoose.Model.findById);
var findAsync = Promise.promisify(mongoose.Model.find);

/*!
 * Plugin
 * --------------------------------------------------------------------------*/

exports = module.exports = function(schema) {
  _.each(schema.tree, _.partial(addVirtualRef, schema));
};

/**
 * Checks if the schema's property at path is a ref - or an Array of refs - and,
 * if yes, adds a virtual property suffixed with 'P', which returns a promise to
 * the reference's query result(s).
 *
 * @param {Schema} schema The node's schema, to add the virtuals into
 * @param {Object} node The tree node to add a virtual ref for
 * @param {String} path The tree node's path to infer the virtual's name
 */

function addVirtualRef(schema, node, path) {
  if(_.isArray(node) && isRef(_.first(node))) {
    schema.virtual(path + 'P').get(function() {
      if(!this[path] || !this[path].length)
        return Promise.resolve([]);

      var model = this.constructor.db.model(_.first(node).ref);
      return findAsync.call(model, {
        $or: _.map(this[path], function(id) { return { _id: id }; })
      });
    });
  }
  else if(isRef(node)) {
    schema.virtual(path + 'P').get(function() {
      var model = this.constructor.db.model(node.ref);
      return findByIdAsync.call(model, this[path]);
    });
  }
}

/**
 * Checks if a schema property is a 'ref' to another document.
 *
 * @param {Object} sprop
 * @return {Boolean}
 */

function isRef(sprop) {
  return !!sprop && !!sprop.ref;
}


/**
*
*	TOPICAL
*
*
*	DESCRIPTION:
*		- PubSub library.
*
*
*	NOTES:
*		[1]
*
*
*	TODO:
*		[1]
*
*
*	LICENSE:
*		MIT
*
*	Copyright (c) 2014. Athan Reines.
*
*
*	AUTHOR:
*		Athan Reines. kgryte@gmail.com.
*
*/

'use strict';

// MODULES //

var // Event-emitter class:
	EventEmitter = require( 'events' ).EventEmitter,

	// Validates if a value is an integer:
	isInteger = require( 'validate.io-integer' ),

	// Validates if a value is an object:
	isObject = require( 'validate.io-object' );

// FUNCTIONS //

/**
* FUNCTION: createTopic( options )
*	Creates a new topic.
*
* @private
* @param {Object} options - topic options
* @returns {Object} topic
*/
function createTopic( options ) {
	var max = Number.MAX_VALUE,
		duplicates = false;
	if ( !options.hasOwnProperty( 'max' )  ) {
		options.max = max;
	} else {
		max = options.max;
	}
	if ( !options.hasOwnProperty( 'duplicates' ) ) {
		options.duplicates = duplicates;
	} else {
		duplicates = options.duplicates;
	}
	if ( !isInteger( max ) || max < 0 ) {
		throw new TypeError( 'createTopic()::invalid option. Max subscribers must be an integer greater than or equal to 0.' );
	}
	if ( typeof duplicates !== 'boolean' ) {
		throw new TypeError( 'createTopic()::invalid option. Duplicates flag must be a boolean.' );
	}
	return {
		'subscribers': [],
		'options': options
	};
} // end FUNCTION createTopic()

// TOPICAL //

/**
* FUNCTION: Topical()
*	Topical constructor.
*
* @constructor
* @returns {Topical} Topical instance
*/
function Topical() {
	if ( !( this instanceof Topical ) ) {
		return new Topical();
	}
	EventEmitter.call( this );
	this._topics = {};
	this._listing = [];
	return this;
} // end FUNCTION Topical()

/**
* Create a prototype which inherits from the parent prototype.
*/
Topical.prototype = Object.create( EventEmitter.prototype );

/**
* Set the constructor.
*/
Topical.prototype.constructor = Topical;

/**
* METHOD: topics()
*	Returns the list of topics.
*
* @return {Array} list of topics
*/
Topical.prototype.topics = function() {
	return Object.keys( this._topics );
}; // end METHOD topics()

/**
* METHOD: add( topic[, options] )
*	Adds a topic.
*
* @param {String} topic - topic to add
* @param {Object} [options] - topic options
* @returns {Topical} Topical instance
*/
Topical.prototype.add = function( topic, options ) {
	var topics = this._topics;
	if ( typeof topic !== 'string' ) {
		throw new TypeError( 'add()::invalid input argument. Topic must be a string.' );
	}
	if ( topics.hasOwnProperty( topic ) ) {
		return this;
	}
	if ( arguments.length < 2 ) {
		options = {};
	} else {
		if ( !isObject( options ) ) {
			throw new TypeError( 'add()::invalid input argument. Options must be an object.' );
		}
	}
	topics[ topic ] = createTopic( options );
	this.emit( 'add', {
		'topic': topic,
		'data': null
	});
	return this;
}; // end METHOD add()

/**
* METHOD: remove( topic )
*	Removes a topic and all associated subscribers.
*
* @param {String|RegExp} topic - topic or topic pattern to remove
* @returns {Topical} Topical instance
*/
Topical.prototype.remove = function( topic ) {
	var topics = this._topics,
		type = typeof topic,
		matches = [],
		keys,
		key;
	if ( type !== 'string' && !( topic instanceof RegExp ) ) {
		throw new TypeError( 'remove()::invalid input argument. Topic must be either string or regular expression.' );
	}
	if ( type === 'string' ) {
		if ( !topics.hasOwnProperty( topic ) ) {
			return this;
		}
		matches.push( topic );
	} else {
		keys = Object.keys( topics );
		for ( var i = 0; i < keys.length; i++ ) {
			key = keys[ i ];
			if ( topic.test( key ) ) {
				matches.push( key );
			}
		}
	}
	for ( var j = 0; j < matches.length; j++ ) {
		topic = matches[ j ];
		delete topics[ topic ];
		this.emit( 'remove', {
			'topic': topic,
			'data': null
		});
	}
	return this;
}; // end METHOD remove()

/**
* METHOD: list( clbk )
*	Subscribes a callback to public broadcasts.
*
* @param {Function} clbk - callback to subscribe
* @returns {Topical} Topical instance
*/
Topical.prototype.list = function( clbk ) {
	var list = this._listing,
		idx;
	if ( typeof clbk !== 'function' ) {
		throw new TypeError( 'list()::invalid input argument. Must provide a function.' );
	}
	idx = list.indexOf( clbk );
	if ( idx === -1 ) {
		list.push( clbk );
		this.emit( 'list', {
			'topic': null,
			'data': clbk
		});
	}
	return this;
}; // end METHOD list()

/**
* METHOD: unlist( clbk )
*	Unsubscribes a callback from public broadcasts.
*
* @param {Function} clbk - callback to unsubscribe
* @returns {Topical} Topical instance
*/
Topical.prototype.unlist = function( clbk ) {
	var list = this._listing,
		idx;
	if ( typeof clbk !== 'function' ) {
		throw new TypeError( 'unlist()::invalid input argument. Must provide a function.' );
	}
	idx = list.indexOf( clbk );
	if ( idx !== -1 ) {
		list.splice( idx, 1 );
		this.emit( 'unlist', {
			'topic': null,
			'data': clbk
		});
	}
	return this;
}; // end METHOD unlist()

/**
* METHOD: subscribe( topic, clbk )
*	Subscribes a callback to a topic.
*
* @param {String|RegExp} topic - topic or topic pattern to which to subscribe
* @param {Function} clbk - callback to subscribe
* @returns {Topical} Topical instance
*/
Topical.prototype.subscribe = function( topic, clbk ) {
	var topics = this._topics,
		type = typeof topic,
		matches = [],
		keys,
		key,
		funcs,
		opts,
		idx,
		t;

	if ( type !== 'string' && !( topic instanceof RegExp ) ) {
		throw new TypeError( 'subscribe()::invalid input argument. Topic must be a string or regular expression.' );
	}
	if ( typeof clbk !== 'function' ) {
		throw new TypeError( 'subscribe()::invalid input argument. Callback must be a function.' );
	}
	if ( type === 'string' ) {
		// Have we seen this topic before?
		if ( !topics.hasOwnProperty( topic ) ) {
			this.emit( 'error', {
				'topic': topic,
				'data': 'Unknown topic.'
			});
			return this;
		}
		matches.push( topic );
	} else {
		keys = Object.keys( topics );
		for ( var i = 0; i < keys.length; i++ ) {
			key = keys[ i ];
			if ( topic.test( key ) ) {
				matches.push( key );
			}
		}
	}
	for ( var j = 0; j < matches.length; j++ ) {
		topic = matches[ j ];
		t = topics[ topic ];
		funcs = t.subscribers;
		opts = t.options;
		if ( !opts.duplicates ) {
			// Ensure that a callback is only called once per topic publish event...
			idx = funcs.indexOf( clbk );
			if ( idx !== -1 ) {
				continue;
			}
		}
		if ( opts.max === funcs.length ) {
			this.emit( 'error', {
				'topic': topic,
				'data': 'Maximum number of subscribers exceeded.'
			});
			continue;
		}
		funcs.push( clbk );
		this.emit( 'subscribe', {
			'topic': topic,
			'data': clbk
		});
	}
	return this;
}; // end METHOD subscribe()

/**
* METHOD: unsubscribe( topic, clbk )
*	Unsubscribes a callback from a topic.
*
* @param {String|RegExp} topic - topic or topic pattern from which to unsubscribe
* @param {Function} clbk - callback to unsubscribe
* @returns {Topical} Topical instance
*/
Topical.prototype.unsubscribe = function( topic, clbk ) {
	var topics = this._topics,
		type = typeof topic,
		matches = [],
		flg = false,
		keys,
		key,
		funcs;

	if ( type !== 'string' && !( topic instanceof RegExp ) ) {
		throw new TypeError( 'unsubscribe()::invalid input argument. Topic must be a string or regular expression.' );
	}
	if ( typeof clbk !== 'function' ) {
		throw new TypeError( 'unsubscribe()::invalid input argument. Callback must be a function.' );
	}
	if ( type === 'string' ) {
		if ( !topics.hasOwnProperty( topic ) ) {
			// No topic...
			return this;
		}
		matches.push( topic );
	} else {
		keys = Object.keys( topics );
		for ( var i = 0; i < keys.length; i++ ) {
			key = keys[ i ];
			if ( topic.test( key ) ) {
				matches.push( key );
			}
		}
	}
	for ( var j = 0; j < matches.length; j++ ) {
		flg = false;
		topic = matches[ j ];
		funcs = topics[ topic ].subscribers;
		for ( var k = 0; k < funcs.length; k++ ) {
			if ( clbk === funcs[ k ] ) {
				// Remove the callback...
				funcs.splice( k, 1 );
				// Reset the index:
				k = k - 1;
				// Flag that we have unsubscribed:
				flg = true;
			}
		}
		if ( flg ) {
			this.emit( 'unsubscribe', {
				'topic': topic,
				'data': clbk
			});
		}
	}
	return this;
}; // end METHOD unsubscribe()

/**
* METHOD: once( topic, clbk )
*	Subscribes a callback to a topic and unsubscribes the callback after its first invocation.
*
* @param {String} topic - topic to which to subscribe
* @param {Function} clbk - callback to subscribe
* @returns {Topical} Topical instance
*/
Topical.prototype.once = function( topic, clbk ) {
	var self = this,
		topics = this._topics,
		funcs;
	if ( typeof topic !== 'string' ) {
		throw new TypeError( 'once()::invalid input argument. Topic must be a string.' );
	}
	if ( typeof clbk !== 'function' ) {
		throw new TypeError( 'once()::invalid input argument. Callback must be a function.' );
	}

	// Have we seen this topic before?
	if ( !topics.hasOwnProperty( topic ) ) {
		this.emit( 'error', {
			'topic': topic,
			'data': 'Unknown topic.'
		});
		return this;
	}

	// Check if the callback is already subscribed...
	funcs = topics[ topic ].subscribers;
	for ( var i = 0; i < funcs.length; i++ ) {
		if ( clbk === funcs[ i ] ) {
			// Callback already subscribed. Remove it...
			funcs.splice( i, 1 );
			// Reset the index:
			i = i - 1;
		}
	}
	if ( topics[ topic ].options.max === funcs.length ) {
		this.emit( 'error', {
			'topic': topic,
			'data': 'Maximum number of subscribers exceeded.'
		});
		return this;
	}
	funcs.push( wrapper );
	this.emit( 'subscribe', {
		'topic': topic,
		'data': null
	});
	return this;

	/**
	* FUNCTION: wrapper( event )
	*	Wraps a callback to ensure call `once` behavior.
	*
	* @private
	* @param {*} event - emitted event
	*/
	function wrapper( event ) {
		self.on( 'publish', unsubscribe );
		clbk( event );
	}

	/**
	* FUNCTION: unsubscribe()
	*	Unsubscribes the subscriber.
	*
	* @private
	*/
	function unsubscribe() {
		self.removeListener( 'publish', unsubscribe );
		self.unsubscribe( topic, wrapper );
	}
}; // end METHOD once()

/**
* METHOD: publish( topic, event )
*	Publishes an event on a specified topic.
*
* @param {String} topic - topic to which the event pertains
* @param {*} event - topic event
* @returns {Topical} Topical instance
*/
Topical.prototype.publish = function( topic, event ) {
	var topics = this._topics,
		funcs;
	if ( typeof topic !== 'string' ) {
		throw new TypeError( 'publish()::invalid input argument. Topic must be a string.' );
	}
	if ( arguments.length !== 2 ) {
		throw new Error( 'publish()::insufficient input arguments. Must provide an event.' );
	}
	if ( !topics.hasOwnProperty( topic ) ) {
		return this;
	}
	funcs = topics[ topic ].subscribers;
	for ( var i = 0; i < funcs.length; i++ ) {
		funcs[ i ]( event );
	}
	if ( funcs.length ) {
		this.emit( 'publish', {
			'topic': topic,
			'data': null
		});
	}
	return this;
}; // end METHOD publish()

/**
* METHOD: broadcast( event )
*	Broadcasts an event to all publicly listed subscribers.
*
* @param {*} event - event to broadcast
* @returns {Topical} Topical instance
*/
Topical.prototype.broadcast = function( event ) {
	var list = this._listing;
	if ( !arguments.length ) {
		throw new Error( 'broadcast()::insufficient input arguments. Must provide an event to broadcast.' );
	}
	for ( var i = 0; i < list.length; i++ ) {
		list[ i ]( event );
	}
	if ( list.length ) {
		this.emit( 'broadcast', {
			'topic': null,
			'data': null
		});
	}
	return this;
}; // end METHOD broadcast()

// EXPORTS //

module.exports = new Topical();
module.exports.ctor = Topical;


/**
 * Copyright (c) 2016 Apentle.com
 *
 * This source code is licensed under the MIT-style license found in
 * the LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

const theme = require('react-native-theme');
const addLayout = require('react-native-theme/layout');
const addTranslation = require('./i18n');
const addActions = require('./addActions');

/**
 * loadModule - load module (theme or plugin) for app
 *
 * @param  {object} data            module's configuration
 * @param  {string|undefined} name  theme's name (if module is theme)
 * @returns {undefined}
 */
function loadModule(data, name) {
  if (typeof data !== 'object') {
    return;
  }
  // Run before load module
  if (typeof data.willLoad === 'function') {
    data.willLoad();
  }

  // CSS Style
  if (Array.isArray(data.styles)) {
    data.styles.forEach(function(styles) {
      theme.add(styles, name);
    });
  } else if (typeof data.styles === 'object') {
    theme.add(data.styles, name);
  }
  // Components
  if (data.components !== undefined) {
    theme.addComponents(data.components, name);
  }
  // Layouts
  if (data.layouts !== undefined) {
    addLayout(data.layouts);
  }
  // I18n
  if (data.i18n !== undefined) {
    addTranslation(data.i18n);
  }
  // Redux actions
  if (data.actions !== undefined) {
    addActions(data.actions);
  }
  // Redux reducers
  if (typeof data.reducers === 'function') {
    data.reducers();
  }

  // Run after load module
  if (typeof data.loaded === 'function') {
    data.loaded();
  }
}

module.exports = loadModule;