/**
 * Assign
 *
 * @param {Object} object
 * @param {...Object} source
 * @returns {Object}
 * @private
 */
function assign(object, source) {
	for (var i = 1, c = arguments.length; i < c; i++) {
		for (var x in arguments[i]) {
			if (arguments[i].hasOwnProperty(x) && arguments[i][x] !== undefined) {
				object[x] = arguments[i][x];
			}
		}
	}

	return object;
}
/**
 * Compact
 *
 * @param {Object} collection

 * @returns {Object}
 * @private
 */
function compact(collection) {
	return pick(collection, function (value) {
		return !isEmpty(value);
	});
}

/**
 * Decode + characters to spaces in application/x-www-form-urlencoded string
 *
 * @param {string} string
 * @returns {string}
 * @private
 */
function decodeForm (string) {
	return string.replace(/\+/g, ' ');
}

/**
 * Is empty
 *
 * @param {*} value
 * @returns {Boolean}
 * @private
 */
function isEmpty(value) {
	if (!value || typeof value !== 'object') {
		return !value;
	}

	return !Object.keys(value).length;
}

/**
 * Join paths
 *
 * @param {...String} parts
 * @returns {String}
 * @private
 */
function joinPaths(parts) {
	return Array.prototype.slice.call(arguments)
		.join('/')
		.replace(/\/+/g, '/');
}

/**
 * Parents
 *
 * @param {Element} el
 * @param {String} name
 * @returns {Element|null}
 * @private
 */
function parents(el, name) {
	while (el && el.nodeName.toLowerCase() !== name) {
		el = el.parentNode;
	}

	return el && el.nodeName.toLowerCase() === name
		? el
		: null;
}

/**
 * Parse hash
 *
 * @param {String} hash
 * @param {Array} [keys]
 * @returns {Object}
 * @private
 */
function parseHash(hash, keys) {
	try {
		var parsed = compact(JSON.parse(decodeURIComponent(hash.substr(2))));

		return keys
			? pick(parsed, keys)
			: parsed;
	} catch (e) {
		return {};
	}
}

/**
 * Parse JSON
 *
 * @param {String} string
 * @returns {*}
 * @private
 */
function parseJSON(string) {
	try {
		return JSON.parse(string);
	} catch (e) {
		return string || '';
	}
}

/**
 * Parse URI
 *
 * @param {String} uri
 * @returns {{protocol: string, host: string, path: string, qs: string, hash: string}}
 * @private
 */
