var mathHelpers = require('../helpers-math.js');

var WORD_SPLIT = /\s+/;
var LINE_SPLIT = /\n+/;
var CONTAIN_SENTENCE = /\.[^a-zA-Z]+(?=[A-Z]|$)/;

function TextContainerItem(algoritme, node, text) {
  this._algoritme = algoritme;

  this.node = node;

  // Diffrent text properties
  this.text = text;
  this.lines = text.split(LINE_SPLIT);
  this.words = text.split(WORD_SPLIT);

  // A linear appear value
  this._count = 0;
  this.increaseCount();

  // The scores
  this._shortline = 0;
  this._linebreaks = 0;
  this._classcount = 0;
  this._styleratio = 0;
  this._paragraphs = 0;
  this._density = 0;
  this._match = [];

  // Calculate scores
  this._calculateScores();
  for (var i = 0, l = algoritme._meta.length; i < l; i++) {
    this.appendDistance(algoritme._meta[i]);
  }
}
module.exports = TextContainerItem;

TextContainerItem.prototype.increaseCount = function () {
  var algoritme = this._algoritme;

  this._count += 1;
  algoritme._adjustContainerCount.update(this._count);
};

TextContainerItem.prototype._calculateScores = function () {
  this._calculateShortline();
  this._calculateLinebreaks();
  this._calculateClasscount();
  this._calculateStyleratio();
  this._calculateSentenses();
  this._calculateParagraphs();
  this._calculateDensity();
};

// Containers with short lines should be punished
TextContainerItem.prototype._calculateShortline = function () {
  var algoritme = this._algoritme;

  // Count the amount of short lines (40 chars)
  var shortlines = 0;
  for (var a = 0, m = this.lines.length; a < m; a++) {
    if (this.lines[a].length <= 40) shortlines += 1;
  }

  // Update property
  this._shortlines = shortlines;
  algoritme._adjustShortline.update(shortlines);
};

// This function is made by the following requirements:
// x > 5, y = 1
// f(x) = a*sqrt(x) + b*x^2 + c
// f(0) = 0.2
// f(5) = 1
// f'(5) = 0
function linebreakScore(x) {
  if (x > 5) return 1;
  else return Math.min(1, 0.477 * Math.sqrt(x) - 0.0106 * Math.pow(x, 2) + 0.2);
}

// Containers with very few lines should be punished
TextContainerItem.prototype._calculateLinebreaks = function () {
  var algoritme = this._algoritme;
  var linebreaks = linebreakScore(this.lines.length);

  // Update property
  this._linebreaks = linebreaks;
  algoritme._adjustLinebreak.update(linebreaks);
};

var CLASS_COUNT_SCORES = [1, 0.85, 0.73, 0.63, 0.54, 0.44, 0.34, 0.26, 0.22, 0.2];
function classcountScore(x) {
  if (x <= 1) return 1;
  else if (x >= 10) return 0.2;
  else return CLASS_COUNT_SCORES[x - 1];
}

// Containers classes there aren't wildly used is prefered
TextContainerItem.prototype._calculateClasscount = function () {
  var algoritme = this._algoritme;
  var classlist = this.node.root.classlist;

  // Calculcate a classcount score
  var max = 0, count = 0;
  for (var i = 0, l = this.node.classes.length; i < l; i++) {
    count = classlist.count(this.node.classes[i]);
    if (max < count) max = count;
  }
  var classcount = classcountScore(max);

  // Update property
  this._classcount = classcount;
  algoritme._adjustClasscount.update(classcount);
};

// Compare the amount of styled text with the total text length
// E.q. this will prevent long lists of links to be registred as a
// text container
TextContainerItem.prototype._calculateStyleratio = function () {
  var algoritme = this._algoritme;
  var noneStyledText = this.node.getNoneStyledText().trim();
  var styleratio = 1 - (this.text.length - noneStyledText.length) / this.text.length;

  // Update property
  this._styleratio = styleratio;
  algoritme._adjustStyleRatio.update(styleratio);
};

// Count the content of the 2 bigest groupes of paragraphs
TextContainerItem.prototype._calculateSentenses = function () {
  var algoritme = this._algoritme;

  // Count the amount of sentenses containing paragraphs in a row.
  var collection = [0];
  var paragraphSize = 0;
  for (var i = 0, l = this.lines.length; i < l; i++) {
    if (CONTAIN_SENTENCE.test(this.lines[i]) === false) {
      // found bad paragraph push and reset counter
      if (paragraphSize !== 0) {
        collection.push(paragraphSize);
        paragraphSize = 0;
      }
    } else {
      paragraphSize += this.lines[i].length;
    }
  }
  if (paragraphSize !== 0) collection.push(paragraphSize);

  // Find the two largest paragraph collections
  var maxes = [0, 0];
  for (var a = 0, r = collection.length; a < r; a++) {
    if (collection[a] >= maxes[1]) {
      maxes[0] = maxes[1];
      maxes[1] = collection[a];
    }
  }

  // Calculate the total text length
  var sentenses = maxes[0] + maxes[1];

  // Update property
  this._sentenses = sentenses;
  algoritme._adjustSentensesCount.update(sentenses);
};

// Simply count the amount of <p> tags
TextContainerItem.prototype._calculateParagraphs = function () {
  var algoritme = this._algoritme;
  var paragraphs = this.node.countTagname('p');

  // Update property
  this._paragraphs = paragraphs;
  algoritme._adjustParagraphCount.update(paragraphs);
};

// Use the parent density as a feature
TextContainerItem.prototype._calculateDensity = function () {
  var algoritme = this._algoritme;
  var density = this.node.density;

  // Update property
  this._density = density;
  algoritme._adjustContainerDensity.update(density);
};

// Calculate meta comparison scores
TextContainerItem.prototype.appendDistance = function (meta) {
  this._match.push(meta.calculateMatch(this));
};

// This function is made by the following requirements:
// : x < 0.01 , y = 0.5
// : x > 0.1 , y = 1
// : f(x), otherwise
// The f(x) polynomium is made so it follows:
// : f(0.01) = 0.5
// : f'(0.01) = 0
// : f(0.1) = 1
// : f'(0.1) = 0
function adjustLiklihood(x) {
  if (x < 0.01) return 0.5;
  else if (x > 0.1) return 1;
  else return Math.min(1, - 1371 * Math.pow(x, 3) + 226 * Math.pow(x, 2) - 4.11 * x + 0.52);
}

TextContainerItem.prototype.calculateLikelihood = function () {
  var algoritme = this._algoritme;
  var meta = algoritme._meta;

  // Adjust the single value scores
  var count = algoritme._adjustContainerCount.adjust(this._count);
  var shortline = 1 - algoritme._adjustShortline.adjust(this._shortlines);
  var linebreaks = algoritme._adjustLinebreak.adjust(this._linebreaks);
  var classcount = algoritme._adjustClasscount.adjust(this._classcount);
  var styleratio = algoritme._adjustStyleRatio.adjust(this._styleratio);
  var sentenses = algoritme._adjustSentensesCount.adjust(this._sentenses);
  var paragraphs = algoritme._adjustParagraphCount.adjust(this._paragraphs);
  var density = algoritme._adjustContainerDensity.adjust(this._density);

  // Use the descriptions to calculate a match feature if possible
  var match = 1;
  if (meta.length > 0) {
    var total = 0, divider = 0;
    for (var i = 0, l = meta.length; i < l; i++) {
      total += meta[i]._match.appearAdjust(meta[i].appear, this._match[i]);
      divider += meta[i].appear;
    }
    match = total / divider;
  }

  // Calculate the likelihood
  var scores = [
    count, shortline, linebreaks, classcount, styleratio,
    sentenses, paragraphs, density, match
  ];
  var likelihood = mathHelpers.distance(scores);
  var minimum = Math.min.apply(Math, scores);

  // Some containers may win on adjused features like (1,0) under the right
  // conditions. The goal of this transformation of likelihood is to make
  // those elements unlikly, however not to the point where removing them
  // would be the same.
  // SEE: 26c3b98f33bb6902f32535235fd7d32792df87779bdf1f86c3be15fbf3161d
  this.likelihood = adjustLiklihood(minimum) * likelihood;
};


"use strict";
/**
 * create Date object from yyyymmddhhmmss string
 * @param {string} dateStr yyyymmddhhmmss
 * @returns {Date}
 */
Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.dateFromString = dateFromString;
exports.parseLineByLine = parseLineByLine;

function dateFromString(dateStr) {
    // dateStr
    // yyyymmddhhmmss
    return new Date(dateStr.substring(0, 4), parseInt(dateStr.substr(4, 2), 10) - 1, dateStr.substr(6, 2), dateStr.substr(8, 2), dateStr.substr(10, 2), dateStr.substr(12, 2));
}

/**
 * create tuple that
 * @param text
 * @returns {{bookmarks: string[], lines: string[]}}
 */

function parseLineByLine(text) {
    var lines = text.trim().split("\n");
    var bookmarks = lines.splice(0, lines.length * 3 / 4);
    return {
        bookmarks: bookmarks,
        lines: lines
    };
}
//# sourceMappingURL=hatebu-mydata-utils.js.map


var ttest = require('ttest');
var summary = require('summary');

//
// The exposed API used in all.js
//
function CommonalityInterface(MatrixConstructor, top) {
  this.top = top;
  this.length = null;
  this.matrix = null;

  this.MatrixConstructor = MatrixConstructor;
}
exports.Interface = CommonalityInterface;

CommonalityInterface.prototype.collect = function (nodelist) {
  this.length = nodelist.length;

  this.matrix = new this.MatrixConstructor(this.top);
  for (var i = 0, l = nodelist.length; i < l; i++) {
    this.matrix.append(nodelist[i]);
  }
  this.matrix.summarise();
};

var ttest_options = {
  alpha: 0.05,
  alternative: "greater"
};

CommonalityInterface.prototype.reduce = function (maxRatio) {
  var maxRemoval = this.top.getText().length * maxRatio;

  var dim = this.matrix.dim;
  var matrix = this.matrix.summaryMatrix;
  var best = matrix[this.matrix.bestIndex[0]][this.matrix.bestIndex[1]];

  for (var a = 0; a < dim[0]; a++) {
    for (var b = 0; b < dim[1]; b++) {
      var compare = matrix[a][b];

      // Skip self and none compare summary
      if (compare === best || compare.size() === 0) {
        continue;
      }

      else if (this.matrix.cellTextLength(a, b) > maxRemoval) {
        continue;
      }

      // If the maximal value in compare is much less than the minimal value in
      // the best dataset, its pretty safe to remove the currently compared
      // nodes
      else if (compare.max() * 1.25 < best.min()) {
        this.matrix.removeCell(a, b);
      }

      // If there is only two values then there is not enogth data to perfrom
      // a statical valid analysis, but if maxmean is 25% greater than the
      // alternative it should be good enogth
      else if (compare.size() === 1 || best.size() === 1) {
        if (compare.mean() * 1.25 < best.mean()) {
          this.matrix.removeCell(a, b);
        }
      }

      // Check if the maxdataset mean is greater than the current tagname
      else if (ttest(best, compare, ttest_options).valid() === false) {
        this.matrix.removeCell(a, b);
      }
    }
  }
};

//
// Some abstact API for the commanality matrix
//
function CommanalityMatrix(classlist) {
  // space will identify a text node and/or an element node without any classnames
  this.rowKeys = {};
  this.rowNames = [];

  this.collumKeys = {};
  this.collumNames = [];

  this.nodeMatrix = [];
  this.summaryMatrix = [];

  this.dim = [0, 0];

  this.bestIndex = [-1, -1];
  this._bestNodes = {};
}
exports.Matrix = CommanalityMatrix;

//
// Extend the rows or the collums
//
function arrayVector(size) {
  var vec = new Array(size);
  for (var i = 0; i < size; i++) vec[i] = [];
  return vec;
}

function zeroVector(size) {
  var vec = new Array(size);
  for (var i = 0; i < size; i++) vec[i] = 0;
  return vec;
}

CommanalityMatrix.prototype.collum = function (name) {
  if (this.collumKeys.hasOwnProperty(name) === false) {
    this.collumKeys[name] = this.collumNames.length;
    this.collumNames.push(name);

    this.nodeMatrix.push(arrayVector(this.rowNames.length));
    this.summaryMatrix.push(zeroVector(this.rowNames.length));
  }

  return this.collumKeys[name];
};

