'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _promise = require('babel-runtime/core-js/promise');

var _promise2 = _interopRequireDefault(_promise);

var _defineProperty = require('babel-runtime/core-js/object/define-property');

var _defineProperty2 = _interopRequireDefault(_defineProperty);

var _setPrototypeOf = require('babel-runtime/core-js/object/set-prototype-of');

var _setPrototypeOf2 = _interopRequireDefault(_setPrototypeOf);

var _create = require('babel-runtime/core-js/object/create');

var _create2 = _interopRequireDefault(_create);

var _getPrototypeOf = require('babel-runtime/core-js/object/get-prototype-of');

var _getPrototypeOf2 = _interopRequireDefault(_getPrototypeOf);

var _assign = require('babel-runtime/core-js/object/assign');

var _assign2 = _interopRequireDefault(_assign);

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; (0, _defineProperty2.default)(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

exports.parseStream = parseStream;
exports.parseFile = parseFile;
exports.parseStringSync = parseStringSync;
exports.formatSync = formatSync;
exports.formatStream = formatStream;
exports.formatFile = formatFile;

var _parse = require('./parse');

var _parse2 = _interopRequireDefault(_parse);

var _util = require('./util');

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = (0, _create2.default)(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) _setPrototypeOf2.default ? (0, _setPrototypeOf2.default)(subClass, superClass) : subClass.__proto__ = superClass; }

var _require = require('stream'),
    Transform = _require.Transform;

var Decoder = require('string_decoder').StringDecoder;

// don't load fs native module if running in webpacked code
var fs = typeof __webpack_require__ !== 'function' ? require('fs') : null;

// call a callback on the next process tick if running in
// an environment that supports it
function _callback(callback) {
  if (process && process.nextTick) process.nextTick(callback);else callback();
}

// shared arg processing for the parse routines
function _processParseOptions(options) {
  var additionalDefaults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  var out = (0, _assign2.default)({
    parseFeatures: true,
    parseDirectives: false,
    parseSequences: true,
    parseComments: false
  }, additionalDefaults, options);

  if (options.parseAll) {
    out.parseFeatures = true;
    out.parseDirectives = true;
    out.parseComments = true;
    out.parseSequences = true;
  }

  return out;
}

var GFFTransform = function (_Transform) {
  _inherits(GFFTransform, _Transform);

  function GFFTransform() {
    var inputOptions = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

    _classCallCheck(this, GFFTransform);

    var options = _processParseOptions(inputOptions);

    var _this = _possibleConstructorReturn(this, (GFFTransform.__proto__ || (0, _getPrototypeOf2.default)(GFFTransform)).call(this, { objectMode: true }));

    _this.encoding = inputOptions.encoding || 'utf8';

    _this.decoder = new Decoder();
    _this.textBuffer = '';

    var push = _this.push.bind(_this);
    _this.parser = new _parse2.default({
      featureCallback: options.parseFeatures ? push : null,
      directiveCallback: options.parseDirectives ? push : null,
      commentCallback: options.parseComments ? push : null,
      sequenceCallback: options.parseSequences ? push : null,
      errorCallback: function errorCallback(err) {
        return _this.emit('error', err);
      },
      bufferSize: options.bufferSize
    });
    return _this;
  }

  _createClass(GFFTransform, [{
    key: '_addLine',
    value: function _addLine(data) {
      var line = data.toString('utf8');
      if (line) {
        this.parser.addLine(line);
      }
    }
  }, {
    key: '_nextText',
    value: function _nextText(buffer) {
      var _this2 = this;

      var pieces = (this.textBuffer + buffer).split(/\r?\n/);
      this.textBuffer = pieces.pop();

      if (this.maxLineLength && this.textBuffer.length > this.maxLineLength) {
        this.emit('error', new Error('maximum line size exceeded'));
        return;
      }

      pieces.forEach(function (piece) {
        return _this2._addLine(piece);
      });
    }
  }, {
    key: '_transform',
    value: function _transform(chunk, encoding, callback) {
      this._nextText(this.decoder.write(chunk));
      _callback(callback);
    }
  }, {
    key: '_flush',
    value: function _flush(callback) {
      if (this.decoder.end) this._nextText(this.decoder.end());
      if (this.textBuffer != null) this._addLine(this.textBuffer);
      this.parser.finish();
      _callback(callback);
    }
  }]);

  return GFFTransform;
}(Transform);

/**
 * Parse a stream of text data into a stream of feature,
 * directive, and comment objects.
 *
 * @param {Object} options optional options object
 * @param {string} options.encoding text encoding of the input GFF3. default 'utf8'
 * @param {boolean} options.parseAll default false.  if true, will parse all items. overrides other flags
 * @param {boolean} options.parseFeatures default true
 * @param {boolean} options.parseDirectives default false
 * @param {boolean} options.parseComments default false
 * @param {boolean} options.parseSequences default true
 * @param {Number} options.bufferSize maximum number of GFF3 lines to buffer. defaults to 1000
 * @returns {ReadableStream} stream (in objectMode) of parsed items
 */


function parseStream() {
  var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

  var newOptions = (0, _assign2.default)({ bufferSize: 1000 }, options);
  return new GFFTransform(newOptions);
}

/**
 * Read and parse a GFF3 file from the filesystem.
 *
 * @param {string} filename the filename of the file to parse
 * @param {Object} options optional options object
 * @param {string} options.encoding the file's string encoding, defaults to 'utf8'
 * @param {boolean} options.parseAll default false.  if true, will parse all items. overrides other flags
 * @param {boolean} options.parseFeatures default true
 * @param {boolean} options.parseDirectives default false
 * @param {boolean} options.parseComments default false
 * @param {boolean} options.parseSequences default true
 * @param {Number} options.bufferSize maximum number of GFF3 lines to buffer. defaults to 1000
 * @returns {ReadableStream} stream (in objectMode) of parsed items
 */
