/*
 * Copyright 2013 Yahoo! Inc. All rights reserved.
 * Copyrights licensed under the BSD License.
 * See the accompanying LICENSE.txt file for terms.
 */

/*jslint nomen:true, node:true */
"use strict";

var libfs       = require('fs'),
    libpath     = require('path'),
    walk        = require('walk'),
    libsemver   = require('semver'),
    Module      = require("module").Module,
    Bundle      = require('./bundle.js'),
    debug       = require('debug')('locator'),
    DEFAULT_RULESET = 'main',
    DEFAULT_RULESETS = require('./rulesets.js'),
    DEFAULT_SELECTOR = '{}',
    DEFAULT_MAX_PACKAGES_DEPTH = 9999,
    DEFAULT_VERSION = '1.0.0',  // for bundles w/out a version or parent
    PATH_SEP = libpath.sep;

function mix(target, source, overwrite) {
    for (var prop in source) {
        if (source.hasOwnProperty(prop) && (overwrite || !target.hasOwnProperty(prop))) {
            target[prop] = source[prop];
        }
    }
    return target;
}

/**
 * The Locator walks the filesystem and gives semantic meaning to
 * files in the application.
 * @module Locator
 */

/**
 * @class Locator
 * @constructor
 * @param {object} [options] Options for how the configuration files are located.
 *   @param {string} [options.applicationDirectory] Where the application will be found.
 *   If not given it defaults to the current working directory.
 *   @param {[string]} [options.exclude] folder names that should not be analyzed
 * @param {integer} options.maxPackageDepth Maximum depth in `node_modules/` to walk.
 * Defaults to 9999.
 * @param {function} options.rulesetFn Function hook to compute rules per bundle dynamically.
 * This hook allow to analyze the pkg, and produce the proper rules when needed.
 *
 * @example (note constructor "BundleLocator" is exported as "Locator")
 *      var locOpts = {
 *              applicationDirectory: __dirname,
 *              exclude: ['build'],
 *              maxPackageDepth: 5
 *          },
 *          locator = new Locator(locOpts);
 */
function BundleLocator(options) {
    this._options = options || {};
    if (this._options.applicationDirectory) {
        this._options.applicationDirectory = libpath.resolve(process.cwd(), this._options.applicationDirectory);
    } else {
        this._options.applicationDirectory = process.cwd();
    }
    this._options.maxPackageDepth = this._options.maxPackageDepth || DEFAULT_MAX_PACKAGES_DEPTH;
    this._options.exclude = this._options.exclude || [];

    this._cacheRules = {};      // rulename + package directory: rules

    this._bundles = {};
    this._bundlePaths = {};     // path: name
    this._bundleUpdates = {};   // name: object describing why the update happened
}