function parseUri(uri) {
	var parts = uri.match(/^(?:([\w+.-]+):\/\/([^/]+))?([^?#]*)?(\?[^#]*)?(#.*)?/);

	return {
		protocol: parts[1] || '',
		host: parts[2] || '',
		path: parts[3] || '',
		qs: parts[4] || '',
		hash: parts[5] || ''
	};
}

/**
 * Parse QS
 *
 * @param {String} qs
 * @param {Array} [keys]
 * @returns {Object}
 * @private
 */
function parseQS(qs, keys) {
	var index = qs.indexOf('?');
	var parsed = {};

	if (index !== -1) {
		var pairs = qs.substr(index + 1).split('&');
		var pair = [];

		for (var i = 0, c = pairs.length; i < c; i++) {
			pair = pairs[i].split('=');

			if((!isEmpty(pair[1])) && (!isEmpty(parseJSON(pair[1])))) {
				parsed[decodeForm(decodeURIComponent(pair[0]))] = parseJSON(decodeForm(decodeURIComponent(pair[1])));
			}
		}
	}

	return keys
		? pick(parsed, keys)
		: parsed;
}

/**
 * Pick
 *
 * @param {Object} object
 * @param {Array|Function} keys
 * @returns {Object}
 * @private
 */
function pick(object, keys) {
	var data = {};

	if (typeof keys === 'function') {
		for (var x in object) {
			if (object.hasOwnProperty(x) && keys(object[x], x)) {
				data[x] = object[x];
			}
		}
	} else {
		for (var i = 0, c = keys.length; i < c; i++) {
			data[keys[i]] = object[keys[i]];
		}
	}

	return data;
}

/**
 * Scroll to
 *
 * @param {String} id
 * @private
 */
function scrollTo(id) {
	var el = document.getElementById(id);

	if (el) {
		window.scrollBy(0, el.getBoundingClientRect().top);
	} else {
		window.scrollTo(0, 0);
	}
}

/**
 * Stringify
 *
 * @param {*} value
 * @returns {String}
 * @private
 */
function stringify(value) {
	if (!value || typeof value !== 'object') {
		return value;
	}

	return JSON.stringify(value);
}

/**
 * Stringify hash
 *
 * @param {Object} data
 * @returns {String}
 * @private
 */
function stringifyHash(data) {
	data = compact(data);

	return !isEmpty(data)
		? '#!' + encodeURIComponent(stringify(data))
		: '';
}

/**
 * Stringify QS
 * @param {Object} data
 * @returns {String}
 */
function stringifyQS(data) {
	var qs = '';

	for (var x in data) {
		if (data.hasOwnProperty(x) && !isEmpty(data[x])) {
			qs += '&' + encodeURIComponent(x) + '=' + encodeURIComponent(stringify(data[x]));
		}
	}

	return qs
		? '?' + qs.substr(1)
		: '';
}


/**
 * Router
 *
 * @param {Object} options
 * @constructor
 * @public
 */
function Router(options) {
	this.globals = options.globals || [];
	this.basePath = options.basePath || '';
	this.el = options.el;
	this.data = options.data || function () { return {}; };
	this.history = options.history || history;
	this.strictMode = !!options.strictMode;
	this.reloadOnClick = options.reloadOnClick;
	this.linksWatcher = null;
	this.stateWatcher = null;
	this.route = null;
	this.routes = [];
	this.uri = {};
}

/**
 * Add route
 *
 * @param {String} pattern
 * @param {Function} Handler
 * @param {Object} [observe]
 * @returns {Router}
 * @public
 */
Router.prototype.addRoute = function (pattern, Handler, observe) {
	this.routes.push(new Route(pattern, Handler, observe, this));

	return this;
};

/**
 * Build hash
 *
 * @param {String} mixIn
 * @returns {String}
 * @private
 */
Router.prototype.buildHash = function (mixIn) {
	var data = this.route.getState().hash;

	return !isEmpty(data) || !mixIn
		? stringifyHash(data)
		: mixIn;
};

/**
 * Build QS
 *
 * @param {Array} mixIn
 * @returns {String}
 * @private
 */
Router.prototype.buildQS = function (mixIn) {
	return stringifyQS(assign.apply(null, [{}].concat(mixIn, this.route.getState().qs)));
};

/**
 * Dispatch
 *
 * @param {String} request
 * @param {Object} [options]
 * @returns {Router}
 * @public
 */
Router.prototype.dispatch = function (request, options) {
	options = options || {};
	var uri = parseUri(request);
	var route = this.match(uri.path);
	var oldUri = this.uri;

	// 404
	if (!route) {
		return this.redirect(request);
	}

	if (options.reload || shouldDispatch(this.uri, uri, route)) {
		// prepare data
		if (this.route && this.route.view) {
			options.state = options.state || {};

			this.globals.forEach(function (global) {
				if (options.state[global] === undefined) {
					options.state[global] = this.route.view.get(global);
				}
			}, this);
		}

		var defaults = typeof this.data === 'function' ? this.data() : this.data;
		var data = assign(defaults, options.state, options.hash, options.qs);

		// destroy existing route
		if (this.route) {
			this.route.destroy();
		}

		// init new route
		this.uri = uri;
		this.route = route.init(uri, data);
	}

	// will scroll to the top if there is no matching element
	if (!options.noScroll) {
		scrollTo(uri.hash.substr(1));
	}

	// update history
	return this.update(!oldUri.path || oldUri.path !== uri.path, !options.noHistory, uri);
};

/**
 * Get URI
 *
 * @returns {String}
 * @public
 */
Router.prototype.getUri = function () {
	return location.pathname.substr(this.basePath.length) + location.search + location.hash;
};

/**
 * Init
 *
 * @param {Object} [options]
 * @returns {Router}
 * @public
 */
Router.prototype.init = function (options) {
	return this.dispatch(this.getUri(), assign({ noHistory: true }, options));
};

/**
 * Get the first `route` matching the `request`
 *
 * @param {String} request
 * @returns {Object|null}
 * @public
 */
Router.prototype.match = function (request) {
	var i = -1;

	while (this.routes[++i]) {
		if (this.routes[i].match(request)) {
			return this.routes[i];
		}
	}

	return null;
};

/**
 * Redirect
 *
 * @param {String} request
 * @returns {Router}
 * @private
 */
Router.prototype.redirect = function (request) {
	location.pathname = joinPaths(this.basePath, request);

	return this;
};

/**
 * Unwatch links
 *
 * @returns {Router}
 * @public
 */
Router.prototype.unwatchLinks = function () {
	if (this.linksWatcher) {
		document.body.removeEventListener('click', this.linksWatcher);
		this.linksWatcher = null;
	}

	return this;
};

/**
 * Unwatch state
 *
 * @returns {Router}
 * @public
 */
Router.prototype.unwatchState = function () {
	if (this.stateWatcher) {
		window.removeEventListener('popstate', this.stateWatcher);
		this.stateWatcher = null;
	}

	return this;
};

/**
 * Update
 *
 * @param {Boolean} [pathChange]
 * @param {Boolean} [history] - true = always, false = never, undefined = if something changed
 * @param {Object} [uri]
 * @returns {Router}
 * @private
 */
Router.prototype.update = function (pathChange, history, uri) {
	if (!this.route) {
		return this;
	}

	uri = uri || { qs: '', hash: '' };
	var path = joinPaths(this.basePath, this.uri.path);
	var qs = this.buildQS([ parseQS(uri.qs) ].concat(!pathChange ? [ parseQS(location.search) ] : []));
	var hash = this.buildHash(uri.hash);
	var newUri = path + qs + hash;
	var oldUri = location.pathname + location.search + location.hash;
	var state = this.route.getState().state;
	this.uri.qs = qs;
	this.uri.hash = hash;

	if (history === true) {
		this.history.pushState(state, null, newUri);
	} else if (history === false) {
		this.history.replaceState(state, null, newUri);
	} else if (newUri !== oldUri) {
		this.history.pushState(state, null, newUri);
	}

	return this;
};

/**
 * Watch links
 *
 * @param {RegExp} [pattern]
 * @returns {Router}
 * @public
 */
Router.prototype.watchLinks = function (pattern) {
	pattern = pattern || new RegExp('^((https?:)?\\/\\/' + location.hostname.replace(/\./g, '\\.') + '.*|((?!\\/\\/)[^:]+))$');
	var _this = this;

	document.body.addEventListener('click', this.unwatchLinks().linksWatcher = function (e) {
		var el = parents(e.target, 'a');

		if (el) {
			var href = el.getAttribute('href') || el.getAttribute('data-href');

			if (href && !el.classList.contains('router-ignore') && pattern.test(href)) {
				if (_this.reloadOnClick) {
					_this.dispatch(href, { reload: true });
				} else {
					_this.dispatch(href);
				}

				e.preventDefault();
			}
		}
	});

	return this;
};

/**
 * Watch state
 *
 * @returns {Router}
 * @public
 */
Router.prototype.watchState = function () {
	var _this = this;

	window.addEventListener('popstate', this.unwatchState().stateWatcher = function(e) {
		if (e.state) {
			_this.init({ state: e.state });
		}
	});

	return this;
};

/**
 * Should dispatch
 *
 * @param {Object} oldUri
 * @param {Object} newUri
 * @param {Object} route
 * @returns {Boolean}
 * @private
 */
function shouldDispatch(oldUri, newUri, route) {
	return oldUri.path !== newUri.path
		|| oldUri.qs !== newUri.qs
		|| (decodeURIComponent(oldUri.hash) !== decodeURIComponent(newUri.hash) && (!route || route.observe.hash.length));
}


'use strict';

var DockerCmd = require('./docker-cmd');
var fs = require('fs');
var path = require('path');
var util = require('util');
var extend = require('extend');

/**
 * This class manages the DockerCmd and handles dockerdesc.json
 * @constructor
 * @param {string} [dockerdescPath='./dockerdesc.json'] - Path to `dockerdesc.json` file or parent dir
 */
function DockerCmdManager(dockerdescPath) {
    dockerdescPath = dockerdescPath || './dockerdesc.json';

    if (!fs.existsSync(dockerdescPath)) {
        throw new Error(util.format('The path "%s" does not exists.', dockerdescPath));
    }
    /** @type {string} */
    this.dockerdescDir = path.dirname(dockerdescPath);

    var dockerdescPathStat = fs.statSync(dockerdescPath);
    if (dockerdescPathStat.isDirectory()) {
        this.dockerdescDir = dockerdescPath;
        dockerdescPath = path.join(dockerdescPath, 'dockerdesc.json');
    }
    /** @type {Dockerdesc} */
    var dockerdescContent = fs.readFileSync(dockerdescPath);
    try {
        this.dockerdesc = JSON.parse(dockerdescContent);
    } catch (err) {
        throw new Error('Problem in the dockerdesc.json file format.\n' + err.stack);
    }
}

DockerCmdManager.prototype.build = _dockerCommandBuilder('build', ['t', 'tag'],
    /** @type {DockerCmdManager~CommandHandler} */ function(commandName, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback) {
        // getting the Dockerfile path and parent dir
        var dockerfilePath = finalDescription.path || (commandOptions._ || [])[0] || descriptionTarget;
        if (path.resolve(dockerfilePath) !== path.normalize(dockerfilePath)) { // checking if absolute path, thanks to http://stackoverflow.com/a/24225816/535203
            // this is not an absolute path let's prepend dockerdesc.json dir
            dockerfilePath = path.join(this.dockerdescDir, dockerfilePath);
        }
        if (!fs.existsSync(dockerfilePath)) {
            throw new Error(util.format('The path "%s" defined for image name "%s" does not exists.', dockerfilePath, descriptionTarget));
        }
        var dockerfileDirPath;
        var dockerfilePathStat = fs.statSync(dockerfilePath);
        if (dockerfilePathStat.isDirectory()) {
            // this path defines actually the directory to the Dockerfile
            dockerfileDirPath = dockerfilePath;
            dockerfilePath = path.join(dockerfilePath, 'Dockerfile');
        } else if (dockerfilePathStat.isFile()) {
            // this path correctly points to the Dockerfile, let's take its parent as dir path
            dockerfileDirPath = path.dirname(dockerfilePath);
        }
        // checking whether to build its parent or not
        if (finalDescription.buildParent) {
            // read the Dockerfile, looking for the "FROM" docker directive & check if this is described in the dockerdesc

            var dockerfileContent = fs.readFileSync(dockerfilePath);
            var fromRe = /^\s*from\s*(\S*)/i;
            var fromMatch = fromRe.exec(dockerfileContent);
            if (fromMatch) {
                var parentImageName = fromMatch[1];
                // looking for that name in descriptions
                var parentLookedupDescription = this._lookupDescription('build', parentImageName, ['t', 'tag']);

                if (parentLookedupDescription) {
                    // found the build description, let's build it
                    console.log(util.format('Found "%s" as a valid parent image to build before building "%s".', parentImageName, descriptionTarget));
                    var self = this;
                    this.build(parentImageName, descriptionOptions, commandOptions, dockerOptions, function(exitCode) {
                        if (!exitCode) {
                            // Parent image was correctly built, let's build now this image
                            self._computeOptionsThenBuild(dockerfileDirPath, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback);
                        }
                    });
                    return;
                }
            }
        }
        this._computeOptionsThenBuild(dockerfileDirPath, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback);
    });

DockerCmdManager.prototype._computeOptionsThenBuild = function (dockerfilePath, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback) {
    // now compute all the build options
    /** @type {BuildOptions} */
    var buildOptions = finalDescription.options || {};
    if (finalDescription.buildTagFromBuildName && typeof buildOptions.tag === 'undefined' && typeof buildOptions.t === 'undefined') {
        // current tag is not defined, and option buildTagFromBuildName is set, let's add the buildName as a tag
        buildOptions.tag = descriptionTarget;
    }
    // set the argument for docker build to be the path to the Dockerfile
    buildOptions._ = [dockerfilePath];
    // replace it in the finalDescription because it is this parameter that is used by _defaultCommandHandler
    finalDescription.options = buildOptions;
    console.log('Building "'+descriptionTarget+'"');
    _defaultCommandHandler('build', descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback);
};

DockerCmdManager.prototype.run = _dockerCommandBuilder('run', ['name'],
    /** @type {DockerCmdManager~CommandHandler} */ function(commandName, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback) {
        var runOptions = finalDescription.options || {};
        runOptions._ = runOptions._ || [];

        if (finalDescription.useRunName && typeof runOptions.name === 'undefined') {
            runOptions.name = descriptionTarget;
        }
        if (runOptions._.length < 1) {
            runOptions._.push(finalDescription.image || descriptionTarget);
        }
        if (finalDescription.command && runOptions._.length === 1) {
            runOptions._ = runOptions._.concat(finalDescription.command);
        }
        // replace it in the finalDescription because it is this parameter that is used by _defaultCommandHandler
        finalDescription.options = runOptions;
        // TODO handle dependencies between containers, and check if the image exists before running it
        _defaultCommandHandler(commandName, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback);
    });

DockerCmdManager.prototype._defaultDescriptions = {
    run: {
        useRunName: true
    },
    build: {
        buildTagFromBuildName: true,
        buildParent: true
    }
};

/**
 * @param {string} commandName - Docker command name
 * @param {string} descriptionTarget
 * @param {string[]} commandOptionsMappingToAName - command options name which refers to a potential descriptionTarget
 * @return {{dockerdescName: string, description: Description}}
 * @private
 */
DockerCmdManager.prototype._lookupDescription = function(commandName, descriptionTarget, commandOptionsMappingToAName) {
    // First, look directly at the descriptions in the dockerdesc
    var dockerdescForThatCommand = this.dockerdesc[commandName] || {};
    var description = dockerdescForThatCommand[descriptionTarget];
    var dockerdescName = descriptionTarget;
    // If not found, try to search for that description with its name field
    if (!description && commandOptionsMappingToAName) {
        for (dockerdescName in dockerdescForThatCommand) {
            /** @type {Description} */
            var iDescription = dockerdescForThatCommand[dockerdescName];
            var optionNameValue = null;
            commandOptionsMappingToAName.forEach(function(optionName) {
                optionNameValue = optionNameValue || (iDescription.options && iDescription.options[optionName]);
            });
            if (optionNameValue === descriptionTarget) {
                // we have a match
                description = iDescription;
                break;
            }
        }
    }
    if (!description) {
        return null;
    } else {
        return {
            dockerdescName: dockerdescName,
            description: description
        };
    }
};

/**
 * @callback DockerCmdManager~CommandHandler
 * @param {string} commandName - Docker command name
 * @param {string} descriptionTarget
 * @param {Object} descriptionOptions - Original description options overrides
 * @param {Options} commandOptions - Original command options
 * @param {Object} dockerOptions - Original docker options
 * @param {Description} finalDescription - Found and enriched description
 * @param {string} dockerdescName - Name of the found description
 * @param {DockerCmd~callback} callback
 */

/**
 * @param {string} commandName - Docker command name
 * @param {string[]} commandOptionsMappingToAName - command options name which refers to a potential descriptionTarget
 * @param {DockerCmdManager~CommandHandler} commandHandler
 * @private
 */
function _dockerCommandBuilder(commandName, commandOptionsMappingToAName, commandHandler) {
    /**
     *
     * @param {string} descriptionTarget
     * @param {Object} descriptionOptions - overrides from Description
     * @param {Options} commandOptions
     * @param {Object} dockerOptions
     * @param {DockerCmd~callback} callback
     * @private
     */
    return function(descriptionTarget, descriptionOptions, commandOptions, dockerOptions, callback) {
        // the callback is always the last argument
        switch (arguments.length) {
            case 2:
                callback = descriptionOptions;
                descriptionOptions = undefined;
                break;
            case 3:
                callback = commandOptions;
                commandOptions = undefined;
                break;
            case 4:
                callback = dockerOptions;
                dockerOptions = undefined;
        }
        // Now will create the final description : beginning from defaults, append the fields from the "default" template, then all the templates from the given
        // description, then the fields of the description, and finally the given commandOptions & dockerOptions
        var finalDescription = extend(true, {}, this._defaultDescriptions[commandName]); // copy defaults
        var templates = (this.dockerdesc.templates || {})[commandName] || {};
        function applyTemplate(template) {
            if (template) {
                extend(true, finalDescription, template);
            }
        }
        // Apply templates beginning with `default`
        applyTemplate(templates.default);
        // Look directly at the descriptions in the dockerdesc
        var lookup = this._lookupDescription(commandName, descriptionTarget, commandOptionsMappingToAName);
        var dockerdescName = descriptionTarget;
        if (lookup) {
            var description = lookup.description;
            dockerdescName = lookup.dockerdescName;
            var descriptionTemplates = description.templates;
            if (descriptionTemplates) {
                [].concat(descriptionTemplates).forEach(function (templateName) {
                    var template = templates[templateName];
                    if (!template) {
                        throw new Error(util.format('%s template "%s" is missing', commandName, templateName));
                    } else {
                        applyTemplate(template);
                    }
                });
            }
            // Apply all fields from given description
            applyTemplate(description);
        } else {
            console.warn(util.format('Warn: couldn\'t find the %s description named "%s"', commandName, descriptionTarget));
        }
        // And finally, the options passed from the command line
        applyTemplate(descriptionOptions);
        applyTemplate({dockerOptions: dockerOptions, options: commandOptions});

        commandHandler.call(this, commandName, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback);
    }
}

/**
 * @type {DockerCmdManager~CommandHandler}
 * @private
 */
function _defaultCommandHandler(commandName, descriptionTarget, descriptionOptions, commandOptions, dockerOptions, finalDescription, dockerdescName, callback) {
    new DockerCmd().executeCommand(commandName, finalDescription.options, finalDescription.dockerOptions, callback);
}

/**
 * @param {string} commandName - Docker command name
 * @param {string[]} [commandOptionsMappingToAName] - command options name which refers to a potential descriptionTarget
 */
function _defaultDockerCommandBuilder(commandName, commandOptionsMappingToAName) {
    return _dockerCommandBuilder(commandName, commandOptionsMappingToAName, _defaultCommandHandler);
}

/// copy default commands if not already exists
Object.getOwnPropertyNames(DockerCmd.prototype).forEach(function(dockerCmdPropertyName) {
    var dockerCmdProperty = DockerCmd.prototype[dockerCmdPropertyName];
    if (typeof dockerCmdProperty === 'function' && !DockerCmdManager.prototype[dockerCmdPropertyName]) {
        // let's define the not yet defined function linked to corresponding DockerCmd function
        DockerCmdManager.prototype[dockerCmdPropertyName] = _defaultDockerCommandBuilder(dockerCmdPropertyName);
    }
});

module.exports = DockerCmdManager;


// drilldown.js
// simple nevernull alternative
'use strict';

var isFunction = (function() {
    // Thanks to underscore for identifying typeof bugs
    var regexTypeofIsCorrect = typeof /./ !== 'function';
    var int8ArrayTypeofIsCorrect = typeof Int8Array !== 'object';
    /* istanbul ignore else */
    if (regexTypeofIsCorrect && int8ArrayTypeofIsCorrect) {
        return function(obj) {
            return typeof obj === 'function' || false;
        };
    }
    /* istanbul ignore next */
    return function(obj) {
        return Object.prototype.toString.call(obj) === '[object Function]';
    };
})();

/**
 * drilldown
 * Safely accesses deep properties of objects.
 *
 * var foo;
 * foo.bar;
 * // TypeError: Cannot read property 'bar' of undefined
 *
 * var foo = {abc: {def: {ghi: 'jkl'}}};
 * dd(foo)('abc')('def')('ghi').val is 'jkl'
 * dd(foo)('abc')('zzz')('yyy').val is undefined
 *
 * Check if a deep property exists:
 * dd(foo)('abc').exists
 *
 * Works with arrays too:
 * var foo = {abc: [ {bar: 'def'},{bar: 'ghi'} ]};
 * dd(foo)('abc')(0)('bar') is 'def'
 *
 * Safely call functions:
 * var foo = {abc: {addOne: function(x) { return x + 1; }}};
 * dd(foo)('abc')('addOne').invoke(5); returns 6
 * dd(foo)('zzz')('aaa').invoke(5); returns undefined
 *
 * Update values if the original value exists:
 * var foo = {abc: {def: {ghi: 'jkl'}}};
 * var newValue = {ping: 'pong'};
 * dd(foo)('abc')('def').update(newValue);
 *   - foo is now {abc: {def: {ping: 'pong'}}}
 *   - {ping: 'pong'} is returned
 * dd(foo)('abc')('zzz').update(5);
 *   - foo is unchanged
 *   - undefined is returned
 *
 * Set values even if the path drilled to does not exist:
 * var foo = {abc: {}};
 * dd(foo)('abc')('def')('ghi').set('jkl');
 *   - foo is now {abc: {def: {ghi: 'jkl}}}
 *
 * To prevent confusion, only own properties are drilled into.
 *
 * Available properties:
 *  - val - the value
 *  - exists - true if val is defined
 *  - update function(value) - sets the value if the value exists
 *  - set function(value) - sets the value at any path
 *  - invoke - the value if the value is a function, or else a dummy function
 *
 * @param {object} object
 * @param _context
 * @param _key
 * @param _root
 * @param _rootPath
 * @returns {Function}
 */
function dd(object, _context, _key, _root, _rootPath) {
    _root = _root || object;
    _rootPath = _rootPath || [];
    var drill = function(key) {
        var nextObject = (
            object &&
            object.hasOwnProperty(key) &&
            object[key] ||
            undefined
        );
        return dd(nextObject, object, key, _root, _rootPath.concat(key));
    };
    drill.val = object;
    drill.exists = object !== undefined;
    drill.set = function(value) {
        if (_rootPath.length === 0) {
            return;
        }
        var contextIterator = _root;
        for (var depth = 0; depth < _rootPath.length; depth++) {
            var key = _rootPath[depth];
            var isFinalDepth = (depth === _rootPath.length - 1);
            if (!isFinalDepth) {
                contextIterator[key] = (
                    contextIterator.hasOwnProperty(key) &&
                    typeof contextIterator[key] === 'object' ?
                        contextIterator[key] : {}
                );
                contextIterator = contextIterator[key];
            } else {
                _context = contextIterator;
                _key = key;
            }
        }
        _context[_key] = value;
        drill.val = value;
        drill.exists = value !== undefined;
        return value;
    };
    drill.update = function(value) {
        if (drill.exists) {
            _context[_key] = value;
            drill.val = value;
            return value;
        }
    };
    drill.invoke = isFunction(object) ? Function.prototype.bind.call(object, _context) : function () {
    };

    return drill;
}

module.exports = dd;


var Features = require("./lib/features.js");
var Logger = require("yog-log");
var fs = require("fs");

/**
 * feature flag控制类，提供接口给模板调用，判断某个feature是否开启
 * @param {[type]} option [description]
 * @param {[type]} req    [description]
 * @param {[type]} res    [description]
 */
var DoorKeeper = function(option,req,res){
    this.params  = {
        'req' : req,
        'res' : res
    };
    this.option = option;
    this.features = {};//feature实例配置列表
    this.logger = Logger.getLogger();
}

/**
 * 获取某个模块内的feature
 * @param  {[type]} module [description]
 * @return {[type]}        [description]
 */
DoorKeeper.prototype.getModuleFeature = function(nameSpace) {
    //已读取配置不重复读取
    if(this.features[nameSpace]){
        return false;
    }
    var option = this.option;
    var logger = this.logger;
    var stat;
    if(option['config_dir']){
        var feature_json = option['config_dir'] + "/" + nameSpace + "-features.json";
        try {
            stat = fs.statSync(feature_json);
        }catch(e) {
            logger.log('fatal',{
                'stack': e ,
                'msg' : 'get ' +  feature_json + ' error! detail:' + e
            });

        }
        if (stat && stat.isFile()) {
            try {
                var json = JSON.parse(fs.readFileSync(feature_json));
                this.features[nameSpace] = json['features'];
            } catch (e) {
                logger.log('fatal',{
                    'stack': e,
                    'msg' : 'parse json file '+ feature_json + " error! detail :" + e
                });
            }
        }
    }
};

/**
 * 给模板层调用接口，判断某个feature是否开启
 * @param  {[type]} name [description]
 * @return {[type]}      [description]
 */
DoorKeeper.prototype.getFlag = function(str){
    var logger = this.logger;
    var _arr = str.split(":");
    if(_arr.length != 2){
        logger.log('warning',{
            'msg' : "wrong feature name " + str
        });
        return false;
    }
    var nameSpace = _arr[0], featureName = _arr[1];
    //初始化该模块的feature配置
    this.getModuleFeature(nameSpace);

    //如果模块里不存在此feature配置，报错返回
    if(!this.features[nameSpace] || !this.features[nameSpace][featureName]){
        logger.log('warning',{
            'msg' : "no conf for feature: " + featureName + " and module: " + nameSpace
        });
        return false;
    }
    var req = this.params['req'],
        res = this.params['res'],
        feature_dir =  this.option['feature_dir'],
        conf = this.features[nameSpace][featureName],
        value = conf['value'] || null,//feature阈值
        type  = conf['type']; //feature类型

    //如果没有设置feature type,报错返回
    if(!conf.hasOwnProperty('type')){
        logger.log('warning',{
            'msg': 'feature type not set in ' + featureName
        });
        return false;
    }

    //默认的feature
    if(Features[type] &&  'function' == typeof(Features[type])){
        try{
            return Features[type](value,req,res);
        }catch(e){
            logger.log('fatal',{
                'stack': e,
                'msg' : 'exec feature type:' + type + ' and featureName ' + featureName + ' error! detail: ' + e
            });
            return false;
        }
    }else{  //自定义扩展的feature
        var file = feature_dir + "/" + type + ".js";
        if(feature_dir && fs.existsSync(file)){
            try{
                var feature = require(file);
                return feature(value,req,res);
            }catch(e){
                logger.log('fatal',{
                    'stack': e,
                    'msg' : 'exec custom feature type ' + file + ' error! detail:' + e
                });
                return false;
            }
        }else{
            //不存在feature类型
            logger.log('warning',{
                'msg': 'no feature type: ' + type
            });
            return false;
        }
    }
}

module.exports = function(config){

    var config = config || {};

    return function (req, res, next) {

        //由于feature配置随时可能变化，request参数也不同，故每次请求进行一次初始化
        var dooKeeper = new DoorKeeper(config,req,res);
        //test
        //console.log(dooKeeper.getFlag("common:featureD"));

        //绑定在res对象的locals上的变量将直接传递到swig中
        //便于模板层编译时直接调用
        res.locals._doorKeeper = dooKeeper;
        next();
    }
};

//for test
module.exports.dooKeeper = DoorKeeper;


'use strict';

module.exports = printTasks;

/**
 * Render output from tasks
 * @param {TaskList} tasks
 * @param {boolean}  [verbose=false]
 * @returns {string}
 */
function printTasks(tasks, verbose) {
  tasks = tasks
    .filterHidden(verbose)
    .sort();

  var results = [
    'Usage: gulp [task] [task2] ...',
    '',
    'Tasks: '
  ];

  var fieldTaskLen = tasks.getLongestNameLength();

  tasks.forEach(function(task) {
    var comment = task.comment || {};
    var lines = comment.lines || [];
    results.push(formatColumn(task.name, fieldTaskLen) + (lines[0] || ''));
    for (var i = 1; i < lines.length; i++) {
      results.push(formatColumn('', fieldTaskLen) + '  ' + lines[i]);
    }
  });

  return results.join('\n');
}

/**
 * Return a text surrounded by space
 * @param {string} text
 * @param {number} width Column width without offsets
 * @param {number} [offsetLeft=3]  space count before text
 * @param {number} [offsetRight=3] space count after text
 * @returns {string}
 */
function formatColumn(text, width, offsetLeft, offsetRight) {
  offsetLeft  = undefined !== offsetLeft  ? offsetLeft  : 3;
  offsetRight = undefined !== offsetRight ? offsetRight : 3;

  return new Array(offsetLeft + 1).join(' ') +
    text +
    new Array(Math.max(width - text.length, 0) + 1).join(' ') +
    new Array(offsetRight + 1).join(' ');
}


'use strict';

var gulp = require('gulp');

var parser   = require('./lib/parser');
var printer  = require('./lib/printer');
var TaskList = require('./lib/models').TaskList;

var argv = {
  verbose: -1 !== process.argv.indexOf('-v') ||
           -1 !== process.argv.indexOf('--verbose')
};

module.exports = inheritGulp();

/**
 * Emits files matching provided glob or an array of globs.
 * @param {string|Array} glob
 * @param {Object}       [options]
 * @returns {Stream}
 * @name module.exports.src
 */

/**
 * Can be piped to and it will write files.
 * @param {string|Function} path
 * @param {Object}          [options]
 * @name module.exports.dest
 */

/**
 * Define a task
 * @param {string}   name  The name of the task.
 * @param {Array}    [dep] Task dependencies
 * @param {Function} [fn]  The function that performs the task's operations
 * @return {Stream|Promise}
 */
module.exports.task = function task(name, dep, fn) {
  this.taskList.push(parser.makeTaskInfo(name));
  return gulp.task.apply(gulp, arguments);
};

/**
 * Watch files and do something when a file changes.
 * @param {string|Array}   glob
 * @param {Object}         [opt]
 * @param {Array|Function} [fn]
 * @return {EventEmitter}
 * @name module.exports.watch
 */
module.exports.watch = function watch(glob, opt, fn) {
  return gulp.watch.apply(gulp, arguments);
};

/**
 * Factory that creates gulp task function
 * @param {Object}   [options]
 * @param {Object}   options.parser
 * @param {Function} options.print
 * @returns {Function}
 */
module.exports.help = function help(options) {
  options = options || {};
  var list = module.exports.taskList;
  var print = options.print || printer;

  return function(done) {
    list.forEach(function(task) {
      parser.parseComments(task, options.parser);
    });
    console.log(print(list, argv.verbose));
    if (typeof done === 'function') {
      done();
    }
  };
};

/**
 * Alternative way of the library usage, apply monkey-patching to gulp.task
 * @param {Gulp} [gulpInst]
 */
module.exports.patchGulp = function patchGulp(gulpInst) {
  gulpInst = gulpInst || gulp;

  var originalTasks = gulpInst.task;
  gulpInst.task = function task(name, dep, fn) {
    module.exports.taskList.push(parser.makeTaskInfo(name));
    return originalTasks.apply(gulpInst, arguments);
  };

  return this;
};

/**
 * Return a new instance that is inherited from Gulp
 * @returns {TaskDoc}
 */
function inheritGulp() {
  function TaskDoc() {
    this.taskList = new TaskList();
    gulp.Gulp.call(this);
  }
  TaskDoc.prototype = gulp;
  return new TaskDoc();
}


// jasmine-where.js
// david kaye (@dfkaye)
// 24 Oct 2103
//
// Provides a `where()` clause for data-table support to the Jasmine BDD test framework
// (https://github.com/pivotal/jasmine), similar to Cucumber's scenario-outline
// Examples (https://github.com/cucumber/cucumber/wiki/Scenario-Outlines) or Spock's
// where: blocks (https://code.google.com/p/spock/wiki/SpockBasics#Where_Blocks).
//
// For example:
//
//    it('should pass with correct data and expectation', function () {
//      where(function(){/***
//         a | b | c
//         1 | 2 | 2
//         4 | 3 | 4
//         6 | 6 | 6
//        ***/
//        expect(Math.max(a, b)).toBe(c);
//      });
//    });
//
//  Tables may also contain left and right borders:
//
//    it('should pass with left and right table borders', function () {
//      where(function(){/***
//        | a | b | c |
//        | 1 | 2 | 2 |
//        | 4 | 3 | 4 |
//        | 6 | 6 | 6 |
//        ***/
//        expect(Math.max(a, b)).toBe(c);
//      });
//    });
//
//  Numeric data is automatically coerced to Number type.
//
(function () {
  /*
   * EXPORT JAZZ
   */
  if (typeof global != "undefined") {
    global.where = where;
  }
  if (typeof window != "undefined") {
    window.where = where;
  }

  /*
   * CONSTANTS
   */
  var SEP = '|';
  var PAD = ' ' + SEP + ' ';
  var MESSAGE = 'Passed';

  ////////////////////////////////////////////////////////////////////////////////////////
  // main api method defined on the jazz environment in imitation of the jasmine lib src.
  //
  // where() accepts only a function with a commented data-table and an expectation.
  //
  // parses the function into data array, passing each array row into a new Function()
  // that contains the original function body's expectation statements.
  //
  // entire data-table with pass/fail messages is passed to the jasmine report only if an
  // expectation fails.
  //
  // returns the data table values array for further use in other expectations.
  //
  ////////////////////////////////////////////////////////////////////////////////////////
  function where(fn) {

    if (typeof fn != 'function') {
      throw new Error('where(param) expected param should be a function');
    }

    var fnBody = fn.toString().replace(/\s*function[^\(]*[\(][^\)]*[\)][^\{]*{/,'')
                              .replace(/[\}]$/, '');
    var values = parseFnBody(fnBody);
    var labels = values[0];

    /**
     * {labels} array is toString'd so the values became param symbols in the new Function.
     * {fnBody} is what's left of the original function, mainly the expectation.
     */
    var fnTest = new Function(labels.toString(), fnBody);
    var failedCount = 0;
    var trace = '\n [' + labels.join(PAD) + '] : ';

    /*
     * 1.x.x - jasmine.getEnv().currentSpec
     * 2.x.x - .currentSpec is no longer exposed (leaking state) so use a shim for it with
     *          the v2 .result property
     */

    var currentSpec = jasmine.getEnv().currentSpec || { result : {} };
    var result = /* jasmine 2.x.x. */ currentSpec.result ||
                 /* jasmine 1.x.x. */ currentSpec.results_;

    var item, message;

    for (var i = 1; i < values.length; ++i) {

      message = MESSAGE;

      fnTest.apply(currentSpec, values[i]);

      // TODO - extract method, perhaps...

      // collect any failed expectations
      if (result.failedExpectations && result.failedExpectations.length) {

        /*
         * jasmine 2.x.x.
         */

        if (failedCount < result.failedExpectations.length) {
          failedCount += 1;
          item = result.failedExpectations[failedCount - 1];
          message = item.message;
          item.message = trace + '\n [' + values[i].join(PAD) + '] (' + message + ')';
        }

      } else if (result.items_) {

        /*
         * jasmine 1.x.x.
         */

        item = result.items_[result.items_.length - 1];

        if (item && !item.passed_) {
          failedCount += 1;
          message = item.message;
          item.message = trace + '\n [' + values[i].join(PAD) + '] (' + message + ')';
        }
      }

    }

    // use these in further assertions
    return values;
  };

  /**
   * private method
   * parseFn() takes a function or string and extracts the data table labels and values.
   * returns a result object with {body} the function as a string and {table} data array.
   */
  function parseFnBody(fnBody) {

    var fs = fnBody.toString();
    var table = fs.match(/\/(\*){3,3}[^\*]+(\*){3,3}\//);
    var data = table[0].replace(/[\/\*]*[\r]*[\*\/]*/g, '').split('\n');
    var rows = [];

    var row, size;

    for (var i = 0; i < data.length; i++) {

      row = data[i].replace(/\b[\s*]/,'').replace(/(\s)*\b/, '');

      if (row.match(/\S+/)) {

        row = row.replace(/\s+/g, '');

        // empty column
        if (row.match(/[\|][\|]/g)) {
          throw new Error('where() data table has unbalanced columns: ' + row);
        }

        row = balanceRowData(row);

        // visiting label row
        if (typeof size != 'number') {
          shouldNotHaveDuplicateLabels(row);
          size = row.length;
        }

        // data row length
        if (size !== row.length) {
          throw new Error('where-data table has unbalanced row; expected ' + size +
                          ' columns but has ' + row.length + ': [' + row.join(', ') +
                          ']');
        }

        convertNumerics(row);

        rows.push(row);
      }
    }

    // num rows
    if (rows.length < 2) {
      throw new Error('where() data table should contain at least 2 rows but has ' +
                      rows.length);
    }

    return rows;
  }

  function balanceRowData(row) {

    var cells = row.split(SEP);
    var left  = cells[0] === '';    //left border
    var right = cells[cells.length - 1] === '';    //right border

    if (left != right) {
      throw new Error('where-data table borders are not balanced: ' + row);
    }

    if (left) {
      cells.shift();
    }

    if (right) {
      cells.pop();
    }

    return cells;
  }

  /*
   * @private
   * @method shouldNotHaveDuplicateLabels() checks that row of data contains no duplicated
   *  data values - mainly used for checking column headers (a,b,c vs. a,b,b).
   * @param row array of values.
   */
  function shouldNotHaveDuplicateLabels(row) {
    for (var label, visited = {}, j = 0; j < row.length; j += 1) {

      label = row[j];

      if (visited[label]) {
        throw new Error('where-data table contains duplicate label \'' + label +
                        '\' in [' + row.join(', ') + ']');
      }

      visited[label] = 1;
    }
  }

  /*
   * @private
   * @method convertNumerics() replaces row data with numbers if value is numeric, or a
   *  'quoted' numeric string.
   * @param row array of values.
   */
  function convertNumerics(row) {
    for (var t, i = 0; i < row.length; i += 1) {

      t = parseFloat(row[i].replace(/\'|\"|\,/g,''));

      isNaN(t) || (row[i] = t);
    }
  }

}());


'use strict';

// We are creating one generic version of logger.
function _log(level) {

  return function() {
    var meta = null;
    var args = arguments;
    if (arguments.length === 0) {
      // we only check here current level, but we also should more but restify uses it only for trace checks
      return this._winston.level === level;
    } else if (arguments[0] instanceof Error) {
      // winston supports Error in meta, so pass it as last
      meta = arguments[0].toString();
      args = Array.prototype.slice.call(arguments, 1);
      args.push(meta);
    } else if (typeof (args[0]) === 'string') {
      // just arrayize for level
      args = Array.prototype.slice.call(arguments);
    } else {
      // push provided object as meta
      meta = arguments[0];
      args = Array.prototype.slice.call(arguments, 1);
      args.push(meta);
    }
    args.unshift(level);
    this._winston.log.apply(this._winston, args);
  }
}

var Bunyan2WinstonAdapter = {
  trace: _log('debug'),
  debug: _log('debug'),
  info: _log('info'),
  warn: _log('warn'),
  error: _log('error'),
  fatal: _log('error'),

  // Child at the moment does nothing, just return main adapter.
  child: function() {
    return this;
  },

  createAdapter: function(winston, mapping) {
    var obj = Object.create(this, {
      _winston: {value: winston}
    });

    if (typeof mapping === 'object') {
      for (var key in mapping) {
        obj[key] = _log(mapping[key]);
      }
    }

    return obj;
  }

};

module.exports = Bunyan2WinstonAdapter;


var argy = require('argy');
var debug = require('debug')('async-chainable');
var util = require('util');

// Utility functions {{{
/**
* Try and return a value from a deeply nested object by a dotted path
* This is functionally the same as lodash's own _.get() function
* @param {*} obj The array or object to traverse
* @param {string} path The path to find
* @param {*} [defaultValue=undefined] The value to return if nothing is found
*/
function getPath(obj, path, defaultValue) {
	var pointer = obj;
	path.split('.').every(function(slice) {
		if (pointer[slice]) {
			pointer = pointer[slice];
			return true;
		} else {
			pointer = defaultValue;
			return false;
		}
	});
	return pointer;
}

/**
* Try and idenfity if the given item is a promise or is promise like
* @param {*} item The item to examine
* @returns {boolean} Whether the item looks like a promise
*/
function isPromise(item) {
	if (!item) return false;
	return (
		(util.types && util.types.isPromise && util.types.isPromise(item))
		|| (item.constructor && item.constructor.name == 'Promise')
		|| (!item instanceof objectInstance && item.then && typeof item.then == 'function')
	);
};

/**
* Figure out if a function declaration looks like it takes a callback
* Really this just checks the setup of a function for an argument - it cannot check if that argument is a function
* @param {function} fn The function to examine
* @returns {boolean} Whether the function LOOKS like it takes a callback
*/
function hasCallback(fn) {
	var fnString = fn.toString();
	// console.log('---');
	// console.log('GIVEN', '>>> ' + fnString + ' <<<');

	var bits, fnArgs;
	if (/^async /.test(fnString)) {
		// console.log('IS ASYNC');
		return false; // Its an async function and should only ever return a promise
	} else if (bits = /^function\s*(?:.*?)\s*\((.*?)\)/.exec(fnString)) {
		// console.log('> FUNC', bits[1]);
		fnArgs = bits[1];
	} else if (/^\(\s*\)\s*=>/.test(fnString)) {
		// console.log('ARROW (no args)');
		return false;
	} else if (bits = /^\s\((.*?)\)\s*?=>/.exec(fnString)) {
		// console.log('> ARROW (braces)', bits[1]);
		fnArgs = bits[1];
	} else if (bits = /^(.*?)\s*=>/.exec(fnString)) {
		// console.log('> ARROW (no braces)', bits[1]);
		fnArgs = bits[1];
	} else {
		// console.log('> EMPTY');
		return false;
	}

	fnArgs = fnArgs.replace(/^\s+/, '').replace(/\s+$/, ''); // Clean up args by trimming whitespace
	// console.log('ARGS:', fnArgs);

	return !! fnArgs;

};
// }}}

// Plugin functionality - via `use()`
var _plugins = {};
function use(module) {
	module.call(this);
	return this;
};
// }}}

/**
* Queue up a function(s) to execute in series
* @param {array|Object|function} The function(s) to execute
* @return {Object} This chainable object
*/
function series() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('function', function(callback) {
			self._struct.push({ type: 'seriesArray', payload: [callback] });
		})
		.ifForm('string function', function(id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'seriesObject', payload: payload});
		})
		.ifForm('array', function(tasks) {
			self._struct.push({ type: 'seriesArray', payload: tasks });
		})
		.ifForm('object', function(tasks) {
			self._struct.push({ type: 'seriesObject', payload: tasks });
		})
		// Async library compatibility {{{
		.ifForm('array function', function(tasks, callback) {
			self._struct.push({ type: 'seriesArray', payload: tasks });
			self.end(callback);
		})
		.ifForm('object function', function(tasks, callback) {
			self._struct.push({ type: 'seriesObject', payload: tasks });
			self.end(callback);
		})
		// }}}
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .series(): ' + form);
		});

	return self;
};

/**
* Queue up a function(s) to execute in parallel
* @param {array|Object|function} The function(s) to execute
* @return {Object} This chainable object
*/
function parallel() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('function', function(callback) {
			self._struct.push({ type: 'parallelArray', payload: [callback] });
		})
		.ifForm('string function', function(id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'parallelArray', payload: payload });
		})
		.ifForm('array', function(tasks) {
			self._struct.push({ type: 'parallelArray', payload: tasks });
		})
		.ifForm('object', function(tasks) {
			self._struct.push({ type: 'parallelObject', payload: tasks });
		})
		// Async library compatibility {{{
		.ifForm('array function', function(tasks, callback) {
			self._struct.push({ type: 'parallelArray', payload: tasks });
			self.end(callback);
		})
		.ifForm('object function', function(tasks, callback) {
			self._struct.push({ type: 'parallelObject', payload: tasks });
			self.end(callback);
		})
		// }}}
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .parallel(): ' + form);
		})

	return self;
};