function parseFile(filename, options) {
  return fs.createReadStream(filename).pipe(parseStream(options));
}

/**
 * Synchronously parse a string containing GFF3 and return
 * an arrayref of the parsed items.
 *
 * @param {string} str
 * @param {Object} inputOptions optional options object
 * @param {boolean} inputOptions.parseAll default false.  if true, will parse all items. overrides other flags
 * @param {boolean} inputOptions.parseFeatures default true
 * @param {boolean} inputOptions.parseDirectives default false
 * @param {boolean} inputOptions.parseComments default false
 * @param {boolean} inputOptions.parseSequences default true
 * @returns {Array} array of parsed features, directives, and/or comments
 */
function parseStringSync(str) {
  var inputOptions = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};

  if (!str) return [];

  var options = _processParseOptions(inputOptions);

  var items = [];
  var push = items.push.bind(items);

  var parser = new _parse2.default({
    featureCallback: options.parseFeatures ? push : null,
    directiveCallback: options.parseDirectives ? push : null,
    commentCallback: options.parseComments ? push : null,
    sequenceCallback: options.parseSequences ? push : null,
    bufferSize: Infinity,
    errorCallback: function errorCallback(err) {
      throw err;
    }
  });

  str.split(/\r?\n/).forEach(parser.addLine.bind(parser));
  parser.finish();

  return items;
}

/**
 * Format an array of GFF3 items (features,directives,comments) into string of GFF3.
 * Does not insert synchronization (###) marks.
 *
 * @param {Array[Object]} items
 * @returns {String} the formatted GFF3
 */
function formatSync(items) {
  // sort items into seq and other
  var other = [];
  var sequences = [];
  items.forEach(function (i) {
    if (i.sequence) sequences.push(i);else other.push(i);
  });
  var str = other.map(_util.formatItem).join('');
  if (sequences.length) {
    str += '##FASTA\n';
    str += sequences.map(_util.formatSequence).join('');
  }
  return str;
}

var FormattingTransform = function (_Transform2) {
  _inherits(FormattingTransform, _Transform2);

  function FormattingTransform() {
    var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

    _classCallCheck(this, FormattingTransform);

    var _this3 = _possibleConstructorReturn(this, (FormattingTransform.__proto__ || (0, _getPrototypeOf2.default)(FormattingTransform)).call(this, (0, _assign2.default)(options, { objectMode: true })));

    _this3.linesSinceLastSyncMark = 0;
    _this3.minLinesBetweenSyncMarks = options.minSyncLines || 100;
    _this3.insertVersionDirective = options.insertVersionDirective || false;
    _this3.haveWeEmittedData = false;
    _this3.fastaMode = false;
    return _this3;
  }

  _createClass(FormattingTransform, [{
    key: '_transform',
    value: function _transform(chunk, encoding, callback) {
      // if we have not emitted anything yet, and this first
      // chunk is not a gff-version directive, emit one
      var str = void 0;
      if (!this.haveWeEmittedData && this.insertVersionDirective && (chunk[0] || chunk).directive !== 'gff-version') this.push('##gff-version 3\n');

      // if it's a sequence chunk coming down, emit a FASTA directive and
      // change to FASTA mode
      if (chunk.sequence && !this.fastaMode) {
        this.push('##FASTA\n');
        this.fastaMode = true;
      }

      if (Array.isArray(chunk)) str = chunk.map(_util.formatItem).join('');else str = (0, _util.formatItem)(chunk);

      this.push(str);

      if (this.linesSinceLastSyncMark >= this.minLinesBetweenSyncMarks) {
        this.push('###\n');
        this.linesSinceLastSyncMark = 0;
      } else {
        // count the number of newlines in this chunk
        var count = 0;
        for (var i = 0; i < str.length; i += 1) {
          if (str[i] === '\n') count += 1;
        }
        this.linesSinceLastSyncMark += count;
      }

      this.haveWeEmittedData = true;
      _callback(callback);
    }
  }]);

  return FormattingTransform;
}(Transform);

/**
 * Format a stream of items (of the type produced
 * by this script) into a stream of GFF3 text.
 *
 * Inserts synchronization (###) marks automatically.
 *
 * @param {Object} options
 * @param {Object} options.minSyncLines minimum number of lines between ### marks. default 100
 * @param {Boolean} options.insertVersionDirective
 *  if the first item in the stream is not a ##gff-version directive, insert one.
 *  default false
 */


function formatStream(options) {
  return new FormattingTransform(options);
}

/**
 * Format a stream of items (of the type produced
 * by this script) into a GFF3 file and write it to the filesystem.

 * Inserts synchronization (###) marks and a ##gff-version
 * directive automatically (if one is not already present).
 *
 * @param {ReadableStream} stream the stream to write to the file
 * @param {String} filename the file path to write to
 * @param {Object} options
 * @param {String} options.encoding default 'utf8'. encoding for the written file
 * @param {Number} options.minSyncLines
 *  minimum number of lines between sync (###) marks. default 100
 * @param {Boolean} options.insertVersionDirective
 *  if the first item in the stream is not a ##gff-version directive, insert one.
 *  default true
 * @returns {Promise} promise for the written filename
 */
function formatFile(stream, filename) {
  var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};

  var newOptions = (0, _assign2.default)({
    insertVersionDirective: true
  }, options);

  return new _promise2.default(function (resolve, reject) {
    stream.pipe(new FormattingTransform(newOptions)).on('end', function () {
      return resolve(filename);
    }).on('error', reject).pipe(fs.createWriteStream(filename, {
      encoding: newOptions.encoding || 'utf8'
    }));
  });
}