BundleLocator.prototype = {

    /**
     * Parses the directory to turn it into a bundle.
     * @method parseBundle
     * @param {string} dir The directory for the bundle.
     * @param {object} [options] Options for processing the bundle.
     *
     * @example (note constructor "BundleLocator" is exported as "Locator")
     *      var locator = new Locator();
     *      var appBundle = locator.parseBundle(__dirname, {});
     *
     * @return {Bundle} The bundle.
     */
    parseBundle: function (dir, options) {
        var self = this,
            bundleSeeds;

        // Normalize the root directory before storing its value. If it is
        // stored before normalizing, other paths created afterwards may be
        // normalized and fail comparisons against the root directory
        dir = libpath.normalize(dir);

        this._rootDirectory = dir;

        bundleSeeds = this._walkNPMTree(dir);
        bundleSeeds = this._filterBundleSeeds(bundleSeeds);
        bundleSeeds.forEach(function (bundleSeed) {
            var opts = (bundleSeed.baseDirectory === dir) ? options : {};
            self._walkBundle(bundleSeed, opts);
        });
        this._rootBundleName = this._bundlePaths[libfs.realpathSync(dir)];
        return this._bundles[this._rootBundleName];
    },

    /**
     * Returns the named bundle, no matter how deeply found.
     * @method getBundle
     * @param {string} name The name of the bundle to retrieve.
     * @return {Bundle|undefined} The named bundle, or undefined if not found.
     */
    getBundle: function (name) {
        return this._bundles[name];
    },

    /**
     * Returns the root of the bundles.  This is the root bundle that was parsed
     * by the call to `parseBundle()`.
     * @method getRootBundle
     * @return {Bundle} The root bundle.
     */
    getRootBundle: function () {
        return this._bundles[this._rootBundleName];
    },

    /**
     * Utility method for listing all files in a bundle.
     * @method getBundleFiles
     * @param {string} bundleName The name of the bundle.
     * @param {object} filter Filter for deciding which files to return.
     * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which files to return.
     * @return {[string]} An array of filesystem paths.
     */
    getBundleFiles: function (bundleName, filter) {
        var bundle,
            files = [];
        bundle = this._bundles[bundleName];
        if (!bundle) {
            throw new Error('Unknown bundle "' + bundleName + '"');
        }
        Object.keys(bundle.files).forEach(function (fullpath) {
            var res = {
                    ext: libpath.extname(fullpath).substr(1)
                };
            if (this._filterResource(res, filter)) {
                files.push(fullpath);
            }
        }, this);
        return files;
    },

    /**
     * Utility method for listing all resources in a bundle.
     * @method getBundleResources
     * @param {string} bundleName The name of the bundle.
     * @param {object} filter Filter for deciding which resources to return.
     * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which resources to return.
     * @param {string|[string]} [filter.types] The resources types for which resources to return.
     * @return {[string]} An array of filesystem paths.
     */
    getBundleResources: function (bundleName, filter) {
        var bundle = this._bundles[bundleName];
        if (!bundle) {
            throw new Error('Unknown bundle "' + bundleName + '"');
        }
        return this._walkBundleResources(bundle, filter);
    },

    /**
     * Returns a list of resources in all the bundles.
     * @method getAllResources
     * @param {object} filter Filter for deciding which resources to list.
     * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) to return.
     * @param {string|[string]} [filter.types] The resources types to return.
     * @return {[LocatorResource]} An array of resources.
     */
    getAllResources: function (filter) {
        var self = this,
            ress = [];
        Object.keys(this._bundles).forEach(function (bundleName) {
            var bundle = self._bundles[bundleName];
            self._walkBundleResources(bundle, filter).forEach(function (res) {
                ress.push(res);
            });
        });
        return ress;
    },
    listAllResources: function (filter) {
        debug('`listAllResources()` is deprecated in favor of `getAllResources()`.');
        return this.getAllResources(filter);
    },

    /**
     * Returns a list of all located bundle names.
     * The names are not ordered.
     * @method getBundleNames
     * @param {Function} [filter] function to execute on each bundle.
     *   If no `filter` is supplied, all bundle names will be returned. The
     *   function will receive the following argument:
     * @param {Object} filter.bundle the current bundle being iterated on
     * @param {boolean} filter.return Return true to indicate that the
     *   bundle name should be returned in the list. Otherise the bundle
     *   name will be skipped.
     * @return {Array} list of bundles
     */
    getBundleNames: function (filter) {
        var bundleName,
            bundles = this._bundles,
            bundleNames = [];
        if ('function' !== typeof filter) {
            return Object.keys(this._bundles);
        }
        for (bundleName in bundles) {
            if (bundles.hasOwnProperty(bundleName)) {
                if (filter(bundles[bundleName])) {
                    bundleNames.push(bundleName);
                }
            }
        }
        return bundleNames;
    },
    listBundleNames: function (filter) {
        debug('`listBundleNames()` is deprecated in favor of `getBundleNames()`.');
        return this.getBundleNames(filter);
    },

    /**
     * Returns the name of the bundle to which the path belongs.
     * @private
     * @method _getBundleNameByPath
     * @param {string} findPath The path to consider.
     * @return {string} The name the bundle to which the findPath most closely matches.
     */
    _getBundleNameByPath: function (findPath) {
        // FUTURE OPTIMIZATION:  use a more complicated datastructure for faster lookups
        var found = {}, // length: path
            longest;
        // expands path in case of symlinks
        findPath = libfs.realpathSync(findPath);
        // searchs based on expanded path
        Object.keys(this._bundlePaths).forEach(function (bundlePath) {
            if (0 === findPath.indexOf(bundlePath) &&
                    (findPath.length === bundlePath.length ||
                    libpath.sep === findPath.charAt(bundlePath.length))) {
                found[bundlePath.length] = bundlePath;
            }
        });
        longest = Math.max.apply(Math, Object.keys(found));
        return this._bundlePaths[found[longest]];
    },

    /**
     * Creates the seed of a potential bundle.
     *
     * The seed includes:
     *  <dl>
     *      <dt> baseDirectory {string} </dt>
     *      <dd> The directory of the bundle. </dd>
     *      <dt> name {string} </dt>
     *      <dd> The name of the bundle. </dd>
     *      <dt> version {string} </dt>
     *      <dd> The version of the bundle. </dd>
     *      <dt> npmDepth {integer} </dt>
     *      <dd> The depth in the NPM package dependency tree. </dd>
     *      <dt> options {object} </dt>
     *      <dd> Options for the bundle, taken in part from the `"locator"` section of `package.json` </dd>
     *  </dl>
     * @private
     * @method _makeBundleSeed
     * @param {string} baseDirectory Full path to the bundle.
     * @param {string} name The name of the bundle.
     * Might be overrriden by the name in package.json (if it exists).
     * @param {string} version The version to use if not specified in the package.json.
     * Might be overriden by the version in package.json (if it exists).
     * @param {object} [pkg] Contents of the bundles package.json.
     * @param {object} [options] Additional options to apply. Lower priority
     * than those found in package.json.
     * @return {object} The bundle seed, as described above.
     */
    _makeBundleSeed: function (baseDirectory, name, version, pkg, options) {
        var seed;
        seed = {
            baseDirectory: baseDirectory,
            name: name,
            version: version
        };
        if (pkg) {
            seed.name = (pkg.locator && pkg.locator.name ? pkg.locator.name : pkg.name);
            seed.version = pkg.version;
            seed.options = pkg.locator;
            seed.pkg = pkg;
        }
        if (options) {
            if (seed.options) {
                // merge options under seed.options
                mix(seed.options, options);
            } else {
                seed.options = options;
            }
        }
        return seed;
    },

    /**
     * Makes a bundle out of a directory.
     * @private
     * @method _makeBundle
     * @param {object} seed The seed bundle, @see _makeBundleSeed()
     * @param {Bundle} parent Parent bundle. Only the root bundle doesn't have a parent.
     * @return {Bundle} The new bundle
     */
    _makeBundle: function (seed, parent) {
        var bundle,
            ruleset = this._loadRuleset(seed),
            msg;

        if (seed.options.location) {
            // This is fairly legacy, and we might be able to remove it.
            seed.baseDirectory = libpath.resolve(seed.baseDirectory, seed.options.location);
        }

        if (!ruleset) {
            msg = 'Bundle "' + seed.name + '" has unknown ruleset ' + JSON.stringify(seed.options.ruleset);
            if (seed.options.rulesets) {
                msg += ' in rulesets ' + JSON.stringify(seed.options.rulesets);
            }
            throw new Error(msg);
        }

        bundle = new Bundle(seed.baseDirectory, seed.options);
        bundle.name = seed.name;
        bundle.version = seed.version;
        bundle.type = ruleset._name;
        this._bundles[bundle.name] = bundle;
        this._bundlePaths[libfs.realpathSync(bundle.baseDirectory)] = bundle.name;

        // wire into parent
        if (parent) {
            if (!parent.bundles) {
                parent.bundles = {};
            }
            parent.bundles[bundle.name] = bundle;
        }
        return bundle;
    },

    /**
     * Turns the path into a resource in the associated bundle, if applicable.
     * @private
     * @method _processFile
     * @param {string} fullPath the path to the file to be processed
     */
    _processFile: function (fullPath) {
        var bundleName,
            bundle,
            ruleset,
            relativePath,
            pathParts,
            subBundleSeed,
            res;

        bundleName = this._getBundleNameByPath(fullPath);
        bundle = this._bundles[bundleName];
        if (bundle.baseDirectory === fullPath.substr(0, bundle.baseDirectory.length)) {
            relativePath = fullPath.substr(bundle.baseDirectory.length + 1);
        }

        // This mainly happens during watch(), since we skip node_modules
        // in _walkBundle().
        if (relativePath.indexOf('node_modules') === 0) {
            pathParts = relativePath.split(libpath.sep);
            while (pathParts[0] === 'node_modules' && pathParts.length >= 2) {
                pathParts.shift();
                bundleName = pathParts.shift();
            }
            relativePath = pathParts.join(libpath.sep);
            bundle = this._bundles[bundleName];

            // The package's directory is not a resource (... and is mostly uninteresting).
            if (!relativePath) {
                return;
            }

            // unknown bundle
            if (!bundle) {
                return;
            }
        }

        ruleset = this._loadRuleset(bundle);
        if (ruleset._skip && this._ruleSkip(fullPath, relativePath, ruleset._skip)) {
            return;
        }
        if (ruleset._bundles) {
            subBundleSeed = this._ruleBundles(fullPath, relativePath, ruleset._bundles, bundle);
            if (subBundleSeed) {
                // sub-bundle inherits options.rulesets from parent
                if (!subBundleSeed.options) {
                    subBundleSeed.options = {};
                }
                if (!subBundleSeed.options.rulesets) {
                    subBundleSeed.options.rulesets = bundle.options.rulesets;
                }
                this._makeBundle(subBundleSeed, bundle);
                return;
            }
        }

        // This is the base "meta" for a file.  If a rule matches we'll
        // augment this.
        res = {
            bundleName: bundleName,
            fullPath: fullPath,
            relativePath: relativePath,
            ext: libpath.extname(fullPath).substr(1)
        };

        this._onFile(res, ruleset);
    },

    /**
     * Processes the "_skip" rule to decide if the path should be skipped.
     * @private
     * @method _ruleSkip
     * @param {string} fullPath The full path to the file.
     * @param {string} relativePath The path relative to the bundle.
     * @param {object} rule The skip rule to process.
     * @return {boolean} True if the path should be skipped, false otherwise.
     */
    _ruleSkip: function (fullPath, relativePath, rule) {
        var r, regex;

        relativePath = BundleLocator._toUnixPath(relativePath);

        for (r = 0; r < rule.length; r += 1) {
            regex = rule[r];
            if (regex.test(relativePath)) {
                return true;
            }
        }
        return false;
    },

    /**
     * Processes the "_bundles" rule looking for child bundles.
     * Returns a "bundle seed" as described by _makeBundleSeed().
     *
     * @private
     * @method _ruleBundles
     * @param {string} fullPath The full path to the file.
     * @param {string} relativePath The path relative to the parent bundle.
     * @param {object} rule The bundles rule.
     * @param {Bundle} parent The parent bundle.
     * @return {object|undefined} The processing options if the path is a child bundle.
     */
    _ruleBundles: function (fullPath, relativePath, rule, parent) {
        var r,
            matches,
            defaultVersion = DEFAULT_VERSION,
            pkg;
        if (parent) {
            defaultVersion = parent.version;
        }

        relativePath = BundleLocator._toUnixPath(relativePath);

        for (r = 0; r < rule.length; r += 1) {
            matches = relativePath.match(rule[r].regex);
            if (matches) {
                try {
                    pkg = require(libpath.resolve(fullPath, 'package.json'));
                } catch (packageErr) {
                    // It's OK for a sub-bundle to not have a package.json.
                }
                return this._makeBundleSeed(fullPath, libpath.normalize(matches[1]), defaultVersion, pkg, rule[r].options);
            }
        }
    },

    /**
     * Handles the file.
     * @private
     * @method _onFile
     * @param {object} res Metadata about the file.
     * @param {object} ruleset Rules to attempt on the file.
     */
    _onFile: function (res, ruleset) {
        var bundle = this._bundles[res.bundleName],
            ruleName,
            rule,
            relativePath = BundleLocator._toUnixPath(res.relativePath),
            match;

        bundle.files[res.fullPath] = true;

        for (ruleName in ruleset) {
            if (ruleset.hasOwnProperty(ruleName)) {
                // Rules that start with "_" are special directives,
                // and have already been handle by the time we get here.
                if ('_' !== ruleName.charAt(0)) {
                    rule = ruleset[ruleName];
                    match = relativePath.match(rule.regex);
                    if (match) {
                        res.name = match[rule.nameKey || 1];
                        res.type = ruleName;
                        if (rule.subtypeKey) {
                            res.subtype = match[rule.subtypeKey] || '';
                        }
                        if (rule.selectorKey && match[rule.selectorKey]) {
                            res.selector = match[rule.selectorKey];
                        } else {
                            res.selector = DEFAULT_SELECTOR;
                        }
                        // file will become a resource after the first match
                        return this._onResource(res);
                    }
                }
            }
        }
    },

    /**
     * Handles the resource.
     * @private
     * @method _onResource
     * @param {object} res The resource.
     */
    _onResource: function (res) {
        var bundle = this._bundles[res.bundleName],
            type = res.type,
            subtype,
            selector = res.selector,
            name = res.name;

        if (!bundle.resources[selector]) {
            bundle.resources[selector] = {};
        }
        if (!bundle.resources[selector][type]) {
            bundle.resources[selector][type] = {};
        }
        if (res.hasOwnProperty('subtype')) {
            subtype = res.subtype;
            if (!bundle.resources[selector][type][subtype]) {
                bundle.resources[selector][type][subtype] = {};
            }
            bundle.resources[selector][type][subtype][name] = res;
        } else {
            bundle.resources[selector][type][name] = res;
        }
    },

    /**
     * Determines whether a resource is filtered or not.
     * @private
     * @method _filterResource
     * @param {LocatorResource} res The resource to filter.
     * @param {object} filter The filter.
     * @return {boolean} Whether resource passes the filter.
     */
    _filterResource: function (res, filter) {
        if (!filter || Object.keys(filter).length === 0) {
            return true;
        }
        var prop;
        for (prop in filter) {
            if ('extensions' === prop) {
                // sugar for users
                if ('string' === typeof filter.extensions) {
                    filter.extensions = filter.extensions.split(',');
                }
                if (!filter.extensions || filter.extensions.indexOf(res.ext) === -1) {
                    return false;
                }
            } else if ('types' === prop) {
                // sugar for users
                if ('string' === typeof filter.types) {
                    filter.types = filter.types.split(',');
                }
                if (!filter.types || filter.types.indexOf(res.type) === -1) {
                    return false;
                }
            } else {
                return false; // unknown filters should fail to pass
            }
        }
        return true;
    },

    /**
     * Walks the resources in the bundle, optionally applying the filter along the way.
     * @private
     * @method _walkBundleResources
     * @param {Bundle} bundle The bundle containing resources.
     * @param {object} filter Filter for deciding which resources to return.
     * @param {string|[string]} [filter.extensions] The filesystem extensions (NOT including dot) for which resources to return.
     * @param {string|[string]} [filter.types] The resources types for which resources to return.
     * @return {[LocatorResource]} A collection of filtered resources.
     */
    _walkBundleResources: function (bundle, filter) {
        var self = this,
            ruleset = this._loadRuleset(bundle),
            ress = [];
        Object.keys(bundle.resources).forEach(function (selector) {
            Object.keys(bundle.resources[selector]).forEach(function (resType) {
                var rule = ruleset[resType];
                if (rule.subtypeKey) {
                    Object.keys(bundle.resources[selector][resType]).forEach(function (subtype) {
                        Object.keys(bundle.resources[selector][resType][subtype]).forEach(function (name) {
                            var res = bundle.resources[selector][resType][subtype][name];
                            if (self._filterResource(res, filter)) {
                                ress.push(res);
                            }
                        });
                    });
                } else {
                    Object.keys(bundle.resources[selector][resType]).forEach(function (name) {
                        var res = bundle.resources[selector][resType][name];
                        if (self._filterResource(res, filter)) {
                            ress.push(res);
                        }
                    });
                }
            });
        });
        return ress;
    },

    /**
     * Walks a directory and returns a list of metadata about locator packages
     * installed in that directory (including the package for the directory itself).
     * @private
     * @method _walkNPMTree
     * @param {string} dir Directory to walk.
     * @param {integer} _depth [internal] Depth of the directory being walked.
     * Internally used for recursion.
     * @return {Array} information about locator packages in the directory.
     * If the directory is not an NPM package then it returns undefined value.
     */
    _walkNPMTree: function (dir, _depth) {
        var self = this,
            pkg,
            seed,
            seeds = [],
            subdirs;

        _depth = _depth || 0;

        try {
            pkg = require(libpath.resolve(dir, 'package.json'));
            // top level package doesn't require a locator flag in package.json
            if ((0 === _depth) && (!pkg.locator)) {
                pkg.locator = {};
            }
            seed = this._makeBundleSeed(dir, libpath.basename(dir), DEFAULT_VERSION, pkg);
            if (seed.options) {
                seed.npmDepth = _depth;
                seeds.push(seed);
            }
        } catch (packageErr) {
            // Some build environments leave extraneous directories in
            // node_modules and we should ignore them gracefully.
            // (trello board:Modown card:124)
            if ('MODULE_NOT_FOUND' !== packageErr.code) {
                throw packageErr;
            }
            return seeds;
        }

        if (_depth < this._options.maxPackageDepth) {
            try {
                subdirs = libfs.readdirSync(libpath.join(dir, 'node_modules'));
            } catch (readdirErr) {
                if ('ENOENT' === readdirErr.code) {
                    // missing node_modules/ directory is OK
                    return seeds;
                }
                throw readdirErr;
            }
            subdirs.reduce(function (memo, subdir) {
                return memo.concat(
                    '@' === subdir.substring(0, 1) ?
                        libfs.readdirSync(libpath.join(dir, 'node_modules', subdir)).map(function (scopeddir) {
                            return libpath.join(subdir, scopeddir);
                        }) :
                        [subdir]
                );
            }, []).forEach(function (subdir) {
                var subpkgResults;
                if ('.' === subdir.substring(0, 1)) {
                    return;
                }
                subpkgResults = self._walkNPMTree(libpath.join(dir, 'node_modules', subdir), _depth + 1);
                // merge in found packages
                if (subpkgResults && subpkgResults.length) {
                    seeds = seeds.concat(subpkgResults);
                }
            });
        }
        return seeds;
    },

    /**
     * Figures out which seed to use from the list of available packages.
     * Select by depth, then by semver.
     * @private
     * @method _dedupeSeeds
     * @param {object} pkgDepths List of seed by deep.
     * @return {object} The metas of the selected package.
     */
    _dedupeSeeds: function (pkgDepths) {
        // pkgDepths -> depth: [metas]
        var depths,
            minDepth,
            maxDepth,
            seeds;
        depths = Object.keys(pkgDepths);
        minDepth = Math.min.apply(Math, depths);
        maxDepth = Math.max.apply(Math, depths);
        seeds = pkgDepths[minDepth];
        if (1 === seeds.length) {
            if (minDepth !== maxDepth) {
                debug('multiple "' + seeds[0].name + '" packages found, using version ' +
                    seeds[0].version + ' from ' + seeds[0].baseDirectory);
            }
            return seeds[0];
        }
        seeds.sort(function (a, b) {
            return libsemver.rcompare(a.version, b.version);
        });
        debug('multiple "' + seeds[0].name + '" packages found, using version ' +
            seeds[0].version + ' from ' + seeds[0].baseDirectory);
        return seeds[0];
    },

    /**
     * Figures out which bundles to use from the list.
     * The returned list is sorted first by NPM package depth then by name.
     * @private
     * @method _filterBundleSeeds
     * @param {array} all List of all bundle seeds from NPM packages.
     * @return {array} The metas of the packages to actually use.
     */
    _filterBundleSeeds: function (all) {
        var byDepth = {}; // name: depth: [metas]
        all.forEach(function (seed) {
            if (!byDepth[seed.name]) {
                byDepth[seed.name] = {};
            }
            if (!byDepth[seed.name][seed.npmDepth]) {
                byDepth[seed.name][seed.npmDepth] = [];
            }
            byDepth[seed.name][seed.npmDepth].push(seed);
        });
        return Object.keys(byDepth).map(function (name) {
            return this._dedupeSeeds(byDepth[name]);
        }, this);
    },

    /**
     * Creates a bundle from an NPM package, and queues up files in the package.
     * @private
     * @method _walkBundle
     * @param {object} bundleSeed Metadata about the package. See the docs for _makeBundleSeed()
     * for format of this metadata.
     * @return {Bundle} The bundle made from the NPM package.
     */
    _walkBundle: function (bundleSeed) {
        var self = this,
            parentName,
            parent,
            bundle,
            filters;
        // TODO -- merge options (second arg) over bundleSeed.options

        parentName = this._getBundleNameByPath(libpath.dirname(bundleSeed.baseDirectory));
        parent = this._bundles[parentName];

        bundle = this._makeBundle(bundleSeed, parent);
        this._bundles[bundle.name] = bundle;
        filters = this._options.exclude.concat(['node_modules', /^\./]);
        // adding the bundle dir itself for BC
        this._processFile(bundle.baseDirectory);
        walk.walkSync(bundle.baseDirectory, {
            filters: [],
            listeners: {
                directories: function (root, dirStatsArray, next) {
                    var i, dirStats, exclude;
                    function filterDir(filter) {
                        if (dirStats.name.match(filter)) {
                            return true;
                        }
                    }
                    for (i = dirStatsArray.length - 1; i >= 0; i -= 1) {
                        dirStats = dirStatsArray[i];
                        exclude = filters.some(filterDir);
                        if (exclude) {
                            // the sync walk api is pretty bad, it requires to
                            // mutate the actual dir array
                            dirStatsArray.splice(i, 1);
                        } else {
                            self._processFile(libpath.join(root, dirStats.name));
                        }
                    }
                    next();
                },
                file: function(root, fileStats, next) {
                    self._processFile(libpath.join(root, fileStats.name));
                    next();
                },
                errors: function(root, nodeStatsArray, next) {
                    next();
                }
            }
        });

        return bundle;
    },

    /**
     * Loads the rulesets for the bundle (or seed).
     * @private
     * @method _loadRuleset
     * @param {Bundle|object} bundle The bundle (or bundle seed, see _makeBundleSeed())
     * to load the ruleset for.
     * @return {object} The ruleset, or undefined if we couldn't load it.
     */
    _loadRuleset: function (bundle) {
        var name = (bundle.options && bundle.options.ruleset) || DEFAULT_RULESET,
            cacheKey = name + '@' + bundle.baseDirectory,
            rulesetsPath,
            rulesets,
            dir,
            rules;

        rules = this._cacheRules[cacheKey];
        if (rules) {
            return rules;
        }
        if (bundle.options && bundle.options.rulesets) {
            try {
                rulesetsPath = libpath.resolve(bundle.baseDirectory, bundle.options.rulesets);
                rulesets = require(rulesetsPath);
            } catch (errLocal) {
                if ('MODULE_NOT_FOUND' !== errLocal.code) {
                    throw errLocal;
                }
            }
            if (!rulesets) {
                dir = bundle.baseDirectory;
                while (dir) {
                    try {
                        rulesetsPath = libpath.resolve(dir, bundle.options.rulesets);
                        rulesets = require(rulesetsPath);
                        break;
                    } catch (errDir) {
                        if ('MODULE_NOT_FOUND' !== errDir.code) {
                            throw errDir;
                        }
                    }
                    try {
                        rulesetsPath = libpath.resolve(dir, 'node_modules', bundle.options.rulesets);
                        rulesets = require(rulesetsPath);
                        break;
                    } catch (errDep) {
                        if ('MODULE_NOT_FOUND' !== errDep.code) {
                            throw errDep;
                        }
                    }

                    // not found, iterate
                    dir = libpath.dirname(dir);
                    if ('node_modules' === libpath.basename(dir)) {
                        dir = libpath.dirname(dir);
                    }
                    if (this._rootDirectory && dir.length < this._rootDirectory.length) {
                        // if we can find the ruleset anywhere near in the filesystem
                        // we should try to rely on npm lookup process
                        try {
                            rulesetsPath = Module._resolveFilename(bundle.options.rulesets,
                                    Module._cache[__filename]);
                            rulesets = require(rulesetsPath);
                        } catch (errLocalMod) {
                            return;
                        }
                    }
                }
            }
            // a ruleset pkg can contain multiple rulesets
            rules = rulesets[name];
        } else if (this._options.rulesetFn) {
            // using the rulesetFn() hook to produce custom rules
            rules = this._options.rulesetFn(bundle);
        } else {
            rules = DEFAULT_RULESETS[name];
        }
        if (rules) {
            rules._name = name;
            this._cacheRules[cacheKey] = rules;
        }
        return rules;
    },

    /**
     * Creates a new object with the certain keys excluded.
     * This is used instead of "delete" since that has performance implications in V8.
     * @private
     * @method _objectExclude
     * @param {object} srcObject The original object.
     * @param {array} excludeKeys The keys to exclude from the results.
     * @return {object} A version of the original object with the keys excluded.
     */
    _objectExclude: function (srcObject, excludeKeys) {
        var destObject = {},
            key;
        for (key in srcObject) {
            if (srcObject.hasOwnProperty(key)) {
                if (-1 === excludeKeys.indexOf(key)) {
                    destObject[key] = srcObject[key];
                }
            }
        }
        return destObject;
    }

};

