'use strict';

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

var _from = require('babel-runtime/core-js/array/from');

var _from2 = _interopRequireDefault(_from);

var _isIterable2 = require('babel-runtime/core-js/is-iterable');

var _isIterable3 = _interopRequireDefault(_isIterable2);

var _getIterator2 = require('babel-runtime/core-js/get-iterator');

var _getIterator3 = _interopRequireDefault(_getIterator2);

var _entries = require('babel-runtime/core-js/object/entries');

var _entries2 = _interopRequireDefault(_entries);

var _stringify = require('babel-runtime/core-js/json/stringify');

var _stringify2 = _interopRequireDefault(_stringify);

var _keys = require('babel-runtime/core-js/object/keys');

var _keys2 = _interopRequireDefault(_keys);

var _values = require('babel-runtime/core-js/object/values');

var _values2 = _interopRequireDefault(_values);

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

var _assign2 = _interopRequireDefault(_assign);

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

var _defineProperty2 = _interopRequireDefault(_defineProperty);

var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = (0, _getIterator3.default)(arr), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if ((0, _isIterable3.default)(Object(arr))) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();

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; }; }();

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

var GFF3 = _interopRequireWildcard(_util);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

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

function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return (0, _from2.default)(arr); } }

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

var containerAttributes = {
  Parent: 'child_features',
  Derives_from: 'derived_features'
};

var FASTAParser = function () {
  function FASTAParser(seqCallback) {
    _classCallCheck(this, FASTAParser);

    this.seqCallback = seqCallback;
    this.currentSequence = undefined;
  }

  _createClass(FASTAParser, [{
    key: 'addLine',
    value: function addLine(line) {
      var defMatch = /^>\s*(\S+)\s*(.*)/.exec(line);
      if (defMatch) {
        this._flush();
        this.currentSequence = { id: defMatch[1], sequence: '' };
        if (defMatch[2]) this.currentSequence.description = defMatch[2].trim();
      } else if (this.currentSequence && /\S/.test(line)) {
        this.currentSequence.sequence += line.replace(/\s/g, '');
      }
    }
  }, {
    key: '_flush',
    value: function _flush() {
      if (this.currentSequence) this.seqCallback(this.currentSequence);
    }
  }, {
    key: 'finish',
    value: function finish() {
      this._flush();
    }
  }]);

  return FASTAParser;
}();