CommanalityMatrix.prototype.row = function (name) {
  if (this.rowKeys.hasOwnProperty(name) === false) {
    this.rowKeys[name] = this.rowNames.length;
    this.rowNames.push(name);

    for (var a = 0, b = this.collumNames.length; a < b; a++) {
      this.nodeMatrix[a].push([]);
      this.summaryMatrix[a].push(0);
    }
  }

  return this.rowKeys[name];
};

//
// Summarize the matrix
//
function node2density(node) {
  return node.density;
}

// Create a summary matrix and find the highest mean
CommanalityMatrix.prototype.summarise = function () {
  var dim = this.dim = [this.summaryMatrix.length, this.summaryMatrix[0].length];
  var bestMean = -1;

  for (var a = 0; a < dim[0]; a++) {
    for (var b = 0; b < dim[1]; b++) {
      var stat = this.summaryMatrix[a][b] = summary(this.nodeMatrix[a][b].map(node2density));
      var mean = stat.mean();
      if (isNaN(mean) === false && mean > bestMean) {
        bestMean = mean;
        this.bestIndex = [a, b];
      }
    }
  }

  var nodes = this.nodeMatrix[this.bestIndex[0]][this.bestIndex[1]];
  for (var i = 0, l = nodes.length; i < l; i++) {
    this._bestNodes[ nodes[i].identifyer ] = 1;
  }
};

//
// Some basic cell operations
//
CommanalityMatrix.prototype.removeCell = function (a, b) {
  var nodelist = this.nodeMatrix[a][b];

  for (var i = 0, l = nodelist.length; i < l; i++) {
    if (this._bestNodes.hasOwnProperty(nodelist[i].identifyer) === false) {
      nodelist[i].remove();
    }
  }
};

CommanalityMatrix.prototype.cellTextLength = function (a, b) {
  var nodelist = this.nodeMatrix[a][b];
  var length = 0;
  for (var i = 0, l = nodelist.length; i < l; i++) {
    length += nodelist[i].getText().length;
  }
  return length;
};

//
// Debug methods
//
CommanalityMatrix.prototype.print = function () {
  var table = require('text-table');
  var matrix = [ [''].concat(this.collumNames) ];

  for (var b = 0; b < this.dim[1]; b++) {
    var row = [this.rowNames[b]];
    matrix.push(row);

    for (var a = 0; a < this.dim[0]; a++) {
      var str = '';
      if (a === this.bestIndex[0] && b === this.bestIndex[1]) str += '**';
      str += this.summaryMatrix[a][b].size() + ' ~ ' + this.summaryMatrix[a][b].mean().toPrecision(3);
      if (a === this.bestIndex[0] && b === this.bestIndex[1]) str += '**';

      row.push(str);
    }
  }

  return table(matrix);
};


var util = require('util');
var entities = require('entities');

var WHITE_SPACE = /\s+/g;

var BLOCK_ELEMENTS = exports.BLOCK_ELEMENTS = {
	address: true, article: true, aside: true,
	blockqoute: true, canvas: true, dd: true,
  div: true, dl: true, fieldset: true,
  figcaption: true, figure: true, footer: true,
  form: true, h1: true, h2: true, h3: true,
  h4: true, h5: true, h6: true, header: true,
  hgroup: true, hr: true, img: true, object: true,
  ol: true, output: true, p: true, pre: true,
  section: true, table: true, tfoot: true,
  ul: true, li: true, tr: true
};

var NO_TEXT_ELEMENTS = exports.NO_TEXT_ELEMENTS = {
	script: true, noscript: true, style: true,
  iframe: true, object: true, embed: true,
  video: true, audio: true
};

var IGNORE_TEXT_ELEMENT = exports.IGNORE_TEXT_ELEMENT = {
	aside: true, caption: true, menu: true,
	details: true, fieldset: true, button: true,
	label: true, input: true, textarea: true
};

var HEADERS = exports.HEADERS = {
	h1: true, h2: true, h3: true,
	h4: true, h5: true, h6: true
};

var INLINE_TEXT_TAGS = exports.INLINE_TEXT_TAGS = {
	strong: true, span: true, em: true,
	b: true, i: true, u: true, del: true,
	ins: true, a: true, time: true, sub: true,
	sup: true, big: true, small: true, tt: true,
	strike: true
};

var INLINE_STYLE_TAGS = exports.INLINE_STYLE_TAGS = {
	h1: true, h2: true, h3: true,
	h4: true, h5: true, h6: true,
	strong: true, span: true, em: true,
	b: true, i: true, u: true, del: true,
	ins: true, a: true, time: true, sub: true,
	sup: true, big: true, small: true, tt: true,
	strike: true
};

// Map the entities decodeHTML5 method
exports.decodeEntities = entities.decodeHTML5;

// Builds and attribute matching function, complexity is for uncomfirmed
// performace sake
function buildAttributeMatcher(match) {
  var keys = Object.keys(match);
  var jskey, i, l;

  var transform = '';
  var bool = '';

  transform = 'transform = {\n';
  for (i = 0, l = keys.length; i < l; i++) {
    jskey = JSON.stringify(keys[i]);

    transform += '  ' + jskey + ': attr.hasOwnProperty(' + jskey + ') ? attr[' + jskey + '].toLowerCase() : false';
    if (i !== l - 1) transform += ',';
    transform += '\n';
  }
  transform += '};\n';

  bool = 'return !!(';
  for (i = 0, l = keys.length; i < l; i++) {
    jskey = JSON.stringify(keys[i]);

    if (i > 0) bool += '    ||    ';
    bool += ' ( transform[' + jskey + ']';

    if (Array.isArray(match[keys[i]])) {
      bool += ' && ( ';

      for (var j = 0, s = match[keys[i]].length; j < s; j++) {
        if (j > 0) bool += ' || ';

        if (typeof match[keys[i]][j] === 'string') {
          bool += 'transform[' + jskey + '] === \'' + match[keys[i]][j].toLowerCase() + '\'';
        } else if (util.isRegExp(match[keys[i]][j])) {
          bool += 'match[' + jskey + '][' + j + '].test(transform[' + jskey + '])';
        }
      }

      bool += ' )';
    }

    bool += ' ) \n';
  }

  bool += '         );';

  var anonymous = new Function('attr', 'match', transform + '\n' + bool);
  return function (attr) {
    return anonymous(attr, match);
  };
}
exports.buildAttributeMatcher = buildAttributeMatcher;

// Handle the HTML entities and replace all collections of whitespace with
// just a single space
function normalizeString(text) {
  return entities.decodeHTML5(text).replace(WHITE_SPACE, ' ');
}
exports.normalizeString = normalizeString;

// Checks if container `a` is a container of `b`
function containerOf(a, b) {
  while (b = b.parent) {
    if (a === b) return true;
  }
  return false;
}
exports.containerOf = containerOf;

// Find the common parent of a and b
function commonParent(a, b) {
  if (a === b) {
    return a;
  } else if (containerOf(a, b)) {
    return a;
  } else if (containerOf(b, a)) {
    return b;
  } else {
    // This will happen at some point, since the root is a container of
    // everything
    while (b = b.parent) {
      if (containerOf(b, a)) return b;
    }
  }
}
exports.commonParent = commonParent;

// Parse style attribute intro object
function styleParser(style) {
  style = style || '';

  var tokens = style.trim().split(/\s*(?:;|:)\s*/);
  var output = {};
  for (var i = 1, l = tokens.length; i < l; i += 2) {
    output[tokens[i - 1]] = tokens[i];
  }
  return output;
}
exports.styleParser = styleParser;

// Calculate the tree distance between a and b
function treeDistance(a, b) {
  if (a === b) return 0;
  var parent = commonParent(a, b);

  var aParent = a;
  var aCount = 0;

  var bParent = b;
  var bCount = 0;

  if (parent !== a) {
    while (parent !== aParent.parent) {
      aCount += 1;
      aParent = aParent.parent;
    }
  } else {
    bCount += 1;
  }

  if (parent !== b) {
    while (parent !== bParent.parent) {
      bCount += 1;
      bParent = bParent.parent;
    }
  } else {
    aCount += 1;
  }

  var abCount = 0;
  if (parent !== a && parent !== b) {
    abCount = Math.abs(
      parent.children.indexOf(aParent) - parent.children.indexOf(bParent)
    );
  }

  return aCount + bCount + abCount;
}
exports.treeDistance = treeDistance;

function lastElementChild(node) {
  var i = node.children.length;
  while (i--) {
    if (node.children[i].type === 'element') return node.children[i];
  }
  return null;
}
function positionRange(node) {
  var curr, prev = node;
  while (curr = lastElementChild(prev)) {
    prev = curr;
  }

  return [node.identifyer, prev.identifyer];
}
exports.positionRange = positionRange;


/** casperjs-geolocation
  * A capser module that mocks the geolocation API
  */

// functions to be evaluated in remote
var Remote = {
  add_geolocation_api : function(pos) {
    navigator.geolocation = {
      // store pos here
      __casperFakeLocation : pos,

      // getcurrentposition
      getCurrentPosition : function(callback, err) {
        console.log('getCurrentPosition called');
        callback(this.__casperFakeLocation);
      },

      // stores the callback passed to watchPosition
      __casperWatchCallback : undefined,

      // watchposition
      watchPosition : function(callback) {
        this.__casperWatchCallback = callback;
        console.log('watchPosition called');
        callback(this.__casperFakeLocation);
      }
    };
  },

  update_position : function(pos) {
    navigator.geolocation.__casperFakeLocation = pos;
    if(navigator.geolocation.__casperWatchCallback) {
        navigator.geolocation.__casperWatchCallback(pos);
    }
  }
};

// module
var Geolocation = function(casper, pos) {
  this._pos = pos || {
    timestamp: Date.now(),
    coords: {
      longitude: 0,
      latitude: 0,
      accuracy: 0
    }
  };
  this._casper = casper;
};

Geolocation.prototype.getPos = function () {
  return this._pos
};

Geolocation.prototype.setPos = function(pos) {
  this._pos = pos;
  this._casper.evaluate(Remote.update_position, {
    timestamp: Date.now(),
    coords: {
      longitude: pos.longitude,
      latitude: pos.latitude,
      accuracy: pos.accuracy || 10
    }
  });
};

module.exports = function(casper, pos) {
  var geo = new Geolocation(casper, pos);
  casper.on('page.initialized', function() {
    geo._casper.evaluate(Remote.add_geolocation_api, pos);
  });
  return geo;
}


'use strict';

var debug = require('debug')('dry');
var RegexCache = require('./regex');
var Position = require('./position');
var utils = require('./utils');

/**
 * Create a new `Lexer` for the given `file` and `options`.
 * @param {object} `file`
 * @param {object} `options`
 */

function Lexer(file, options) {
  this.options = utils.extend({}, options);
  this.file = file;
  this.regex = new RegexCache();
  this.names = [];
  this.ast = {
    tags: {},
    type: 'root',
    name: 'root',
    nodes: []
  };

  this.unknown = {tags: [], blocks: []};
  this.known = {
    tags: ['extends', 'layout'],
    blocks: ['block']
  };

  this.delimiters = {
    variable: ['{{', '}}'],
    block: ['{%', '%}'],
    es6: ['${', '}'],
  };

  this.tokens = [this.ast];
  this.errors = [];
  this.stack = [];
  this.stash = [];
  this.lexers = {};
  this.fns = [];
}

/**
 * Prototype methods
 */