/**
 * Rules are written in regular expressions that test paths as if they
 * were unix-based paths. Replacing path separators with unix style
 * separators ensures that these rules apply in other OSs too.
 *
 * @method toUnixPath
 * @param {String} path A path string
 * @return {String} A path with Unix style separators
 * @static
 * @private
 */
BundleLocator._toUnixPath = '/' !== libpath.sep ? function toUnixPath(path) {
    return path.split(PATH_SEP).join('/');
} : function toUnixPath(path) {
    return path;
};

/**
 * This object represents a resource on the filesystem.
 * @class LocatorResource
 */
/**
 * The name of the bundle to which the resource belongs.
 * @property bundleName
 * @type string
 */
/**
 * The full filesystem path to the resources.
 * @property fullPath
 * @type string
 */
/**
 * The filesystem path relative to the bundle.
 * @property relativePath
 * @type string
 */
/**
 * The filesystem extension of the resource.
 * Does not include the dot.
 * @property ext
 * @type string
 */
/**
 * The name of the resource.
 * @property name
 * @type string
 */
/**
 * The type of the resource.
 * @property type
 * @type string
 */
/**
 * The subtype of the resource, if it has one.
 * @property subtype
 * @type string
 * @optional
 */
/**
 * The selector for the resource.
 * @property selector
 * @type string
 */

module.exports = BundleLocator;


/*
 * Copyright 2013 Yahoo! Inc. All rights reserved.
 * Copyrights licensed under the BSD License.
 * See the accompanying LICENSE.txt file for terms.
 */

/*jslint nomen:true, node:true */
"use strict";

var libpath = require('path'),
    DEFAULT_SELECTOR = '{}';

/**
 * The Locator walks the filesystem and gives semantic meaning to
 * files in the application.
 * @module ModownLocator
 */

/**
 * @class Bundle
 * @constructor
 * @param {string} baseDirectory Directory in which a bundle is rooted.
 * @param {object} options Options for how the configuration files are handled.
 */
function Bundle(baseDirectory, options) {
    this.options = options || {};
    this.name = libpath.basename(baseDirectory);
    this.baseDirectory = baseDirectory;
    this.type = undefined;
    this.files = {};
    this.resources = {};
}

Bundle.prototype = {

    /**
     * Returns resources that match the selector.
     * @method getResources
     * @param {object} options Options for returned resources
     * @param {string} [selector] Selector for the returned resources.
     * If none is given then the resources which have no selector will
     * be returned.
     * @return {object} The resources.
     */
    getResources: function (options, selector) {
        selector = selector || DEFAULT_SELECTOR;
        return this.resources[selector];
    }

};

module.exports = Bundle;


// Copyright 2018 Kensho Technologies, LLC.

import {findIndex, get, last, map} from 'lodash'
import {scaleLinear, scaleLog, scaleOrdinal, scalePoint, scaleUtc} from 'd3-scale'

import {DOMAIN, NICE, RANGE, TICK_COUNT, TYPE} from '../defaults'

// `getScale` returns the scale for a key according to configurations on props object
// e.g. getScale({xType, xDomain, xRange, xTickCount}, 'x') => {...props, xScale}

function createScale(factory, domain, range) {
  return factory()
    .domain(domain)
    .range(range)
}

// get a scale with logic for the x and y axis, if the domain starts and finishes on the same
// number returns the mid range value
function getBaseScales(type, domain, range, nice, tickCount) {
  const factory = (type === 'time' && scaleUtc) || (type === 'log' && scaleLog) || scaleLinear
  const scale = createScale(factory, domain, range)
  if (nice) scale.nice(tickCount)
  return scale
}

export function getOrdinalInvert(scale) {
  const mapArray = map(scale.domain(), raw => ({raw, mapped: scale(raw)}))
  return input => {
    const hoverIndex = findIndex(mapArray, d => get(d, 'mapped') > input)
    if (hoverIndex === 0) {
      const hoverData = mapArray[hoverIndex]
      return hoverData.raw
    }
    if (hoverIndex === -1) {
      const hoverData = last(mapArray)
      return hoverData.raw
    }
    const px = get(mapArray[hoverIndex], 'mapped')
    const x = get(mapArray[hoverIndex - 1], 'mapped')
    if (input - px < x - input) {
      const hoverData = mapArray[hoverIndex - 1]
      return hoverData.raw
    }
    const hoverData = mapArray[hoverIndex]
    return hoverData.raw
  }
}