var Parser = function () {
  function Parser(args) {
    _classCallCheck(this, Parser);

    var nullFunc = function nullFunc() {};

    (0, _assign2.default)(this, {
      featureCallback: args.featureCallback || nullFunc,
      endCallback: args.endCallback || nullFunc,
      commentCallback: args.commentCallback || nullFunc,
      errorCallback: args.errorCallback || nullFunc,
      directiveCallback: args.directiveCallback || nullFunc,
      sequenceCallback: args.sequenceCallback || nullFunc,

      // number of lines to buffer
      bufferSize: args.bufferSize === undefined ? 1000 : args.bufferSize,

      // features that we have to keep on hand for now because they
      // might be referenced by something else
      _underConstructionTopLevel: [],
      // index of the above by ID
      _underConstructionById: {},

      _completedReferences: {},

      // features that reference something we have not seen yet
      // structured as:
      // {  'some_id' : {
      //     'Parent' : [ orphans that have a Parent attr referencing it ],
      //     'Derives_from' : [ orphans that have a Derives_from attr referencing it ],
      //    }
      // }
      _underConstructionOrphans: {},

      // if this is true, the parser ignores the
      // rest of the lines in the file.  currently
      // set when the file switches over to FASTA
      eof: false,

      lineNumber: 0
    });
  }

  _createClass(Parser, [{
    key: 'addLine',
    value: function addLine(line) {
      // if we have transitioned to a fasta section, just delegate to that parser
      if (this.fastaParser) {
        this.fastaParser.addLine(line);
        return;
      } else if (this.eof) {
        // otherwise, if we are done, ignore this line
        return;
      }

      this.lineNumber += 1;

      if (/^\s*[^#\s>]/.test(line)) {
        // feature line, most common case
        this._bufferLine(line);
        return;
      }

      var match = /^\s*(#+)(.*)/.exec(line);
      if (match) {
        // directive or comment
        var _match = _slicedToArray(match, 3),
            hashsigns = _match[1],
            contents = _match[2];

        if (hashsigns.length === 3) {
          // sync directive, all forward-references are resolved.
          this._emitAllUnderConstructionFeatures();
        } else if (hashsigns.length === 2) {
          var directive = GFF3.parseDirective(line);
          if (directive.directive === 'FASTA') {
            this._emitAllUnderConstructionFeatures();
            this.eof = true;
            this.fastaParser = new FASTAParser(this.sequenceCallback);
          } else {
            this._emitItem(directive);
          }
        } else {
          contents = contents.replace(/\s*/, '');
          this._emitItem({ comment: contents });
        }
      } else if (/^\s*$/.test(line)) {
        // blank line, do nothing
      } else if (/^\s*>/.test(line)) {
        // implicit beginning of a FASTA section
        this._emitAllUnderConstructionFeatures();
        this.eof = true;
        this.fastaParser = new FASTAParser(this.sequenceCallback);
        this.fastaParser.addLine(line);
      } else {
        // it's a parse error
        var errLine = line.replace(/\r?\n?$/g, '');
        throw new Error('GFF3 parse error.  Cannot parse \'' + errLine + '\'.');
      }
    }
  }, {
    key: '_emitItem',
    value: function _emitItem(i) {
      if (i[0]) this.featureCallback(i);else if (i.directive) this.directiveCallback(i);else if (i.comment) this.commentCallback(i);
    }
  }, {
    key: 'finish',
    value: function finish() {
      this._emitAllUnderConstructionFeatures();
      if (this.fastaParser) this.fastaParser.finish();
      this.endCallback();
    }
  }, {
    key: '_enforceBufferSizeLimit',
    value: function _enforceBufferSizeLimit() {
      var _this = this;

      var additionalItemCount = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 0;

      var _unbufferItem = function _unbufferItem(item) {
        if (item && item[0] && item[0].attributes && item[0].attributes.ID && item[0].attributes.ID[0]) {
          var ids = item[0].attributes.ID;
          ids.forEach(function (id) {
            delete _this._underConstructionById[id];
            delete _this._completedReferences[id];
          });
          item.forEach(function (i) {
            if (i.child_features) i.child_features.forEach(function (c) {
              return _unbufferItem(c);
            });
            if (i.derived_features) i.derived_features.forEach(function (d) {
              return _unbufferItem(d);
            });
          });
        }
      };

      while (this._underConstructionTopLevel.length + additionalItemCount > this.bufferSize) {
        var item = this._underConstructionTopLevel.shift();
        this._emitItem(item);
        _unbufferItem(item);
      }
    }

    /**
     * return all under-construction features, called when we know
     * there will be no additional data to attach to them
     * @private
     */

  }, {
    key: '_emitAllUnderConstructionFeatures',
    value: function _emitAllUnderConstructionFeatures() {
      this._underConstructionTopLevel.forEach(this._emitItem.bind(this));

      this._underConstructionTopLevel = [];
      this._underConstructionById = {};
      this._completedReferences = {};

      // if we have any orphans hanging around still, this is a
      // problem. die with a parse error
      if ((0, _values2.default)(this._underConstructionOrphans).filter(function (entry) {
        return (0, _keys2.default)(entry).length;
      }).length) {
        throw new Error('some features reference other features that do not exist in the file (or in the same \'###\' scope). ' + (0, _stringify2.default)(this._underConstructionOrphans));
      }
    }

    // do the right thing with a newly-parsed feature line

  }, {
    key: '_bufferLine',
    value: function _bufferLine(line) {
      var _this2 = this;

      var featureLine = GFF3.parseFeature(line);
      featureLine.child_features = [];
      featureLine.derived_features = [];
      // featureLine._lineNumber = this.lineNumber //< debugging aid

      // NOTE: a feature is an arrayref of one or more feature lines.
      var ids = featureLine.attributes.ID || [];
      var parents = featureLine.attributes.Parent || [];
      var derives = featureLine.attributes.Derives_from || [];

      if (!ids.length && !parents.length && !derives.length) {
        // if it has no IDs and does not refer to anything, we can just
        // output it
        this._emitItem([featureLine]);
        return;
      }

      var feature = void 0;
      ids.forEach(function (id) {
        var existing = _this2._underConstructionById[id];
        if (existing) {
          // another location of the same feature
          if (existing[existing.length - 1].type !== featureLine.type) {
            _this2._parseError('multi-line feature "' + id + '" has inconsistent types: "' + featureLine.type + '", "' + existing[existing.length - 1].type + '"');
          }
          existing.push(featureLine);
          feature = existing;
        } else {
          // haven't seen it yet, so buffer it so we can attach
          // child features to it
          feature = [featureLine];

          _this2._enforceBufferSizeLimit(1);
          if (!parents.length && !derives.length) {
            _this2._underConstructionTopLevel.push(feature);
          }
          _this2._underConstructionById[id] = feature;

          // see if we have anything buffered that refers to it
          _this2._resolveReferencesTo(feature, id);
        }
      });

      // try to resolve all its references
      this._resolveReferencesFrom(feature || [featureLine], { Parent: parents, Derives_from: derives }, ids);
    }
  }, {
    key: '_resolveReferencesTo',
    value: function _resolveReferencesTo(feature, id) {
      var references = this._underConstructionOrphans[id];
      //   references is of the form
      //   {
      //     'Parent' : [ orphans that have a Parent attr referencing this feature ],
      //     'Derives_from' : [ orphans that have a Derives_from attr referencing this feature ],
      //    }
      if (!references) return;

      (0, _keys2.default)(references).forEach(function (attrname) {
        var pname = containerAttributes[attrname] || attrname.toLowerCase();
        feature.forEach(function (loc) {
          var _loc$pname;

          (_loc$pname = loc[pname]).push.apply(_loc$pname, _toConsumableArray(references[attrname]));
          delete references[attrname];
        });
      });
    }
  }, {
    key: '_parseError',
    value: function _parseError(message) {
      this.eof = true;
      this.errorCallback(this.lineNumber + ': ' + message);
    }
  }, {
    key: '_resolveReferencesFrom',
    value: function _resolveReferencesFrom(feature, references, ids) {
      var _this3 = this;

      // this is all a bit more awkward in javascript than it was in perl
      function postSet(obj, slot1, slot2) {
        var subObj = obj[slot1];
        if (!subObj) {
          subObj = {};
          obj[slot1] = subObj;
        }
        var returnVal = subObj[slot2] || false;
        subObj[slot2] = true;
        return returnVal;
      }

      (0, _entries2.default)(references).forEach(function (_ref) {
        var _ref2 = _slicedToArray(_ref, 2),
            attrname = _ref2[0],
            toIds = _ref2[1];

        var pname = void 0;
        toIds.forEach(function (toId) {
          var otherFeature = _this3._underConstructionById[toId];
          if (otherFeature) {
            if (!pname) pname = containerAttributes[attrname] || attrname.toLowerCase();

            if (!ids.filter(function (id) {
              return postSet(_this3._completedReferences, id, attrname + ',' + toId);
            }).length) {
              otherFeature.forEach(function (location) {
                location[pname].push(feature);
              });
            }
          } else {
            if (!_this3._underConstructionOrphans[toId]) _this3._underConstructionOrphans[toId] = {};
            if (!_this3._underConstructionOrphans[toId][attrname]) _this3._underConstructionOrphans[toId][attrname] = [];
            _this3._underConstructionOrphans[toId][attrname].push(feature);
          }
        });
      });
    }
  }]);

  return Parser;
}();

exports.default = Parser;