Lexer.prototype = {
  constructor: Lexer,

  /**
   * Create default delimiters and tags
   */

  init: function() {
    if (this.isInitialized) return;

    this.isInitialized = true;
    var lexer = this;
    this.lineno = 1;
    this.column = 1;
    this.lexed = '';

    this.file = utils.normalize(this.file);
    this.file.orig = this.file.contents;
    this.input = this.file.contents.toString();
    this.file.ast = this.ast;
    this.file.ast.variables = {};
    this.file.ast.blocks = {};

    this.input = this.input.split('{% body %}')
      .join('{% block "body" %}{% endblock %}');

    if (this.file.extends) {
      this.prependNode(this.file, 'extends');
    }

    /**
     * Tags
     */
    this.captureTag('extends');
    this.captureTag('layout');

    /**
     * Block tags
     */
    this.captureBlock('block');

    /**
     * Captures
     */

    this.capture('text', utils.negateDelims(this.delimiters));
    this.capture('newline', /^\n+/);
    this.capture('es6', /^\$\{([^\\}]*(?:\\.[^\\}]*)*)\}/);
    this.capture('variable', /^\{{2,}([^\\}]*(?:\\.[^\\}]*)*)\}{2,}/);
    this.capture('escape', /^\\(.)/);
    this.capture('space', /^[ \t]+/);

    /**
     * Custom helpers
     */
    var helpers = this.options.helpers || {};
    if (utils.isObject(helpers)) {
      helpers = Object.keys(helpers);
    }

    helpers.forEach(function(key) {
      lexer.known.blocks.push(key);
      lexer.captureBlock(key);
    });

    /**
     * Add other names to return un-rendered
     */

    var matches = this.input.match(/\{%\s*([^%}]+)/g);
    var names = utils.getNames(matches);

    names.tags.forEach(function(key) {
      if (!utils.isRegistered(lexer, key)) {
        lexer.unknown.tags.push(key);
        lexer.captureTag(key);
      }
    });

    names.blocks.forEach(function(key) {
      if (!utils.isRegistered(lexer, key)) {
        lexer.unknown.blocks.push(key);
        lexer.captureBlock(key);
      }
    });
  },

  /**
   * Set an error message with the current line number and column.
   * @param {String} `msg` Message to use in the Error.
   */

  error: function(msg) {
    var message = this.file.relative
      + ' line:' + this.lineno
      + ' column:' + this.column
      + ': ' + msg;

    var err = new Error(message);
    err.reason = msg;
    err.line = this.lineno;
    err.column = this.column;
    err.source = this.input;
    err.path = this.file.path;

    if (this.options.silent) {
      this.errors.push(err);
    } else {
      throw err;
    }
  },

  /**
   * Mark position and patch `node.position`.
   */

  position: function() {
    var start = { line: this.lineno, column: this.column };
    var self = this;

    return function(node) {
      utils.define(node, 'position', new Position(start, self));
      return node;
    };
  },

  /**
   * Capture `type` with the given regex.
   * @param {String} `type`
   * @param {RegExp} `regex`
   * @return {Function}
   */

  capture: function(type, regex) {
    var cached = this.regex.create(type, regex);
    var lexer = this;

    var fn = this.lexers[type] = function() {
      var pos = lexer.position();
      var m = lexer.match(cached.val);
      if (!m || !m[0]) return;

      var parent = lexer.prev();
      var node = pos({
        type: type,
        val: m[0]
      });

      utils.define(node, 'parent', parent);
      utils.define(node, 'rawArgs', m[1]);
      utils.define(node, 'args', function() {
        return utils.parseArgs(m[1]);
      });

      parent.nodes.push(node);
    };

    this.addLexer(fn);
    return this;
  },

  /**
   * Create a lexer for the given variable `type`.
   * @param {String} `type`
   * @return {Function}
   */

  captureVariable: function(type) {
    var cached = this.regex.createVariable(type);
    var file = this.file;
    var lexer = this;

    var fn = this.lexers[type] = function() {
      var pos = lexer.position();
      var m = lexer.match(cached.strict);
      if (!m) return;

      var parent = this.prev();
      var node = pos({
        type: type,
        known: utils.has(lexer.known.tags, type),
        val: m[0].trim()
      });

      parent.known = node.known;
      var nodes = parent.nodes;

      Object.defineProperty(file.ast.variables, type, {
        configurable: true,
        set: function(val) {
          nodes = val;
        },
        get: function() {
          return nodes;
        }
      });

      Object.defineProperty(parent, 'nodes', {
        configurable: true,
        set: function(val) {
          nodes = val;
        },
        get: function() {
          return nodes;
        }
      });

      utils.define(node, 'parent', parent);
      parent.nodes.push(node);
    };

    this.addLexer(fn);
    return this;
  },

  /**
   * Create a tag lexer for the given `type`.
   * @param {String} `type`
   * @return {Function}
   */

  captureTag: function(type) {
    this.ast.tags[type] = null;
    this.names.push(type);

    var cached = this.regex.createTag(type);
    var file = this.file;
    var lexer = this;

    var fn = this.lexers[type] = function() {
      var pos = lexer.position();
      var m = lexer.match(cached.strict);
      if (!m) return;

      var name = utils.getName(m[1]);
      if (this.options.strict) {
        var isKnown = utils.has(lexer.known.tags, type);
        if (isKnown && file.hasOwnProperty(type) && !file.hasOwnProperty('isParsed')) {
          throw new Error(`only one "${type}" tag may be defined per template`);
        }
      }

      file[type] = name;
      lexer.ast.tags[type] = name;
      lexer.createNode(type, name, m, pos);
    };

    this.addLexer(fn);
    return this;
  },

  /**
   * Push AST node `type` onto `parent.nodes`
   * @param {String} `type`
   * @param {String} `name`
   * @param {String} `val`
   * @param {Function} `pos`
   */

  createNode: function(type, name, m, pos) {
    var parent = this.prev();
    var val = m[1];
    var tok = { type: 'args', val: val };

    var node = pos({
      type: type,
      name: name,
      known: utils.has(this.known.tags, type),
      val: val.trim(),
      nodes: [tok]
    });

    utils.define(node, 'parent', parent);
    utils.define(tok, 'parent', node);
    parent.nodes.push(node);
  },

  /**
   * Create an opening tag lexer for block `name`.
   * @param {String} `name`
   * @return {Function}
   */

  captureOpen: function(type) {
    this.names.push(type);
    var cached = this.regex.createOpen(type);
    var file = this.file;
    var lexer = this;

    return function() {
      var pos = lexer.position();
      var m = lexer.match(cached.strict);
      if (!m) return;

      var name = utils.getName(m[1]);
      var action = utils.getAction(m[1]);
      var val = m[0];

      if (!name && lexer.options[type] && lexer.options[type].args === 'required') {
        throw new Error(`no arguments defined on "${type}": ${m[0]}`);
      }

      if (!name) name = 'unnamed';
      var node = pos({
        type: `${type}.open`,
        known: utils.has(lexer.known.blocks, type),
        name: name,
        val: val.trim()
      });

      var parent = lexer.prev();
      if (parent && parent.name && parent.name !== 'root') {
        name = parent.name + '.' + name;
      }

      var block = {
        type: type,
        name: name,
        known: node.known,
        action: action,
        nodes: [node]
      };

      utils.define(node, 'parent', block);
      utils.define(block, 'parent', parent);
      block.rawArgs = m[1];
      block.args = utils.parseArgs(m[1]);

      Object.defineProperty(file.ast.blocks[type], name, {
        configurable: true,
        enumerable: true,
        set: function(val) {
          block = val;
        },
        get: function() {
          return block;
        }
      });

      parent.nodes.push(block);
      lexer.tokens.push(block);
      return block;
    };
  },

  /**
   * Create a closing tag lexer for block `name`.
   * @param {String} `name`
   * @return {Function}
   */

  captureClose: function(type) {
    var cached = this.regex.createClose(type);
    var file = this.file;
    var lexer = this;

    return function() {
      var pos = lexer.position();
      var m = lexer.match(cached.strict);
      if (!m) return;

      var block = lexer.tokens.pop();
      if (typeof block === 'undefined' || block.type !== type) {
        throw new Error(`missing opening ${type}`);
      }

      if (block.name === 'body') {
        lexer.ast.isLayout = true;
        file.ast.isLayout = true;
      }

      var nodes = block.nodes;
      Object.defineProperty(file.ast.blocks, block.name, {
        configurable: true,
        set: function(val) {
          nodes = val;
        },
        get: function() {
          return nodes;
        }
      });

      Object.defineProperty(block, 'nodes', {
        configurable: true,
        set: function(val) {
          nodes = val;
        },
        get: function() {
          return nodes;
        }
      });

      var tok = pos({
        known: block.known,
        type: `${type}.close`,
        val: m[0].trim()
      });

      utils.define(block, 'position', tok.position);
      utils.define(tok, 'parent', block);
      block.nodes.push(tok);
      return block;
    };
  },

  /**
   * Create a block lexer with opening and closing tags for the given `name`.
   * @param {String} `name`
   * @return {Function}
   */

  captureBlock: function(type) {
    this.file.ast.blocks[type] = this.file.ast.blocks[type] || {};
    this.addLexer(this.captureOpen(type));
    this.addLexer(this.captureClose(type));
    return this;
  },

  /**
   * Unshift node `prop` onto the AST
   */

  prependNode: function(file, prop) {
    return this.createNode(prop, file[prop], `{% ${prop} "${file[prop]}" %}`, this.position());
  },

  /**
   * Get the previous AST node
   * @return {Object}
   */

  prev: function() {
    return utils.last(this.tokens);
  },

  /**
   * Push a lexer `fn` onto the `fns` array
   * @param {Function} `fn`
   * @return {undefined}
   */

  addLexer: function(fn) {
    this.fns.push(fn);
    return this;
  },

  /**
   * Consume the given `len` of input.
   * @param {Number} len
   */

  consume: function(str, len) {
    this.input = this.input.substr(len);
  },

  /**
   * Update lineno and column based on `str`.
   */

  updatePosition: function(str, len) {
    var lines = str.match(/\n/g);
    if (lines) this.lineno += lines.length;
    var i = str.lastIndexOf('\n');
    this.column = ~i ? len - i : this.column + len;
    this.lexed += str;
    this.consume(str, len);
  },

  /**
   * Match `regex`, return captures, and update the cursor position by `match[0]` length.
   * @param {RegExp} `regex`
   * @return {Object}
   */

  match: function(regex) {
    var m = regex.exec(this.input);
    if (m) {
      this.updatePosition(m[0], m[0].length);
      return m;
    }
  },

  /**
   * Run lexers to advance the curson position
   */

  advance: function() {
    var len = this.fns.length;
    var idx = -1;
    while (++idx < len) {
      this.fns[idx].call(this);
      if (!this.input) {
        break;
      }
    }
  },

  /**
   * Get the next AST token
   */

  next: function() {
    while (this.input) {
      var prev = this.input;
      this.advance();
      if (this.input && prev === this.input) {
        throw new Error(`no lexers registered for: "${this.input.substr(0, 10)}"`);
      }
    }
  },

  /**
   * Tokenize the given string.
   * @return {Array}
   */

  tokenize: function(file) {
    debug('lexing <%s>', this.file.path);
    if (file) this.file = file;
    this.init();
    while (this.input) this.next();
    return this.ast;
  }
};

/**
 * Expose `Lexer`
 */

exports = module.exports = Lexer;


/*
 * grunt-notify
 * https://github.com/dylang/grunt-notify
 *
 * Copyright (c) 2013 Dylan Greene
 * Licensed under the MIT license.
 */
'use strict';