export function getAxisScale(props, key) {
  const {
    [`${key}Type`]: type = TYPE,
    [`${key}Domain`]: domain = DOMAIN,
    [`${key}Range`]: range = RANGE,
    [`${key}TickCount`]: tickCount = TICK_COUNT,
    [`${key}Nice`]: nice = NICE,
  } = props
  if (type === 'ordinal') {
    const ordinalScale = createScale(scalePoint, domain, range).padding(0.5)
    ordinalScale.invert = getOrdinalInvert(ordinalScale)
    return ordinalScale
  }
  if (domain[0] === domain[1]) {
    const midRange = range[0] + (range[1] - range[0]) / 2
    const linearScaleFlatDomain = () => midRange
    linearScaleFlatDomain.tickFormat = () => d => d
    linearScaleFlatDomain.ticks = () => [domain[0]]
    return linearScaleFlatDomain
  }
  return getBaseScales(type, domain, range, nice, tickCount)
}

export function getDefaultScale(props, key) {
  const {
    [`${key}Type`]: type = TYPE,
    [`${key}Domain`]: domain = DOMAIN,
    [`${key}Range`]: range = RANGE,
    [`${key}TickCount`]: tickCount = TICK_COUNT,
    [`${key}Nice`]: nice = NICE,
  } = props
  return type === 'ordinal'
    ? createScale(scaleOrdinal, domain, range)
    : getBaseScales(type, domain, range, nice, tickCount)
}

// main exported function, used outside of the module on the Chart props transform flow
export default function getScale(props, key) {
  return key === 'x' || key === 'y' ? getAxisScale(props, key) : getDefaultScale(props, key)
}


// Copyright 2018 Kensho Technologies, LLC.

import {find, findIndex, get, head, isEmpty, last, map, reduce, some} from 'lodash'

import getPath2D from '../utils/getPath2D'
import notPlotNumber from '../utils/notPlotNumber'

import plotValue from './plotValue'
import getPlotValues from './getPlotValues'
import splineInterpolation from './splineInterpolation'

// `points` is used to generate render data for lines and multilines
// it handles `x`, `y`, 'stroke'(color) and 'lineWisdth'

export function getPointData(props, datum) {
  const path2D = getPath2D()
  const x = plotValue(props, datum, undefined, 'x')
  const y = plotValue(props, datum, undefined, 'y')
  const r = plotValue(props, datum, undefined, 'strokeWidth', 2) + 1.5
  if (notPlotNumber([x, y, r])) return undefined
  path2D.arc(x, y, r, 0, 2 * Math.PI)
  return {
    hoverAlpha: 0.8,
    path2D,
    type: 'area',
  }
}

const getHoverSolverObj = (props, renderDatum, hoverData) => ({
  hoverRenderData: [renderDatum, getPointData(props, hoverData)],
  renderDatum,
  hoverData,
})

/* eslint-disable react/destructuring-assignment */

export function hoverSolver(props, _hoverData, renderDatum, localMouse) {
  const xRaw = props.xScale.invert(localMouse.x)
  if (props.xType === 'ordinal') {
    const hoverData = find(_hoverData, d => get(d, props.x) === xRaw)
    return getHoverSolverObj(props, renderDatum, hoverData)
  }
  const hoverIndex = findIndex(_hoverData, d => get(d, props.x) > xRaw)
  if (hoverIndex === 0) {
    const hoverData = _hoverData[hoverIndex]
    return getHoverSolverObj(props, renderDatum, hoverData)
  }
  if (hoverIndex === -1) {
    const hoverData = last(_hoverData)
    return getHoverSolverObj(props, renderDatum, hoverData)
  }
  const px = get(_hoverData[hoverIndex], props.x)
  const x = get(_hoverData[hoverIndex - 1], props.x)
  if (xRaw - px < x - xRaw) {
    const hoverData = _hoverData[hoverIndex - 1]
    return getHoverSolverObj(props, renderDatum, hoverData)
  }
  const hoverData = _hoverData[hoverIndex]
  return getHoverSolverObj(props, renderDatum, hoverData)
}

// generate the array of render data
function getLineRenderData(props, data, idx) {
  if (isEmpty(data)) return undefined
  const path2D = getPath2D()
  const values = getPlotValues(props, head(data), idx, {hoverAlpha: 0.2})
  if (props.interpolate) {
    splineInterpolation(props, data, path2D)
  } else {
    path2D.moveTo(values.x, values.y)
    reduce(
      data,
      (shouldDrawPoint, d) => {
        const x = plotValue(props, d, idx, 'x')
        const y = plotValue(props, d, idx, 'y')
        if (notPlotNumber([x, y])) return false
        if (shouldDrawPoint) path2D.lineTo(x, y)
        else path2D.moveTo(x, y)
        return true
      },
      true
    )
  }
  return {
    ...values,
    data,
    hoverSolver,
    path2D,
    type: 'line',
  }
}

export default function lines(props) {
  if (!props.xScale || !props.yScale) return undefined
  const data = some(props.data, Array.isArray) ? props.data : [props.data]
  return map(data, (d, i) => getLineRenderData(props, d, i))
}


// Copyright 2018 Kensho Technologies, LLC.

import {map, flatMap} from 'lodash'

import getMidX from '../utils/rect/getMidX'
import getMidY from '../utils/rect/getMidY'
import getPath2D from '../utils/getPath2D'

import getPlotValues from './getPlotValues'

/*
`points` is used to generate render data for dots and similar.
it handles `x`, `y`, 'radius' and 'fill'.

@calling logic
points{
  getPointRenderData(retrievePointsData){}
}
*/

/*
generates the array of render data
*/
function getPointRenderData(props, datum, idx) {
  const values = getPlotValues(props, datum, idx, {
    hoverAlpha: 0.75,
    radius: 4,
    x: getMidX(props.plotRect),
    y: getMidY(props.plotRect),
  })
  const path2D = getPath2D()
  const hover2ndPath2D = getPath2D()
  path2D.arc(values.x, values.y, values.radius, 0, 2 * Math.PI)
  hover2ndPath2D.arc(values.x, values.y, values.radius + 8, 0, 2 * Math.PI)

  return {
    ...values,
    hover2ndPath2D,
    path2D,
    type: 'area',
  }
}

// main entry point, if there's only `xMap` or `yMap` it will output a one dimension plot
export default function points(props) {
  if (!props.xScale && !props.yScale) return undefined
  return flatMap(props.data, (datum, idx) =>
    Array.isArray(datum)
      ? map(datum, (d, i) => getPointRenderData(props, d, i))
      : getPointRenderData(props, datum, idx)
  )
}


// Copyright 2018 Kensho Technologies, LLC.

import {map, omit, reduce} from 'lodash'

const PROPS_TO_OMIT = ['onUpdate', 'onState', 'layerProps', 'rootProps']

// Start with the input props, send them to the first transform, merge the returned new props back
// to the props. Do again with the next transform.
// const rootProps = chartTransformFlow(props, t1, t2, t3)

function removeDimArrays(props) {
  const names = map(props.groupedKeys, key => `${key}Array`)
  return omit(props, names)
}

function transformFlow([first, ...rest]) {
  return reduce(rest, (acc, d) => ({...acc, ...d(acc)}), omit(first, PROPS_TO_OMIT))
}

export default function chartTransformFlow(...args) {
  return removeDimArrays(transformFlow(args))
}


// Copyright 2018 Kensho Technologies, LLC.

import {map, flatten} from 'lodash'

import getMinX from '../utils/rect/getMinX'
import getMinY from '../utils/rect/getMinY'
import getMidX from '../utils/rect/getMidX'
import getMidY from '../utils/rect/getMidY'
import getMaxX from '../utils/rect/getMaxX'
import getMaxY from '../utils/rect/getMaxY'
import fitCheckText from '../utils/fitCheckText'

import getPlotValues from './getPlotValues'

// generate the array of render data
function getTextRenderData(props, datum, idx) {
  const {plotRect, theme, width, height} = props
  const values = getPlotValues(props, datum, idx, {
    x: getMidX(props.plotRect),
    y: getMidY(props.plotRect),
  })

  if (values.textSnap === 'top') values.y = getMinY(plotRect)
  if (values.textSnap === 'bottom') values.y = getMaxY(plotRect)
  if (values.textSnap === 'left') values.x = getMinX(plotRect)
  if (values.textSnap === 'right') values.x = getMaxX(plotRect)
  if (values.textSnap === 'topLeft') {
    values.x = getMinX(plotRect)
    values.y = getMinY(plotRect)
  }
  if (values.textSnap === 'topRight') {
    values.x = getMaxX(plotRect)
    values.y = getMinY(plotRect)
  }
  if (values.textSnap === 'bottomLeft') {
    values.x = getMinX(plotRect)
    values.y = getMaxY(plotRect)
  }
  if (values.textSnap === 'bottomRight') {
    values.x = getMaxX(plotRect)
    values.y = getMaxY(plotRect)
  }

  const newValues = fitCheckText(values, width, height, theme)
  return {...newValues, type: 'text'}
}

// if there's only `xMap` or `yMap`, output a one-dimensional plot
export default function text(props) {
  if (!props.xScale && !props.yScale) return undefined
  return map(flatten(props.data), (datum, idx) => getTextRenderData(props, datum, idx))
}


// Copyright 2012 Rackspace Hosting, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * Tracers only for Node.
 * Note: Node tracers assume tryfer library is included in a long running
 * process.
 */

if (typeof module === 'undefined' || !module.exports) {
  return;
}

var util = require('util');

var request = require('request');
var async = require('async');

var EndAnnotationTracer = require('./tracers').EndAnnotationTracer;
var formatters = require('./formatters');

/**
 * Buffer traces and defer recording until maxTraces have been received or
 * sendInterval has elapsed since the last trace was recorded.
 *
 * @param {Tracer} tracer Tracer provider to record buffered tracers to.
 * @param {Options} options Options object with the following keys:
 * - maxTraces - Number of traces to buffer before recording occurs (default:
 *   50).
 * - sendInterval - An average number of seconds which can pass after the last
 *   trace has been sent to the backend before sending all the buffered traces
 *   to the backend again (default: 10).
 */
function BufferingTracer(tracer, options) {
  options = options || {};

  var self = this;

  this._tracer = tracer;
  this._maxTraces = options.maxTraces || 50;
  this._sendInterval = options.sendInterval ? (options.sendInterval * 1000) : 10 * 1000;
  this._lastSentTs = Date.now();
  this._buffer = [];
  this._stopped = false;

  this._periodSendTimeoutId = setTimeout(this._periodicSendFunction.bind(this),
                                         this._sendInterval);
}

BufferingTracer.prototype.sendTraces = function(traces) {
  var buffer;

  this._buffer = this._buffer.concat(traces);

  if (this._buffer.length >= this._maxTraces) {
    buffer = this._buffer.slice();
    this._buffer = [];

    // Flush the buffer in the next tick
    process.nextTick(this._sendTraces.bind(this, buffer));
  }
};

/**
 * Clear any outgoing timers and stop the tracer.
 */
BufferingTracer.prototype.stop = function() {
  clearTimeout(this._periodSendTimeoutId);
  this._stopped = true;
};

BufferingTracer.prototype._periodicSendFunction = function() {
  var now = Date.now(), buffer;

  if (((now - this._sendInterval) > this._lastSentTs) &&
      this._buffer.length >= 1) {
    buffer = this._buffer.slice();
    this._buffer = [];
    this._sendTraces(buffer);
  }

  // Re-schedule itself
  this._periodSendTimeoutId = setTimeout(this._periodicSendFunction.bind(this),
                                         this._sendInterval);
};