/**
* Like parallel but only return the first, non-undefined, non-null result
* @param {string} The ID to set when the first function returns a non-undefined, non-null result
* @param {array} The functions to execute
* @return {Object} This chainable object
*/
function race() {
	var self = this;

	argy(arguments)
		.ifForm('', function() {})
		.ifForm('array', function(tasks) {
			self._struct.push({ type: 'race', payload: tasks });
		})
		.ifForm('string array', function(id, tasks) {
			self._struct.push({ type: 'race', id: arguments[0], payload: arguments[1] });
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .parallel(): ' + form);
		})

	return self;
};

/**
* Run an array/object though a function
* This is similar to the async native .each() function but chainable
*/
function forEach() {
	var self = this;

	argy(arguments)
		.ifForm('', function() {})
		.ifForm('array function', function(tasks, callback) {
			self._struct.push({ type: 'forEachArray', payload: tasks, callback: callback });
		})
		.ifForm('object function', function(tasks, callback) {
			self._struct.push({ type: 'forEachObject', payload: tasks, callback: callback });
		})
		.ifForm('string function', function(tasks, callback) {
			self._struct.push({ type: 'forEachLateBound', payload: tasks, callback: callback });
		})
		.ifForm('number function', function(max, callback) {
			self._struct.push({ type: 'forEachRange', min: 1, max: max, callback: callback });
		})
		.ifForm('number number function', function(min, max, callback) {
			self._struct.push({ type: 'forEachRange', min: min, max: max, callback: callback });
		})
		.ifForm('string array function', function(output, tasks, callback) {
			self._struct.push({ type: 'mapArray', output: output, payload: tasks, callback: callback });
		})
		.ifForm('string object function', function(output, tasks, callback) {
			self._struct.push({ type: 'mapObject', output: output, payload: tasks, callback: callback });
		})
		.ifForm('string string function', function(output, tasks, callback) {
			self._struct.push({ type: 'mapLateBound', output: output, payload: tasks, callback: callback });
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .forEach(): ' + form);
		});

	return self;
}