module.exports = function(grunt, options) {

  var message_count = 0;
  var StackParser = require('stack-parser');
  var notify = require('../notify-lib');
  var removeColor = require('../util/removeColor');

  var cwd = process.cwd();

  function watchForContribWatchWarnings(e) {

    if (!e || typeof e !== 'string') {
        return;
    }

    var msg = removeColor(e);
    msg = msg.replace(' Use --force to continue.', '');

    if (msg.indexOf('Warning:') === 0) {
        return notifyHook(msg.replace('Warning: ', ''));
    }
    if (msg.indexOf('Fatal error:') === 0) {
        return notifyHook(msg.replace('Fatal error: ', ''));
    }

    return e;
  }

  function exception(e) {
    var stackDump, stack, message;

    if (e.stack && typeof e.stack !== 'string'){
      stackDump = StackParser.parse(e.stack);
      stack = stackDump[0];

      // Find the first stack that isn't a node module or an internal Node function
      while (stack && (stack.file.match('/node_modules') || !stack.file.match('/'))) {
        stack = stackDump.shift();
      }
    }

    if (stack && stack.file) {
      message = [
        stack.file.replace(cwd, ''),
        'Line ' + stack.line,
        e.message
      ].join('\n');
    } else {
      message = e.message;
    }

    return message;
  }

  /**
   * Hook for showing the automatic message
   * @param e Exception or message
   * @returns {*}
   */
  function notifyHook(e) {

    message_count++;

    var message;

    if (!options.enabled) {
      return;
    }

    if (!e) {
      return;
    }

    if (e && e.length === 1) {
      e = e[0];
    }

    if (/Task .* failed\./.test(e.message)) {
      message = e.message;
    } else if (e.message && e.stack) {
      message = exception(e);
    } else {
      message = e + '';
    }

    if (message_count > 0 && message === 'Aborted due to warnings.') {
      // skip unhelpful message because there was probably another one that was more helpful
      return;
    }

    // shorten message by removing full path
    // TODO - make a global replace
    message = message.replace(cwd, '').replace('\x07', '');

    return notify({
      title:    options.title + (grunt.task.current.nameArgs ? ' ' + grunt.task.current.nameArgs : ''),
      message:  message
    });
  }

  // run on warning
  grunt.util.hooker.hook(grunt, 'warn', notifyHook);
  grunt.util.hooker.hook(grunt.fail, 'warn', notifyHook);
  // run on error
  grunt.util.hooker.hook(grunt.fail, 'error', notifyHook);
  grunt.util.hooker.hook(grunt.log, 'fail', notifyHook);
  grunt.util.hooker.hook(grunt.log, 'error', notifyHook);
  // run on fatal
  grunt.util.hooker.hook(grunt.fail, 'fatal', notifyHook);

  // grunt-contrib-watch replaces some of these functions so
  // we need to watch all writeln's too
  // https://github.com/gruntjs/grunt-contrib-watch/issues/232
  grunt.util.hooker.hook(grunt.log, 'writeln', watchForContribWatchWarnings);

  function setOptions(opts) {
    options = opts;
  }

  return {
    setOptions: setOptions
  };
};


var now = require('./date-now'),
  raf = require('./raf');

module.exports = Timer;

/**
 * @constructor
 */
function Timer(options) {
  this._initialized = false;

  this._curTime = 0;
  this._frametime = 0;
  this._fixedDeltaTime = 1000 / 60;
  this._fixedDeltaTimeInSeconds = this._fixedDeltaTime / 1000;
  this._FRAME_TIME_MAX = 250;
  this._elapsed = 0;

  this._config(options);

  if (this._autoStart) {
    this.start();
  }
}

Timer.prototype = {
  start: function() {
    if (!this._initialized) {
      this._initialized = true;

      this._accumulator = 0;
      this._tick = tick.bind(this);

      this._isPaused = false;
      this._prevTime = now();
      this._requestID = raf.request(this._tick);

      return true;
    }
    return false;
  },

  /** pauses the timer */
  pause: function() {
    if (this._initialized && this._isPaused) {
      return false;
    }

    this._isPaused = true;
    raf.cancel(this._requestID);

    this._pauseTime = now();
    this._onPause();

    return true;
  },

  /** resumes the timer */
  resume: function() {
    if (this._initialized && !this._isPaused) {
      return false;
    }

    var pauseDuration;

    this._isPaused = false;
    this._prevTime = now();

    pauseDuration = this._prevTime - this._pauseTime;
    this._onResume(pauseDuration);

    this._requestID = raf.request(this._tick);

    return true;
  },

  togglePause: function() {
    if (this._isPaused) {
      this.resume();
    } else {
      this.pause();
    }
  },

  /** returns true if the timer is paused */
  isPaused: function () {
    return this._isPaused;
  },

  _config: function(options) {
    var empty = function() {};

    this._update = options.update || empty;
    this._render = options.render || empty;
    this._onPause = options.onPause || empty;
    this._onResume = options.onResume || empty;

    this._autoStart = options.autoStart == null ? true : options.autoStart;
  }
};

function tick() {
  this._curTime = now();
  this._frameTime = this._curTime - this._prevTime;

  if (this._frameTime > this._FRAME_TIME_MAX) {
    this._frameTime = this._FRAME_TIME_MAX;
  }

  this._prevTime = this._curTime;

  this._accumulator += this._frameTime;

  while(this._accumulator >= this._fixedDeltaTime) {
    this._accumulator -= this._fixedDeltaTime;
    this._elapsed += this._fixedDeltaTime;
    this._update(this._fixedDeltaTimeInSeconds, this._elapsed);
  }

  this._render();

  this._requestID = raf.request(this._tick);
}


/**
 * A lightweight webhook server for Telegram Bot API
 *
 * @module telekit/client/webhook
 * @author Denis Maslennikov <mrdenniska@gmail.com> (nofach.com)
 * @license MIT
 */

/** Dependencies */
const { EventEmitter } = require('events');
const https = require('https');
const http = require('http');
const url = require('url');

/**
 * Methods
 * @private
 */
function createServer(options, callback) {
    if (options && options.cert && options.key) {
        return https.createServer(options, callback);
    }

    return http.createServer(callback || null);
}

/**
 * Implementation
 * @public
 */
class Webhook extends EventEmitter {
    constructor(api, options) {
        super();

        /** Default options */
        this.options = Object.assign({
            allowed_updates: [],
            connections: 40,
            port: 443,
            url: '',
            ssl: { cert: null, key: null },
        }, options);

        if (options.url === '') {
            throw new Error('URL of webhook is missing');
        }

        this.server = null;
        this.api = api;
        this.url = url.parse(options.url.replace('{token}', this.api.token));

        /** Set webhook and start listening */
        this.api.setWebhook({
            allowed_updates: this.options.allowed_updates,
            connections: this.options.connections,
            certificate: this.options.cert,
            url: this.url.href,
        }).then(() => {
            this.server = this.listen(this.options.port);
        }).catch((error) => {
            error();
        });
    }

    listen(...args) {
        this.server = createServer(this.options.ssl, this.route.bind(this));
        return this.server.listen(...args);
    }

    route(req, res) {
        if (req.method === 'POST') {
            const query = url.parse(req.url || '/');

            if (query.pathname === this.url.pathname) {
                req.on('data', (data) => {
                    try {
                        const body = JSON.parse(data.toString());

                        this.emit('update', body);
                        Webhook.response(res, 200);
                    } catch (e) {
                        Webhook.response(res, 400);
                    }
                });
            }

            return;
        }

        Webhook.response(res, 404);
    }

    static response(res, status) {
        res.writeHead(status);
        res.end(JSON.stringify({
            message: http.STATUS_CODES[status],
            status,
        }));
    }
}

/** Exports */
module.exports = Webhook;


/**
 * Context is context
 *
 * @module telekit/context
 * @author Denis Maslennikov <mrdenniska@gmail.com> (nofach.com)
 * @license MIT
 */

/** Dependencies */
const debug = require('debug')('telekit:context');

/**
 * Constants
 * @private
 */

/** Methods that available for user and chat */
const METHOD_COMMON = [
    'sendMessage',
    'forwardMessage',
    'sendPhoto',
    'sendAudio',
    'sendDocument',
    'sendSticker',
    'sendVideo',
    'sendVoice',
    'sendLocation',
    'sendVenue',
    'sendContact',
    'sendChatAction',
    'editMessageText',
    'editMessageCaption',
    'editMessageReplyMarkup',

];

/** Methods that available only for user */
const METHOD_USER = [
    ...METHOD_COMMON,
    'getUserProfilePhotos',

];

/** Methods that available only for chat */
const METHOD_CHAT = [
    ...METHOD_COMMON,
    'leaveChat',
    'getChat',
    'getChatAdministrators',
    'getChatMembersCount',
    'getChatMember',
];

/**
 * Methods
 * @private
 */
function createProxy(api, obj, methods) {
    return new Proxy(obj, {
        get(target, name) {
            if (methods.includes(name)) {
                return (params, callback) => {
                    const inject = Object.assign({ chat_id: obj.id }, params);
                    return api.method(name, inject, callback);
                };
            }

            return target[name];
        },

        set() {
            return new Error('Cannot change `chat` or `user`.');
        },
    });
}

/**
 * Implementation and Exports
 * @public
 */
module.exports = function Context(API) {
    const api = API;
    let update = null;
    let chat = null;
    let user = null;

    return {
        /** Used for edited or chosen updates */
        isEdited: false,
        isChosen: false,

        /** Raw incoming update */
        get update() { return update; },
        set update(raw) {
            chat = null;
            user = null;

            if (raw.chat) {
                chat = createProxy(api, raw.chat, METHOD_CHAT);
                debug('chat(%s) is apply', chat.id);
            }

            if (raw.from) {
                user = createProxy(api, raw.from, METHOD_USER);
                debug('user(%s) is apply', user.id);
            }

            update = raw;
        },

        /** Chat and user instances with API for them */
        get chat() { return chat; },
        get user() { return user; },

        /** Global API */
        get api() { return api; },
    };
};


'use strict';

// MODULES //

var isInteger = require( 'validate.io-integer-primitive' );

// CONSTANTS //

var MAX = require( 'const-max-uint32' );

// IS ARRAY-LIKE //

/**
* FUNCTION: isArrayLike( value )
*	Validates if a value is array-like.
*
* @param {*} value - value to validate
* @param {Boolean} boolean indicating if a value is array-like
*/
function isArrayLike( value ) {
	return (
		value !== void 0 &&
		value !== null &&
		typeof value !== 'function' &&
		isInteger( value.length ) &&
		value.length >= 0 &&
		value.length <= MAX
	);
} // end FUNCTION isArrayLike()

// EXPORTS //

module.exports = isArrayLike;


//Import libraries
var $             = require('jquery-browserify')
  , GLOW          = require('./GLOW.js').GLOW
  , ArcballCamera = require('arcball').ArcballCamera
  , utils         = require('./utils.js')
  , EventEmitter  = require('events').EventEmitter;

exports.makeShell = function(params) {
  if(!params) {
    params = {};
  }
  var shell = {};
  var bg_color    = params.bg_color   || [ 0.3, 0.5, 0.87 ]
    , camera_pos  = params.camera_pos || [ 0, 0, 50 ]
    , container   = params.container  || "#container";

  var citem = $(container)[0]

  //Create event emitter
  shell.events = new EventEmitter();

  //Initialize GLOW
  shell.context = new GLOW.Context({ width:citem.clientWidth, height:citem.clientHeight });
  shell.context.setupClear({
      red:    bg_color[0]
    , green:  bg_color[1]
    , blue:   bg_color[2]
  });
  GLOW.defaultCamera.localMatrix.setPosition(
      camera_pos[0]
    , camera_pos[1]
    , camera_pos[2]
  );
  GLOW.defaultCamera.update();

  //Create arcball camera
  shell.camera = new ArcballCamera();
  shell.buttons = {
      rotate: false
    , pan: false
    , zoom: false
  };

  //Hook up controls
  var buttons = shell.buttons;
  shell.container = container;
  $(container)[0].appendChild( shell.context.domElement );
  $(container).mousemove(function(e) {
    var c = $(container);
    shell.camera.update(e.pageX/c.width()-0.5, e.pageY/c.height()-0.5, {
      rotate: buttons.rotate || !(e.ctrlKey || e.shiftKey) && (e.which === 1),
      pan:    buttons.pan    || (e.ctrlKey && e.which !== 0) || (e.which === 2),
      zoom:   buttons.zoom   || (e.shiftlKey && e.which !== 0) || e.which === 3
    })
  });
  $(document).keydown(function(e) {
    if(e.keyCode === 65) {
      buttons.rotate = true;
    }
    if(e.keyCode === 83) {
      buttons.pan = true;
    }
    if(e.keyCode === 68) {
      buttons.zoom = true;
    }
  });
  $(document).keyup(function(e) {
    if(e.keyCode === 65) {
      buttons.rotate = false;
    }
    if(e.keyCode === 83) {
      buttons.pan = false;
    }
    if(e.keyCode === 68) {
      buttons.zoom = false;
    }
  });
  $(container).blur(function(e) {
    buttons.rotate = buttons.pan = buttons.zoom = false;
  })

  var forceUpdate = false

  function updateShape() {
    var w = citem.clientWidth|0
    var h = citem.clientHeight|0
    if(forceUpdate ||
       w !== shell.context.domElement.width ||
       h !== shell.context.domElement.height ) {
      shell.context.width = w;
      shell.context.height = h;
      shell.context.viewport.width = w;
      shell.context.viewport.height = h;
      shell.context.domElement.width = w;
      shell.context.domElement.height = h;
      GLOW.defaultCamera.aspect = w / h;
      GLOW.Matrix4.makeProjection(
        GLOW.defaultCamera.fov,
        GLOW.defaultCamera.aspect,
        GLOW.defaultCamera.near,
        GLOW.defaultCamera.far,
        GLOW.defaultCamera.projection
      );
      forceUpdate = false
    }
  }
  $(container).bind("DOMSubtreeModified", updateShape)
  $(window).resize(updateShape)
  shell.updateShape = function() {
    forceUpdate = true
    updateShape()
  }

  //Render loop
  function render() {
    shell.context.cache.clear();
    shell.context.setViewport();
    GLOW.defaultCamera.update()
    shell.context.enableDepthTest(true);
    if(params.cullCW) {
      shell.context.enableCulling(true, {frontFace: GL.CW, cullFace: GL.BACK});
    } else if(params.cullCCW) {
      shell.context.enableCulling(true, {frontFace: GL.CCW, cullFace: GL.BACK});
    } else {
      shell.context.enableCulling(false);
    }
    shell.context.clear();

    shell.events.emit("render");

    utils.nextFrame(render);
  }
  shell.updateShape();
  render();

  return shell;
}