BufferingTracer.prototype._sendTraces = function(traces) {
  this._lastSentTs = Date.now();
  this._tracer.sendTraces(traces);
};

/**
 * A tracer that records to zipkin through the RESTkin http interface.
 * Requires a keystone client ({node-keystone-client}).
 *
 * This implementation posts all traces immediately and does not implement
 * buffering.
 *
 * @param {String} traceUrl The URL to the RESTkin endpoint including the
 * version. For example: https://example.com/v1.0
 * @param {keystone-client.KeystoneClient object} keystoneClient The
 * keystone client to use to authenticate against keystone to get a token
 * and tenant id.
 */
function RawRESTkinHTTPTracer(traceUrl, keystoneClient) {
  if (traceUrl.charAt(traceUrl.length - 1) === '/') {
    traceUrl = traceUrl.slice(0, -1);
  }

  this._traceUrl = traceUrl;
  this._keystoneClient = keystoneClient;

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RawRESTkinHTTPTracer, EndAnnotationTracer);

RawRESTkinHTTPTracer.prototype.performRequest = function (token, tenantId, body, callback) {
  var url = this._traceUrl + '/' + tenantId + '/trace', headers;

  headers = {
    'X-Auth-Token': token,
    'X-Tenant-Id': tenantId,
    'Content-Type': 'application/json'
  };

  request.post({
    url: url,
    headers: headers,
    body: body
  }, callback);
};

RawRESTkinHTTPTracer.prototype.sendTraces = function(traces) {
  var self = this;

  async.waterfall([
    this._keystoneClient.getTenantIdAndToken.bind(this._keystoneClient, {}),

    function formatTraces(result, callback) {
      formatters.formatForRestkin(traces, function(err, formattedTraces) {
        callback(err, result, formattedTraces);
      });
    },

    function makeRequest(result, formattedTraces, callback) {
      self.performRequest(result.token, result.tenantId, formattedTraces, callback);
    }
  ],

  function(err) {
    if (err) {
      console.log('Failed to send traces to backend: ' + err.toString());
    }
  });
};

/**
 * A tracer that records to zipkin through the RESTkin http interface.
 * Requires a keystone client ({node-keystone-client}).
 *
 * This is equivalent to EndAnnotationTracer(BufferingTracer(RawRESTkinHTTPTracerTests())).
 *
 * @param {String} traceUrl The URL to the RESTkin endpoint including the
 * version. For example: https://example.com/v1.0
 * @param {keystone-client.KeystoneClient object} keystoneClient The
 * keystone client to use to authenticate against keystone to get a token
 * and tenant id.
 * @param {Object} options Options passed to the BufferingTracer constructor.
 */
function RESTkinHTTPTracer(traceUrl, keystoneClient, options) {
  var rawTracer = new module.exports.RawRESTkinHTTPTracer(traceUrl, keystoneClient);
  this._tracer = new module.exports.BufferingTracer(rawTracer, options);

  this.stop = this._tracer.stop.bind(this._tracer);

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RESTkinHTTPTracer, EndAnnotationTracer);

RESTkinHTTPTracer.prototype.sendTraces = function(traces) {
  this._tracer.sendTraces(traces);
};

/**
 * A tracer that records directly to Zipkin Query HTTP API.
 *
 * This implementation posts all annotations to /api/v1/spans
 * immediately and does not implement buffering of any sort.
 *
 * @param {String} traceUrl The URL to the Zipkin query endpoint.
 *                          For example: https://example.com.
 */
function RawZipkinQueryServiceHTTPTracer(traceUrl) {
  if (traceUrl.charAt(traceUrl.length - 1) === '/') {
    traceUrl = traceUrl.slice(0, -1);
  }

  this._traceUrl = traceUrl;

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RawZipkinQueryServiceHTTPTracer, EndAnnotationTracer);

RawZipkinQueryServiceHTTPTracer.prototype.performRequest = function (body, callback) {
  var url = this._traceUrl + '/api/v1/spans';

  var headers = {
    'Content-Type': 'application/json'
  };

  request.post({
    url: url,
    headers: headers,
    body: body
  }, callback);
};

RawZipkinQueryServiceHTTPTracer.prototype.sendTraces = function(traces) {
  var self = this;

  async.waterfall([
    function formatTraces(callback) {
      formatters.formatForQueryService(traces, function(err, formattedTraces) {
        callback(err, formattedTraces);
      });
    },

    function makeRequest(formattedTraces, callback) {
      self.performRequest(formattedTraces, function (err, httpResp, body) {
        if (err || httpResp.statusCode >= 400) {
          return callback(err || body);
        }
        callback();
      });
    }
  ],

  function(err) {
    if (err) {
      console.log('Failed to send traces to backend: ' + err.toString());
    }
  });
};

/**
 * A tracer that records directly to Zipkin Query HTTP API.
 *
 * This implementation posts all annotations to /api/v1/spans
 * immediately and does not implement buffering of any sort.
 *
 * @param {String} traceUrl The URL to the Zipkin query endpoint.
 *                          For example: https://example.com.
 * @param {Object} options Options passed to the BufferingTracer constructor.
 */