// Defer functionality - Here be dragons! {{{
/**
* Timeout handler during an await() defer operation
* @var {number}
*/
var _deferTimeoutHandle;

/**
* Collection of items that have been deferred
* @type {array} {payload: function, id: null|String, prereq: [dep1, dep2...]}
* @access private
*/
function deferAdd(id, task, parentChain) {
	var self = this;
	parentChain.waitingOn = (parentChain.waitingOn || 0) + 1;

	if (! parentChain.waitingOnIds)
		parentChain.waitingOnIds = [];
	parentChain.waitingOnIds.push(id);

	self._deferred.push({
		id: id || null,
		prereq: parentChain.prereq || [],
		payload: function(next) {
			self._context._id = id;

			run(self._options.context, task, function(err, value) { // Glue callback function to first arg
				if (id) self._context[id] = value;

				self._deferredRunning--;

				if (--parentChain.waitingOn == 0) {
					parentChain.completed = true;

					if (self._struct.length && self._struct[self._structPointer].type == 'await') self._execute(err);
				}

				self._execute(err);
			}, (parentChain.prereq || []).map(function(pre) {
				return self._context[pre];
			}));
		}
	});
};

function _deferCheck() {
	var self = this;
	if (self._options.limit && self._deferredRunning >= self._options.limit) return; // Already over limit

	self._deferred = self._deferred.filter(function(item) {
		if (self._options.limit && self._deferredRunning >= self._options.limit) {
			return true; // Already over limit - all subseqent items should be left in place
		}
		if (
			item.prereq.length == 0 || // No pre-reqs - can execute now
			item.prereq.every(function(dep) { // All pre-reqs are satisfied
				return self._context.hasOwnProperty(dep);
			})
		) {
			self._deferredRunning++;
			setTimeout(item.payload);
			return false;
		} else { // Can't do anything with self right now
			// Timeout functionality {{{
			if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
			if (self._options.timeout) {
				_deferTimeoutHandle = setTimeout(self._options.timeoutHandler.bind(self), self._options.timeout);
			}
			// }}}
			return true;
		}
	});
};
// }}}