var cp = require('child_process');
var util = require('./util');

function ZPool(info) {
    "use strict";

    var obj = Object.create({});
    if (typeof info === 'string') {
        info = info.split(/\s+/);
    }

    if (info.length !== 10) {
        return null;
    }

    obj.name = info[0];
    obj.size = info[1];
    obj.alloc = info[2];
    obj.free = info[3];
    obj.cap = info[4];
    obj.health = info[5];
    if (info[6] !== '-') {
        obj.altroot = info[6];
    }

    Object.freeze(obj);
    return obj;
}

var zpoolBin = util.findCmd('zpool');
function zpool(args, callback) {
    "use strict";

    cp.execFile(zpoolBin, args, {maxBuffer: 8000000}, function (err, stdout, stderr) {
        if (callback && typeof callback === 'function') {
            if (err) {
                err.message = util.compact(err.message.split('\n')).join('; ').trim();
                callback(err);
            } else {
                if (!stdout || stdout == '') stdout = 'Request succesfully fulfilled!';
                callback(null, stdout);
            }
        }
    });
}

/*
*
*  Creates a new storage pool containing the virtual devices specified
*  on the	command	line. The pool name must begin with a letter, and can
*  only contain alphanumeric characters as well as underscore ("_"),
*  dash ("-"), and period	(".").
*
*  zpool create -f datastore raidz /dev/vdb /dev/vdc /dev/vdd
*
*  PARAMS:
*  opts: {
*   options: { property: String, value: String } //all extra options you want to set for the new datastore, like quota,... (optional)
 *   OR:
 *   options = [ { property: String, value: String }, { property: String, value: String } ],
 *   mountpoint: String, //Optional, Sets the mount point for the root dataset. The	default	mount point is "/pool" or "altroot/pool" if altroot is specified.
 *   name: String, //must exist, the name of the datastore
 *   devices: String or Array //Must exist, a list or array containing the device paths and things like mirror and raidz. (see example above)
 *}
 *
 */

function create (opts, cb) {
    //

    "use strict";

    var params = [ 'create', '-f' ];

    /*
     immediately add some options to the create procedure
     done by adding argument -o option=value
     the opts.options parameter is an array of objects, or a single object

     opts.options = { property: String, value: String }

     OR:

     opts.options = [ { property: String, value: String }, { property: String, value: String } ]

     */

    if (opts.options) {
        if (opts.options.length) {
            //opts.options is an array
            for (var x =0; x < opts.options.length; x++) {
                params.push('-o', opts.options[x].property + "=" + opts.options[x].value);
            }
        } else {
            //opts.options is a single object
            params.push('-o', opts.options.property + "=" + opts.options.value);
        }
    }

    if (opts.mountpoint) {
        params.push('-m', opts.mountpoint);
    }

    params.push(opts.name);

    if (opts.devices.length) {
        params = params.concat(opts.devices);
    } else {
        var devices = opts.devices.split(/\s+/);
        params = params.concat(devices);
    }

    zpool(params, cb);
}

/*
*
*  Adds the specified virtual devices to the given pool.
*
*  PARAMS:
*  opts: {
*   name: String, //Must exist, the name of the pool to which to add the device
*   devices: String or Array //Must exist, a list of devices to add to the given pool.
* }
 */

function add (opts, cb) {
    "use strict";

    var params = [ 'add', '-f' ];

    params.push(opts.name);

    //devices is an array or a string of devices
    if (opts.devices.length) {
        params = params.concat(opts.devices);
    } else {
        var devices = opts.devices.split(/\s+/);
        params = params.concat(devices);
    }

    zpool(params, cb);
}

/*
*
* Sets the given property on the specified pool. See the zpool manpage for possible options and values
*
* PARAMS:
* opts: {
*   property: String //Must exist, which property to set
*   value: String //Must exits, the value for the described property
*   name: String //Must exist, the name of the pool on which to set the property
*}
*
 */

function set(opts, cb) {
    "use strict";

    var params = [ 'set' ];

    params.push(opts.property + "=" + opts.value);

    params.push(opts.name);

    zpool(params, cb);
}

/*
*
* Destroys the given pool, freeing up any devices for other use.
* This command tries to unmount any active datasets before destroying the pool.
*
* PARAMS:
* opts: {
*   name: String //Must exist, the name of the pool to destroy
* }
*
 */

function destroy(opts, cb) {
    "use strict";

    var params = [ 'destroy', '-f' ];

    params.push(opts.name);

    zpool(params, cb);
}

/*
*
* Lists the given pools along with a health status and space usage. If
* no pools are specified, all pools in the system are listed.
*
* PARAMS:
* opts: {
*   name: String, //optional, the name of the pool for which to list the status
* }
*
 */

function list(opts, cb) {
    //list the statistics from a (specific) pool. -o option is NOT available
    "use strict";

    if (typeof opts === 'function') {
        cb = opts;
        opts = undefined;
    }

    var params = ['list', '-H'];

    if (opts && opts.name) {
        params.push(opts.name);
    }

    zpool(params, function (err, stdout) {
        if (cb && typeof cb === 'function') {
            if (err) {
                cb(err);
                return;
            }
            var lines = util.compact(stdout.split('\n'));
            var list = lines.map(function (x) { return new ZPool(x); });
            cb(err, list);
        }
    });
}

/*
*
* Retrieves the given list of properties	(or all	properties if "all" is
* used) for the specified storage pool(s). These	properties are displayed
* with the following fields:
*   name	        Name of storage pool
*   property	    Property name
*   value	        Property value
*   source	        Property source, either 'default' or 'local'.
*
*
* PARAMS:
* opts: {
*  name: String //Optional, define a pool name for which to show the options
*  property: String //Optional, comma-separated list of properties to show. If not defined this
 *  function returns all options.
 * }
 */

function get(opts, cb) {
    "use strict";

    var params = [ 'get', '-pH' ];

    if (opts.property) {
        params.push(opts.property);
    } else {
        params.push('all');
    }

    if (opts.name) {
        params.push(opts.name);
    }

    zpool(params, function (err, stdout) {
        if (cb && typeof cb === 'function') {
            if (err) return cb(err);
            var lines = util.compact(stdout.split('\n'));
            var list = lines.map(function (x) { return new util.Property(x); });
            cb(err, list);
        }
    });
}

exports.get = get;
exports.set = set;
exports.list = list;
exports.destroy = destroy;
exports.create = create;
exports.add = add;


var fs = require('fs')
var rimraf = require('rimraf')
var tmpname = require('./tmpname');

module.exports = mktmpdir;

/**
 * Creates a temporary directory.
 *
 * The directory is created with 0700 permission.
 * Application should not change the permission to make the temporary directory accessible from other users.
 *
 * The prefix and suffix of the name of the directory is specified by
 * the optional first argument, prefixSuffix.
 * - If it is not specified or null, "d" is used as the prefix and no suffix is used.
 * - If it is a string, it is used as the prefix and no suffix is used.
 * - If it is an array, first element is used as the prefix and second element is used as a suffix.
 *
 *   mktmpdir(function(err, dir) { dir is ".../d..." });
 *   mktmpdir('foo', function(err, dir) { dir is ".../foo..." });
 *   mktmpdir(['foo', 'bar'], function(err, dir) { dir is ".../foo...bar" });
 *
 * The directory is created under os.tmpdir() or
 * the optional second argument tmpdir if non-null value is given.
 *
 *   mktmpdir(function(err, dir) { dir is os.tmpdir() + "/d..." });
 *   mktmpdir(null, '/var/tmp', function(err, dir) { dir is "/var/tmp/d..." });
 *
 * If a callback is invoked,
 * the path of the directory and its contents are removed.
 *
 *   mktmpdir(function(err, dir, done) {
 *     if (err) throw err;
 *     // use the directory...
 *     fs.open(dir + '/foo', 'w', function(err, fd) {
 *       done(err);
 *     });
 *   }, function(err) {
 *     // the directory has been removed.
 *   });
 *
 * If a callback is not invoked, mktmpdir doesn't remove the directory.
 *
 *   mktmpdir(function(err, dir) {
 *     if (err) throw err;
 *     // use the directory...
 *     fs.open(dir + '/foo', 'w', function(err, fd) {
 *       // remove the directory.
 *       exec('rm -rf ' + dir);
 *     });
 *   });
 *
 * @param {String|Array} prefix, or an array including prefix and suffix.
 * @param {String} base tmpdir
 * @param {Function} callback
 * @param {Function} onend
 * @api public
 */

function mktmpdir(prefixSuffix, tmpdir, callback, onend) {
  if ('function' == typeof prefixSuffix) {
    onend = tmpdir;
    callback = prefixSuffix;
    tmpdir = null;
    prefixSuffix = null;
  } else if ('function' == typeof tmpdir) {
    onend = callback;
    callback = tmpdir;
    tmpdir = null;
  }

  prefixSuffix = prefixSuffix || 'd';
  onend = onend || function() {};

  tmpname.create(prefixSuffix, tmpdir, function(err, path, next) {
    if (err) return callback(err);

    fs.mkdir(path, 0700, next);
  }, function(err, path) {
    if (err) return callback(err);

    callback(null, path, function(err) {
      if (!path) return onend(err);

      rimraf(path, function(_err) {
        onend(err || _err, path);
      });
    });
  });
}


var cp = require('child_process');
var util = require('./util');

var zfsBin = util.findCmd('zfs');
function zfs(args, callback) {
    "use strict";

    cp.execFile(zfsBin, args, {maxBuffer: 8000000}, function (err, stdout, stderr) {
        if (callback && typeof callback === 'function') {
            if (err) {
                err.message = util.compact(err.message.split('\n')).join('; ').trim();
                callback(err);
            } else {
                callback(null, stdout);
            }
        }
    });
}

function spawnzfs (args, callback) {
    "use strict";

    var child;
    try {
        child = cp.spawn(zfsBin, args);
    }
    catch (e) {
        return callback(e);
    }

    return callback(null, child);
}

function ZFS(info) {
    "use strict";

    var obj = Object.create({});
    if (typeof info === 'string') {
        info = info.split(/\s+/);
    }

    if (info.length !== 5) {
        return null;
    }

    obj.name = info[0];
    obj.used = util.parseNumber(info[1]);
    if (info[2] !== '-') { // Snapshots don't have 'avail' field.
        obj.avail = util.parseNumber(info[2]);
    }
    obj.refer = util.parseNumber(info[3]);
    obj.mountpoint = info[4];

    Object.freeze(obj);
    return obj;
}

function Property(info) {
    "use strict";

    var obj = Object.create({});
    if (typeof info === 'string') {
        info = info.split(/\t/, 4);
    }

    if (info.length !== 4) {
        return null;
    }

    obj.name = info[0];
    obj.property = info[1];
    obj.value = info[2];
    obj.source = info[3];

    Object.freeze(obj);
    return obj;
}

/*
*
* List the ZFS folders or datasets
*
* You have the optional opts parameter (if no opts defined, this function returns all the datasets)
*
* PARAMS:
* opts: {
*   type: string //Define a type of dataset to filter on (optional)
*   sort: string //property on which to sort the output list of datasets (optional)
*   name: string //a list of a specific dataset (optional)
*   recursive: boolean //recursively list datasets
*}
*
 */