function ZipkinQueryServiceHTTPTracer (serviceUrl, options) {
  var rawTracer = new module.exports.RawZipkinQueryServiceHTTPTracer(serviceUrl);
  this._tracer = new module.exports.BufferingTracer(rawTracer, options);

  this.stop = this._tracer.stop.bind(this._tracer);

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(ZipkinQueryServiceHTTPTracer, EndAnnotationTracer);

ZipkinQueryServiceHTTPTracer.prototype.sendTraces = function (traces) {
  this._tracer.sendTraces(traces);
};

/**
 * A tracer that records to zipkin through scribe. Requires a scribe
 * client ({node-scribe}).
 *
 * @param {scribe.Scribe object} scribeClient The client to use to write to
 *    scribe
 * @param {String} category The category to use when writing to scribe -
 *    defaults to "zipkin" if not passed
 */
function RawZipkinTracer(scribeClient, category) {
  this.scribeClient = scribeClient;
  this.category = (category) ? category : 'zipkin';

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RawZipkinTracer, EndAnnotationTracer);

RawZipkinTracer.prototype._sendTrace = function(tuple, callback) {
  var trace = tuple[0], annotations = tuple[1];
  var self = this;

  formatters.formatForZipkin(trace, annotations, function (err, message) {
    // scribe.Scribe.send does not take a callback and returns immediately
    callback(
      err,
      (err === null ? self.scribeClient.send(self.category, message) : null));
  });
};

RawZipkinTracer.prototype.sendTraces = function(traces) {
  var callback = function() {};

  async.forEachLimit(traces, 10, this._sendTrace.bind(this),
                     callback);
};

/**
 * A tracer that records to zipkin through scribe. Requires a scribe
 * client ({node-scribe}).
 *
 * @param {scribe.Scribe object} scribeClient The client to use to write to
 *    scribe
 * @param {String} category The category to use when writing to scribe -
 *    defaults to "zipkin" if not passed
 */
function ZipkinTracer(scribeClient, category, options) {
  var rawTracer = new RawZipkinTracer(scribeClient, category);
  this._tracer = new BufferingTracer(rawTracer, options);

  this.stop = this._tracer.stop.bind(this._tracer);

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(ZipkinTracer, EndAnnotationTracer);

ZipkinTracer.prototype.sendTraces = function(traces) {
  this._tracer.sendTraces(traces);
};

/**
 * A tracer that records to RESTkin through scribe.  Requires a scribe
 * client ({node-scribe}).
 *
 * This implementation logs all annotations immediately and does not implement
 * buffering of any sort.
 *
 * @param {scribe.Scribe object} scribeClient The client to use to write to
 *    scribe
 * @param {String} category The category to use when writing to scribe -
 *    defaults to "zipkin" if not passed
 */
function RawRESTkinScribeTracer(scribeClient, category) {
  this.scribeClient = scribeClient;
  this.category = (category) ? category : 'restkin';

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RawRESTkinScribeTracer, EndAnnotationTracer);

RawRESTkinScribeTracer.prototype.sendTraces = function(traces) {
  var self = this;

  async.waterfall([
    formatters.formatForRestkin.bind(null, traces),

    function send(formattedTraces, callback) {
      self.scribeClient.send(self.category, formattedTraces);
      callback();
    }
  ],

  function(err) {
    if (err) {
      console.log('Failed to send traces to backend: ' + err.toString());
    }
  });
};

/**
 * A tracer that records to RESTkin through scribe.  Requires a scribe
 * client ({node-scribe}).
 *
 * This implementation logs all annotations immediately and does not implement
 * buffering of any sort.
 *
 * @param {scribe.Scribe object} scribeClient The client to use to write to
 *    scribe
 * @param {String} category The category to use when writing to scribe -
 *    defaults to "zipkin" if not passed
 * @param {Object} options Options passed to the BufferingTracer constructor.
 */
function RESTkinScribeTracer(scribeClient, category, options) {
  var rawTracer = new RawRESTkinScribeTracer(scribeClient, category);

  this._tracer = new module.exports.BufferingTracer(rawTracer, options);
  this.stop = this._tracer.stop.bind(this._tracer);

  EndAnnotationTracer.call(this, this.sendTraces);
}

util.inherits(RESTkinScribeTracer, EndAnnotationTracer);

RESTkinScribeTracer.prototype.sendTraces = function(traces) {
  this._tracer.sendTraces(traces);
};

module.exports.BufferingTracer = BufferingTracer;
module.exports.RawRESTkinHTTPTracer = RawRESTkinHTTPTracer;
module.exports.RESTkinHTTPTracer = RESTkinHTTPTracer;
module.exports.RawZipkinTracer = RawZipkinTracer;
module.exports.ZipkinTracer = ZipkinTracer;
module.exports.RawRESTkinScribeTracer = RawRESTkinScribeTracer;
module.exports.RESTkinScribeTracer = RESTkinScribeTracer;
module.exports.ZipkinQueryServiceHTTPTracer = ZipkinQueryServiceHTTPTracer;
module.exports.RawZipkinQueryServiceHTTPTracer = RawZipkinQueryServiceHTTPTracer;


/**
 * Implements self-documented gulpfile
 *
 * @author Mykhailo Stadnyk <mikhus@gmail.com>
 */

// load polyfills
require('./lib/string');
require('./lib/object');

if (typeof Promise === 'undefined') {
    var Promise = require('es6-promise').Promise;
}

// load modules
var fs = require('fs');
var chalk = require('chalk');

/**
 * @external Gulp
 * @see {@link https://github.com/gulpjs/gulp/blob/master/docs/API.md}
 */

/**
 * Gulpfile reflection metadata
 *
 * @access private
 */
var reflection = {};

/**
 * @typedef {{
 *     lineWidth: number,
 *     keysColumnWidth: number,
 *     padding: number,
 *     groupPadding: number,
 *     logger: {
 *        log: function
 *     },
 *     isTypescript: boolean,
 *     gulpfile: string,
 *     displayDependencies: boolean,
 *     emptyLineBetweenTasks: boolean,
 *     defaultGroupName: string
 * }} ConfigOptions
 */

/**
 * Default options
 *
 * @type {ConfigOptions} OPTIONS
 */
var OPTIONS = {
    lineWidth: 80,
    keysColumnWidth: 20,
    padding: 4,
    groupPadding: 1,
    groupColor: 'magenta',
    logger: console,
    isTypescript: fs.existsSync('gulpfile.ts'),
    gulpfile: undefined,
    displayDependencies: true,
    emptyLineBetweenTasks: true,
    defaultGroupName: 'Common tasks'
};

function rdeps(nodes) {
    var deps = [];

    if (!nodes.length) return deps;

    nodes.forEach(function(node) {
        if (!node.branch) {
            deps.push(node.label);
        }

        deps = deps.concat(rdeps(node.nodes));
    });

    return deps.reduce(function(p, c) {
        if (p.indexOf(c) < 0) p.push(c);
        return p;
    }, []);
}

function gulpTasks(gulp) {
    if (!gulp.tree) {
        // old gulp
        return gulp.tasks;
    }

    // v4
    var nodes = gulp.tree({ deep: true }).nodes;
    var tasks = {};

    nodes.forEach(function(node) {
        tasks[node.label] = {
            name: node.label,
            dep: rdeps(node.nodes)
        };
    });

    return tasks;
}

/**
 * Analyzes given gulp instance and build internal cache
 * for further printing
 *
 * @param {Gulp} gulp
 * @access private
 */
function build(gulp) {
    // make sure we don't lose anything from required files
    // @see https://github.com/Mikhus/gulp-help-doc/issues/2
    // currently this is not supported for typescript

    var source = OPTIONS.isTypescript ?
        fs.readFileSync('gulpfile.ts').toString() :
        OPTIONS.gulpfile ?
            fs.readFileSync(OPTIONS.gulpfile).toString() :
        Object.keys(require.cache || {'gulpfile.js': ''}).map(function(file) {
            if (!/node_modules|\.json$/.test(file)) {
                return fs.readFileSync(file).toString() + '\n';
            }
        }).join('');

    var rxDoc = '\\/\\*\\*\\r?\n(((?!\\*\\/)[\\s\\S])*?)' +
        '@task\\s+\\{(.*)?\\}((?!\\*\\/)[\\s\\S])*?\\*\\/';

    var rxArgs = '@arg\\s+\\{(.*?)\\}(.*?)\\r?\\n';
    var rxOrder = '@order\\s+\\{(\\d+)\\}(.*?)\\r?\\n';
    var rxGroup = '@group\\s+\\{(.*?)\\}(.*?)\\r?\\n';

    var globalRxDoc = new RegExp(rxDoc, 'g');
    var localRxDoc = new RegExp(rxDoc);

    var globalRxArgs = new RegExp(rxArgs, 'g');
    var localRxArgs = new RegExp(rxArgs);

    var globalRxOrder = new RegExp(rxOrder, 'g');
    var localRxOrder = new RegExp(rxOrder);

    var globalRxGroup = new RegExp(rxGroup, 'g');
    var localRxGroup = new RegExp(rxGroup);

    var jsDoc  = (source.match(globalRxDoc) || []);
    var tasks = gulpTasks(gulp);

    Object.keys(tasks).forEach(function (task) {
        reflection[task] = {
            name: tasks[task].name,
            desc: '',
            dep: tasks[task].dep
        };
    });

    jsDoc.map(function (block) {
        var parts = block.match(localRxDoc);

        var name = parts[3].trim();
        var desc = parts[1].replace(/\s*\*/g, ' ')
            .replace(/\s{2,}/g, ' ')
            .trim();

        if (!reflection[name]) {
            return;
        }

        reflection[name].desc = desc;
        reflection[name].public = true;
        reflection[name].args = (block.match(globalRxArgs) || [])
            .map(function (def) {
                var argsParts = def.match(localRxArgs);

                return {
                    name: argsParts[1],
                    desc: argsParts[2].replace(/\s*\*/g, ' ')
                        .replace(/\s{2,}/g, ' ')
                        .trim()
                };
            });

        reflection[name].order = (function () {
            var orderParts = block.match(globalRxOrder);

            if (orderParts) {
                return +orderParts[0].match(localRxOrder)[1];
            }

            return Number.MAX_SAFE_INTEGER;
        })();

        reflection[name].group = (function () {
            var groupParts = block.match(globalRxGroup);

            if (groupParts) {
                return groupParts[0].match(localRxGroup)[1];
            }

            return OPTIONS.defaultGroupName;
        })();
    });

    // Re-group tasks using user-defined groups
    var tmp = {};

    Object.keys(reflection).forEach(function(task) {
        var group = reflection[task].group || OPTIONS.defaultGroupName;

        tmp[group] = tmp[group] || {};
        tmp[group][task] = reflection[task];
    });

    reflection = tmp;
}

/**
 * Chunks given string into pieces making each chunk less or equal to
 * OPTIONS.lineWidth, taking into account safe word-break
 *
 * @param {string} str
 * @param {number} maxLen
 * @returns {Array}
 * @access private
 */
function chunk(str, maxLen) {
    var len  = maxLen || OPTIONS.lineWidth;
    var curr = len;
    var prev = 0;
    var out  = [];

    while (str[curr]) {
        if (str[curr++] == ' ') {
            out.push(str.substring(prev, curr));
            prev = curr;
            curr += len;
        }
    }

    out.push(str.substr(prev));

    return out;
}

function hasTasks(group) {
    if (!reflection[group]) {
        return false;
    }

    return Object.keys(reflection[group]).some(function (task) {
        return reflection[group][task].public;
    });
}

/**
 * Performs usage strings output
 *
 * @access private
 */
function print() {
    OPTIONS.logger.log(chalk.bold('Usage: gulp [task] [options]'));
    OPTIONS.logger.log(chalk.bold('Tasks:'));

    var groups = Object.keys(reflection);
    var isMultiGroup = groups.length > 1;

    groups.filter(function (groupName) {
        return hasTasks(groupName);
    })
    .sort()
    .sort(function (a, b) { // ordering groups by @order tags
        return Math.min.apply(null,
            Object.keys(reflection[a]).map(function (task) {
                return reflection[a][task].order;
            })) - Math.min.apply(null,
            Object.keys(reflection[b]).map(function (task) {
                return reflection[b][task].order;
            }));
    })
    .map(function (groupName) {
        var reflectionGroup = reflection[groupName];
        var groupTasks = Object.keys(reflectionGroup);

        if (isMultiGroup ||
            !isMultiGroup && groupName !== OPTIONS.defaultGroupName
        ) {
            OPTIONS.logger.log(
                ' '.repeat(OPTIONS.groupPadding) +
                chalk.bold[OPTIONS.groupColor](groupName)
            );
        }

        groupTasks.filter(function (name) {
            return reflectionGroup[name].public;
        })
        .sort()
        .sort(function(a, b) { // ordering tasks inside group by @order tags
            return reflectionGroup[a].order - reflectionGroup[b].order;
        })
        .forEach(function (name) {
            var task = reflectionGroup[name];
            var deps = task.dep.filter(function (dep) {
                return reflectionGroup[dep] && reflectionGroup[dep].public;
            });
            var text = ' '.repeat(OPTIONS.padding) +
                chalk.bold.green(task.name) +
                ' '.repeat(
                    Math.max(1,OPTIONS.keysColumnWidth -
                        OPTIONS.padding -
                        task.name.length)
                );
            var chunks = chunk(task.desc, OPTIONS.lineWidth -
                OPTIONS.keysColumnWidth);
            var i = 0;

            OPTIONS.logger.log(text + chalk.bold(chunks[i]));

            if (chunks.length > 1) {
                while (chunks.length > ++i) {
                    (chunks[i].trim()) && (OPTIONS.logger.log(
                        ' '.repeat(OPTIONS.keysColumnWidth) +
                        chalk.bold(chunks[i])
                    ));
                }
            }

            task.args.forEach(function (arg) {
                var chunks = arg.desc ?
                    chunk(arg.desc,
                        OPTIONS.lineWidth - OPTIONS.keysColumnWidth) :
                    ['[boolean]'];
                var i = 0;

                OPTIONS.logger.log(
                    ' '.repeat(OPTIONS.padding + 1) +
                    chalk.bold.cyan('--' + arg.name) +
                    ' '.repeat(Math.max(1,
                        OPTIONS.keysColumnWidth - OPTIONS.padding -
                        arg.name.length - 3)) +
                    chunks[i]
                );

                while (chunks.length > ++i) {
                    (chunks[i].trim()) && (OPTIONS.logger.log(
                        ' '.repeat(OPTIONS.keysColumnWidth) +
                        chunks[i]
                    ));
                }
            });

            if (OPTIONS.displayDependencies && deps.length) {
                OPTIONS.logger.log(
                    ' '.repeat(OPTIONS.keysColumnWidth) +
                    chalk.bold.gray('Depends: ') +
                    chalk.grey(JSON.stringify(deps))
                );
            }

            if (OPTIONS.emptyLineBetweenTasks) {
                OPTIONS.logger.log('');
            }
        });
    });
}

/**
 * Prints usage help information for the given gulp
 * instance.
 * Usually it is used as a task within gulpfile.js in your project
 * Please make sure all your comments are properly annotated
 *
 * @examples
 * <caption>Typical usage:</caption>
 *  var gulp = require('gulp');
 *
 *  gulp.task('help', => require('gulp-help-doc')(gulp));
 *
 * @param {Gulp} gulp - gulp instance to analyze
 * @param {ConfigOptions} [options]
 * @returns {Promise}
 * @access public
 */
function usage(gulp, options) {
    // re-define options if needed
    if (options) {
        Object.assign(OPTIONS, options);
    }

    return new Promise(function(resolve) {
        build(gulp);
        print();
        resolve();
    });
}

module.exports = usage;


/**
 * eyeD3 Write Adapter
 */

var util = require('util');
var exec = require('child_process').exec;
var AbstractWriter = require('./abstract');

/**
 * @constructor
 *
 * @param {Object} options - Options object
 */
var Writer = function(options)
{
    // Call abstract constructor
    AbstractWriter.prototype.constructor.apply(this, arguments);

    this.name = 'eyeD3';
    this.methods = {
        clear: 'eyeD3',
        write: 'eyeD3'
    };

    if (~this.options.encoding.indexOf('ISO-')) {
        this.options.encoding = 'latin1';
    }

    if ('UTF-8' === this.options.encoding) {
        this.options.encoding = 'utf8';
    }

    if ('UTF-16' === this.options.encoding) {
        this.options.encoding = 'utf16';
    }

    this.version = null;
};

/**
 * Extend from the abstract writer.
 */
util.inherits(Writer, AbstractWriter);

/**
 * Configure the write adapter.
 *
 * @param {Function} callback - Function to call on finish
 */
Writer.prototype.configure = function(callback)
{
    var self = this;

    exec('env ' + this.name + ' --version', function(err, stdout, stderr) {

        // We need to skip error handling - eyeD3 exit with
        // code 1 on --version requests - whyever
        // So stderr contains the normal stdout
        stdout = stderr || '';

        if (~stdout.indexOf('0.6.')) {
            self.version = '0.6';
        }

        if (~stdout.indexOf('0.7.')) {
            self.version = '0.7';
        }

        callback && callback();
    });
};

/**
 * Clear all tags of a file.
 *
 * @param {File} file - File object
 * @param {Number|String} version - Only strip tags of given version
 * @param {Function} callback - Function to call on finish
 * @throws {ReferenceError} Errors while validate version
 */
Writer.prototype.clear = function(file, version, callback)
{
    this.checkVersion(version)
        .checkFile(file);

    var args = [];
    var commandTemplate = '%s %s "%s"';

    if ('0.6' === this.version) {
        args.push('--remove-images');
    }

    if ('0.7' === this.version) {
        args.push('--remove-all-images');
    }

    switch (version) {
        case 'both':
            args = ['--remove-all'];
            break;
        case 1:
            args.push('--remove-v1');
            break;
        case 2:
            args.push('--remove-v2');
            break;
    }

    var command = util.format(
        commandTemplate,
        this.methods.clear,
        args.join(' '),
        file.getPath().replace(/"/g, '\\"')
    );

    this.exec(command, callback)
};

/**
 * Write all given tags to the file.
 *
 * @param {File} file - File object to write to
 * @param {Number|String} version - Only strip tags of given version
 * @param {Meta} meta - Meta object to write
 * @param {Function} callback - Function to call on finish
 * @throws {ReferenceError} Errors while validate version
 */
Writer.prototype.write = function(file, version, meta, callback)
{
    this.checkVersion(version)
        .checkFile(file)
        .checkMeta(meta);

    var args = ['--force-update'];
    var commandTemplate = '%s %s "%s"';

    if ('0.6' === this.version) {
        args.push('--set-encoding ' + this.options.encoding)
    }

    if ('0.7' === this.version) {
        args.push('--encoding ' + this.options.encoding);
    }

    switch (version) {
        case 'both':
            args.push('-1');
            args.push('-2');
            break;
        case 1:
            args.push('-1');
            break;
        case 2:
            args.push('-2');
            break;
    }

    var self = this;

    Object.keys(meta.information).forEach(function(key) {

        var value = meta.information[key];

        // Skip value less tags
        if ('' === value || 0 === value) {
            return;
        }

        // Transcode strings
        if ('string' === typeof value) {
            value = self.options.transcode(value.replace(/"/g, ''));
        }

        switch (key) {
            case 'artist':
                args.push('-a "' + value + '"');
                break;
            case 'title':
                args.push('-t "' + value + '"');
                break;
            case 'album':
                args.push('-A "' + value + '"');
                break;
            case 'genre':
                args.push('-G "' + value + '"');
                break;
            case 'year':
                args.push('-Y ' + value);
                break;
            case 'track':
                args.push('-n ' + value);
                break;
            case 'total':
                args.push('-N ' + value);
                break;
        }
    });

    // We got a comment but no description
    if (meta.information.comment && !meta.information.desc) {
        args.push('-c "' + meta.information.comment + '"');
    }

    // We got no comment but a description, we just set it as a comment
    if (!meta.information.comment && meta.information.desc) {
        args.push('-c "' + meta.information.desc + '"');
    }

    // We got both - a comment with a description
    if (meta.information.comment && meta.information.desc) {
        args.push(
            '--add-comment "' +
            meta.information.comment +
            ':' +
            meta.information.desc +
            '"'
        );
    }

    if (meta.images && 0 !== meta.images.length) {

        meta.images.forEach(function(image) {

            var arg = '--add-image "%s":%s%s';

            args.push(util.format(
                arg,
                image.getFile().getPath().replace(/"/g, '\\"'),
                image.getType(),
                (image.getDescription())
                    ? (':"' + image.getDescription() + '"')
                    : ''
            ));
        });
    }

    var command = util.format(
        commandTemplate,
        this.methods.write,
        args.join(' '),
        file.getPath().replace(/"/g, '\\"')
    );

    this.exec(command, callback)
};

module.exports = Writer;


/**
 * id3tag Write Adapter
 */

var util = require('util');
var AbstractWriter = require('./abstract');

/**
 * @constructor
 *
 * @param {Object} options - Options object
 */
var Writer = function(options)
{
    // Call abstract constructor
    AbstractWriter.prototype.constructor.apply(this, arguments);

    this.name = 'id3tag';
    this.methods = {
        clear: 'id3convert',
        write: 'id3tag'
    };
};

/**
 * Extend from the abstract writer.
 */
util.inherits(Writer, AbstractWriter);

/**
 * Clear all tags of a file.
 *
 * @param {File} file - File object
 * @param {Number|String} version - Only strip tags of given version
 * @param {Function} callback - Function to call on finish
 * @throws {ReferenceError} Errors while validate version
 */
Writer.prototype.clear = function(file, version, callback)
{
    this.checkVersion(version)
        .checkFile(file);

    var args = ['-s'];
    var commandTemplate = '%s %s "%s"';

    switch (version) {
        case 'both':
            args.push('-1');
            args.push('-2');
            break;
        case 1:
            args.push('-1');
            break;
        case 2:
            args.push('-2');
            break;
    }

    var command = util.format(
        commandTemplate,
        this.methods.clear,
        args.join(' '),
        file.getPath()
    );

    this.exec(command, callback)
};

/**
 * Write all given tags to the file.
 *
 * @param {File} file - File object to write to
 * @param {Number|String} version - Only strip tags of given version
 * @param {Meta} meta - Meta object to write
 * @param {Function} callback - Function to call on finish
 * @throws {ReferenceError} Errors while validate version
 */
Writer.prototype.write = function(file, version, meta, callback)
{
    this.checkVersion(version)
        .checkFile(file)
        .checkMeta(meta);

    var args = [];
    var commandTemplate = '%s %s "%s"';

    switch (version) {
        case 'both':
            args.push('-1');
            args.push('-2');
            break;
        case 1:
            args.push('-1');
            break;
        case 2:
            args.push('-2');
            break;
    }

    var self = this;

    Object.keys(meta.information).forEach(function(key) {

        var value = meta.information[key];

        // Skip value less tags
        if ('' === value || 0 === value) {
            return;
        }

        // Transcode strings
        if ('string' === typeof value) {
            value = self.options.transcode(value);
        }

        switch (key) {
            case 'artist':
                args.push('-a "' + value + '"');
                break;
            case 'title':
                args.push('-s "' + value + '"');
                break;
            case 'album':
                args.push('-A "' + value + '"');
                break;
            case 'comment':
                args.push('-c "' + value + '"');
                break;
            case 'desc':
                args.push('-C "' + value + '"');
                break;
            case 'genre':
                args.push('-g ' + value);
                break;
            case 'year':
                args.push('-y ' + value);
                break;
            case 'track':
                args.push('-t ' + value);
                break;
            case 'total':
                args.push('-T ' + value);
                break;
        }
    });

    var command = util.format(
        commandTemplate,
        this.methods.write,
        args.join(' '),
        file.getPath()
    );

    this.exec(command, callback);
};

module.exports = Writer;


/**
 * Meta Information Class
 *
 * @author Hermann Mayer <hermann.mayer92@gmail.com>
 */

var extend = require('extend');
var Image = require('./image');

/**
 * @constructor
 *
 * @param {Object} information - Meta information to set
 *  {
 *      artist: String,
 *      song: String,
 *      album: String,
 *      comment: String,
 *      desc: String,
 *      genre: String,
 *      year: Number,
 *      track: Number,
 *      total: Number
 *  }
 * @param {Array} [images] - Array of Image objects
 */
var Meta = function(information, images)
{
    // Initalize the meta information
    this.information = {};
    this.set(information || {}, true);

    // Initalize images
    this.images = [];
    this.addImages(images || [], true);
};

/**
 * Clear the meta information.
 */
Meta.prototype.clear = function()
{
    this.information = {
        artist: '',
        title: '',
        song: '',
        album: '',
        comment: '',
        desc: '',
        genre: '',
        year: (new Date()).getFullYear(),
        track: 0,
        total: 0
    };
};

/**
 * Validate the meta information.
 *
 * @param {Object} information - Meta information to set
 * @throws {ReferenceError} Error while validating the given keys
 */
Meta.prototype.validate = function(information)
{
    // Validate keys by types
    var numbers = [
        'year', 'track', 'total'
    ];
    var strings = [
        'artist', 'title', 'song', 'album', 'comment',
        'desc', 'genre'
    ];

    var validKeys = [].concat(numbers, strings);
    var givenKeys = Object.keys(information || {});

    {
        // Validate keys
        var unkown = givenKeys.filter(function(key) {
            return !~validKeys.indexOf(key);
        });

        if (0 !== unkown.length) {
            throw new ReferenceError('Unknown keys: ' + unkown.join(', '));
        }
    }

    {
        // Validate types
        var errors = [];

        givenKeys.forEach(function(key) {

            var value = information[key];

            if (~numbers.indexOf(key) &&
                'number' !== typeof value) {
                errors.push(key + ' is not number');
            }

            if (~strings.indexOf(key) &&
                'string' !== typeof value) {
                errors.push(key + ' is not string');
            }
        });

        if (0 !== errors.length) {
            throw new ReferenceError('Bad typed keys: ' + errors.join(', '));
        }
    }
};

/**
 * Set meta information.
 *
 * @param {Object} information - Meta information to set
 * @param {Boolean} [clear] - Clear all other information - Default: false
 * @throws {ReferenceError} Error while validating the given keys
 */
Meta.prototype.set = function(information, clear)
{
    // Check the clear parameter
    if (null === clear || undefined === clear) {
        clear = false;
    }

    // Clear if needed
    if (true === clear) {
        this.clear();
    }

    this.validate(information);

    // Extend the information
    this.information = extend(true, this.information || {}, information);
};

/**
 * Get meta information.
 *
 * @return {Object} Meta information
 */
Meta.prototype.get = function()
{
    return this.information;
};

/**
 * Add images.
 *
 * @param {Array} images - Array of Image objects
 * @param {Boolean} [clear] - Clear all other information - Default: false
 * @throws {TypeError} Error while validating the given images
 */
Meta.prototype.addImages = function(images, clear)
{
    // Clear if needed
    if (true === clear) {
        this.images = [];
    }

    images.forEach(function(image) {

        // Check for a valid Image instance
        if (!(image instanceof Image)) {
            throw new TypeError('Given object is not a Image instance');
        }
    });

    // Extend the information
    this.images = images;
};

/**
 * Get all images.
 *
 * @return {Array} Array of Image objects
 */
Meta.prototype.getImages = function()
{
    return this.images;
};

module.exports = Meta;


/**
 * Module dependencies.
 */
var util = require('util')
  , OAuth2Strategy = require('passport-oauth').OAuth2Strategy
  , InternalOAuthError = require('passport-oauth').InternalOAuthError;

/**
 * `Strategy` constructor.
 *
 * The Weibo authentication strategy authenticates requests by delegating to
 * Weibo using the OAuth 2.0 protocol.
 *
 * Applications must supply a `verify` callback which accepts an `accessToken`,
 * `refreshToken` and service-specific `profile`, and then calls the `done`
 * callback supplying a `user`, which should be set to `false` if the
 * credentials are not valid.  If an exception occured, `err` should be set.
 *
 * Options:
 *   - `clientID`      your Weibo application's Client ID
 *   - `clientSecret`  your Weibo application's Client Secret
 *   - `callbackURL`   URL to which Weibo will redirect the user after granting authorization
 *   - `scope`         array of permission scopes to request.  valid scopes include:
 *                     'user', 'public_repo', 'repo', 'gist', or none.
 *                     (see http://open.weibo.com/wiki/Oauth/en#User_Authentication for more info)
 *
 * Examples:
 *
 *     passport.use(new WeiboStrategy({
 *         clientID: '123-456-789',
 *         clientSecret: 'shhh-its-a-secret'
 *         callbackURL: 'https://www.example.net/auth/weibo/callback'
 *       },
 *       function(accessToken, refreshToken, profile, done) {
 *         User.findOrCreate(..., function (err, user) {
 *           done(err, user);
 *         });
 *       }
 *     ));
 *
 * @param {Object} options
 * @param {Function} verify
 * @api public
 */
function Strategy(options, verify) {
  options = options || {};
  options.authorizationURL = options.authorizationURL || 'https://api.weibo.com/oauth2/authorize';
  options.tokenURL = options.tokenURL || 'https://api.weibo.com/oauth2/access_token';
  options.scopeSeparator = options.scopeSeparator || ',';
  options.customHeaders = options.customHeaders || {};
  if (!options.customHeaders['User-Agent']) {
    options.customHeaders['User-Agent'] = options.userAgent || 'passport-weibo';
  }

  OAuth2Strategy.call(this, options, verify);
  this.name = 'weibo';
  this._getuidAPI = options.getuidAPI || 'https://api.weibo.com/2/account/get_uid.json';
  this._getProfileAPI = options.getProfileAPI || 'https://api.weibo.com/2/users/show.json';

}

/**
 * Inherit from `OAuth2Strategy`.
 */
util.inherits(Strategy, OAuth2Strategy);

/**
 * Retrieve user profile from Weibo.
 *
 * This function constructs a normalized profile, with the following properties:
 *
 *   - `provider`         always set to `weibo`
 *   - `id`               the user's Weibo ID
 *   - `username`         the user's Weibo username
 *   - `displayName`      the user's full name
 *   - `profileUrl`       the URL of the profile for the user on Weibo
 *   - `emails`           the user's email addresses
 *
 * @param {String} accessToken
 * @param {Function} done
 * @api protected
 */
Strategy.prototype.userProfile = function(accessToken, callback) {
    var self = this;
    this._oauth2.getProtectedResource(this._getuidAPI, accessToken, function(err, body, res) {
        if (err) return callback(new InternalOAuthError('', err));

        try {
            var raw = JSON.parse(body);
            self._oauth2.getProtectedResource(self._getProfileAPI + '?uid=' + raw.uid, accessToken, function(err, body, res) {
                if (err) return callback(new InternalOAuthError('', err));

                try {
                  callback(null, self.formatProfile(JSON.parse(body))); // this is different with raw.
                } catch(e) {
                  callback(e);
                }
            })
        } catch(e) {
            callback(e);
        }
    })
}

Strategy.prototype.formatProfile = function(raw) {
    var user = {};
    user.provider = 'weibo';
    user.id = raw.idstr;
    user.displayName = raw.screen_name || raw.name;
    user._raw = raw;
    user._json = raw;

    return user;
};

/**
 * Expose `Strategy`.
 */
module.exports = Strategy;


require('setimmediate')
var validator = require('occamsrazor-validator')
var stable = require('stable')

function getArgs (a) {
  return Array.prototype.slice.call(a)
}

// a filter implementation
// that mutates the original array
function filterArray (arr, toKeep) {
  var i = 0
  while (i < arr.length) {
    if (toKeep(arr[i])) {
      i++
    } else {
      arr.splice(i, 1)
    }
  }
}

function combineValidators (validators) {
  return validator.combine(validators.map(function (v) {
    if (typeof v === 'undefined') {
      return validator()
    } else if (!(typeof v === 'function' && 'score' in v)) {
      return validator().match(v)
    } else {
      return v
    }
  }))
}

function undecorate (results) {
  return results.map(function (r) { return r.payload })
}

function countdown (adapter) {
  adapter.times && adapter.times--
}

function notExausted (adapter) {
  return adapter.times !== 0
}

function getAll (context, args, adapters) {
  return adapters.map(function (adapter) {
    return adapter.func.apply(context, args)
  })
}

function triggerOne (context, args, func) {
  setImmediate(function () { func.apply(context, args) })
}

function triggerAll (context, args, adapters) {
  adapters.forEach(function (adapter) {
    triggerOne(context, args, adapter.func)
  })
}

function decorateAndFilter (args, adapters) {
  var result
  var results = []

  // get the score function
  // decorate
  for (var i = 0; i < adapters.length; i++) {
    result = adapters[i].validators(args)
    // filter
    if (result) {
      result.payload = adapters[i]
      results.push(result)
    }
  }
  return results
}

function compare (a, b) {
  if (a < b) {
    return 1
  }
  if (a > b) {
    return -1
  }
  return 0
}

// get all adapter that validates with args, with the highest score
// the result is a single adapter (func, validators)
function filterAndSortOne (args, adapters) {
  var results = decorateAndFilter(args, adapters)

  if (results.length === 0) {
    return
  }

  // sort
  stable.inplace(results, compare)

  if (results.length > 1 && results[0].toString() === results[1].toString()) {
    throw new Error('Occamsrazor (get): More than one adapter fits')
  }

  // undecorate
  return undecorate(results)[0]
}

// get all the adapter that validates with args. Sorted by score
// the result is a list of adapters (func, validators)
function filterAndSort (args, adapters) {
  var results = decorateAndFilter(args, adapters)
  // sort
  stable.inplace(results, compare)
  // undecorate
  return undecorate(results)
}

function binaryInsert (array, value, comparator, startVal, endVal) {
  var length = array.length
  var start = typeof startVal !== 'undefined' ? startVal : 0
  var end = typeof endVal !== 'undefined' ? endVal : length - 1 // !! endVal could be 0 don't use || syntax
  var m = start + Math.floor((end - start) / 2)

  if (length === 0 || !comparator) {
    array.push(value)
    return
  }

  if (comparator(value, array[end]) > 0) {
    array.splice(end + 1, 0, value)
    return
  }

  if (comparator(value, array[start]) < 0) { // !!
    array.splice(start, 0, value)
    return
  }

  if (start >= end) {
    return
  }

  if (comparator(value, array[m]) < 0) {
    binaryInsert(array, value, comparator, start, m - 1)
    return
  }

  if (comparator(value, array[m]) > 0) {
    binaryInsert(array, value, comparator, m + 1, end)
  }
}

module.exports = {
  getArgs: getArgs,
  combineValidators: combineValidators,
  getAll: getAll,
  triggerAll: triggerAll,
  triggerOne: triggerOne,
  countdown: countdown,
  filterAndSort: filterAndSort,
  filterAndSortOne: filterAndSortOne,
  filterArray: filterArray,
  notExausted: notExausted,
  binaryInsert: binaryInsert
}


var ut = require('./utils')
var _batch = require('./batch')

function occamsrazor (options) {
  options = options || {}
  // these should always mutate!
  var _adapters = options._adapters || []
  var _events = options._events || []
  var _ns = options._ns
  var comparator = options.comparator

  /*
  private methods
  */

  function getAdd (opts) {
    opts = opts || {}
    var times = opts.times
    var doesConsume = opts.doesConsume
    return function add () {
      var lastArg = arguments[arguments.length - 1]
      var func = typeof lastArg === 'function' ? lastArg : function () { return lastArg }
      var validators = arguments.length > 1 ? Array.prototype.slice.call(arguments, 0, -1) : []
      var newAdapter = {
        doesConsume: doesConsume, // calling this adapter will remove the event posted
        times: times,
        func: func,
        validators: ut.combineValidators(validators),
        ns: _ns
      }
      _adapters.push(newAdapter)

      // trigger all published event matching this adapter
      ut.filterArray(_events, function (event) {
        if (newAdapter.times === 0) return true
        var filteredAdapters = ut.filterAndSort(event.args, [newAdapter])
        filteredAdapters.forEach(ut.countdown)
        ut.filterArray(_adapters, ut.notExausted)
        ut.triggerAll(event.context, event.args, filteredAdapters)
        // if the adapter consume the event, this gets filtered out
        return !(doesConsume && filteredAdapters.length)
      })
      return or
    }
  }

  /*
  public methods
  */

  /* this function is posting an event: context "this" and arguments */
  function adapt () {
    var context = this
    var args = ut.getArgs(arguments)
    var filteredAdapter = ut.filterAndSortOne(args, _adapters) // the most specific
    if (filteredAdapter) {
      ut.countdown(filteredAdapter)
      ut.filterArray(_adapters, ut.notExausted)
      return filteredAdapter.func.apply(context, args)
    }
  }

  /* this function is posting an event: context "this" and arguments */
  function all () {
    var context = this
    var args = ut.getArgs(arguments)
    var filteredAdapters = ut.filterAndSort(args, _adapters) // all matching adapters
    filteredAdapters.forEach(ut.countdown)
    ut.filterArray(_adapters, ut.notExausted)
    return ut.getAll(context, args, filteredAdapters)
  }

  /* this function is posting an event: context "this" and arguments */
  function trigger () {
    var context = this
    var args = ut.getArgs(arguments)
    var filteredAdapters = ut.filterAndSort(args, _adapters)
    filteredAdapters.forEach(ut.countdown)
    ut.filterArray(_adapters, ut.notExausted)
    ut.triggerAll(context, args, filteredAdapters)
    return or
  }

  /* this function is posting an event: context "this" and arguments */
  function stick () {
    var context = this
    var args = ut.getArgs(arguments)
    var filteredAdapters = ut.filterAndSort(args, _adapters)
    var consumeAdapters = filteredAdapters.filter(function (adapter) { return adapter.doesConsume })
    // adapters published with "consume" trigger the removal of the registered event
    if (!consumeAdapters.length) {
      ut.binaryInsert(_events, { context: this, args: args }, comparator)
    }
    filteredAdapters.forEach(ut.countdown)
    ut.filterArray(_adapters, ut.notExausted)
    ut.triggerAll(context, args, filteredAdapters)
    return or
  }

  function size () {
    var args = ut.getArgs(arguments)
    var funcs = args.length ? ut.filterAndSort(args, _adapters) : _adapters
    return funcs.length
  }

  function remove (func) {
    if (!func && !_ns) {
      _adapters = []
      return or
    }
    ut.filterArray(_adapters, function (adapter) {
      if (func && _ns) {
        return adapter.func !== func || adapter.ns !== _ns
      } else if (func) {
        return adapter.func !== func
      } else if (_ns) {
        return adapter.ns !== _ns
      }
    })
    return or
  }

  function removeIf () {
    var args = ut.getArgs(arguments)
    var funcs = ut.filterAndSort(args, _adapters)
      .map(function (r) {
        return r.func
      })
    funcs.forEach(remove.bind(this))
    return or
  }

  function unstick () {
    var args = ut.getArgs(arguments)
    var validator = ut.combineValidators(args)
    ut.filterArray(_events, function (event) {
      return !validator(event.args)
    })
    return or
  }

  function proxy (id) {
    id = id || Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10)
    return occamsrazor({ _adapters: _adapters, _events: _events, _ns: id, comparator: comparator })
  }

  function batch () {
    return _batch(or, comparator)
  }

  // returns a callable object
  function or () {
    var args = ut.getArgs(arguments)
    return adapt.apply(this, args)
  }

  // post/trigger event
  or.adapt = adapt
  or.all = or.triggerSync = all
  or.trigger = trigger
  or.stick = or.post = stick

  // listen for an event
  or.add = or.on = getAdd()
  or.one = getAdd({ times: 1 })
  or.consume = getAdd({ doesConsume: true })
  or.consumeOne = getAdd({ doesConsume: true, times: 1 })

  // remove listener
  or.remove = or.off = remove
  or.removeIf = removeIf

  // remove event
  or.unstick = or.unpost = unstick

  // various
  or.size = size
  or.proxy = or.namespace = proxy
  or.batch = batch

  or.getAdapters = function () { return _adapters }
  or.getEvents = function () { return _events }

  return or
}

module.exports = occamsrazor