/**
* Queue up a function(s) to execute as deferred - i.e. dont stop to wait for it
* @param {array|Object|function} The function(s) to execute as a defer
* @return {Object} This chainable object
*/
function defer() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('function', function(callback) {
			self._struct.push({ type: 'deferArray', payload: [callback] });
		})
		.ifForm('string function', function(id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'deferObject', payload: payload });
		})
		.ifForm('array', function(tasks) {
			self._struct.push({ type: 'deferArray', payload: tasks });
		})
		.ifForm('object', function(tasks) {
			self._struct.push({ type: 'deferObject', payload: tasks });
		})
		.ifForm('array function', function(preReqs, callback) {
			self._struct.push({ type: 'deferArray', prereq: preReqs, payload: [callback] });
		})
		.ifForm('string string function', function(preReq, id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'deferObject', prereq: [preReq], payload: payload });
		})
		.ifForm('array string function', function(preReqs, id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'deferObject', prereq: preReqs, payload: payload });
		})
		.ifForm('string array function', function(id, preReqs, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'deferObject', prereq: preReqs, payload: payload });
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .defer():' + form);
		});

	return self;
};

/**
* Queue up an await point
* This stops the execution queue until its satisfied that dependencies have been resolved
* @param {array,...} The dependencies to check resolution of. If omitted all are checked
* @return {Object} This chainable object
*/
function await() {
	var payload = [];

	// Slurp all args into payload
	argy(arguments)
		.getForm()
		.split(',')
		.forEach(function(type, offset) {
			switch (type) {
				case '': // Blank arguments - do nothing
					// Pass
					break;
				case 'string':
					payload.push(args[offset]);
					break;
				case 'array':
					payload.concat(args[offset]);
					break;
				default:
					throw new Error('Unknown argument type passed to .await(): ' + type);
			}
		});

	this._struct.push({ type: 'await', payload: payload });

	return this;
};

/**
* Queue up a timeout setter
* @param {number|function|false} Either a number (time in ms) of the timeout, a function to set the timeout handler to or falsy to disable
* @return {Object} This chainable object
*/
function timeout(newTimeout) {
	var self = this;
	argy(arguments)
		.ifForm('', function() {
			self._struct.push({ type: 'timeout', delay: false });
		})
		.ifForm('boolean', function(setTimeout) {
			if (setTimeout) throw new Error('When calling .timeout(Boolean) only False is accepted to disable the timeout');
			self._struct.push({ type: 'timeout', delay: false });
		})
		.ifForm('number', function(delay) {
			self._struct.push({ type: 'timeout', delay: delay });
		})
		.ifForm('function', function(callback) {
			self._struct.push({ type: 'timeout', callback: callback });
		})
		.ifForm('number function', function(delay, callback) {
			self._struct.push({ type: 'timeout', delay: delay, callback: callback });
		})
		.ifForm('function number', function(delay, callback) {
			self._struct.push({ type: 'timeout', delay: delay, callback: callback });
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .timeout():' + form);
		});

	return self;
}