function list(opts, cb) {
    "use strict";

    if (typeof opts === 'function') {
        cb = opts;
        opts = undefined;
    }

    var params = ['list', '-H'];

    if (opts && opts.type) {
        params.push('-t');
        params.push(opts.type);
    }

    if (opts && opts.sort) {
        params.push('-s');
        params.push(opts.sort);
    }

    if (opts && opts.recursive) {
        params.push('-r');
    }

    if (opts && opts.name) {
        params.push(opts.name);
    }

    zfs(params, function (err, stdout) {
        if (cb && typeof cb === 'function') {
            if (err) {
                cb(err);
                return;
            }
            var lines = util.compact(stdout.split('\n'));
            var list = lines.map(function (x) { return new ZFS(x); });
            cb(err, list);
        }
    });
}

/*
*
* Get the parameters of a specific dataset or all datasets
*
* PARAMS:
* opts: {
*   property: string //which property to show (must exist)
*   source: string //can be one of the following: local,default,inherited,temporary,none (optional)
*   name: string //the name of the dataset (optional)
*}
*
 */
function get(opts, cb) {
    "use strict";

    var params = [ 'get', '-pH' ];
    if (opts.source) {
        params.push('-s', opts.source);
    }

    params.push(opts.property);

    if (opts.name) {
      params.push(opts.name);
    }

    zfs(params, function (err, stdout) {
        if (cb && typeof cb === 'function') {
            if (err) return cb(err);
            var lines = util.compact(stdout.split('\n'));
            var list = lines.map(function (x) { return new util.Property(x); });
            cb(err, list);
        }
    });
}

/*
*
* Remove a dataset from the ZFS filesystem
*
* PARAMS:
* opts: {
*   recursive: Boolean //to make the destroy recursive (add the -r command to the zfs command) (optional)
*   name: string //The name of the dataset to destroy (must exist)
*}
*
 */

function destroy(opts, cb) {
    "use strict";

    var params = [ 'destroy' ];
    if (opts.recursive) {
        params.push('-r');
    }
    params.push(opts.name);

    zfs(params, cb);
}

/*
*
* Create a new dataset inside the ZFS filesystem
*
* PARAMS:
* opts: {
*   name: string //the name for the new dataset (must exist)
*   size: string //the size of the volume (optional)
*   options: { property: String, value: String } //all extra options you want to set for the new dataset, like quota,... (optional)
*   OR:
*   options = [ { property: String, value: String }, { property: String, value: String } ]
*
*}
 */

function create(opts, cb) {
    "use strict";

    var params = [ 'create' ];

    if (opts.options) {
        if (opts.options.length) {
            //opts.options is an array
            for (var x =0; x < opts.options.length; x++) {
                params.push('-o', opts.options[x].property + "=" + opts.options[x].value);
            }
        } else {
            //opts.options is a single object
            params.push('-o', opts.options.property + "=" + opts.options.value);
        }
    }

    if (opts.size) {
      params.push('-V', util.parseNumber(opts.size));
    }
    params.push(opts.name);

    zfs(params, cb);
}

/*
* Set a specific option for a given dataset
*
* PARAMS:
* opts: {
*   name: string //the name of the dataset for which to set the option (must exist)
*   property: string //which property to set (must exist)
*   value: string //which value to set for the property (must exist)
* }
*
 */

function set(opts, cb) {
    "use strict";

    var params = [ 'set' ];

    params.push(opts.property + "=" + opts.value);

    params.push(opts.name);

    zfs(params, cb);
}

/*
*
* Creates a snapshot with the given name.
*
* PARAMS:
* opts: {
*   name: string // the name of the snapshot (must exist)
*   dataset: string //the mountpoint of the snapshot (must exist)
*   recursive: boolean //if true the -r option is added to the zfs command (optional)
* }
*
 */

function snapshot(opts, cb) {
    "use strict";

    var params = [ 'snapshot' ];
    if (opts.recursive) {
        params.push('-r');
    }
    params.push(opts.dataset + '@' + opts.name);

    zfs(params, cb);
}

/*
*
*  Creates a clone of the given snapshot.
*  The target dataset can be  located  anywhere  in  the  ZFS  hierarchy, and is created as the same type as the original.
*
*  PARAMS:
*  opts: {
*   snapshot: string //the location of the snapshot. Follwing structure must be used: pool/project/production@today (must exist)
*   dataset: string //the name of the mount point (must exist)
* }
*
 */

function clone(opts, cb) {
    "use strict";

    var params = [ 'clone' ];
    params.push(opts.snapshot, opts.dataset);

    zfs(params, cb);
}

/*
*
*  Mount the specified dataset/all datasets to the mountpoint
*
*  PARAMS:
*  opts: {
*    dataset: string // the name of the zfs dataset. if the dataset is null, then mount all datasets with '-a'
*    overlay: boolean // whether use overlay mode
*    options: [string, string, ...] // the temporal properties set for the mount duration,
*                                      such as ro/rw for readonly and readwrite (optional)
*  }
 */
function mount(opts, cb) {
    "use strict";

    var params = [ 'mount' ];

    if (opts.overlay) {
        params.push('-O');
    }

    if (opts.options) {
        if (opts.options.length) {
            //opts.options is an array
            for (var x =0; x < opts.options.length; x++) {
                params.push('-o', opts.options[x]);
            }
        } else {
            //opts.options is a single object, callback err and return
            cb({error:'invalid argu: the options should be a string array'});
            return;
        }
    }

    if (opts.dataset) {
        params.push(opts.dataset);
    } else {
        params.push('-a');
    }

    zfs(params, cb);
}

/*
*
*  Unmount the specified filesystem|mountpoint
*
*  PARAMS:
*  opts: {
*    name: string // the name of the zfs dataset or the path of the mountpoint.
*                    if the dataset is null, then unmount all available filesystems with '-a'
*    force: boolean // whether forcely unmount even if the filesystem is still in use.
*  }
 */
function unmount(opts, cb) {
    "use strict";

    var params = [ 'unmount' ];

    if (opts.force) {
        params.push('-f');
    }

    if (opts.name) {
        params.push(opts.name);
    } else {
        params.push('-a');
    }

    zfs(params, cb);
}

/*
*
*  Initiates a send operation of a given snapshot
*
*  Callback contains a readable stream that is the sendStream
*  Signature : zfs.send(opts, function (err, sendStream) {});
*  Events :  sendStream.on('error', function (err) {});
*            sendStream.on('verbose', function (data) {});
*
*  PARAMS:
*  opts: {
*   snapshot: string //the location of the snapshot. Follwing structure must be used: pool/project/production@today (must exist)
*   replication: boolean //create a replication stream
*   deduplicate: boolean //create a deduplicated stream
*   properties: boolean //send dataset properties with the stream
*   noop: boolean //don't actually do anything
*   parseable : boolean //output in machine readable format
*   verbose : boolean //emit verbose events containing the contents of stderr
*   incremental : snapshot //do an incremental send. the snapshot referred to here is the from snapshot
*   intermediary : boolean //only applies when incremental is set. Include intermediary snapshots with the stream
* }
*
 */

function send(opts, cb) {
    "use strict";

    var params = [ 'send' ];
    if (opts.replication) {
        params.push('-R');
    }

    if (opts.deduplicate) {
        params.push('-D');
    }

    if (opts.properties) {
        params.push('-p');
    }

    if (opts.noop) {
        params.push('-n');
    }

    if (opts.parsable) {
        params.push('-P');
    }

    if (opts.verbose) {
        params.push('-v');
    }

    if (opts.incremental) {
        if (opts.intermediary) {
            params.push('-I');
        }
        else {
            params.push('-i');
        }

        params.push(opts.incremental);
    }

    params.push(opts.snapshot);

    spawnzfs(params, function (err, child) {
        if (err) {
            return cb(err);
        }

        var buffer = [];
        var sendStream = child.stdout;

        child.stderr.on('data', function (data) {
            data = data.toString();
            buffer.push(data);

            if (opts.verbose) {
                sendStream.emit('verbose', data);
            }

            //only keep last 5 lines
            if (buffer.length > 5) {
                buffer.shift();
            }
        });

        child.once('exit', function (code) {
            if (code !== 0) {
                var message = 'Send Error:' + util.compact(buffer.join('\n').split('\n')).join('; ').trim();
                var err = new Error(message);
                err.code = code;

                sendStream.emit('error', err);
            }
        });

        return cb(null, sendStream);
    });
}

/*
*
*  Initiates a receive of a snapshot
*
*  Callback is the writable stream
*
*  PARAMS:
*  opts: {
*   snapshot: string //the location of the snapshot. Follwing structure must be used: pool/project/production@today (must exist)
*   verbose: boolean //if true, the receiveStream will emit 'verbose' events containing the output of stderr
*   noop: boolean //if true, zfs will not actually receive anything
*   force: boolean //if true, dataset will be rolled back to most recent snapshot
*   unmounted : boolean //if true, dataset will not be mounted after receive
*   d : boolean //discard first element of received dataset's name
*   e : boolean //discard all elements except last of received dataset's name
* }
*
 */

function receive(opts, cb) {
    "use strict";

    var params = [ 'receive' ];
    if (opts.verbose) {
        params.push('-v');
    }

    if (opts.noop) {
        params.push('-n');
    }

    if (opts.force) {
        params.push('-F');
    }

    if (opts.unmounted) {
        params.push('-u');
    }

    if (opts.d) {
        params.push('-d');
    }

    if (opts.e) {
        params.push('-e');
    }

    params.push(opts.dataset);

    spawnzfs(params, function (err, child) {
        if (err) {
            return cb(err);
        }

        var buffer = [];
        var receiveStream = child.stdin;

        child.stderr.on('data', function (data) {
            data = data.toString();
            buffer.push(data);

            if (opts.verbose) {
                receiveStream.emit('verbose', data);
            }

            //only keep last 5 lines
            if (buffer.length > 5) {
                buffer.shift();
            }
        });

        child.once('exit', function (code) {
            if (code !== 0) {
                var message = 'Receive Error: ' + util.compact(buffer.join('\n').split('\n')).join('; ').trim();
                var err = new Error(message);
                err.code = code;

                receiveStream.emit('error', err);
            }
        });

        return cb(null, receiveStream);
    });
}

exports.get = get;
exports.set = set;
exports.list = list;
exports.destroy = destroy;
exports.create = create;
exports.snapshot = snapshot;
exports.clone = clone;
exports.mount = mount;
exports.unmount = unmount;
exports.send = send;
exports.receive = receive;


'use strict';

var events = require('node-events');
var merge = require('merge');
var util = require('util');

// This is not valid for simpleStrings
function copyString(buffer, length, offsetBegin, offsetEnd) {
  if (length > 2048) {
    return buffer.toString('utf-8', offsetBegin, offsetEnd);
  }

  var string = '';
  while(offsetBegin < offsetEnd) {
    string += String.fromCharCode(buffer[offsetBegin++]);
  }
  return string;
}

// This is not UTF-8 compliant
function parseSimpleString(parser) {
  var offset = parser.offset;
  var length = parser.buffer.length;
  var string = '';

  while (offset < length) {
    var c1 = parser.buffer[offset++];
    if (c1 === 13) {
      var c2 = parser.buffer[offset++];
      if (c2 === 10) {
        parser.offset = offset;
        return string;
      }
      string += String.fromCharCode(c1) + String.fromCharCode(c2);
      continue;
    }
    string += String.fromCharCode(c1);
  }
  return undefined;
}

function parseLength(parser) {
  var string = parseSimpleString(parser);
  if (string !== undefined) {
    var length = parseInt(string, 10);
    if (length === -1) {
      return null;
    }
    return length;
  }
}

function parseBulkString(parser) {
  var length = parseLength(parser);
  /* jshint eqnull: true */
  if (length == null) {
    return length;
  }
  var offsetEnd = parser.offset + length;
  if ((offsetEnd + 2) > parser.buffer.length) {
    return;
  }

  var offsetBegin = parser.offset;
  parser.offset = offsetEnd + 2;

  return copyString(parser.buffer, length, offsetBegin, offsetEnd);
}

