System.register("seven-segment", [], function(exports_1, context_1) {
    "use strict";
    var __moduleName = context_1 && context_1.id;
    var Digit, Segment, Seven;
    return {
        setters:[],
        execute: function() {
            (function (Digit) {
                Digit[Digit["ZERO"] = 0] = "ZERO";
                Digit[Digit["ONE"] = 1] = "ONE";
                Digit[Digit["TWO"] = 2] = "TWO";
                Digit[Digit["THREE"] = 3] = "THREE";
                Digit[Digit["FOUR"] = 4] = "FOUR";
                Digit[Digit["FIVE"] = 5] = "FIVE";
                Digit[Digit["SIX"] = 6] = "SIX";
                Digit[Digit["SEVEN"] = 7] = "SEVEN";
                Digit[Digit["EIGHT"] = 8] = "EIGHT";
                Digit[Digit["NINE"] = 9] = "NINE";
                Digit[Digit["BLANK"] = 10] = "BLANK";
                Digit[Digit["D"] = 11] = "D";
            })(Digit || (Digit = {}));
            exports_1("Digit", Digit);
            Segment = (function () {
                function Segment() {
                    this.on = false;
                    this.points = [
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 }
                    ];
                }
                Object.defineProperty(Segment.prototype, "off", {
                    get: function () {
                        return !this.on;
                    },
                    enumerable: true,
                    configurable: true
                });
                return Segment;
            }());
            exports_1("Segment", Segment);
            /**
             * The Seven class represents the geometry of a seven segment digit.
             * The geometry uses a coordinate system that is a cartesian grid with
             *    -x values to the left of the origin,
             *    +x values to the right of the origin,
             *    +y values below the origin, and
             *    -y values above the origin.
             * This is the coordinate system typically used for computer graphic systems.
             */
            Seven = (function () {
                /**
                 * Construct a new Seven object.
                 * Optionally pass a SevenConfig object to set properties.
                 * Each property of the SevenConfig object is optional.
                 * If the passed in config contains bad values an exception will be thrown.
                 */
                function Seven(_a) {
                    var _b = _a === void 0 ? {} : _a, height = _b.height, width = _b.width, _c = _b.angle, angle = _c === void 0 ? 10 : _c, _d = _b.ratioLtoW, ratioLtoW = _d === void 0 ? 4 : _d, _e = _b.ratioLtoS, ratioLtoS = _e === void 0 ? 32 : _e, _f = _b.digit, digit = _f === void 0 ? Digit.BLANK : _f;
                    /** The cononical points for a horizontal segment for the given configuration. */
                    this._horizontalSegmentGeometry = [
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 }
                    ];
                    /** The cononical points for a vertical segment for the given configuration. */
                    this._verticalSegmentGeometry = [
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 },
                        { x: 0, y: 0 }
                    ];
                    /** The x and y shifts that must be applied to each segment. */
                    this._translations = [
                        { x: 0, y: 0, a: this._horizontalSegmentGeometry },
                        { x: 0, y: 0, a: this._verticalSegmentGeometry },
                        { x: 0, y: 0, a: this._verticalSegmentGeometry },
                        { x: 0, y: 0, a: this._horizontalSegmentGeometry },
                        { x: 0, y: 0, a: this._verticalSegmentGeometry },
                        { x: 0, y: 0, a: this._verticalSegmentGeometry },
                        { x: 0, y: 0, a: this._horizontalSegmentGeometry }
                    ];
                    /** The segments, A-G of the digit. */
                    this.segments = [new Segment(), new Segment(), new Segment(), new Segment(), new Segment(), new Segment(), new Segment()];
                    this._angleDegree = angle;
                    this.digit = digit;
                    this._ratioLtoW = ratioLtoW;
                    this._ratioLtoS = ratioLtoS;
                    this._height = this._width = 100; //initialize so checkConfig passes, and for default case
                    this._isHeightFixed = true;
                    if (height !== undefined) {
                        this._height = height;
                    }
                    else if (width !== undefined) {
                        this._width = width;
                        this._isHeightFixed = false;
                    } //else - neither specified, default to height=100
                    this._positionSegments();
                }
                /**
                 * Check the height, width, angle, ratioLtoH, and ratioLtoS for valid values.
                 * Throws exception on first found issue.
                 *
                 * @private
                 */
                Seven.prototype._checkConfig = function () {
                    var a;
                    var b;
                    //check height
                    a = this._height;
                    b = a * 1; //convert to a number
                    if (b != a || (typeof a === 'string' && a.toString().trim() === '')) {
                        throw new TypeError("Invalid value (" + a + ") for height, not a number.");
                    }
                    else if (b <= 0) {
                        throw new RangeError("Invalid value (" + b + ") for height, must be greater than 0.");
                    }
                    else if (!isFinite(b)) {
                        throw new RangeError("Invalid value (" + b + ") for height, must be finite.");
                    }
                    this._height = b;
                    //check width
                    a = this._width;
                    b = a * 1; //convert to a number
                    if (b != a || (typeof a === 'string' && a.toString().trim() === '')) {
                        throw new TypeError("Invalid value (" + a + ") for width, not a number.");
                    }
                    else if (b <= 0) {
                        throw new RangeError("Invalid value (" + b + ") for width, must be greater than 0.");
                    }
                    else if (!isFinite(b)) {
                        throw new RangeError("Invalid value (" + b + ") for width, must be finite.");
                    }
                    this._width = b;
                    //Check angle
                    a = this._angleDegree;
                    b = a * 1; //convert to a number
                    if (b != a || (typeof a === 'string' && a.toString().trim() === '')) {
                        throw new TypeError("Invalid value (" + a + ") for angle, not a number.");
                    }
                    else if (b <= -90 || b >= 90) {
                        throw new RangeError("Invalid value (" + b + ") for angle, must be between 90 and -90 degrees.");
                    }
                    this._angleDegree = b;
                    //Check ratioLtoW
                    a = this._ratioLtoW;
                    b = a * 1; //convert to a number
                    if (b != a || (typeof a === 'string' && a.toString().trim() === '')) {
                        throw new TypeError("Invalid value (" + a + ") for ratioLtoW, not a number.");
                    }
                    else if (b < 1) {
                        throw new RangeError("Invalid value (" + b + ") for ratioLtoW, must be at least 1.");
                    }
                    else if (!isFinite(b)) {
                        throw new RangeError("Invalid value (" + b + ") for ratioLtoW, must be finite.");
                    }
                    this._ratioLtoW = b;
                    //Check ratioLtoS
                    a = this._ratioLtoS;
                    b = a * 1; //convert to a number
                    if (b != a || (typeof a === 'string' && a.toString().trim() === '')) {
                        throw new TypeError("Invalid value (" + a + ") for ratioLtoS, not a number.");
                    }
                    else if (b <= 0) {
                        throw new RangeError("Invalid value (" + b + ") for ratioLtoS, must be greater than 0.");
                    }
                    else if (!isFinite(b)) {
                        throw new RangeError("Invalid value (" + b + ") for ratioLtoS, must be finite.");
                    }
                    this._ratioLtoS = b;
                };
                /**
                 * This method sets the following values for the object.
                 * _segmentLength, _angleRadian, _spacing, _halfSegmentWidth, _height, _width
                 *
                 * This method populates the _horizontalSegmentGeometry array.
                 * The array contains the six points of the horizontal segment's geometry.
                 * The first element in the array is the left most point.
                 * The points are then listed in clockwise order.
                 * All points besides the first have positive x values.
                 * The first point is always at the origin (0,0).
                 * The second and third points have negative y values;
                 * The fourth point is always at (l, 0). where l is the segment length
                 * The fifth and sixth points have positive y values.
                 *
                 * This method populates the _verticalSegmentGeometry array.
                 * The array contains the six points of the vertical segment's geometry.
                 * The first element in the array is the top most point.
                 * The points are then listed in counterclockwise order.
                 * All points besides the first have positive y values.
                 * The first point is always at the origin (0,0).
                 *
                 * This method throws an error if the calculated geometry is unexpected.
                 *
                 * @private
                 */
                Seven.prototype._calculateSegmentGeometry = function () {
                    this._angleRadian = this._angleDegree * Math.PI / 180;
                    this._segmentEndAngle = (Math.PI / 2 - this._angleRadian) / 2;
                    //These calculations for segmentLength are the same as the height and width calcs below, except solved for segmentLength;
                    if (this._isHeightFixed) {
                        this._segmentLength = this._height / (1 / this._ratioLtoW + 2 * Math.cos(this._angleRadian) + 2 * (Math.sin(this._segmentEndAngle) + Math.cos(this._segmentEndAngle)) / this._ratioLtoS);
                    }
                    else {
                        this._segmentLength = this._width / (2 * Math.sin(Math.abs(this._angleRadian)) + 2 * Math.cos(this._angleDegree >= 0 ? this._segmentEndAngle : Math.PI / 2 - this._segmentEndAngle) / this._ratioLtoS + 1 + Math.tan(this._angleDegree >= 0 ? this._segmentEndAngle : Math.PI / 2 - this._segmentEndAngle) / this._ratioLtoW);
                    }
                    this._spacing = this._segmentLength / this.ratioLtoS;
                    var _segmentWidth = this._segmentLength / this.ratioLtoW;
                    this._halfSegmentWidth = _segmentWidth / 2;
                    this._segmentHorizontalShiftDistance = this._halfSegmentWidth * Math.tan(this._angleDegree >= 0 ? this._segmentEndAngle : Math.PI / 2 - this._segmentEndAngle); //This is the horizontal distance between the left most point in the digit and the nearest point on that same segment.
                    if (this._isHeightFixed) {
                        this._width =
                            2 * this._segmentLength * Math.sin(Math.abs(this._angleRadian)) +
                                2 * this._spacing * Math.cos(this._angleDegree >= 0 ? this._segmentEndAngle : Math.PI / 2 - this._segmentEndAngle) +
                                this._segmentLength +
                                2 * this._segmentHorizontalShiftDistance; //This is the horizontal distance between the outer most (furthest to the left or furthest to the right) point on the 2 outer most segments and the 1st or 4th (whichever is closest) point on that same segment.
                    }
                    else {
                        this._height =
                            _segmentWidth +
                                2 * this._segmentLength * Math.cos(this._angleRadian) +
                                2 * this._spacing * (Math.sin(this._segmentEndAngle) + Math.cos(this._segmentEndAngle)); //This is the sum of the vertical distance between the following (1st point of segment A and 1st point of segment F) and (4th point of segment F and 1st point of segment E) and (4th point of segment E and 1st point of segment D).
                    }
                    var h = this._halfSegmentWidth;
                    var l = this._segmentLength;
                    var t = Math.tan(this._segmentEndAngle); //tangent of the segmentEndAngle, used for several point locations
                    this._horizontalSegmentGeometry[1].x = h / t;
                    this._horizontalSegmentGeometry[1].y = this._horizontalSegmentGeometry[2].y = -h;
                    this._horizontalSegmentGeometry[2].x = l - h * t;
                    this._horizontalSegmentGeometry[3].x = l;
                    this._horizontalSegmentGeometry[4].x = l - h / t;
                    this._horizontalSegmentGeometry[4].y = this._horizontalSegmentGeometry[5].y = h;
                    this._horizontalSegmentGeometry[5].x = h * t;
                    if (this._horizontalSegmentGeometry[1].x > this._horizontalSegmentGeometry[2].x) {
                        throw new RangeError("This digit configuration produces invalid geometry.  angle: " + this._angleDegree + ",   ratioLtoW: " + this._ratioLtoW + ",   ratioLtoS: " + this._ratioLtoS);
                    }
                    //set vertical segment to horizontal segment mirrored over x axis
                    for (var i = 0, hPoint = void 0, vPoint = void 0; (hPoint = this._horizontalSegmentGeometry[i]) && (vPoint = this._verticalSegmentGeometry[i]); ++i) {
                        vPoint.x = hPoint.x;
                        vPoint.y = -hPoint.y;
                    }
                    //rotate vertical segment, https://en.wikipedia.org/wiki/Cartesian_coordinate_system#Rotation
                    var angle = this._angleRadian + Math.PI / 2; //rotate by 90deg + _angleDeg for proper position
                    for (var _i = 0, _a = this._verticalSegmentGeometry; _i < _a.length; _i++) {
                        var p = _a[_i];
                        var tempX = p.x;
                        var tempY = p.y;
                        p.x = tempX * Math.cos(angle) - tempY * Math.sin(angle);
                        p.y = tempX * Math.sin(angle) + tempY * Math.cos(angle);
                    }
                };
                /**
                 * This method first calls _calculateSegmentGeometry to populate the horizontal and vertical geometry arrays.
                 * Then, the x and y translations for each segment are calculated.
                 * Finally, the translations are applied to each segment.
                 *
                 * @private
                 */
                Seven.prototype._positionSegments = function () {
                    this._checkConfig();
                    this._calculateSegmentGeometry();
                    var l = this._segmentLength;
                    var aC = this._spacing * Math.cos(this._segmentEndAngle); //Used for horizontal spacing calculations
                    var aS = this._spacing * Math.sin(this._segmentEndAngle); //Used for horizontal spacing calculations
                    var x = this._translations;
                    //calculate the x translations
                    x[6].x = this._segmentHorizontalShiftDistance + l * Math.sin(Math.abs(this._angleRadian)) + (this._angleDegree >= 0 ? aC : aS);
                    x[5].x = x[6].x - aS + l * Math.sin(this._angleRadian);
                    x[0].x = x[5].x + aC;
                    x[1].x = x[6].x + l + aC + l * Math.sin(this._angleRadian);
                    x[2].x = x[6].x + l + aS;
                    x[3].x = x[6].x + (aS - aC) - l * Math.sin(this._angleRadian);
                    x[4].x = x[4].x = x[6].x - aC;
                    //calculate the y translations
                    x[0].y = this._halfSegmentWidth;
                    x[1].y = x[0].y + aC;
                    x[5].y = x[0].y + aS;
                    x[6].y = x[5].y + l * Math.cos(this._angleRadian) + aC;
                    x[2].y = x[6].y + aC;
                    x[4].y = x[6].y + aS;
                    x[3].y = x[4].y + l * Math.cos(this._angleRadian) + aC;
                    //update all segment positions
                    for (var i = 0, s = void 0, t = void 0; (s = this.segments[i]) && (t = x[i]); ++i) {
                        for (var j = 0, p = void 0, g = void 0; (p = s.points[j]) && (g = t.a[j]); ++j) {
                            p.x = g.x + t.x;
                            p.y = g.y + t.y;
                        }
                    }
                };
                Seven.prototype._set = function (prop, value) {
                    var oldValue = this[prop];
                    try {
                        this[prop] = value;
                        this._positionSegments();
                    }
                    catch (e) {
                        this[prop] = oldValue;
                        this._positionSegments();
                        throw e;
                    }
                };
                Object.defineProperty(Seven.prototype, "angle", {
                    get: function () {
                        return this._angleDegree;
                    },
                    set: function (value) {
                        this._set('_angleDegree', value);
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "ratioLtoW", {
                    get: function () {
                        return this._ratioLtoW;
                    },
                    set: function (value) {
                        this._set('_ratioLtoW', value);
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "ratioLtoS", {
                    get: function () {
                        return this._ratioLtoS;
                    },
                    set: function (value) {
                        this._set('_ratioLtoS', value);
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "digit", {
                    get: function () {
                        return this._digit;
                    },
                    set: function (value) {
                        //Check digit
                        var newValue = value * 1; //convert to a number
                        if (newValue != value || (typeof value === 'string' && value.toString().trim() === '')) {
                            throw new TypeError("Invalid value (" + value + ") for digit, not a Digit.");
                        }
                        else if (Digit[newValue] === undefined) {
                            throw new RangeError("Invalid value (" + newValue + ") for digit, must be a Digit.");
                        }
                        this._digit = newValue;
                        //Set segment's on/off as needed
                        for (var i = 0, s = void 0; s = this.segments[i]; ++i) {
                            s.on = Seven.matrix[this._digit][i];
                        }
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "height", {
                    get: function () {
                        return this._height;
                    },
                    set: function (value) {
                        var orig = this._isHeightFixed;
                        this._isHeightFixed = true;
                        try {
                            this._set('_height', value);
                        }
                        catch (e) {
                            this._isHeightFixed = orig;
                            throw e;
                        }
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "width", {
                    get: function () {
                        return this._width;
                    },
                    set: function (value) {
                        var orig = this._isHeightFixed;
                        this._isHeightFixed = false;
                        try {
                            this._set('_width', value);
                        }
                        catch (e) {
                            this._isHeightFixed = orig;
                            throw e;
                        }
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "isHeightFixed", {
                    get: function () {
                        return this._isHeightFixed;
                    },
                    set: function (value) {
                        this._isHeightFixed = !!value;
                    },
                    enumerable: true,
                    configurable: true
                });
                Object.defineProperty(Seven.prototype, "isWidthFixed", {
                    get: function () {
                        return !this.isHeightFixed;
                    },
                    set: function (value) {
                        this._isHeightFixed = !value;
                    },
                    enumerable: true,
                    configurable: true
                });
                /** Lookup between which segments are used for each digit.  */
                Seven.matrix = [
                    //A     B      C      D      E      F      G
                    [true, true, true, true, true, true, false],
                    [false, true, true, false, false, false, false],
                    [true, true, false, true, true, false, true],
                    [true, true, true, true, false, false, true],
                    [false, true, true, false, false, true, true],
                    [true, false, true, true, false, true, true],
                    [true, false, true, true, true, true, true],
                    [true, true, true, false, false, false, false],
                    [true, true, true, true, true, true, true],
                    [true, true, true, true, false, true, true],
                    [false, false, false, false, false, false, false],
                    [false, true, true, true, true, false, true],
                ];
                return Seven;
            }());
            exports_1("Seven", Seven);
        }
    }
});


/**
 * @author  Jeff Harris
 * @ignore
 */

var debug = require('debug')('friends-of-friends'),
    relationships = require('./relationships'),
    utils = require('techjeffharris-utils');

module.exports = FriendsOfFriends

/**
 * Create a new FriendsOfFriends Object with or without new
 * ```javascript
 * // mongoose is required
 * var mongoose = require('mongoose');
 *
 * var FriendsOfFriends = require('friends-of-friends');
 * var fof = new FriendsOfFriends(mongoose);
 * // works with or without 'new'
 * var FriendsOfFriends = require('friends-of-friends')(mongoose);
 * ```
 *
 * Specifying Configuration Options
 * ```javascript
 * var options = {
 *     personModelName:             'Player',
 *     friendshipModelName:         'Friend_Relationships',
 *     friendshipCollectionName:    'foo_bar_userRelationships',
 * };
 *
 * var FriendsOfFriends = require('friends-of-friends');
 * var fof = new FriendsOfFriends(mongoose, options);
 * // again, works with or without 'new'
 * var FriendsOfFriends = require('friends-of-friends')(mongoose, options);
 * ```
 * @class FriendsOfFriends
 * @param {Object} mongoose - required: mongoose instance used in your application
 * @param {Object} options  - optional object containing configurable options
 */
function FriendsOfFriends(mongoose, options) {

    debug('mongoose', mongoose);
    debug('options', options);

    if (!(this instanceof FriendsOfFriends)) {
      return new FriendsOfFriends(mongoose, options);
    }

    var friendship = require('./friendship'),
        plugin = require('./plugin');

    var defaults = {
        personModelName:            'Person',
        friendshipModelName:        'Friendship',
        friendshipCollectionName:   undefined,
    };

    /**
     * The options defined for the module instance
     * @member      {Object} options
     * @memberOf    FriendsOfFriends
     * @property    {String} personModelName            - The modelName of the Person Schema. Default: `'Person'`
     * @property    {String} friendshipModelName        - The name to call the model to be compiled from the Friendship Schema. Default: `'Friendship'`
     * @property    {String|undefined} friendshipCollectionName   - The name to use for the Friendship Collection. Default: `undefined`.
     */
    this.options = utils.extend(defaults, options);

    /**
     * The Friendship model
     * @member      {Model}     friendship
     * @memberOf    FriendsOfFriends
     * @see         {@link FriendshipModel}
     * @see         [moongoose models]{@link http://mongoosejs.com/docs/models.html}
     */
    this.Friendship = friendship(mongoose, this.options);

    /**
     * Adds friends-of-friends functionality to an existing Schema
     * @function    FriendsOfFriends.plugin
     * @param       {Schema} schema     - The mongoose Schema that gets plugged
     * @param       {Object} options    - Options passed to the plugin
     * @see         {@link AccountModel}
     */
    this.plugin = plugin(mongoose);

};

/**
 * Relationship constants
 * @constant
 * @member      {Object} relationships
 * @memberOf    FriendsOfFriends
 * @property    {String}    '0'                 - Value: `'NOT_FRIENDS'`
 * @property    {String}    '1'                 - Value: `'FRIENDS_OF_FRIENDS'`
 * @property    {String}    '2'                 - Value: `'PENDING_FRIENDS'`
 * @property    {String}    '3'                 - Value: `'FRIENDS'`
 * @property    {Number}    NOT_FRIENDS         - Value: `0`
 * @property    {Number}    FRIENDS_OF_FRIENDS  - Value: `1`
 * @property    {Number}    PENDING_FRIENDS     - Value: `2`
 * @property    {Number}    FRIENDS             - Value: `3`
 */
FriendsOfFriends.prototype.relationships = relationships;

/**
 * Return the value of a property of `this.options`
 * @param   {String} property - The property to get
 * @return  {Mixed}
 * @see     {@link FriendsOfFriends.options}
 */
FriendsOfFriends.prototype.get = function (property) {
    return this.options[property];
};

/**
 * Set the value of a property of `this.options`
 * @param   {String}    property    - The name of the Property
 * @param   {Mixed}     value       - The new value of the property
 * @return  {Mixed}
 * @see     {@link FriendsOfFriends.options}
 */
FriendsOfFriends.prototype.set = function (property, value) {
    return this.options[property] = value;
};


var fs = require('fs');

module.exports = {
  totalRx: totalRx,
  totalTx: totalTx,
  usageRx: usageRx,
  usageTx: usageTx,
  raw: raw,
};

/* PUBLIC */

function totalRx(opts) {
  if (opts) {
    opts.iface = opts.iface || 'lo';
    opts.units = opts.units || 'bytes';
  } else {
    opts = {
      iface: 'lo',
      units: 'bytes',
    };
  }

  var total = parseInt(_parseProcNetDev()[opts.iface].bytes.receive);
  var converted = _bytesTo(total, opts.units);

  return converted;
}

function totalTx(opts) {
  if (opts) {
    opts.iface = opts.iface || 'lo';
    opts.units = opts.units || 'bytes';
  } else {
    opts = {
      iface: 'lo',
      units: 'bytes',
    };
  }

  var total = parseInt(_parseProcNetDev()[opts.iface].bytes.transmit);
  var converted = _bytesTo(total, opts.units);

  return converted;
}

function usageRx(opts, cb) {
  if (typeof opts === 'function') {
    cb = opts;
    opts = {
      iface: 'lo',
      units: 'bytes',
      sampleMs: 1000,
    };
  } else {
    opts.iface = opts.iface || 'lo';
    opts.units = opts.units || 'bytes';
    opts.sampleMs = opts.sampleMs || 1000;
  }

  var time;

  //take first measurement
  var total0 = _parseProcNetDev()[opts.iface].bytes.receive;
  time = process.hrtime();

  setTimeout(function() {
    //take second measurement
    var total1 = _parseProcNetDev()[opts.iface].bytes.receive;
    var diff = process.hrtime(time);
    var diffSeconds = diff[0] + diff[1] * 1e-9;

    //do the calculations
    var total = parseInt(total1) - parseInt(total0);
    var totalPerSecond = total / (diffSeconds * diffSeconds);
    var converted = _bytesTo(totalPerSecond, opts.units);

    return cb(converted);
  }, opts.sampleMs);
}

function usageTx(opts, cb) {
  if (typeof opts === 'function') {
    cb = opts;
    opts = {
      iface: 'lo',
      units: 'bytes',
      sampleMs: 1000,
    };
  } else {
    opts.iface = opts.iface || 'lo';
    opts.units = opts.units || 'bytes';
    opts.sampleMs = opts.sampleMs || 1000;
  }

  var time;

  //take first measurement
  var total0 = _parseProcNetDev()[opts.iface].bytes.transmit;
  time = process.hrtime();

  setTimeout(function() {
    //take second measurement
    var total1 = _parseProcNetDev()[opts.iface].bytes.transmit;
    var diff = process.hrtime(time);
    var diffSeconds = diff[0] + diff[1] * 1e-9;

    //do the calculations
    var total = parseInt(total1) - parseInt(total0);
    var totalPerSecond = total / (diffSeconds * diffSeconds);
    var converted = _bytesTo(totalPerSecond, opts.units);

    return cb(converted);
  }, opts.sampleMs);
}

//NOTE: raw `/proc/net/dev` as object
//NOTE: can use this function to determine what interfaces are available by listing
//      the return objects keys
function raw() {
  return _parseProcNetDev();
}

/* PRIVATE */

//NOTE: borrowed/modifed from `https://github.com/soldair/node-procfs-stats/blob/feca2a940805b31f9e7d5c0bd07c4e3f8d3d5303/index.js#L343`
//TODO: more cleaning, rename the one char variables to something more expressive
function _parseProcNetDev() {
  var buf = fs.readFileSync('/proc/net/dev');
  var lines = buf.toString().trim().split('\n');
  var sections = lines.shift().split('|');
  var columns = lines.shift().trim().split('|');

  var s;
  var l;
  var c;
  var p = 0;
  var map = {};
  var keys = [];
  for (var i = 0; i < sections.length; ++i) {
    s = sections[i].trim();
    l = sections[i].length;
    c = columns[i].trim().split(/\s+/g);
    while (c.length) {
      map[keys.length] = s;
      keys.push(c.shift());
    }
    p += s.length + 1;
  }

  var retObj = {};

  lines.forEach(function(l) {
    l = l.trim().split(/\s+/g);
    var o = {};
    var iface;
    for (var i = 0; i < l.length; ++i) {
      var s = map[i];

      //case for the Interface
      if (s.indexOf('-') === s.length - 1) {
        iface = l[i].substr(0, l[i].length - 1);

      //case for everything else
      } else {
        if (!o[keys[i]]) {
          o[keys[i].toLowerCase()] = {};
        }
        o[keys[i].toLowerCase()][s.toLowerCase()] = l[i];
      }
    }
    retObj[iface] = o;
  });

  return retObj;
}

function _bytesTo(bytes, units) {
  var KiB = 1024;
  var MiB = 1024 * KiB;
  var GiB = 1024 * MiB;

  switch (units) {
    case 'bytes':
      break;
    case 'KiB':
      bytes /= KiB;
      break;
    case 'MiB':
      bytes /= MiB;
      break;
    case 'GiB':
      bytes /= GiB;
      break;
    default:
      var errMsg =
        '[net-stats] Error: Unknown units "' + units + '", use one of: ' +
        '"bytes" (default), "KiB", "MiB" or "GiB"';
      console.log(errMsg);
  }

  //NOTE: the variable named `bytes` may not actually contain a number
  //representing the number of bytes. its done this way to only have to use one
  //variable.
  return bytes;
}


(function(global) { 'use strict'; const factory = function es6lib_dom(exports) { // license: MIT

/**
 * The functions in this module use the global windows context (document, URL, self, top, etc.) by default.
 * To have any of the functions of this module operate on a different scope, call them with that scope as this,
 * e.g. `cerateElement.call(iframe.contentWindow, 'a', { ...});` to create an Element in an iframe.
 */
exports.window = global.window;
/* eslint-disable no-invalid-this */

/**
 * Creates a DOM Element and sets properties/attributes and children.
 * @param  {string}          tagName     Type of the new Element to create.
 * @param  {object}          properties  Optional. Object (not Array) of properties, which are deeply copied onto the new element.
 * @param  {Array<Element>}  childList   Optional. Array of elements or strings (as Text nodes) to set as the children of the new element. Nested Array are recursively flattened.
 * @return {Element}                     The new DOM element.
 */
exports.createElement = createElement; function createElement(tagName, properties, childList) {
	const document = (this || global).window.document;
	const element = document.createElement(tagName);
	if (Array.isArray(properties)) { childList = properties; properties = null; }
	properties && (function assign(target, source) { Object.keys(source).forEach(key => {
		const value = source[key], now = target[key];
		if (typeof value === 'object' && (typeof now === 'object' || typeof now === 'function')) {
			assign(now, value);
		} else {
			target[key] = value;
		}
	}); })(element, properties);
	childList && (function append(child) {
		if (Array.isArray(child)) { child.forEach(append); return; }
		child && element.appendChild(typeof child === 'string' ? document.createTextNode(child) : child);
	})(childList);
	return element;
}

/**
 * Creates a new style Element of the given css string.
 */
exports.createStyleElement = createStyleElement; function createStyleElement(css) {
	const element = (this || global).window.document.createElement('style');
	element.type = 'text/css';
	element.textContent = css;
	return element;
}

/**
 * Adds a css string to the document.
 * @param {string}  css  Style to add to the end of the document head.
 * @return {Element}     The new style Element.
 */
exports.addStyle = addStyle; function addStyle(css) {
	return (this || global).window.document.querySelector('head').appendChild(createStyleElement(css));
}

/**
 * Triggers a 'click' event on a DOM Element, often causing it's default click action.
 * @return {Element}         The clicked Element
 */
exports.clickElement = clickElement; function clickElement(element) {
	const evt = (this || global).window.document.createEvent('MouseEvents');
	evt.initEvent('click', true, true);
	element.dispatchEvent(evt);
	return element;
}

/**
 * Invokes a save dialog for a Blob or an Url object or strings target.
 * @param  {Blob|Url|string}  content  The Blob or url to save.
 * @param  {string}           name     The suggested file name.
 */
exports.saveAs = saveAs; function saveAs(content, name) {
	const window = (this || global).window;
	const isBlob = typeof content.type === 'string';

	const link = Object.assign(window.document.createElement('a'), {
		download: name,
		target: '_blank', // fallback
		href: isBlob ? window.URL.createObjectURL(content) : content,
	});
	clickElement.call(window, link);
	isBlob && global.setTimeout(() => window.URL.revokeObjectURL(link.href), 1000);
}

/**
 * Attempts to open a file picker dialog. Usually requires a direct user action to succeed.
 * @param  {object?}          options  An optional object whose properties are copied to the underlying input element.
 *                                     Useful properties are 'accept' and 'multiple'.
 * @return {Promise<File>}             A Promise to a (possibly empty) Array of Files.
 *                                     Should reject if the dialog fails to open.
 */
exports.loadFile = loadFile; function loadFile(options) { return new Promise((resolve, reject) => {
	const window = (this || global).window, document = window.document; let done = false, open = false;

	const input = Object.assign(document.createElement('input'), { type: 'file', }, options);
	function onBlur() { open = true; } // file picker has opened
	const timer = global.setTimeout(() => {
		if (open || remove()) { return; }
		// file picker didn't open or change
		reject(new Error('Could not select file'));
	}, 500);
	function onFocus() { global.setTimeout(() => {
		if (remove()) { return; }
		resolve([ ]); // file picker was opened and closed, but no files were selected
	}, 500); } // after dialog close, wait for 'change'
	function onChange(event) { try {
		if (remove()) { return; }
		resolve(Array.from(input.files)); // actually got file(s)
	} catch (error) { reject(error); } }
	function remove() {
		if (done) { return true; } done = true;
		global.clearTimeout(timer);
		input.removeEventListener('change', onChange);
		window.removeEventListener('focus', onFocus);
		window.removeEventListener('blur', onBlur);
		input.remove();
		return false;
	}
	input.addEventListener('change', onChange);
	window.addEventListener('focus', onFocus);
	window.addEventListener('blur', onBlur);
	document.head.appendChild(input);
	clickElement.call(window, input);
}); }

/**
 * Loads the data referenced by a Blob (or File) object.
 * @param  {Blob}    blob  The Blob to read.
 * @param  {string}  type  Optional. The type/encoding of the returned data. May be 'string' (default, utf8),
 *                         'arrayBuffer', 'dataURL' or any string encoding accepted by <FileReader>.readAsText(..., type).
 * @return {Promise<String|ArrayBuffer}  Promise to the data read.
 */
exports.readBlob = readBlob; function readBlob(blob, type) {
	return new Promise(function(resolve, reject) {
		const reader = new (this || global).window.FileReader;
		reader.onerror = reject;
		reader.onloadend = () => resolve(reader.result);

		if (type == null || type === 'string') {
			reader.readAsText(blob);
		} else if ((/^dataU[rR][iIlL]$/).test(type)) {
			reader.readAsDataURL(blob);
		} else if ((/^(?:arrayB|b)uffer$/).test(type)) {
			reader.readAsArrayBuffer(blob);
		} else {
			reader.readAsText(blob, type);
		}
	});
}

/**
 * Attempts to write data to the users clipboard.
 * @param  {string|object}  data  Ether a plain string or an object of multiple pairs { [mimeType]: data, } to write.
 * @param  {natural}        time  Optional. Maximum runtime of this asynchronous operation after which it will be canceled and rejected.
 * @return {Promise}              Promise that rejects if the timeout or an error occurred. If it resolves the operation should have succeeded.
 */
exports.writeToClipboard = writeToClipboard; function writeToClipboard(data, time) { return new Promise(function(resolve, reject) {
	const document = (this || global).window.document; let done = false;
	function onCopy(event) { try {
		if (done) { return; } done = true;
		document.removeEventListener('copy', onCopy);
		const transfer = event.clipboardData;
		transfer.clearData();
		if (typeof data === 'string') {
			transfer.setData('text/plain', data);
		} else {
			Object.keys(data).forEach(mimeType => transfer.setData(mimeType, data[mimeType]));
		}
		event.preventDefault();
		resolve();
	} catch (error) { reject(error); } }
	global.setTimeout(() => {
		if (done) { return; } done = true;
		document.removeEventListener('copy', onCopy);
		reject(new Error('Timeout after '+ (time || 1000) +'ms'));
	}, time || 1000);
	document.addEventListener('copy', onCopy);
	document.execCommand('copy', false, null);
}); }

/**
 * Attempts to read data from the users clipboard.
 * @param  {string|[string]|null}   types  An optional (Array of) mime types to read.
 * @param  {natural}                time   Optional. Maximum runtime of this asynchronous operation after which it will be canceled and rejected.
 * @return {Promise<string|object>}        Promise to the current clipboard data as string. If types was an array, it returns an object of { [mimeType]: data, } pairs.
 */
exports.readFromClipboard = readFromClipboard; function readFromClipboard(types, time) { return new Promise(function(resolve, reject) {
	const document = (this || global).window.document; let done = false;
	function onPaste(event) { try {
		if (done) { return; } done = true;
		document.removeEventListener('paste', onPaste);
		const transfer = event.clipboardData;
		if (typeof types === 'string' || !types) {
			resolve(transfer.getData(types || 'text/plain'));
		} else {
			resolve(types.reduce((data, mimeType) => ((data[mimeType] = transfer.getData(mimeType)), data), { }));
		}
		event.preventDefault();
	} catch (error) { reject(error); } }
	global.setTimeout(() => {
		if (done) { return; } done = true;
		document.removeEventListener('paste', onPaste);
		reject(new Error('Timeout after '+ (time || 1000) +'ms'));
	}, time || 1000);
	document.addEventListener('paste', onPaste);
	document.execCommand('paste', false, null);
}); }

/**
 * Listen for a DOM Event on an Element only once and removes the listener afterwards.
 */
exports.once = once; function once(element, event, callback, capture) {
	function handler() {
		element.removeEventListener(event, handler, capture);
		callback.apply(this, arguments);
	}
	element.addEventListener(event, handler, capture);
	return handler;
}

exports.whileVisible = whileVisible; function whileVisible(callback, time) {
	let handle; const document = (this || global).window.document;
	function check() {
		if (document.hidden) {
			handle && global.clearInterval(handle); return;
		} else {
			!handle && (handle = global.setInterval(callback, time));
		}
	}
	check();
	document.addEventListener('visibilitychange', check);
	return function cancel() {
		document.addEventListener('visibilitychange', check);
		handle && global.clearTimeout(handle);
	};
}

/**
 * Get the closest parent element (or the element itself) that matches a selector.
 * @param  {Element}  element   The child element whose parent is searched for
 * @param  {string}   selector  The selector the parent has to match
 * @return {Element||null}      'element', if it matches 'selector' or the first parent of 'element' that matches 'selector', if any
 */
exports.getParent = getParent; function getParent(element, selector) {
	while (element && (!element.matches || !element.matches(selector))) { element = element.parentNode; }
	return element;
}

/**
 * Builds the strongest possible selector of tagNames, ids and classes for an Element (at its current position in the document).
 * @param  {Element}  element  The Element in question.
 * @return {string}            String that matches /^(?!>)((?:^|>){{tagName}}(#{{id}})?(.{{class}})*)*$/
 */
exports.getSelector = getSelector; function getSelector(element) {
	const document = element.ownerDocument, strings = [ ];
	while (element && element !== document) {
		strings.add(
			element.tagName
			+ (element.id ? '#'+ element.id : '')
			+ (element.className ? '.'+ element.className.replace(/ +/g, '.') : '')
		);
		element = element.parentNode;
	}
	return strings.join('>');
}

exports.notify = notify; function notify(options) {
	const window = (this || global).window, Notification = window.Notification;
	return new Promise((resolve, reject) => {
		function doIt() {
			const self = new Notification(options.title, options);
			self.onclick = resolve;
			self.onerror = reject;
			self.onclose = reject;
			self.onshow = global.clearTimeout.bind(null, global.setTimeout(reject, options.timeout || 1500));
		}

		if (Notification.permission === 'granted') {
			doIt();
		} else if (Notification.permission !== 'denied') {
			Notification.requestPermission(permission => {
				if (permission === 'granted') {
					doIt();
				} else {
					reject(new Error('Permission denied'));
				}
			});
		} else  {
			reject(new Error('Permission denied'));
		}
	});
}

/**
 * Promise that resolves once the 'DOMContentLoaded' event is/was fired.
 */
exports.DOMContentLoaded = new Promise((resolve, reject) => {
	const document = global.document;
	if (typeof document !== 'object') { reject(new Error('No `document` global')); return; }
	if (document.readyState !== 'interactive' && document.readyState !== 'complete') {
		document.addEventListener('DOMContentLoaded', resolve);
	} else {
		resolve();
	}
});

}; if (typeof define === 'function' && define.amd) { define([ 'exports', ], factory); } else { const exp = { }, result = factory(exp) || exp; if (typeof exports === 'object' && typeof module === 'object') { module.exports = result; } else { global[factory.name] = result; } } })((function() { return this; })()); // eslint-disable-line


(function(global) { 'use strict'; const factory = function es6lib_observer(exports) { // This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.

const Self = new WeakMap();

exports.onElementChanged = onElementChanged; function onElementChanged(element, attributeFilter, callback) {
	const observer = new (element.ownerDocument || element).defaultView
	.MutationObserver(_=>_.forEach(mutation => {
		if (mutation.target.getAttribute(mutation.attributeName) !== mutation.oldValue) {
			try { callback(mutation.target, mutation.oldValue); } catch(error) { console.error(error); }
		}
	}));
	observer.observe(element, { subtree: false, attributes: true, attributeOldValue: true, attributeFilter: attributeFilter, });
	return observer;
}

/* eslint-disable */ // the code below works, but is old and probably inefficient

exports.InsertObserver = exports.CreationObserver = CreationObserver; function CreationObserver(element) {
	const listeners = [/*{ callback: function(){}, selector: string [, single: true] }*/];
	const observer = new MutationObserver(_=>_.forEach(_=>_.addedNodes.forEach(element => {
		elementCreated(listeners, element);
		element.querySelectorAll && element.querySelectorAll('*').forEach(element => {
			elementCreated(listeners, element);
		});
	})));
	observer.listeners = listeners;
	observer.element = element || global.document;
	Self.set(this, observer);
};
function elementCreated(listeners, element) {
	element.matches && listeners.forEach(function(listener, index) {
		if (element.matches(listener.selector)) {
			global.setTimeout(() => listener.callback(element), 0);
			if (listener.single) {
				delete listeners[index];
			}
		}
	});
}
CreationObserver.prototype.add = function(selector, callback, single) {
	const self = Self.get(this);
	if (self.listeners.find(function(item) { return item.selector == selector && item.callback == callback && !item.single === !single; })) { return; }
	self.listeners.push({ selector: selector, callback: callback, single: single });
	self.listeners.length == 1 && self.observe(self.element, { subtree: true, childList: true, });
};
CreationObserver.prototype.remove = function(selector, callback, single) {
	const self = Self.get(this);
	const length = self.listeners.length;
	self.listeners.filter(function(item) { return !item.selector == selector && item.callback == callback && !item.single == !single; });
	self.listeners.length === 0 && self.disconnect();
	return length - self.listeners.length;
};
CreationObserver.prototype.removeAll = function() {
	const self = Self.get(this);
	const length = self.listeners.length;
	self.listeners.length = 0;
	self.disconnect();
	return length;
};
CreationObserver.prototype.once = CreationObserver.prototype.single = function(selector, callback) {
	const element = Self.get(this).element.querySelector(selector);
	if (element) {
		setTimeout(callback.bind(undefined, element), 0);
	} else {
		this.add(selector, callback, true);
	}
};
CreationObserver.prototype.on = CreationObserver.prototype.all = function(selector, callback) {
	const alreadyExisting = Self.get(this).element.querySelectorAll(selector);
	this.add(selector, callback, false);

	for (const element of alreadyExisting) {
		global.setTimeout(() => callback(element), 0);
	}
};

/* eslint-enable */

class RemoveObserver {

	/**
	 * Remove listener of a parent node. Observe the removal of any of it's child nodes.
	 * @param {DomNode}  node  The parent node of the nodes to observe.
	 */
	constructor(node) {
		RemoveObserver_init(this, node);
	}

	/**
	 * Invokes a callback once a child node or any of it's parents is removed from their parent.
	 * Guaranteed to fire if a node that was part of the DOM gets removed from it in any way.
	 * Listens to a single removal of node.
	 * @param  {Node}      child     The element to observe.
	 * @param  {function}  callback  The function to execute on the nodes removal.
	 * @return {Node?}               The child's parentNode, iff `child` has one.
	 */
	static on(child, callback) {
		const parent = child.parentNode;
		if (!parent) { return null; }
		RemoveObserver_init(parent, parent); // ensure RemoveObserverPrivate(parent)
		return RemoveObserver.prototype.on.call(parent, child, callback);
	}

	/**
	 * Removes a listener added by `RemoveObserver.on()`.
	 * @param  {Node}      child     The reference element.
	 * @param  {function}  callback  The function that should not be executed on the nodes removal anymore.
	 * @return {bool}                True iff a listener was actually removed.
	 */
	static off(child, callback) {
		if (!child) { return false; }
		const parent = child.parentNode;
		if (!parent) { return false; }
		const self = Self.get(parent);
		if (!self) { return false; }
		return RemoveObserver.prototype.off.call(parent, child, callback);
	}

	// The DOM Node this `RemoveObserver` observes.
	get node() { return Self.get(this).node; }

	/**
	 * Same as the static `RemoveObserver.on`, only that `child` must be a direct child of `this.node`.
	 * Faster when adding listeners for a lot of children of the same element.
	 */
	on(child, callback) {
		const self = Self.get(this);
		if (child.parentNode !== self.node) { throw new Error('The observed node must be a direct child of the node passed into the constructor'); }
		let _child = self.children.get(child);
		if (!_child) {
			if (!self.children.size) { self.attach(); }
			_child = new Set;
			self.children.set(child, _child);
		}
		_child.add(callback);
		return this;
	}

	/**
	 * Same as the static `RemoveObserver.off`, only that it can only remove listeners of direct children of `this.node`.
	 */
	off(child, callback) {
		const self = Self.get(this);
		const _child = self.children.get(child);
		if (!_child || !_child.delete(callback)) { return false; }
		if (_child.size) { return true; }
		self.children.delete(child);
		if (!self.children.size) { self.detach(); }
		return true;
	}

} exports.RemoveObserver = Object.freeze(RemoveObserver);

// there are no properties on a `RemoveObserver`, every object that went as ref through this function can be used as `this` in any of the RemoveObserver methods
function RemoveObserver_init(ref, node) {
	let self = Self.get(node);
	if (!self) {
		self = new RemoveObserverPrivate(node);
		Self.set(node, self);
	}
	Self.set(ref, self);
}

/// Private back end class for RemoveObserver. Can not be accessed. There will never be more than one instance per DOM Node.
class RemoveObserverPrivate {
	constructor(node) {
		this.children = new Map;
		this.node = node;
		this.observer = null;
		this.check = this.check.bind(this);
		this.removed = this.removed.bind(this);
	}
	// called then a child was removed
	check(child) {
		const _child = this.children.get(child);
		if (!_child) { return; }
		_child.forEach(callback => { try {
			callback(child);
		} catch (error) { console.error(error); } });
		this.children.delete(child);
		if (!this.children.size) { this.detach(); }
	}
	// called when the node was removed from it's parent
	removed(_this_node) {
		this.children.forEach((_child, child) => {
			_child.forEach(callback => { try {
				callback(child);
			} catch (error) { console.error(error); } });
		});
		this.detach();
	}
	// listens for the removal of children and of the node from it's parent
	attach() {
		this.observer = new (this.node.ownerDocument || this.node).defaultView
		.MutationObserver(_=>_.forEach(_=>_.removedNodes.forEach(this.check)));
		this.observer.observe(this.node, { childList: true, });
		RemoveObserver.on(this.node, this.removed);
	}
	// stops listening of removals and releases all resources
	detach() {
		if (!this.observer) { return; }
		this.children = new Map;
		this.observer.disconnect();
		this.observer = null;
		RemoveObserver.off(this.node, this.removed);
	}
}

}; if (typeof define === 'function' && define.amd) { define([ 'exports', ], factory); } else { const exp = { }, result = factory(exp) || exp; if (typeof exports === 'object' && typeof module === 'object') { module.exports = result; } else { global[factory.name] = result; } } })((function() { return this; })()); // eslint-disable-line


/**
 *  This module exists for the following crappy reasons:
 *
 *  * jsdoc will only provide the version number in documentation output if
 *  you provide the `--package` flag
 *  * when you pass the `--package` flag, jsdoc puts the html inside a folder
 *  whose name is the version number inside a folder whose name is the package name
 *  * I want to have the version number in my docs while having them in the root of `doc/`
 */

var async = require('async'),
    child_process = require('child_process'),
    fs = require('fs'),
    jsdoc2md = require('jsdoc-to-markdown'),
    package = require('../package.json'),
    util = require('util');

    console.log('generating API documentation in doc/');

    async.series([
        // jsdoc
	function (done) {
            async.series([
                function (next) {
                    var child = child_process.exec('jsdoc -c jsdoc.conf.json lib/');

                    child.stdout.on('data', function (data) {
                        console.log(data);
                    });

                    child.stderr.on('data', function (data) {
                        console.log('stderr: ' + data);
                        var err = new Error('error spawning: ', cmd);
                        console.log(err);
                        next(err);
                    });

                    child.on('close', function (code) {
                        if (code === 0) {
                            next();
                        } else {
                            next(new Error('error spawning:', cmd));
                        }
                    });
                },
                function (next) {
                    var cmd = util.format('cp -r doc/%s/%s/* doc/',
                        package.name,
                        package.version
                    );

                    var child = child_process.exec(cmd);

                    child.stdout.on('data', function (data) {
                        console.log(data);
                    });

                    child.stderr.on('data', function (data) {
                        console.log('stderr: ' + data);
                        var err = new Error('error spawning: ', cmd);
                        console.log(err);
                        next(err);
                    });

                    child.on('close', function (code) {
                        if (code === 0) {
                            next();
                        } else {
                            next(new Error('error spawning jsdoc'));
                        }
                    });

                },

                function (next) {
                    var cmd = 'rm -rf doc/'+package.name+'/';

                    var child = child_process.exec(cmd);

                    child.stdout.on('data', function (data) {
                        console.log(data);
                    });

                    child.stderr.on('data', function (data) {
                        console.log('stderr: ' + data);
                        var err = new Error('error spawning: ', cmd);
                        console.log(err);
                        next(err);
                    });

                    child.on('close', function (code) {
                        console.log('child process exited with code ' + code);
                        if (code === 0) {
                            next();
                        } else {
                            next(new Error('error spawning jsdoc'));
                        }
                    });
                }
            ], function (err, results) {

                if (err) done(err);
                else done();
            });

        },

	// jsdoc2md
        function (done) {

            console.log('generating markdown docs...');

            var tasks = {},
                taskNames = ['index', 'friendship', 'plugin', 'relationships'];

            // go through each task name
            taskNames.forEach(function (name) {
                // add this task function to the object
                tasks[name] = function (done) {

                    var sourceFilename = 'lib/' + name + '.js';
                    var targetFilename = 'doc/'+ name +'.md';

                    console.log(sourceFilename + ' -> ' + targetFilename);

                    var reader = fs.createReadStream(sourceFilename);

                    // create a write stream to the new markdown file whose name is the task name
                    var writer = fs.createWriteStream(targetFilename, { flags: 'w+'})
                        .on('finish', done);

                    // now pipe each file to jsdoc2md then pipe its output to
                    // the write stream
                    reader.pipe(jsdoc2md()).pipe(writer);
                };
            });

            // now run our named tasks in parallel with async
            async.parallel(tasks, function (err, results) {
                if (err) done(err);
                else {
                    console.log('finished generating markdown docs.');
                    done();
                }
            });
        }
    ], function (err, results) {
        if (err) throw err;

        console.log('API documentation generated!');

    });


var referenceCount = 0;
var currentReference = 0;
var currentIndex = 0;
var finishedChain = false;
var continuation = false;
var globals = null;
var defaultGlobals = {
  return: null,
  true: null,
  false: null,
  undefined: null,
  null: null,
  this: null,
  window: null,
  Math: null,
  parseInt: null,
  parseFloat: null,
  isNaN: null,
  Array: null,
  typeof: null,
  encodeURI: null,
  encodeURIComponent: null,
  Boolean: null,
  String: null,
  Number: null,
  Infinity: null,
  NaN: null,
  _globals_: null,
  _formatters_: null,
  _value_: null,
  new: null
};

// matches property chains (e.g. `name`, `user.name`, and `user.fullName().capitalize()`)
var propertyRegex = /((\{|,|\.)?\s*)([a-z$_\$](?:[a-z_\$0-9\.-]|\[['"\d]+\])*)(\s*(:|\(|\[)?)/gi;
/**
 * Broken down
 *
 * ((\{|,|\.)?\s*)
 * prefix: matches on object literals so we can skip (in `{ foo: bar }` "foo" is not a property). Also picks up on
 * unfinished chains that had function calls or brackets we couldn't finish such as the dot in `.test` after the chain
 * `foo.bar().test`.
 *
 * ([a-z$_\$](?:[a-z_\$0-9\.-]|\[['"\d]+\])*)
 * property chain: matches property chains such as the following (strings' contents are removed at this step)
 *   `foo, foo.bar, foo.bar[0], foo.bar[0].test, foo.bar[''].test`
 *   Does not match through functions calls or through brackets which contain variables.
 *   `foo.bar().test, foo.bar[prop].test`
 *   In these cases it would only match `foo.bar`, `.test`, and `prop`
 *
 * (\s*(:|\(|\[)?)
 * postfix: matches trailing characters to determine if this is an object property or a function call etc. Will match
 * the colon after "foo" in `{ foo: 'bar' }`, the first parenthesis in `obj.foo(bar)`, the the first bracket in
 * `foo[bar]`.
 */

// links in a property chain
var chainLinksRegex = /\.|\[/g;

// the property name part of links
var chainLinkRegex = /\.|\[|\(/;

var substitutions = {
  ' && ': / and /g,
  ' || ': / or /g,
  ' === ': / is /g
};

exports.parseExpression = function(expr, _globals) {
  // Reset all values
  referenceCount = 0;
  currentReference = 0;
  currentIndex = 0;
  finishedChain = false;
  continuation = false;
  globals = _globals;

  expr = replaceSubstitutions(expr);
  if (expr.indexOf(' = ') !== -1) {
    var parts = expr.split(' = ');
    var setter = parts[0];
    var value = parts[1];
    setter = parsePropertyChains(setter).replace(/^\(|\)$/g, '');
    value = parsePropertyChains(value);
    expr = setter + ' = ' + value;
  } else {
    expr = parsePropertyChains(expr);
  }
  expr = addReferences(expr)

  // Reset after parse is done
  globals = null;

  return expr;
};

/**
 * Finds and parses the property chains in an expression.
 */
function parsePropertyChains(expr) {
  var parsedExpr = '', chain;

  // allow recursion (e.g. into function args) by resetting propertyRegex
  // This is more efficient than creating a new regex for each chain, I assume
  var prevCurrentIndex = currentIndex;
  var prevLastIndex = propertyRegex.lastIndex;

  currentIndex = 0;
  propertyRegex.lastIndex = 0;
  while ((chain = nextChain(expr)) !== false) {
    parsedExpr += chain;
  }

  // Reset indexes
  currentIndex = prevCurrentIndex;
  propertyRegex.lastIndex = prevLastIndex;
  return parsedExpr;
};

function nextChain(expr) {
  if (finishedChain) {
    return (finishedChain = false);
  }
  var match = propertyRegex.exec(expr);
  if (!match) {
    finishedChain = true // make sure next call we return false
    return expr.slice(currentIndex);
  }

  // `prefix` is `objIndicator` with the whitespace that may come after it.
  var prefix = match[1];

  // `objIndicator` is `{` or `,` and let's us know this is an object property
  // name (e.g. prop in `{prop:false}`).
  var objIndicator = match[2];

  // `propChain` is the chain of properties matched (e.g. `this.user.email`).
  var propChain = match[3];

  // `postfix` is the `colonOrParen` with whitespace before it.
  var postfix = match[4];

  // `colonOrParen` matches the colon (:) after the property (if it is an object)
  // or parenthesis if it is a function. We use `colonOrParen` and `objIndicator`
  // to know if it is an object.
  var colonOrParen = match[5];

  match = match[0];

  var skipped = expr.slice(currentIndex, propertyRegex.lastIndex - match.length);
  currentIndex = propertyRegex.lastIndex;

  // skips object keys e.g. test in `{test:true}`.
  if (objIndicator && colonOrParen === ':') {
    return skipped + match;
  }

  return skipped + parseChain(prefix, propChain, postfix, colonOrParen, expr);
}

function parseChain(prefix, propChain, postfix, paren, expr) {
  // continuations after a function (e.g. `getUser(12).firstName`).
  continuation = prefix === '.';
  if (continuation) {
    propChain = '.' + propChain;
    prefix = '';
  }

  var links = splitLinks(propChain);
  var newChain = '';

  if (links.length === 1 && !continuation && !parens[paren]) {
    link = links[0];
    newChain = addThisOrGlobal(link);
  } else {
    if (!continuation) {
      newChain = '(';
    }

    links.forEach(function(link, index) {
      if (index !== links.length - 1) {
        newChain += parsePart(link, index);
      } else {
        if (!parens[paren]) {
          newChain += '_ref' + currentReference + link;
        } else {
          if (continuation && index === 0) {
            index++;
          }
          postfix = postfix.replace(paren, '');
          newChain += paren === '(' ? parseFunction(link, index, expr) : parseBrackets(link, index, expr);
        }
      }
    });

    if (expr.charAt(propertyRegex.lastIndex) !== '.') {
      newChain += ')';
    }
  }

  return prefix + newChain + postfix;
}

function splitLinks(chain) {
  var index = 0;
  var parts = [];
  var match;
  while (match = chainLinksRegex.exec(chain)) {
    if (chainLinksRegex.lastIndex === 1) continue;
    parts.push(chain.slice(index, chainLinksRegex.lastIndex - 1));
    index = chainLinksRegex.lastIndex - 1;
  }
  parts.push(chain.slice(index));
  return parts;
}

function addThisOrGlobal(chain) {
  var prop = chain.split(chainLinkRegex).shift();
  if (globals.hasOwnProperty(prop)) {
    return globals[prop] === null ? chain : '_globals_.' + chain;
  } else if (defaultGlobals.hasOwnProperty(prop)) {
    return chain;
  } else {
    return 'this.' + chain;
  }
}

var parens = {
  '(': ')',
  '[': ']'
};

// Handles a function to be called in its correct scope
// Finds the end of the function and processes the arguments
function parseFunction(link, index, expr) {
  var call = getFunctionCall(expr);

  // Always call functions in the scope of the object they're a member of
  if (index === 0) {
    link = addThisOrGlobal(link);
  } else {
    link = '_ref' + currentReference + link;
  }

  var calledLink = link + '(~~insideParens~~)';

  link = 'typeof ' + link + ' !== \'function\' ? void 0 : ' + calledLink;
  var insideParens = call.slice(1, -1);

  if (expr.charAt(propertyRegex.lastIndex) === '.') {
    currentReference = ++referenceCount;
    var ref = '_ref' + currentReference;
    link = '(' + ref + ' = (' + link + ')) == null ? void 0 : ';
  }

  var ref = currentReference;
  link = link.replace('~~insideParens~~', parsePropertyChains(insideParens));
  currentReference = ref;
  return link;
}

// Handles a bracketed expression to be parsed
function parseBrackets(link, index, expr) {
  var call = getFunctionCall(expr);
  var insideBrackets = call.slice(1, -1);
  var evaledLink = parsePart(link, index);
  index += 1;
  link = '[~~insideBrackets~~]';

  if (expr.charAt(propertyRegex.lastIndex) === '.') {
    link = parsePart(link, index);
  } else {
    link = '_ref' + currentReference + link;
  }

  link = evaledLink + link;

  var ref = currentReference;
  link = link.replace('~~insideBrackets~~', parsePropertyChains(insideBrackets));
  currentReference = ref;
  return link;
}

// returns the call part of a function (e.g. `test(123)` would return `(123)`)
function getFunctionCall(expr) {
  var startIndex = propertyRegex.lastIndex;
  var open = expr.charAt(startIndex - 1);
  var close = parens[open];
  var endIndex = startIndex - 1;
  var parenCount = 1;
  while (endIndex++ < expr.length) {
    var ch = expr.charAt(endIndex);
    if (ch === open) parenCount++;
    else if (ch === close) parenCount--;
    if (parenCount === 0) break;
  }
  currentIndex = propertyRegex.lastIndex = endIndex + 1;
  return open + expr.slice(startIndex, endIndex) + close;
}

function parsePart(part, index) {
  // if the first
  if (index === 0 && !continuation) {
    part = addThisOrGlobal(part);
  } else {
    part = '_ref' + currentReference + part;
  }

  currentReference = ++referenceCount;
  var ref = '_ref' + currentReference;
  return '(' + ref + ' = ' + part + ') == null ? void 0 : ';
}

function replaceSubstitutions(expr) {
  Object.keys(substitutions).forEach(function(replacement) {
    expr = expr.replace(substitutions[replacement], replacement);
  });
  return expr;
}

// Prepends reference variable definitions
function addReferences(expr) {
  if (referenceCount) {
    var refs = [];
    for (var i = 1; i <= referenceCount; i++) {
      refs.push('_ref' + i);
    }
    expr = 'var ' + refs.join(', ') + ';\n' + expr;
  }
  return expr;
}


'use strict';

var gulp = require('gulp')
var path = require('path')
var uglify = require('gulp-uglify')
var concat = require('gulp-concat')
var rimraf = require('gulp-rimraf')
var gutil = require('gulp-util')
var gulpif = require('gulp-if')
var gulpHeader = require('gulp-header')
var merge2 = require('merge2')
var multipipe = require('multipipe')
var gulpFilter = require('gulp-filter')
var cssmin = require('gulp-cssmin')
var rename = require('gulp-rename')
var save = require('./tasks/save')
var webpack = require('webpack')
var hashName = require('gulp-hash-name')
var webpackStream = require('webpack-stream')
var ExtractTextPlugin = require("mini-css-extract-plugin")
var ComponentPlugin = require('./plugins/component')
var _ = require('underscore')
var shortid = require('shortid')
var HASH_LENGTH = 6

var DEFAULT_UGLFIFY = {
  compress: { screw_ie8: false },
  mangle: { screw_ie8: false },
  output: { screw_ie8: false }
}

function componentsBuild(options) {
  var entry = options.entry || './index.js'
  var componentsOptions = options.components || {}
  var componentsModules = componentsOptions.directories || ['c']
  var componentsExtensions = componentsOptions.extensions || ["js", "vue", "jsx", "ts"]
  var extensions = [".webpack.js", ".web.js", "vue", ".js", ".jsx", ".ts"]
  var usingHash = options.hash !== false
  var cssOutputName = (options.name || '[name]') + (usingHash ? '_[contenthash:' + HASH_LENGTH + '].css' : '.css')
  var cssOutputOpts = options.vfePlugins ? options.vfePlugins.extractText : {}

  var plugins = [
    // component rule
    new ComponentPlugin(process.cwd(), componentsModules.map(function(n) {
      if (path.isAbsolute(n)) return n
      else {
        return path.resolve(n)
      }
    }), componentsExtensions),
    // extract css bundle
    new ExtractTextPlugin(_.extend({
      filename: cssOutputName
    }, cssOutputOpts))
  ]

  if (options.rule === false) {
    // remove rule transform plugins
    plugins.shift()
  }
  plugins = (options.plugins || []).concat(plugins)

  var vfeLoaders = options.vfeLoaders || {}
  var enableLessLoader = !!vfeLoaders.less // default disable
  var enableCssLoader = vfeLoaders.css !== false // default enable
  var enableTplLoader = vfeLoaders.tpl !== false // default enable
  var enableImgLoader = vfeLoaders.image !== false // default enable
  var enableFontLoader = vfeLoaders.font !== false // default enable
  function patchOpts(opts) {
    return _.isObject(opts) ? opts : {}
  }
  var vfeLoadersCssOpts = patchOpts(vfeLoaders.css)
  var vfeLoadersLessOpts = patchOpts(vfeLoaders.less)

  var loaders = []
  if (enableTplLoader) {
    loaders.push(_.extend({
      test: /.*?\.tpl$/,
      use: 'html-loader'
    }, patchOpts(vfeLoaders.tpl)))
  }
  if (enableCssLoader) {
    let opts = vfeLoadersCssOpts.options
    delete vfeLoadersCssOpts.options
    loaders.push(_.extend({
      test: /\.css$/,
      use: [{
          loader: ExtractTextPlugin.loader,
          options: _.extend({}, opts)
        },
        "css-loader",
      ]
    }, vfeLoadersCssOpts))
  }
  if (enableLessLoader) {
    let opts = vfeLoadersLessOpts.options
    delete vfeLoadersLessOpts.options
    loaders.push(_.extend({
      test: /\.less$/,
      use: [{
          loader: ExtractTextPlugin.loader,
          options: _.extend({}, opts)
        },
        "css-loader",
        "less-loader",
      ]
    }, vfeLoadersLessOpts))
  }
  if (enableImgLoader) {
    loaders.push(_.extend({
      test: /\.(png|jpg|gif|jpeg|webp)$/,
      use: "file-loader?name=[path][name]" + (usingHash ? "_[hash:" + HASH_LENGTH + "]" : "") + ".[ext]"
    }, patchOpts(vfeLoaders.image)))
  }
  if (enableFontLoader) {
    loaders.push(_.extend({
      test: /\.(woff|woff2|eot|ttf|svg|otf)$/,
      use: 'url-loader?limit=100000'
    }, vfeLoaders.font))
  }
  var loaderDirectories = [
    'node_modules',
    path.join(__dirname, './loaders'),
    path.join(__dirname, './node_modules'),
    path.join(__dirname, '../') // parent node_modules
  ]
  var moduleOpt = options.module || {}
  var resolveOpt = options.resolve || {}
  var resolveLoaderOpt = options.resolveLoader || {}
  var resolveModules = [] // below will set default to "c" directory

  if (!resolveModules.length) {
    resolveModules.push('node_modules')
  }
  resolveModules = componentsModules.concat(resolveModules)

  delete options.libs
  delete options.rule
  delete options.name
  delete options.minify
  delete options.header
  delete options.concats
  delete options.hash
  delete options.components
  delete options.vfeLoaders
  delete options.vfePlugins

  return webpackStream(_.extend({
    mode: 'none'
  }, options, {
    entry: entry,
    module: _.extend({}, moduleOpt, {
      rules: loaders.concat(moduleOpt.rules || [])
    }),
    resolveLoader: _.extend({}, resolveLoaderOpt, {
      modules: loaderDirectories.concat(resolveLoaderOpt.loaderDirectories || [])
    }),
    plugins: plugins,
    resolve: _.extend({}, resolveOpt, {
      modules: resolveModules.concat(resolveOpt.modules || []),
      extensions: extensions.concat(resolveOpt.extensions || [])
    }),
  })).on('error', function() {
    this.emit('end')
  })
}

function addHeader(opt) {
  var hOpt = opt || {}
  return gulpif(function(file) {
    if (!opt) return false
    else if (!/\.(js|css)$/.test(file.path)) return false // css and js only
    else if (!opt.test) return true // match all
    else return opt.test.test(file.path)
  }, gulpHeader(hOpt.text || '', hOpt.data || {}))
}
var builder = function(options) {

  options = options || {}
  var jsOnlyFilter = gulpFilter(['*.js']) // remove all files except js
  var jsFilter = gulpFilter(['**/*', '!*.js']) // remove js from matched files

  // var entry = options.entry || './index.js'
  var libs = options.libs
  var isConcatLibs = libs && libs.length && options.name
  var streams = []
  var cssimgId = shortid.generate()
  var cssminId = shortid.generate()
  var jsId = shortid.generate()

  /**
   * concat component js bundle with lib js
   */
  isConcatLibs && streams.push(
    gulp.src(libs)
  )

  /**
   * using webpack build component modules
   */
  streams.push(
    componentsBuild(_.extend({}, options))
    .pipe(jsFilter)
    .pipe(save('components:css,images:' + cssimgId))
    .pipe(gulpif(options.minify !== false,
      multipipe(
        gulpFilter(['*.css']),
        cssmin(),
        rename({ suffix: '.min' }),
        save('components:css.min:' + cssminId)
      )
    ))
    .pipe(jsFilter.restore())
    .pipe(jsOnlyFilter) // here will output js/css/images, remove css and images
  )
  return merge2.apply(null, streams)
    .pipe(gulpif(isConcatLibs, concat(options.name + '.js', { newLine: ';' })))
    .pipe(gulpif(options.hash !== false, hashName({
      hashLength: HASH_LENGTH,
      template: '{name}_{hash}{ext}'
    })))
    .pipe(gulpif(options.minify !== false,
      multipipe(
        save('components:js:' + jsId),
        uglify(_.extend({}, DEFAULT_UGLFIFY, options.uglify)).on('error', gutil.log),
        rename({ suffix: '.min' }),
        save.restore('components:css.min:' + cssminId),
        save.restore('components:js:' + jsId)
      )
    ))
    .pipe(save.restore('components:css,images:' + cssimgId))
    .pipe(addHeader()) // add here for css files
    .on('error', function() {
      this.emit('end')
    })
}
/**
 * TODO: use webpack concat
 */
builder.bundle = function(src, options) {
  options = options || {}

  var bid = shortid.generate()
  var usingMinify = options.minify !== false
  var usingHash = options.hash !== false
  var bundleFileName = options.name + '.js'
  var concats = options.concats
  var hashOpt = {
    hashLength: HASH_LENGTH,
    template: '{name}_{hash}{ext}'
  }
  var headerOpt = options.header
  var hasConcats = _.isArray(concats) && !!concats.length
  var stream = gulp.src(src)

  if (usingMinify) {
    stream = stream
      .pipe(concat(bundleFileName))
      .pipe(gulpif(usingHash, hashName(hashOpt)))
      .pipe(gulpif(!hasConcats, save('bundle:js:' + bid)))
      .pipe(uglify(_.extend({}, DEFAULT_UGLFIFY, options.uglify)).on('error', gutil.log))

    /**
     * concats do not output source files
     */
    if (hasConcats) {
      stream = merge2(stream, gulp.src(concats))
        .pipe(concat(bundleFileName))
        .pipe(gulpif(usingHash, hashName(hashOpt)))
    }
    stream = stream
      .pipe(rename({ suffix: '.min' }))
      .pipe(gulpif(!hasConcats, save.restore('bundle:js:' + bid)))
  } else {
    if (hasConcats) {
      stream = merge2(stream, gulp.src(concats))
    }
    stream = stream.pipe(concat(bundleFileName))
      .pipe(gulpif(usingHash, hashName(hashOpt)))
  }

  return stream.pipe(addHeader(headerOpt))
}
builder.HASH_LENGTH = HASH_LENGTH
builder.clean = rimraf
builder.concat = concat
builder.uglify = uglify
builder.cssmin = cssmin
builder.rename = rename
builder.merge = merge2
builder.hash = builder.hashName = hashName
builder.if = gulpif
builder.filter = gulpFilter
builder.multipipe = multipipe
builder.header = gulpHeader
builder.util = {
  /**
   * Run once and lastest one
   */
  once: function(fn) {
    var pending
    var hasNext

    function next() {
      setTimeout(function() {
        if (pending === false) return
        pending = false

        if (hasNext) {
          hasNext = false
          fn(next)
        }
      }, 50) // call after gulp ending handler done
    }
    return function() {
      if (pending) return (hasNext = true)

      pending = true
      fn(next)
    }
  }
}
builder.webpack = webpack
builder.ExtractTextPlugin = ExtractTextPlugin

module.exports = builder


module.exports = assert

class AssertionError extends Error {}
AssertionError.prototype.name = 'AssertionError'

/**
 * Minimal assert function
 * @param  {any} t Value to check if falsy
 * @param  {string=} m Optional assertion error message
 * @throws {AssertionError}
 */
function assert (t, m) {
  if (!t) {
    var err = new AssertionError(m)
    if (Error.captureStackTrace) Error.captureStackTrace(err, assert)
    throw err
  }
}


var slice = Array.prototype.slice;
var strings = require('./strings');
var formatterParser = require('./formatters');
var propertyChains = require('./property-chains');
var valueProperty = '_value_';
var cache = {};

exports.globals = {};

exports.parse = function(expr, globals, formatters) {
  if (typeof expr !== 'string') {
    throw new TypeError('Invalid expr, must be type String');
  }
  var extraArgs = slice.call(arguments, 3);
  var cacheKey = expr + '|' + extraArgs.join(',');
  // Returns the cached function for this expression if it exists.
  var func = cache[cacheKey];
  if (func) {
    return func;
  }

  var original = expr;
  var isSetter = (extraArgs[0] === valueProperty);

  expr = strings.pullOutStrings(expr);
  expr = formatterParser.parseFormatters(expr);
  expr = propertyChains.parseExpression(expr, getVariables(globals, extraArgs));
  if (!isSetter) {
    var lines = expr.split('\n');
    lines[lines.length - 1] = 'return ' + lines[lines.length - 1];
    expr = lines.join('\n');
  }
  expr = strings.putInStrings(expr);
  func = compileExpression(original, expr, globals, formatters, extraArgs);
  func.expr = expr;
  cache[cacheKey] = func;
  return func;
};

exports.parseSetter = function(expr, globals, formatters, extraArgs) {
  var extraArgs = slice.call(arguments, 3);

  if (expr.charAt(0) === '!') {
    // Allow '!prop' to become 'prop = !value'
    expr = expr.slice(1).replace(/(\s*\||$)/, ' = !_value_$1');
  } else {
    expr = expr.replace(/(\s*\||$)/, ' = _value_$1');
  }

  // Add _value_ as the first extra argument
  return exports.parse.apply(exports, [expr, globals, formatters, valueProperty].concat(extraArgs));
};

function getVariables(globals, extraArgs) {
  var variables = {};

  Object.keys(exports.globals).forEach(function(key) {
    variables[key] = exports.globals[key];
  });

  if (globals) {
    Object.keys(globals).forEach(function(key) {
      variables[key] = globals[key];
    });
  }

  extraArgs.forEach(function(key) {
    variables[key] = null;
  });

  return variables;
}

function compileExpression(original, expr, globals, formatters, extraArgs) {
  var func, args = ['_globals_', '_formatters_'].concat(extraArgs).concat(expr);

  try {
    func = Function.apply(null, args);
  } catch (e) {
    // Throws an error if the expression was not valid JavaScript
    throw new Error('Bad expression: ' + original + '\n' + 'Compiled expression:\n' + expr + '\n' + e.message);
  }

  return bindArguments(func, globals, formatters);
}

// a custom "bind" function to bind arguments to a function without binding the context
function bindArguments(func) {
  function binder() {
    return func.apply(this, args.concat(slice.call(arguments)));
  }

  var args = slice.call(arguments, 1);
  return binder;
}


// @flow
import lonlat from '@conveyal/lonlat'
import {hsl} from 'd3-color'

import {
  MAX_DISSIMILARITY,
  MAX_TRANSITIVE_PATHS
} from './constants'
import {pixelToLat, pixelToLon} from './mercator'
import {getStopPatternStopSets} from './origin'

import type {
  Journey,
  Origin,
  PathDescriptor,
  Pattern,
  Point,
  Query,
  Segment,
  Stop,
  StopPatternStops,
  TransitiveData
} from './types'

/**
 * Create transitive data for journeys from a particular origin to a
 * particular destination
 */
export default function getTransitiveData ({
  from,
  log,
  origin,
  paths,
  query,
  to,
  transitiveNetwork
}: {
  from: Point,
  log: Function,
  origin: Origin,
  paths: Array<PathDescriptor>,
  query: Query,
  to: Point,
  transitiveNetwork: TransitiveData
}): TransitiveData {
  const places = []
  const journeys = []

  // from can be left null and it will be inferred from the destination
  let fromCoord = {}
  let fromName = null
  if (from != null) {
    const {x, y, ...rest} = from
    try {
      fromCoord = lonlat(rest)
    } catch (e) {
      log(e.message)
    }
    fromName = from.name
  }

  places.push({
    place_id: 'from',
    place_name: fromName, // todo do this with icons, avoid English works (or Portuguese words, for that matter)
    place_lat: fromCoord.lat || pixelToLat(query.north + origin.y, query.zoom),
    place_lon: fromCoord.lon || pixelToLon(query.west + origin.x, query.zoom)
  })

  // to cannot be undefined
  // Omit X and Y so as not to confuse lonlng
  const { x, y, ...rest } = to
  let toCoord = {}
  try {
    toCoord = lonlat(rest)
  } catch (e) {
    log(e.message)
  }

  places.push({
    place_id: 'to',
    place_name: to.name,
    place_lat: toCoord.lat || pixelToLat(query.north + to.y, query.zoom),
    place_lon: toCoord.lon || pixelToLon(query.west + to.x, query.zoom)
  })

  // function to convert pattern ID to route ID
  function getRouteIndexFromPatternId (pattern: string | number): number {
    const pat = transitiveNetwork.patterns.find(p => p.pattern_id === String(pattern))
    if (!pat) return -1
    return transitiveNetwork.routes.findIndex(r => r.route_id === pat.route_id)
  }

  // get the most common, unique paths
  const commonPaths = getCommonPaths({
    getRouteIndexFromPatternId,
    log,
    origin,
    paths: paths.filter((p) => !!p)
  })

  for (let pidx = 0; pidx < commonPaths.length; pidx++) {
    const journey = {
      journey_id: pidx,
      journey_name: pidx,
      segments: []
    }

    const path = commonPaths[pidx]

    // bail if the path-finding algorithm gave up, this is a bug but we'd rather
    // log a javascript error than crash the tab (??)
    if (path === null) continue

    if (path.length > 20) {
      log('excessive path length, more than 20 segments')
      continue
    }

    if (path.length === 0) {
      // walk to destination
      journey.segments.push({
        type: 'WALK',
        from: {
          type: 'PLACE',
          place_id: 'from'
        },
        to: {
          type: 'PLACE',
          place_id: 'to'
        }
      })
    } else {
      // add the first walk segment
      const originStop = path[0][0]
      journey.segments.push({
        type: 'WALK',
        from: {
          type: 'PLACE',
          place_id: 'from'
        },
        to: {
          type: 'STOP',
          stop_id: originStop + ''
        }
      })

      let previousStop = -1

      for (let segmentIdx = 0; segmentIdx < path.length; segmentIdx++) {
        const [boardStop, pattern, alightStop] = path[segmentIdx]

        // figure out from and to stop indices
        const fromStopId = boardStop + ''
        const toStopId = alightStop + ''

        let index = 0
        const patternData = transitiveNetwork.patterns[pattern]

        while (patternData.stops[index].stop_id !== fromStopId) index++
        const fromStopIdx = index
        while (patternData.stops[index].stop_id !== toStopId) index++
        const toStopIdx = index

        if (previousStop > 0 && previousStop !== boardStop) {
          // there is an on-street transfer
          journey.segments.push({
            type: 'WALK',
            from: {
              type: 'STOP',
              stop_id: previousStop + ''
            },
            to: {
              type: 'STOP',
              stop_id: boardStop + ''
            }
          })
        }

        // add the transit segment
        journey.segments.push({
          type: 'TRANSIT',
          pattern_id: pattern + '',
          from_stop_index: fromStopIdx,
          to_stop_index: toStopIdx
        })

        previousStop = alightStop
      }

      // add the final walk
      journey.segments.push({
        type: 'WALK',
        from: {
          type: 'STOP',
          stop_id: previousStop + ''
        },
        to: {
          type: 'PLACE',
          place_id: 'to'
        }
      })
    }

    journeys.push(journey)
  }

  const clusteredJourneys = clusterJourneys({
    ...transitiveNetwork,
    journeys,
    log
  })

  const color = hsl('#b00')
  clusteredJourneys.forEach(c => {
    color.h += 67
    const rgb = color + ''
    c.segments.forEach(s => {
      s.color = rgb
    })
  })

  return {
    ...transitiveNetwork,
    journeys: clusteredJourneys,
    places
  }
}

type PathCount = {
  count: number,
  path: PathDescriptor
}

function countCommonPaths (
  memo: Array<PathCount>,
  path: PathDescriptor,
  index: number
): Array<PathCount> {
  if (index === 0) return [{count: 1, path}]
  if (memo[0].path[0] !== path[0] || memo[0].path[1] !== path[1]) {
    return [{count: 1, path}, ...memo]
  }
  memo[0].count++
  return memo
}

const byCount = (p1, p2) => p2.count - p1.count
const byStopIdAndPathIndex = (p1, p2) => {
  const v0 = p1[0] - p2[0] // sort first by stop ID
  if (v0 !== 0) return v0
  else return p1[1] - p2[1] // then by path index
}
const uniquePathIds = (p, idx, arr) =>
  idx === 0 || p[0] !== arr[idx - 1][0] || p[1] !== arr[idx - 1][1]

/**
 * Get the most common paths
 */
function getCommonPaths ({
  getRouteIndexFromPatternId,
  log,
  origin,
  paths
}: {
  getRouteIndexFromPatternId: (string | number) => number,
  log: Function,
  origin: Origin,
  paths: Array<PathDescriptor>
}): Array<StopPatternStops> {
  paths.sort(byStopIdAndPathIndex)
  const pathCounts = paths.reduce(countCommonPaths, [])
  pathCounts.sort(byCount)

  log(`${pathCounts.length} unique paths`)
  if (pathCounts.length > MAX_TRANSITIVE_PATHS) {
    log(`eliminated ${pathCounts.length - MAX_TRANSITIVE_PATHS} paths with frequency <= ${pathCounts[MAX_TRANSITIVE_PATHS].count} / ${paths.length} `)
  }

  let allStopPatternStopSets = pathCounts
    .slice(0, MAX_TRANSITIVE_PATHS)
    .map((p) => p.path)
    .filter(uniquePathIds) // uniquify the paths
    .map((p) => getStopPatternStopSets({log, pathDescriptor: p, origin}))

  const inPathCount = allStopPatternStopSets.length

  // Sort paths by the sequence of routes they use, and choose an example,
  // eliminating the multiple-board/transfer-stops problem
  allStopPatternStopSets.sort((p1, p2) => {
    if (p1.length < p2.length) return -1
    else if (p1.length > p2.length) return 1
    else {
      for (let i = 0; i < p1.length; i++) {
        const r1 = getRouteIndexFromPatternId(p1[i][1])
        const r2 = getRouteIndexFromPatternId(p2[i][1])
        if (r1 < r2) return -1
        if (r1 > r2) return 1
      }

      // identical patterns
      return 0
    }
  })

  allStopPatternStopSets = allStopPatternStopSets.filter((p, i, a) => {
    if (i === 0) return true
    const prev = a[i - 1]
    if (p.length !== prev.length) return true

    for (let s = 0; s < p.length; s++) {
      const r1 = getRouteIndexFromPatternId(p[s][1])
      const r2 = getRouteIndexFromPatternId(prev[s][1])
      if (r1 !== r2) {
        return true
      }
    }

    return false
  })

  const pathCountAfterMultiTransfer = allStopPatternStopSets.length

  // eliminate longer paths if there is a shorter path that is a subset
  // (eliminate the short access/egress/transfer leg problem)
  allStopPatternStopSets = allStopPatternStopSets.filter((path, i, rest) => {
    for (const otherPath of rest) {
      // longer paths cannot be subsets. Also don't evaluate the same path.
      if (otherPath.length >= path.length) continue

      let otherPathIsSubset = true
      const routes = path.map(seg => getRouteIndexFromPatternId(seg[1]))

      for (const seg of otherPath) {
        if (routes.indexOf(getRouteIndexFromPatternId(seg[1])) === -1) {
          otherPathIsSubset = false
          break
        }
      }

      if (otherPathIsSubset) {
        return false
      }
    }

    return true
  })

  log(`filtering reduced ${inPathCount} paths to ${pathCountAfterMultiTransfer} after multiple-stop elimination, to ${allStopPatternStopSets.length} after stemming`)

  return allStopPatternStopSets
}

/**
 * Bundle similar journeys in transitive data together. Works by computing a
 * score for each segment based on where the endpoints are relative to each
 * other. It might also make sense to use a metric based on speed so that a very
 * slow bus isn't bundled with a fast train, but we don't currently do this.
 */
function clusterJourneys ({
  journeys,
  log,
  patterns,
  stops
}: {
  journeys: Array<Journey>,
  log: Function,
  patterns: Array<Pattern>,
  stops: Array<Stop>
}) {
  // perform hierarchical clustering on journeys
  // see e.g. James et al., _An Introduction to Statistical Learning, with Applications in R_. New York: Springer, 2013, pg. 395.
  // convert to arrays
  const clusters = journeys.map((j) => [j])
  const inputSize = journeys.length

  // prevent infinite loop, makes sense only to loop until there's just one cluster left
  while (clusters.length > 1) {
    // find the minimum dissimilarity
    let minDis = Infinity
    let minI = 0
    let minJ = 0

    for (let i = 1; i < clusters.length; i++) {
      for (let j = 0; j < i; j++) {
        const d = getClusterDissimilarity(clusters[i], clusters[j], {patterns, stops})

        if (d < minDis) {
          minDis = d
          minI = i
          minJ = j
        }
      }
    }

    log(`dissimilarity ${minDis}`)
    if (minDis > MAX_DISSIMILARITY) break

    // cluster the least dissimilar clusters
    clusters[minI] = clusters[minI].concat(clusters[minJ])
    clusters.splice(minJ, 1) // remove clusters[j]
  }

  log(`replaced ${inputSize} journeys with ${clusters.length} clusters`)
  // merge journeys together
  return clusters.map((c) => {
    return c.reduce((j1, j2) => {
      for (let i = 0; i < j1.segments.length; i++) {
        if (j1.segments[i].type !== 'TRANSIT') continue

        // convert to pattern groups
        if (!j1.segments[i].patterns) {
          j1.segments[i].patterns = [{...j1.segments[i]}]
          j1.segments[i].pattern_id =
            j1.segments[i].from_stop_index =
            j1.segments[i].to_stop_index =
            undefined
        }

        // don't modify from and to indices, Transitive will use the stops from the first pattern
        // TODO replace with "places" (e.g. "Farragut Square Area")
        j1.segments[i].patterns.push({...j2.segments[i]})
      }

      return j1
    })
  })
}

/**
 * Get the dissimilarity between two clusters, using complete linkages (see
 * James et al., _An Introduction to Statistical Learning, with Applications in
 * R_. New York: Springer, 2013, pg. 395.)
 */
function getClusterDissimilarity (c1: Array<Journey>, c2: Array<Journey>, {
  patterns,
  stops
}: {
  patterns: Array<Pattern>,
  stops: Array<Stop>
}): number {
  let dissimilarity = 0

  for (const j1 of c1) {
    for (const j2 of c2) {
      // if they are not the same length, don't cluster them
      if (j1.segments.length !== j2.segments.length) return Infinity

      // otherwise compute maximum dissimilarity of stops at either start or end
      for (let segment = 0; segment < j1.segments.length; segment++) {
        const s1 = j1.segments[segment]
        const s2 = j2.segments[segment]

        // if one has a walk segment where the other has a transit segment these
        // are not comparable
        if (s1.type !== s2.type) return Infinity

        // Only cluster  on the stop positions which we get from transit segments
        if (s1.type !== 'WALK') {
          dissimilarity = Math.max(
            dissimilarity,
            segmentDissimilarity(s1, s2, {patterns, stops})
          )

          // no point in continuing, these won't be merged
          if (dissimilarity > MAX_DISSIMILARITY) return Infinity
        }
      }
    }
  }

  return dissimilarity
}

/** return the dissimilarity between two individual transit segments (each with only a single pattern, not yet merged) */
function segmentDissimilarity (s1: Segment, s2: Segment, {
  patterns,
  stops
}: {
  patterns: Array<Pattern>,
  stops: Array<Stop>
}): number {
  const pat1 = patterns.find((p) => p.pattern_id === s1.pattern_id)
  const pat2 = patterns.find((p) => p.pattern_id === s2.pattern_id)

  if (!pat1 || !pat2) return Infinity

  const s1f = s1.from_stop_index
  const s1t = s1.to_stop_index
  const s2f = s2.from_stop_index
  const s2t = s2.to_stop_index

  if (s1f == null || s1t == null || s2f == null || s2t == null) return Infinity

  function findStop (id: string): ?Stop {
    return stops.find((stop) => stop.stop_id === id)
  }

  const from1 = findStop(pat1.stops[s1f].stop_id)
  const to1 = findStop(pat1.stops[s1t].stop_id)
  const from2 = findStop(pat2.stops[s2f].stop_id)
  const to2 = findStop(pat2.stops[s2t].stop_id)

  if (!from1 || !from2 || !to1 || !to2) return Infinity

  const d1 = stopDistance(from1, from2)
  const d2 = stopDistance(to1, to2)
  return Math.max(d1, d2)
}

/** return the Ersatz (squared) distance between two stops, in undefined units */
function stopDistance (s1: Stop, s2: Stop): number {
  const cosLat = Math.cos(s1.stop_lat * Math.PI / 180)
  return Math.pow(s1.stop_lat - s2.stop_lat, 2) + Math.pow(s1.stop_lon * cosLat - s2.stop_lon * cosLat, 2)
}