/**
* The default timeout handler
* This function displays a simple error message and also fires the 'timeout' hook
*/
function _timeoutHandler() {
	var currentTaskIndex = this._struct.findIndex(function(task) { return ! task.completed });

	if (!currentTaskIndex < 0) {
		console.log('Async-Chainable timeout on unknown task');
		console.log('Full structure:', this._struct);
	} else {
		console.log('Async-Chainable timeout: Task #', currentTaskIndex + 1, '(' + this._struct[currentTaskIndex].type + ')', 'elapsed timeout of', this._options.timeout + 'ms');
	}

	this.fire('timeout');
}

/**
* Queue up a limit setter
* @param {number|null|false} Either the number of defer processes that are allowed to execute simultaniously or falsy values to disable
* @return {Object} This chainable object
*/
function setLimit(setLimit) {
	this._struct.push({ type: 'limit', payload: setLimit });
	return this;
};

/**
* Queue up a context setter
* @param {Object} newContext The new context to pass to all subsequent functions via `this`
* @return {Object} This chainable object
*/
function setContext(newContext) {
	this._struct.push({ type: 'context', payload: newContext });
	return this;
};

/**
* Queue up a varable setter (i.e. set a hash of variables in context)
* @param {string} The named key to set
* @param {*} The value to set
* @return {Object} This chainable object
*/
function set() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('string scalar|array|object|date|regexp|null', function(id, value) {
			var payload = {};
			payload[id] = value;
			self._struct.push({ type: 'set', payload: payload });
		})
		.ifForm('object', function(obj) {
			self._struct.push({ type: 'set', payload: obj });
		})
		.ifForm('function', function(callback) {
			self._struct.push({ type: 'seriesArray', payload: [callback] });
		})
		.ifForm('string function', function(id, callback) {
			var payload = {};
			payload[id] = callback;
			self._struct.push({ type: 'seriesObject', payload: payload});
		})
		.ifForm(['string', 'string undefined'], function(id) {
			var payload = {};
			payload[id] = undefined;
			self._struct.push({ type: 'set', payload: payload });
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .set():' + form);
		});

	return self;
};

/**
* Set a context items value
* Not to be confused with `set()` which is the chainable external visible version of this
* Unlike `set()` this function sets an item of _context immediately
* @access private
* @see _setRaw()
*/
function _set() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('string scalar|array|object|date|regexp|null', function(id, value) {
			self._setRaw(id, value);
		})
		.ifForm('object', function(obj) {
			for (var key in obj)
				self._setRaw(key, obj[key]);
		})
		.ifForm('string function', function(id, callback) {
			self._setRaw(id, callback.call(this));
		})
		.ifForm('function', function(callback) { // Expect func to return something which is then processed via _set
			self._set(callback.call(this));
		})
		.ifForm(['string', 'string undefined'], function(id) {
			self._setRaw(id, undefined);
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .set():' + form);
		});

	return self;
}

/**
* Actual raw value setter
* This function is the internal version of _set which takes exactly two values, the key and the value to set
* Override this function if some alternative _context platform is required
* @param {string} key The key within _context to set the value of
* @param {*} value The value within _context[key] to set the value of
* @access private
*/
function _setRaw(key, value) {
	this._context[key] = value;
	return this;
}

/**
* Internal function executed at the end of the chain
* This can occur either in sequence (i.e. no errors) or a jump to this position (i.e. an error happened somewhere)
* @access private
*/
function _finalize(err) {
	// Sanity checks {{{
	if (this._struct.length == 0) return; // Finalize called on dead object - probably a defer() fired without an await()
	if (this._struct[this._struct.length - 1].type != 'end') {
		throw new Error('While trying to find an end point in the async-chainable structure the last item in the this._struct does not have type==end!');
		return;
	}
	// }}}
	var self = this;
	this.fire('end', function(hookErr) {
		self._struct[self._struct.length-1].payload.call(self._options.context, err || hookErr);
		if (self._options.autoReset) self.reset();
	});
};

/**
* Internal function to execute the next pending queue item
* This is usually called after the completion of every asyncChainable.run call
* @access private
*/
function _execute(err) {
	var self = this;
	if (err) return this._finalize(err); // An error has been raised - stop exec and call finalize now

	if (!self._executing) { // Never before run this object - run fire('start') and defer until it finishes
		self._executing = true;
		return self.fire.call(self, 'start', self._execute.bind(self));
	}

	do {
		var redo = false;
		if (self._structPointer >= self._struct.length) return this._finalize(err); // Nothing more to execute in struct
		self._deferCheck(); // Kick off any pending deferred items
		var currentExec = self._struct[self._structPointer];
		// Sanity checks {{{
		if (!currentExec.type) {
			throw new Error('No type is specified for async-chainable structure at offset ' + self._structPointer);
			return self;
		}
		// }}}
		self._structPointer++;

		// Skip step when function supports skipping if the argument is empty {{{
		if (
			[
				'parallelArray', 'parallelObject',
				'forEachArray', 'forEachObject',
				'seriesArray', 'seriesObject',
				'deferArray', 'deferObject',
				'set'
			].indexOf(currentExec.type) > -1 &&
			(
				!currentExec.payload || // Not set OR
				(argy.isType(currentExec.payload, 'array') && !currentExec.payload.length) || // An empty array
				(argy.isType(currentExec.payload, 'object') && !Object.keys(currentExec.payload).length) // An empty object
			)
		) {
			currentExec.completed = true;
			redo = true;
			continue;
		}
		// }}}

		switch (currentExec.type) {
			case 'forEachRange':
				var iterArray = Array(currentExec.max - currentExec.min + 1).fill(false);
				self.runArray(iterArray.map(function(v, i) {
					var val = self._context._item = currentExec.min + i;
					var index = self._context._key = i;
					return function(next) {
						if (currentExec.translate) val = currentExec.translate(val, index, iterArray.length);
						run(self._options.context, currentExec.callback, next, [val, index, iterArray.length]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachArray':
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, next, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'forEachArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'forEachObject';
				} else {
					throw new Error('Cannot perform forEach over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;

			case 'mapArray':
				var output = new Array(currentExec.payload.length);
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, function(err, value) {
							if (err) return next(err);
							output[iter] = value;
							next();
						}, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapObject':
				var output = {};
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value, outputKey) {
							output[outputKey || key] = value;
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'mapArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'mapObject';
				} else {
					throw new Error('Cannot perform map over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;
			case 'parallelArray':
			case 'seriesArray':
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, next);
					};
				}), currentExec.type == 'parallelArray' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'seriesObject':
			case 'parallelObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						run(self._options.context, currentExec.payload[key], function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						});
					};
				}), currentExec.type == 'parallelObject' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'race':
				var hasResult = false;
				var hasError = false;
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, function(err, taskResult) {
							if (err) {
								hasError = true
								next(err, taskResult);
							} else if (!hasResult && !hasError && taskResult !== null && typeof taskResult !== 'undefined') {
								self._set(currentExec.id, taskResult); // Allocate returned value to context
								hasResult = true;
								next('!RACEDONE!', taskResult); // Force an error to stop the run() function
							} else {
								next(err, taskResult);
							}
						});
					};
				}), self._options.limit, function(err, val) {
					currentExec.completed = true;

					// Override race finish error as it was just to stop the race and not a real one
					if (err == '!RACEDONE!') return self._execute();

					self._execute(err);
				});
				break;
			case 'deferArray':
				currentExec.payload.forEach(function(task) {
					self._deferAdd(null, task, currentExec);
				});

				redo = true;
				break;
			case 'deferObject':
				Object.keys(currentExec.payload).forEach(function(key) {
					self._deferAdd(key, currentExec.payload[key], currentExec);
				});

				redo = true;
				break;
			case 'await': // Await can operate in two modes, either payload=[] (examine all) else (examine specific keys)
				if (!currentExec.payload.length) { // Check all tasks are complete
					if (self._struct.slice(0, self._structPointer - 1).every(function(stage) { // Examine all items UP TO self one and check they are complete
						return stage.completed;
					})) { // All tasks up to self point are marked as completed
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one task is outstanding - rewind to self stage so we repeat on next resolution
					}
				} else { // Check certain tasks are complete by key
					if (currentExec.payload.every(function(dep) { // Examine all named dependencies
						return !! self._context[dep];
					})) { // All are present
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one dependency is outstanding - rewind to self stage so we repeat on next resolution
					}
				}
				break;
			case 'limit': // Set the options.limit variable
				self._options.limit = currentExec.payload;
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'timeout': // Set the timeout function or its timeout value

				if (currentExec.delay === false) { // Disable
					self._options.timeout = false;
				} else {
					// Set the callback if one was passed
					if (currentExec.callback) self._options.timeoutHandler = currentExec.callback;

					// Set the delay if one was passed
					if (currentExec.delay) self._options.timeout = currentExec.delay;
				}

				currentExec.completed = true;
				redo = true; // Move to next action
				break;
			case 'context': // Change the self._options.context object
				self._options.context = currentExec.payload ? currentExec.payload : self._context; // Set context (if null use internal context)
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'set': // Set a hash of variables within context
				Object.keys(currentExec.payload).forEach(function(key) {
					self._set(key, currentExec.payload[key]);
				});
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'end': // self should ALWAYS be the last item in the structure and indicates the final function call
				this._finalize();
				break;
			default:
				if (this._plugins[currentExec.type]) { // Is there a plugin that should manage this?
					this._plugins[currentExec.type].call(this, currentExec);
				} else {
					throw new Error('Unknown async-chainable exec type: ' + currentExec.type);
				}
				return;
		}
	} while (redo);
};

// runArray() - dispatch an array of callbacks (with a parallel run limit) and run a callback when complete {{{
/**
* Internal function to run an array of functions (usually in parallel)
* Functions can be run in series by passing limit=1
* NOTE: Since this function is the central bottle-neck of the application code here is designed to run as efficiently as possible. This can make it rather messy and unpleasent to read in order to maximize thoughput.
* Series execution can be obtained by setting limit = 1
* @param {array} tasks The array of tasks to execute
* @param {number} limit The limiter of tasks (if limit==1 tasks are run in series, if limit>1 tasks are run in limited parallel, else tasks are run in parallel)
* @param {function} callback(err) The callback to fire on finish
* @return {Object} This chainable object
*/
function runArray(tasks, limit, callback) {
	var self = this;
	var nextTaskOffset = 0;
	var running = 0;
	var err;

	// Empty
	if (!tasks || !tasks.length) return callback();

	// Timeout functionality {{{
	var _timeoutTimer;
	var resetTimeout = function(setAgain) {
		if (_timeoutTimer) clearTimeout(_timeoutTimer);
		if (setAgain) _timeoutTimer = self._options.timeout ? setTimeout(self._options.timeoutHandler.bind(self), self._options.timeout) : null;
	};
	// }}}

	var taskFinish = function(taskErr, taskResult) {
		if (taskErr) err = taskErr;
		--running;
		if (err && !running) {
			resetTimeout(false);
			callback(err);
		} else if (err) { // Has an err - stop allocating until we empty
			resetTimeout(false);
			// Pass
		} else if (!running && nextTaskOffset > tasks.length - 1) { // Finished everything
			resetTimeout(false);
			callback(err);
		} else if (nextTaskOffset < tasks.length) { // Still more to alloc
			running++;
			resetTimeout(true);
			setTimeout(tasks[nextTaskOffset++].bind(this._context, taskFinish));
		}
	};

	var maxTasks = limit && limit <= tasks.length ? limit : tasks.length;
	for (var i = 0; i < maxTasks; i++) {
		running++;

		setTimeout(tasks[i].bind(this, taskFinish));

	}
	resetTimeout(true); // Start initial timeout

	nextTaskOffset = maxTasks;

	return this;
}