function parseArray(parser) {
  var length = parseLength(parser);
  /* jshint eqnull: true */
  if (length == null) {
    return length;
  }

  var responses = new Array(length);
  var bufferLength = parser.buffer.length;
  for (var i = 0; i < length; i++) {
    if (parser.offset >= bufferLength) {
      return;
    }
    var response = parseType(parser, parser.buffer[parser.offset++]);
    if (response === undefined) {
      return;
    }
    responses[i] = response;
  }

  return responses;
}

function parseInteger(parser) {
  var string = parseSimpleString(parser);
  if (string !== undefined) {
    return parseInt(string, 10);
  }
}

function parseError(parser) {
  var string = parseSimpleString(parser);
  if (string !== undefined) {
    return new Error(string);
  }
}

function handleError(parser, error) {
  if (!parser.emit('error', error)) {
    throw error;
  }
}

function parseType(parser, type) {
  /* jshint maxcomplexity: 6 */
  switch (type) {
    case 43: // +
      return parseSimpleString(parser);
    case 36: // $
      return parseBulkString(parser);
    case 42: // *
      return parseArray(parser);
    case 58: // :
      return parseInteger(parser);
    case 45: // -
      return parseError(parser);
    default:
      return handleError(parser, new Error('Unexpected type: ' + type));
  }
}

function appendBuffer(parser, buffer) {
  var oldLength = parser.buffer.length;
  var remainingLength = oldLength - parser.offset;
  var newLength = remainingLength + buffer.length;
  if (newLength > parser.options.maxBufferLength) {
    handleError(parser, new Error('Maximum buffer length exceeded'));
    return;
  }
  var newBuffer = new Buffer(newLength);
  parser.buffer.copy(newBuffer, 0, parser.offset, oldLength);
  buffer.copy(newBuffer, remainingLength, 0, buffer.length);
  parser.buffer = newBuffer;
  parser.offset = 0;
}

var DEFAULT_OPTIONS = {
  maxBufferLength: 16777216
};

function ResponseParser(options) {
  this.options = merge(DEFAULT_OPTIONS, options);

  this.buffer = null;
  this.offset = 0;

  events.EventEmitter.call(this);
}
util.inherits(ResponseParser, events.EventEmitter);

ResponseParser.DEFAULT_OPTIONS = DEFAULT_OPTIONS;

ResponseParser.prototype.parse = function (buffer) {
  if (this.buffer === null) {
    this.buffer = buffer;
    this.offset = 0;
  } else {
    appendBuffer(this, buffer);
  }

  var length = this.buffer.length;
  while (this.offset < length) {
    var offset = this.offset;
    var response = parseType(this, this.buffer[this.offset++]);
    if (response === undefined) {
      this.offset = offset;
      return;
    }

    this.emit('response', response);
  }

  this.buffer = null;
};

exports.ResponseParser = ResponseParser;

exports.createRequestString = function () {
  var length = arguments.length;
  var request = '*' + length + '\r\n';
  for (var i = 0; i < length; i++) {
    if (typeof arguments[i] === 'string') {
      request += '$' + arguments[i].length + '\r\n' + arguments[i] + '\r\n';
    } else {
      var string = '' + arguments[i];
      request += '$' + string.length + '\r\n' + string + '\r\n';
    }
  }
  return request;
};


const webpack = require('webpack');
const path = require('path');
const assign = require('object-assign');
const nodeExternals = require('webpack-node-externals');
const CleanWebpackPlugin = require('clean-webpack-plugin');

const LIBRARY_DESC = require('./package.json').library;

const UglifyJsPlugin = webpack.optimize.UglifyJsPlugin;

module.exports = function getConfig(optionsArg) {
  const options = optionsArg || {};

  const isProd = (options.BUILD_ENV || process.env.BUILD_ENV) === 'PROD';
  const isWeb = (options.TARGET_ENV || process.env.TARGET_ENV) === 'WEB';

  // get library details from JSON config
  const libraryName = LIBRARY_DESC.name;

  // determine output file name
  const outputName = buildLibraryOutputName(LIBRARY_DESC, isWeb, isProd);
  const outputFolder = isWeb ? 'dist' : 'lib';

  // get base config
  let config;

  // for the web
  if (isWeb) {
    config = assign(getBaseConfig(isProd), {
      output: {
        path: path.join(__dirname, outputFolder),
        filename: outputName,
        library: libraryName,
        libraryTarget: 'umd',
        umdNamedDefine: true,
      },
    });
  } else {
    // for the backend
    config = assign(getBaseConfig(isProd), {
      output: {
        path: path.join(__dirname, outputFolder),
        filename: outputName,
        library: libraryName,
        libraryTarget: 'commonjs2',
      },
      target: 'node',
      node: {
        __dirname: true,
        __filename: true,
      },
      externals: [nodeExternals()],
    });
  }

  config.plugins.push(new CleanWebpackPlugin([outputFolder]));

  return config;
};

/**
 * Build base config
 * @param  {Boolean} isProd [description]
 * @return {[type]}         [description]
 */
function getBaseConfig(isProd) {
  // get library details from JSON config
  const libraryEntryPoint = path.join('src', LIBRARY_DESC.entry);

  // generate webpack base config
  return {
    entry: [
      // "babel-polyfill",
      path.join(__dirname, libraryEntryPoint),
    ],
    output: {
      devtoolLineToLine: true,
      pathinfo: true,
    },
    module: {
      preLoaders: [
        {
          test: /\.js$/,
          exclude: /(node_modules|bower_components)/,
          loader: "eslint-loader",
        },
      ],
      loaders: [
        {
          exclude: /(node_modules|bower_components)/,
          loader: "babel-loader",
          plugins: [
            "transform-runtime",
          ],
          query: {
            presets: [
              "es2015",
              "stage-0",
              "stage-1",
              "stage-2",
            ],
            cacheDirectory: false,
          },
          test: /\.js$/,
        },
      ],
    },
    eslint: {
      configFile: './.eslintrc',
    },
    resolve: {
      root: path.resolve('./src'),
      extensions: ['', '.js'],
    },
    devtool: isProd ? "source-map"/* null*/ : "source-map"/* '#eval-source-map'*/,
    debug: !isProd,
    plugins: isProd ? [
      new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"production"' } }),
      new UglifyJsPlugin({
        compress: { warnings: true },
        minimize: true,
        sourceMap: true,
      }),
      // Prod plugins here
    ] : [
      new webpack.DefinePlugin({ 'process.env': { NODE_ENV: '"development"' } }),
      new UglifyJsPlugin({
        compress: { warnings: true },
        minimize: true,
        sourceMap: true,
      }),
      // Dev plugins here
    ],
  };
}

function buildLibraryOutputName(libraryDesc, isWeb, isProd) {
  if (isWeb) {
    return libraryDesc["dist-web"] || [libraryDesc.name, 'web', (isProd ? 'min.js' : 'js')].join('.');
  }
  return libraryDesc["dist-node"] || [libraryDesc.name, (isProd ? 'min.js' : 'js')].join('.');
}


/**
 * Basic is a wrapper over updates from Telegram Bot API;
 * This module adopts an incoming updates from Telegram Bot API
 * and checks them to before distribution of between own methods.
 *
 * @module telekit/basic
 * @author Denis Maslennikov <mrdenniska@gmail.com> (nofach.com)
 * @license MIT
 */

/** Dependencies */
const { EventEmitter } = require('events');
const isArrow = require('isarrow');
const isClass = require('isclass');
const debug = require('debug')('telekit:basic');

/**
 * Methods
 * @private
 */

/**
 * Returns info about raw updates from Telegram Bot API
 *
 * @param  {Object} update - Raw incoming update
 * @return {Object}
 */
function getUpdateInfo(update) {
    /** Is it chosen inline or edited message/post */
    let isChosen = false;
    let isEdited = false;

    /** Raw and formatted names of update */
    let original = '';
    let name = '';

    if (update.message) {
        name = 'message';
        original = 'message';
    } else if (update.edited_message) {
        isEdited = true;
        original = 'edited_message';
        name = 'message';
    } else if (update.channel_post) {
        original = 'channel_post';
        name = 'post';
    } else if (update.edited_channel_post) {
        isEdited = true;
        original = 'edited_channel_post';
        name = 'post';
    } else if (update.inline_query) {
        original = 'inline_query';
        name = 'inline';
    } else if (update.chosen_inline_result) {
        isChosen = true;
        original = 'chosen_inline_result';
        name = 'inline';
    } else if (update.callback_query) {
        original = 'callback_query';
        name = 'callback';
    }

    return { isChosen, isEdited, original, name };
}

/** Implementation */
class Basic extends EventEmitter {
    constructor() {
        super();

        /** Store of middlewares */
        this.middlewares = [];
    }

    /**
     * To spread params between middleware with method
     *
     * @param  {String}   method -
     * @param  {...[Any]} params -
     * @return {Promise}
     */
    dispatch(method, ...params) {
        return new Promise((resolve, reject) => {
            const handles = this.middlewares.keys();
            const fn = () => {
                const result = handles.next();
                if (result.done) return resolve(true);
                if (!this.middlewares[result.value][method]) return fn();

                try {
                    this.middlewares[result.value][method](...params, fn);
                } catch (error) {
                    reject(error);
                }

                return false;
            };

            debug('dispatch params to %s method from middlewares', method);
            fn();
        });
    }

    /**
     * Takes a raw update for processing and
     * distribution of between the middlewares
     *
     * @param  {Object} update - Raw incoming update
     * @return {Helper}
     */
    fetch(update) {
        const info = getUpdateInfo(update);

        debug('update: %s', info.original);
        debug('%s: %O', info.name, update[info.original]);

        if (this.handle) this.handle(update, info);
        this.emit('handle', update, info);

        return this;
    }

    /**
     * Register middleware(s) in queue stack for updates
     *
     * @param  {...[Class|Function]} params - middleware(s)
     * @return {Helper}
     */
    use(...params) {
        let handle = null;

        /** Params is empty */
        if (params.length === 0) {
            throw new Error('Params should not be empty.');
        }

        this.emit('beforeUse', this, this.middleware);
        params.forEach((param) => {
            /** Param is array */
            if (Array.isArray(param)) {
                this.use(...param);
                return;
            }

            /** Param is function */
            if (typeof param === 'function') {
                /** Param is class */
                if (isClass(param) && !isArrow(param)) {
                    /* eslint new-cap: "off" */
                    handle = new param(this);
                } else {
                    handle = param(this);
                }
            }

            /** Add handle into stack(if it there's) */
            if (handle) {
                debug('added %s into stack', param.name || '_');
                this.middlewares.push(handle);
            }

            handle = undefined;
        });

        this.emit('afterUse', this, this.middlewares);
        return this;
    }
}

/** Exports */
module.exports = Basic;


/**
 * Stream (as readable or writeable) buffers, typead arrays, strings
 *
 * @author ukrbublik
 */

const Readable = require('stream').Readable;
const Writable = require('stream').Writable;
const assert = require('assert');

function _toBuffer (buf) {
  if(buf instanceof Buffer) {
    buf = buf;
  } else if (typeof buf == 'string') {
    buf = Buffer.from(buf);
  } else if(buf.buffer && buf.buffer instanceof ArrayBuffer) {
    //it's typed array
    buf = Buffer.from(buf.buffer, buf.byteOffset, buf.byteLength);
  } else {
    throw new Error("Unknown type of buf: " +
      (typeof buf == 'object' ? buf.constructor.name : typeof buf));
  }
  return buf;
}

class ReadBufferStream extends Readable {

  /**
   * To stream from 1 buffer use constructor(buf), callback - use event 'end'
   * To stream from N buffers user constructor(null) + N times addBuffer(buf, callback)
   */
  constructor (buf = null, info = null, options = {}) {
    let opts = {};
    opts.encoding = null;
    opts.highWaterMark = ReadBufferStream.highWaterMark;
    opts.objectMode = false;
    options.isMultiBuffers = (buf === null);
    Object.assign(opts, options);
    super(opts);
    this.options = opts;

    this.id = ++ReadBufferStream.lastId;
    if (info)
      this.info = info;
    this.bufOffset = 0;
    this.totalOffset = 0;
    this.buf = null;
    this.totalLength = 0;

    if (this.options.isMultiBuffers) {
      this.nextBuffers = [];
      this.nextCallbacks = [];
    } else {
      this.buf = _toBuffer(buf);
      this.totalLength += this.buf.length;
    }
  }