/**
* Run a single function, promise return, promise factory or any other combination - then a callback when finished
* Take a function reference and treat it as a callback style function
* If the function returns a promise this behaviour is transformed into a callback style function
*
* This function accepts all types of input functions:
* - callback functions (e.g. `next => ...`)
* - async functions (e.g. `async ()=> ...`)
* - promise functions (e.g. `new Promise(resolve => ...)`)
* - promise factories (e.g. `()=> new Promise(resolve => ...)`)
*
* @param {Object} context The context object to pass to the function
* @param {function|Promise} fn The function to run or the promise to resolve
* @param {function} finish The function to call when finished
* @param {array} [args] Additional arguments to pass when calling the function
*
* @example Run a regular callback function - if func is callback its just passed through, if its a promise its mapped normally
* run(func, thenFunc)
*/
function run(context, fn, finish, args) {
	// Argument mangling {{{
	if (typeof context == 'function') { // called as run(fn, finish, args);
		args = finish;
		finish = fn;
		fn = context;
		context = this;
	}
	// }}}

	if (isPromise(fn)) { // Given a promise that has already resolved?
		fn
			.then(function(value) {
				finish.apply(context, [null, value]);
			})
			.catch(function(err) {
				finish.call(context, err || 'An error occured');
			});
	} else if (hasCallback(fn)) { // Callback w/ (err, result) pattern
		var result = fn.apply(context, args ? [finish].concat(args) : [finish]);
		if (isPromise(result)) {
			result
				.then(function(value) { // Remap result from (val) => (err, val)
					finish.apply(context, [null, value]);
				})
				.catch(function(err) {
					finish.call(context, err || 'An error occured');
				});
		}
	} else { // Maybe either a promise or sync function?
		var result;
		try {
			result = fn.apply(context, args || []); // Run the function and see what it gives us
		} catch (e) {
			finish.call(context, e);
		}
		if (isPromise(result)) { // Got back a promise - attach to the .then() function
			result
				.then(function(value) { // Remap result from (val) => (err, val)
					finish.apply(context, [null, value]);
				})
				.catch(function(err) {
					finish.call(context, err || 'An error occured');
				});
		} else { // Didn't provide back a promise - assume it was a sync function
			finish.apply(context, [null, result]);
		}
	}
};

/**
* Internal function to run a callback until it returns a falsy value
* @param {function} iter The function to invoke on each call with the arguments (next, index)
* @param {number} limit The number of parallel threads to execute. This must be a finite number (defaults to 10 if not)
* @param {function} callback The function to invoke when iter finally returns a falsy value
* @return {Object} This chainable object
*/
function runWhile(iter, limit, callback) {
	var index = 0;
	var hasExited = false;
	var err;
	var running = 0;
	if (!Number.isFinite(limit)) limit = 10;

	var invoke = function() {
		iter.call(this._context, function(taskErr, taskResult) {
			if (taskErr) err = taskErr;
			if (taskErr || !taskResult) hasExited = true;
			--running;
			if (err && !running) {
				callback(err, res);
			} else if (running <= 0 && hasExited) {
				callback(err);
			} else if (!hasExited) {
				setTimeout(invoke);
			}
		}, index++);
	};

	for (var i = 0; i < limit; i++) {
		running++;

		setTimeout(invoke);
	}

	return this;
};
// }}}

/**
* Reset all state variables and return the object into a pristine condition
* @return {Object} This chainable object
*/
function reset() {
	this._struct = [];
	this._structPointer = 0;

	var reAttachContext = (this._options.context == this._context); // Reattach the context pointer after reset?
	this._context = {
		_struct: this._struct,
		_structPointer: this._structPointer,
		_options: this._options,
		_deferredRunning: this._deferredRunning,
		hook: this.hook.bind(this),
		fire: this.fire.bind(this),
	};

	if (reAttachContext) this._options.context = this._context;
};

// Hooks {{{
/**
* Set up a hook
* @param {string} hook The hook name
* @param {string} [name] The function ID
* @param {array} [prereqs] Optional prerequisites to fired
* @param {function} callback Callback to run when hook fires (each callback takes a next paramter which must be called)
* @return {Object} This chainable object
*/
function hook() {
	var self = this;
	argy(arguments)
		.ifForm('', function() {})
		.ifForm('string function', function(hook, callback) { // Attach to one hook
			if (!self._hooks[hook]) self._hooks[hook] = [];
			self._hooks[hook].push({cb: callback});
		})
		.ifForm('string string function', function(hook, id, callback) { // Attach a named hook
			if (!self._hooks[hook]) self._hooks[hook] = [];
			self._hooks[hook].push({id: id, cb: callback});
		})
		.ifForm('string array function', function(hook, prereqs, callback) { // Attach to a hook with prerequisites
			if (!self._hooks[hook]) self._hooks[hook] = [];
			self._hooks[hook].push({prereqs: prereqs, cb: callback});
		})
		.ifForm('string string array function', function(hook, id, prereqs, callback) { // Attach a named hook with prerequisites
			if (!self._hooks[hook]) self._hooks[hook] = [];
			self._hooks[hook].push({id: id, prereqs: prereqs, cb: callback});
		})
		.ifForm('array function', function(hooks, callback) { // Attach to many hooks
			hooks.forEach(function(hook) {
				if (!self._hooks[hook]) self._hooks[hook] = [];
				self._hooks[hook].push({cb: callback});
			});
		})
		.ifFormElse(function(form) {
			throw new Error('Unknown call style for .on(): ' + form);
		});

	return self;
};

/**
* Fire a hook and run all callbacks in _series_
* NOTE: The calback will fire even if there are no entities matching that hook
* @param {string} hook The hook name to run
* @param {function} [callback] Optional callback to execute on completion
* @return {Object} this chainable object
*/
var fire = argy('string [function]', function fire(hook, callback) {
	if (!this._hooks[hook] || !this._hooks[hook].length) { // Nothing to do anyway
		if (callback) callback(); // Nothing to do anyway
		return;
	}

	var hookRunner = new objectInstance();

	this._hooks[hook].forEach(function(h) {
		var args = [];
		if (h.prereqs) args.push(h.prereqs);
		if (h.id) args.push(h.id)
		args.push(h.cb);
		hookRunner.defer.apply(hookRunner, args);
	});

	hookRunner
		.await()
		.end(callback);

	return this;
});
// }}}

/**
* Queue up an optional single function for execution on completion
* This function also starts the queue executing
* @param {string} [extract] If supplied this context key will be supplied as the output to the callback (i.e. this is the same as `.end(err => callback(err, this[extract]))`))
* @param {function} [final] Optional final function to run. This is passed the optional error state of the chain
* @return {Object} This chainable object
*/
var end = argy('[string] [function]', function end(extract, callback) {
	if (!callback) {
		this._struct.push({ type: 'end', payload: function() {} }); // .end() called with no args - make a noop()
	} else if (argy.isType(extract, 'string') && argy.isType(callback, 'function')) {
		this._struct.push({
			type: 'end',
			payload: function(err) {
				if (err) {
					return callback(err);
				} else {
					callback(null, this[extract]);
				}
			},
		});
	} else if (!extract && argy.isType(callback, 'function')) {
		this._struct.push({ type: 'end', payload: callback });
	} else {
		throw new Error('Unknown end type');
	}

	this._execute();
	return this;
});

/**
* Alternative to end() which returns a JS standard Promise
* @param {string} [key] The context key to resolve the promise with
* @param {function} [callback] Optional callback (to call the same as .end()), this is called before the promise resolves / rejects
* @return {Promise} A promise representing the async chain
*/
var promise = argy('[string] [function]', function(key, callback) {
	var self = this;

	return new Promise(function(resolve, reject) {
		self._struct.push({type: 'end', payload: function(err) {
			if (err) {
				if (callback) callback(err);
				reject(err);
			} else {
				if (callback) callback(null, key ? self._context[key] : null);
				resolve(key ? self._context[key] : null);
			}
		}});

		self._execute();
	});
});

/**
* Take a callback style function and return a hybrid (callback OR promise) wrapper
* @param {function} fn The function to transform
* @param {function} [finish] The callback to optionally call when done
* @return {function} The hybrid function which runs as a callback but could accept promises
*
* @example Transform a callback-style function into a promise
* async.hybrid((done, path) => fs.stat(path, done), '/').then(stats => console.log('Stats are', stats))
*
* @example Copy a callback-style function into another
* asyncChainable.hybrid((done, path) => fs.stat(path, done), (err, stats) => console.log('Stats are', stats))
*/
function hybrid(fn, cb) {
	if (typeof cb === 'function') { // Already in callback style - wrap the function so that cb gets called when it completes
		return function() {
			var args = Array.prototype.slice.call(arguments, 0);
			fn.apply(this, [cb].concat(args));
		};
	} else { // Wrap the function in a promise
		return function() {
			var args = Array.prototype.slice.call(arguments, 0);
			return new Promise(function(resolve, reject) {
				fn.apply(this, [
					function(err, res) {
						if (err) {
							reject(err);
						} else {
							resolve(res);
						}
					}
				].concat(args));
			});
		};
	}
};

var objectInstance = function() {
	// Variables {{{
	this._struct = [];
	this._structPointer = 0;
	this._context = {};
	this._hooks = {};

	this._options = {
		autoReset: true, // Run asyncChainable.reset() after finalize. Disable this if you want to see a post-mortem on what did run
		limit: 10, // Number of defer functions that are allowed to execute at once
		context: this._context, // The context item passed to the functions (can be changed with .context())
		timeout: false, // Whether we should support timeouts (any number >0 will trigger after domant for that number of ms)
		timeoutHandler: _timeoutHandler, // The timeout function to run if timeout expires
	};
	// }}}

	// Async-Chainable functions {{{
	// Private {{{
	this._execute = _execute;
	this._deferCheck = _deferCheck;
	this._deferAdd = deferAdd;
	this._deferred = [];
	this._deferredRunning = 0;
	this._finalize = _finalize;
	this._plugins = _plugins;
	// }}}

	this.await = await;
	this.context = setContext;
	this.defer = defer;
	this.end = end;
	this.fire = fire;
	this.forEach = forEach;
	this.hasCallback = hasCallback;
	this.isPromise = isPromise;
	this.map = forEach;
	this.getPath = getPath;
	this.hook = hook;
	this.limit = setLimit;
	this.new = function() { return new objectInstance };
	this.parallel = parallel;
	this.promise = promise;
	this.race = race;
	this.reset = reset;
	this.run = run;
	this.runArray = runArray;
	this.runWhile = runWhile;
	this.series = series;
	this._setRaw = _setRaw;
	this._set = _set;
	this.set = set;
	this.then = series;
	this._timeoutHandler = _timeoutHandler;
	this.timeout = timeout;
	this.use = use;
	this.hybrid = hybrid;
	// }}}

	// Detect and act on debug mode {{{
	if (debug.enabled) {
		this._options.timeout = 5000;
		this._options.timeoutHandler = _timeoutHandler;
	}
	// }}}

	this.reset();
	return this;
}