  addBuffer (buf, onBufProcessed = null) {
    if (this.isDone())
      return false;
    if (!this.options.isMultiBuffers)
      throw new Error("It's not multi-buffer stream");
    buf = _toBuffer(buf);
    this.nextBuffers.push(buf);
    this.nextCallbacks.push(onBufProcessed);
    this.totalLength += buf.length;
    return true;
  }

  isStarted() {
    return this.buf !== null && this.bufOffset != 0;
  }
  isDone() {
    return this.availableBytesLength() <= 0;
  }
  availableBytesLength () {
    return (this.totalLength - this.totalOffset);
  }
  toString() {
    return "ReadBufferStream #" + this.id + " ("+this.totalLength+" bytes)"
      + (this.info ? ' '+JSON.stringify(this.info) : '');
  }
  getBuf() {
    return this.buf;
  }

  _read (size) {
    if (1)
      this.__readInLoop (size);
    else
      this.__readByTicks (size);
  }

  __readByTicks (size) {
    let bytesToRead = 0,
      readMore = true,
      bufferToPush = null,
      onBufProcessed;
    size = size || this.options.highWaterMark;

    if (this.options.isMultiBuffers && this.buf === null && this.nextBuffers.length)
      this.buf = this.nextBuffers.shift();

    if (this.isDone()) {
      this.push(null);
    } else {
      bytesToRead = Math.min(this.buf.length - this.bufOffset, size);
      bufferToPush = this.buf.slice(this.bufOffset, this.bufOffset + bytesToRead);
      readMore = this.push(bufferToPush);
      this.totalOffset += bytesToRead;
      this.bufOffset += bytesToRead;
      if (this.bufOffset == this.buf.length) {
        if (this.nextCallbacks) {
          onBufProcessed = this.nextCallbacks.shift();
          if (onBufProcessed)
            onBufProcessed(this.buf);
        }
        if (this.options.isMultiBuffers && this.nextBuffers.length) {
          this.buf = this.nextBuffers.shift();
          this.bufOffset = 0;
        }
      }
      if (this.isDone()) {
        this.push(null);
      } else if (readMore) {
        process.nextTick(() => { this.__readByTicks(size) });
      }
    }
  }

  __readInLoop (size) {
    let bytesToRead = 0,
      readMore = true,
      bufferToPush = null,
      onBufProcessed;
    size = size || this.options.highWaterMark;

    if (this.options.isMultiBuffers && this.buf === null && this.nextBuffers.length)
      this.buf = this.nextBuffers.shift();

    while (!this.isDone()) {
      bytesToRead = Math.min(this.buf.length - this.bufOffset, size);
      bufferToPush = this.buf.slice(this.bufOffset, this.bufOffset + bytesToRead);
      readMore = this.push(bufferToPush);
      this.totalOffset += bytesToRead;
      this.bufOffset += bytesToRead;
      if (this.bufOffset == this.buf.length) {
        if (this.nextCallbacks) {
          onBufProcessed = this.nextCallbacks.shift();
          if (onBufProcessed)
            onBufProcessed(this.buf);
        }
        if (this.options.isMultiBuffers && this.nextBuffers.length) {
          this.buf = this.nextBuffers.shift();
          this.bufOffset = 0;
        }
      }
      if (!readMore)
        break;
    }
    if (this.isDone()) {
      this.push(null);
    }
  }
}

ReadBufferStream.lastId = -1;

//--------------

class WriteBufferStream extends Writable {

  /**
   * To stream to 1 buffer use constructor(buf), callback - use event 'finish'
   * To stream to N buffers user constructor(null) + N times addBuffer(buf, callback)
   */
  constructor (buf = null, info = null, options = {}) {
    let opts = {};
    opts.decodeStrings = true;
    opts.highWaterMark = WriteBufferStream.highWaterMark;
    opts.objectMode = false;
    options.isMultiBuffers = (buf === null);
    Object.assign(opts, options);
    super(opts);
    this.options = opts;

    this.id = ++WriteBufferStream.lastId;
    if (info)
      this.info = info;
    this.bufOffset = 0;
    this.totalOffset = 0;
    this.restBuffer = null;
    this.buf = null;
    this.totalLength = 0;

    if (this.options.isMultiBuffers) {
      this.nextBuffers = [];
      this.nextCallbacks = [];
    } else {
      this.buf = _toBuffer(buf);
      this.totalLength += this.buf.length;
    }
  }

  addBuffer (buf, onBufProcessed = null) {
    if (this.isDone())
      return false;
    if (!this.options.isMultiBuffers)
      throw new Error("It's not multi-buffer stream");
    buf = _toBuffer(buf);
    this.nextBuffers.push(buf);
    this.nextCallbacks.push(onBufProcessed);
    this.totalLength += buf.length;
    return true;
  }

  isStarted() {
    return this.buf !== null && this.bufOffset != 0;
  }
  isDone() {
    return this.availableBytesLength() <= 0;
  }
  availableBytesLength() {
    return (this.totalLength - this.totalOffset);
  }
  toString() {
    return "WriteBufferStream #" + this.id + " ("+this.totalLength+" bytes)"
      + (this.info ? ' '+JSON.stringify(this.info) : '');
  }
  getBuf() {
    return this.buf;
  }
  hasRestBuffer() {
    return (this.restBuffer !== null);
  }
  getRestBuffer() {
    return this.restBuffer;
  }
  clearRestBuffer() {
    this.restBuffer = null;
  }

  _write (chunk, encoding, callback) {
    let bytedCopied = 0,
      bytesToCopy = 0,
      bufferToCopy = null,
      bytesLeft = 0,
      onBufProcessed;

    if (this.options.isMultiBuffers && this.buf === null && this.nextBuffers.length)
      this.buf = this.nextBuffers.shift();

    do {
      bytesToCopy = Math.min(this.buf.length - this.bufOffset, chunk.length);
      bytesLeft = chunk.length - bytesToCopy;
      if (bytesToCopy > 0) {
        bufferToCopy = this.buf.slice(this.bufOffset, this.bufOffset + bytesToCopy);
        bytedCopied = chunk.copy(bufferToCopy, 0, 0, bytesToCopy);
         this.totalOffset += bytedCopied;
        this.bufOffset += bytedCopied;
      }
      if (this.bufOffset == this.buf.length) {
        if (this.nextCallbacks) {
          onBufProcessed = this.nextCallbacks.shift();
          if (onBufProcessed)
            onBufProcessed(this.buf);
        }
        if (this.options.isMultiBuffers && this.nextBuffers.length) {
          this.buf = this.nextBuffers.shift();
          this.bufOffset = 0;
        }
      }
    } while (bytesLeft && this.bufOffset < this.buf.length);

    if (bytesLeft) {
      assert(this.isDone());
      let restBuf = chunk.slice(bytesToCopy, bytesToCopy + bytesLeft);
      this.restBuffer = restBuf; //no copy
      //this.restBuffer = Buffer.from(restBuf); //copy
    }

    if (callback)
      callback(null);
    if (this.isDone()) {
      this.end();
    }
  };

}

WriteBufferStream.lastId = -1;

//--------------

function attachStreamToSocket(readStream, socketWriteStream, callback, usePipe = false) {
  if (!usePipe) {
    _attachStreamToSocket(readStream, socketWriteStream, callback);
  } else {
    readStream.pipe(socketWriteStream, { end: false });
    readStream.once('end', () => {
      readStream.unpipe();
      callback();
    });
  }
}

function _attachStreamToSocket(readStream, socketWriteStream, callback, callbackOnFlush = false) {
  let canWrite = true,
    canRead = false,
    chunksToWrite = 0,
    chunksFlushed = 0,
    readEnd = false;

  let callbackWhenDone = () => {
    if (readEnd && (callbackOnFlush ? chunksToWrite == chunksFlushed : true)) {
      callback();
    }
  };

  let readAndWrite = () => {
    if (!canWrite)
      return; //wait for drain
    let chunk;
    while (canRead && canWrite) {
      chunk = readStream.read();
      if(chunk === null) {
        canRead = false;
      } else {
        chunksToWrite++;
        canWrite = socketWriteStream.write(chunk, null, () => {
          chunksFlushed++;
          if (callbackOnFlush)
            callbackWhenDone();
        });
        if (!canWrite) {
          socketWriteStream.once('drain', () => {
            canWrite = true;
            if (canRead)
              readAndWrite();
          });
        }
      }
    }
    if (!callbackOnFlush)
      callbackWhenDone();
  };

  readStream.on('readable', () => {
    canRead = true;
    readAndWrite();
  });

  readStream.once('end', () => {
    readEnd = true;
    callbackWhenDone();
  });
}

function attachSocketToStream(socketReadStream, writeStream, firstChunk = null, callback, usePipe = false) {
  if (!usePipe) {
    _attachSocketToStream(socketReadStream, writeStream, firstChunk, callback);
  } else {
    socketReadStream.pipe(writeStream, { end: true });
    if (firstChunk) {
      socketReadStream.unshifting = true;
      socketReadStream.unshift(firstChunk);
      socketReadStream.unshifting = false;
    }
    writeStream.once('finish', () => {
      socketReadStream.unpipe();
      let restBuffer = null;
      if (writeStream instanceof WriteBufferStream) {
        if (writeStream.hasRestBuffer()) {
          restBuffer = writeStream.getRestBuffer();
          writeStream.clearRestBuffer();
        }
      }
      callback(restBuffer);
    });
  }
}

function _attachSocketToStream(socketReadStream, writeStream, firstChunk = null, callback) {
  let writeEnd = false,
    canWrite = true,
    canRead = false,
    restBuffer = null;

  let readAndWrite = (_firstChunk = null) => {
    if (!canWrite)
      return; //wait for drain
    if (writeEnd)
      return;
    let isLastChunk = false,
      chunk = null,
      bytesToRead = null,
      availBytesToWrite = null;
    while (canRead && canWrite && !isLastChunk) {
      if (writeStream instanceof WriteBufferStream) {
        availBytesToWrite = writeStream.availableBytesLength();
        if (_firstChunk) {
          bytesToRead = Math.min(availBytesToWrite, _firstChunk.length);
        } else {
          bytesToRead = Math.min(availBytesToWrite,
            (socketReadStream.bufferSize ? socketReadStream.bufferSize : 64*1024));
        }
        isLastChunk = (bytesToRead == availBytesToWrite);
      }
      if (_firstChunk) {
        if (availBytesToWrite && availBytesToWrite < _firstChunk.length) {
          //get part of first chunk and set rest
          chunk = _firstChunk.slice(0, bytesToRead);
          restBuffer = _firstChunk.slice(bytesToRead, _firstChunk.length);
        } else {
          //just get full first chunk
          chunk = _firstChunk;
          restBuffer = null;
        }
      } else {
        //read chunk
        chunk = socketReadStream.read(bytesToRead);
        if (!chunk)
          canRead = false;
      }
      if (chunk) {
        //write
        if (isLastChunk) {
          writeStream.end(chunk);
          writeEnd = true;
        } else {
          canWrite = writeStream.write(chunk);
          if (!canWrite) {
            writeStream.once('drain', () => {
              canWrite = true;
              if (canRead)
                readAndWrite();
            });
          }
        }
      }
      if (_firstChunk) {
        _firstChunk = null;
        break;
      }
    }
  };

  writeStream.once('finish', () => {
    writeEnd = true;
    callback(restBuffer);
  });

  socketReadStream.pause();

  if (firstChunk) {
    canRead = true;
    readAndWrite(firstChunk);
    //if we are already done, now writeEnd = true
  }

  if (!writeEnd) {
    socketReadStream.on('readable', () => {
      canRead = true;
      readAndWrite();
    });

    //manual start w/o waiting for 'readable'
    canRead = true;
    readAndWrite();
  }
}

//--------------

WriteBufferStream.highWaterMark = 128*1024;
ReadBufferStream.highWaterMark = 128*1024;

module.exports.ReadBufferStream = ReadBufferStream;
module.exports.WriteBufferStream = WriteBufferStream;
module.exports.attachStreamToSocket = attachStreamToSocket;
module.exports.attachSocketToStream = attachSocketToStream;