// Return the output object
module.exports = function asyncChainable() {
	return new objectInstance;
};

// Glue utility functions onto the main prototype
module.exports.run = run;
module.exports.runArray = runArray;
module.exports.hasCallback = hasCallback;
module.exports.hybrid = hybrid;


'use strict';

module.exports = {
  TaskList: TaskList,
  TaskInfo: TaskInfo
};

function TaskInfo(data) {
  data = data || {};
  this.name    = data.name;
  this.file    = data.file;
  this.line    = data.line;
  this.comment = data.comment || {};
}

TaskInfo.prototype = {
  /**
   * Return the first tag with a name=name
   * @param {string} name
   * @returns {*}
   */
  tag: function tag(name) {
    if (!this.comment || !this.comment.tags) {
      return null;
    }

    for (var i = 0; i < this.comment.tags.length; i++) {
      var tagObj = this.comment.tags[i];
      if (tagObj.name === name) {
        return tagObj.value;
      }
    }
    return null;
  }
};

function TaskList(tasks) {
  this.tasks = tasks || [];
}

TaskList.prototype = {
  constructor: TaskList,

  push: function push(task) {
    if (!(task instanceof TaskInfo)) {
      if (task.name) {
        return this.push(new TaskInfo(task));
      } else {
        return false;
      }
    }

    return this.tasks.push(task);
  },

  forEach: function forEach(cb) {
    for (var i = 0; i < this.tasks.length; i++) {
      if (false === cb(this.tasks[i])) {
        break;
      }
    }
  },

  filter: function filter(cb) {
    var tasks = [];
    for (var i = 0; i < this.tasks.length; i++) {
      if (cb(this.tasks[i])) {
        tasks.push(this.tasks[i]);
      }
    }
    return new this.constructor(tasks);
  },

  sort: function sort(cb) {
    cb = cb || function(a, b) {
      return a.name.localeCompare(b.name);
    };
    this.tasks.sort(cb);
    return this;
  },

  /**
   * Return all tasks that have no internal tag and verbose.
   * If verbose argument is provided than check only or internal tag.
   * @param {boolean} verbose
   * @returns {TaskList}
   */
  filterHidden: function(verbose) {
    return this.filter(function(task) {
      return !task.tag('internal') && (verbose || !task.tag('verbose'));
    });
  },

  getLongestNameLength: function getLongestNameLength() {
    var maxLength = 0;
    this.forEach(function(task) {
      maxLength = Math.max(maxLength, task.name.length);
    });
    return maxLength;
  }
};


module.exports = FixedValueHistory

// A fixed-length storage mechanism for a value (ideally a number)
// with some statistics methods. Expires oldest values to
// Input is not type checked, but non-numeric input will be considered NULL
// WRT the statistical methods.
function FixedValueHistory(maxLength, initial) {
  if (!(this instanceof FixedValueHistory))
    return new FixedValueHistory(maxLength, initial)
  if (!isNumber(maxLength) || maxLength == 0) {
    throw new Error("maxLength must be a positive number.")
  }
  this.maxLength = Math.floor(+maxLength)
  if (initial != null) {
    this.push(initial)
  }
}
FixedValueHistory.prototype.sum = 0
FixedValueHistory.prototype.maxLength = undefined
FixedValueHistory.prototype.array = []

FixedValueHistory.prototype.push = function (vals) {
  this.array = this.array.concat(vals)
  var expired = this.array.splice(0, this.array.length - this.maxLength)
  var expired_sum = FixedValueHistory.sum(expired)
  this.sum -= expired_sum
  var new_sum = FixedValueHistory.sum(vals)
  this.sum += new_sum
}
FixedValueHistory.prototype.mean = function () {
  if (this.array.length === 0) return NaN
  return this.sum / this.array.length
}
FixedValueHistory.prototype.variance = function () {
  if (this.array.length === 0) return NaN
  var mean = this.mean()
  return this.array.reduce(function(prevVal,currVal) {
      var diff
      if(isNumber(currVal)) {
          diff = currVal - mean
      } else {
          diff = mean
      }
      return prevVal + diff*diff
  }, 0) / this.array.length
}
FixedValueHistory.prototype.max = function () {
  if (this.array.length === 0) return NaN
  return Math.max.apply(null, this.array)
}
FixedValueHistory.prototype.min = function () {
  if (this.array.length === 0) return NaN
  return Math.min.apply(null, this.array)
}
FixedValueHistory.prototype.length = function () {
  return this.array.length
}
FixedValueHistory.prototype.values = function () {
  return this.array.slice(0)
}

// Static methods.
FixedValueHistory.sum = function (vals) {
  var sum = 0
  ;[].concat(vals).forEach(function (n) {
    if (isNumber(n)) {
      sum += n
    }
  })
  return sum
}

function isNumber(n) {
  return !isNaN(parseFloat(n)) && isFinite(n)
}


"use strict";

var util          = require('util');
var EventEmitter  = require('events').EventEmitter;
var CronJob       = require('cron').CronJob;
var fs            = require('fs');
var pathUtil      = require('path');

/**
 * Shorthand for common intervalls, so you don't need to work with milliseconds
 * @type {{sec: number, min: number, min15: number, hour: number, day: number}}
 */
var times = {
  sec:   1000,
  min:   60000, // 60 * 1000
  min15: 900000, // 15 * 60 * 1000
  hour:  3600000, // 60 * 60 * 1000
  day:   86400000 // 24 * 60 * 60 * 1000
};

/**
 * The default options for FileCleaner.
 * @type {{}}
 */
var defaultOptions = {
  start: false,
  timeZone: undefined,
  recursive: false,
  timeField: 'atime',
  blackList: undefined,
  whiteList: undefined
};

/**
 * Constructor function, takes the following paramters:
 *
 * @param string path - Filepath to the folder to watch
 * @param integer maxAge - value in milliseconds
 * @param string cronTime - Crontab like string
 * @param object options - Object with additional options
 * @constructor
 */
var FileCleaner = function (path, maxAge, cronTime, options) {

  this.job = null;
  this.path = path;
  this.maxAge = maxAge;
  this.cronTime = cronTime;
  this.options = util._extend({}, defaultOptions);
  if(typeof options === 'object' && options !== null){
    this.options = util._extend(this.options, options);
  }

  if(this.options.start === true){
    this.start();
  }

};

//We need to emit events:
util.inherits(FileCleaner, EventEmitter);

/**
 * Starts to wach the given path.
 * Will emit the event 'start'.
 */
FileCleaner.prototype.start = function(){
  this.job = new CronJob(
    this.cronTime,
    this.cleanUp,
    function(){},
    true,
    this.options.timeZone,
    this
  );

  this.emit('start', {
    path: this.path,
    cronTime: this.cronTime,
    maxAge: this.maxAge
  });
};

/**
 * Stops watching the given path.
 * Will emit the event 'stop'.
 */
FileCleaner.prototype.stop = function(){
  this.job.stop();

  this.emit('stop', {
    path: this.path,
    cronTime: this.cronTime,
    maxAge: this.maxAge
  });
};

/**
 * Will cleanup the given path and will
 * remove all files that are older than maxAge.
 *
 * Emits the event 'delete' if a file will be deleted.
 */
FileCleaner.prototype.cleanUp = function(){

  var self      = this;
  var maxAge    = this.maxAge;
  var recursive = this.options.recursive;
  var timeField = this.options.timeField;
  var blackList = this.options.blackList;
  var whiteList = this.options.whiteList;

  function worker(path) {
    fs.readdir(path, function (err, files) {
      if (err) {
        self.emit('error', new Error("Can't read directory " + path));
        return;
      }
      files.forEach(function (file) {
        var filePath = pathUtil.join(path, file);
        fs.stat(filePath, function (err, stats) {
          if (err) {
            self.emit('error', new Error("Can't read file " + filePath));
            return;
          }
          if (
            stats.isFile() &&
            (Date.now() - stats[timeField].getTime()) > maxAge &&
            checkPattern(file, blackList, whiteList)
          ) {
            fs.unlink(filePath, function (err) {
              if (err) {
                self.emit('error', new Error("Can't delete " + filePath));
                return;
              }
              self.emit('delete',{
                name: file,
                folder: path,
                path: filePath
              });
            });
          } else if (stats.isDirectory() && recursive) {
            worker(filePath);
          }
        });
      });
    });
  }

  worker(this.path);

};

/**
 * Helper functions
 */

/**
 * Checks blackList- and whitList-regex against
 * the given file name and returns if this file can
 * be deleted.
 *
 * @param string file - the gile name
 * @param regex|undefined blackList
 * @param regex|undefined - whiteList
 * @returns {boolean}
 */
function checkPattern(file, blackList, whiteList){

  if (util.isRegExp(blackList) && blackList.test(file)) {
    return false;
  }

  if (util.isRegExp(whiteList)) {
    if (whiteList.test(file)) {
      return true;
    }
    return false;
  }

  return true;
}

/**
 * Exports:
 */

module.exports.FileCleaner = FileCleaner;
module.exports.CronJob     = CronJob;
module.exports.times       = times;


// list prefixes and case transforms
// (reverse order as a decrementing for loop is used)
var prefixes = [
  'ms',
  'ms', // intentional: 2nd entry for ms as we will also try Pascal case for MS
  'O',
  'Moz',
  'Webkit',
  ''
];

var caseTransforms = [
  toCamelCase,
  null,
  null,
  toCamelCase,
  null,
  toCamelCase
];

var props = {};
var style;

module.exports = function (prop) {
  var ii;
  var propName;
  var pascalCaseName;

  style = style || document.body.style;

  // if we already have a value for the target property, return
  if (props[prop] || style[prop]) {
    return props[prop];
  }

  // convert a dash delimited propertyname (e.g. box-shadow) into
  // pascal cased name (e.g. BoxShadow)
  pascalCaseName = prop.split('-').reduce(function (memo, val) {
    return memo + val.charAt(0).toUpperCase() + val.slice(1);
  }, '');

  // check for the property
  for (ii = prefixes.length; ii--;) {
    propName = prefixes[ii] + (caseTransforms[ii] ?
      caseTransforms[ii](pascalCaseName) :
      pascalCaseName);

    if (typeof style[propName] != 'undefined') {
      props[prop] = createGetterSetter(propName);
      break;
    }
  }

  return props[prop];
};

/* internal helper functions */

function createGetterSetter(propName) {
  function gs(element, value) {
    // if we have a value update
    if (typeof value != 'undefined') {
      element.style[propName] = value;
    }

    return window.getComputedStyle(element)[propName];
  }

  // attach the property name to the getter and setter
  gs.property = propName;
  return gs;
}

function toCamelCase(input) {
  return input.charAt(0).toLowerCase() + input.slice(1);
}