'use strict';

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

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 _from = require('babel-runtime/core-js/array/from');

var _from2 = _interopRequireDefault(_from);

var _map = require('babel-runtime/core-js/array/map');

var _map2 = _interopRequireDefault(_map);

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

var _keys2 = _interopRequireDefault(_keys);

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

exports.unescape = unescape;
exports.escape = escape;
exports.escapeColumn = escapeColumn;
exports.parseAttributes = parseAttributes;
exports.parseFeature = parseFeature;
exports.parseDirective = parseDirective;
exports.formatAttributes = formatAttributes;
exports.formatFeature = formatFeature;
exports.formatDirective = formatDirective;
exports.formatComment = formatComment;
exports.formatSequence = formatSequence;
exports.formatItem = formatItem;

var _typical = require('typical');

var _typical2 = _interopRequireDefault(_typical);

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); } } /** @module util */

// Fast, low-level functions for parsing and formatting GFF3.
// JavaScript port of Robert Buels's Bio::GFF3::LowLevel Perl module.

var fieldNames = ['seq_id', 'source', 'type', 'start', 'end', 'score', 'strand', 'phase', 'attributes'];

/**
 * Unescape a string value used in a GFF3 attribute.
 *
 * @param {String} s
 * @returns {String}
 */
function unescape(s) {
  if (s === null) return null;

  return String(s).replace(/%([0-9A-Fa-f]{2})/g, function (match, seq) {
    return String.fromCharCode(parseInt(seq, 16));
  });
}

/**
 * Escape a value for use in a GFF3 attribute value.
 *
 * @param {String} s
 * @returns {String}
 */
function _escape(regex, s) {
  return String(s).replace(regex, function (ch) {
    var hex = ch.charCodeAt(0).toString(16).toUpperCase();

    // lol, apparently there's no native function for fixed-width hex output
    if (hex.length < 2) hex = '0' + hex;
    return '%' + hex;
  });
}

function escape(s) {
  return _escape(/[\n;\r\t=%&,\x00-\x1f\x7f-\xff]/g, s);
}

/**
 * Escape a value for use in a GFF3 column value.
 *
 * @param {String} s
 * @returns {String}
 */
function escapeColumn(s) {
  return _escape(/[\n\r\t%\x00-\x1f\x7f-\xff]/g, s);
}

/**
 * Parse the 9th column (attributes) of a GFF3 feature line.
 *
 * @param {String} attrString
 * @returns {Object}
 */
function parseAttributes(attrString) {
  if (!(attrString && attrString.length) || attrString === '.') return {};

  var attrs = {};

  attrString.replace(/\r?\n$/, '').split(';').forEach(function (a) {
    var _arec;

    var nv = a.split('=', 2);
    if (!(nv[1] && nv[1].length)) return;

    nv[0] = nv[0].trim();
    var arec = attrs[nv[0].trim()];
    if (!arec) {
      arec = [];
      attrs[nv[0]] = arec;
    }

    (_arec = arec).push.apply(_arec, _toConsumableArray(nv[1].split(',').map(function (s) {
      return s.trim();
    }).map(unescape)));
  });
  return attrs;
}

/**
 * Parse a GFF3 feature line
 *
 * @param {String} line
 */
function parseFeature(line) {
  // split the line into columns and replace '.' with null in each column
  var f = line.split('\t').map(function (a) {
    return a === '.' ? null : a;
  });

  // unescape only the ref, source, and type columns
  f[0] = unescape(f[0]);
  f[1] = unescape(f[1]);
  f[2] = unescape(f[2]);

  f[8] = parseAttributes(f[8]);
  var parsed = {};
  for (var i = 0; i < fieldNames.length; i += 1) {
    parsed[fieldNames[i]] = f[i] === '.' ? null : f[i];
  }
  if (parsed.start !== null) parsed.start = parseInt(parsed.start, 10);
  if (parsed.end !== null) parsed.end = parseInt(parsed.end, 10);
  if (parsed.score !== null) parsed.score = parseFloat(parsed.score, 10);
  if (parsed.strand != null) parsed.strand = parsed.strand;
  return parsed;
}

/**
 * Parse a GFF3 directive line.
 *
 * @param {String} line
 * @returns {Object} the information in the directive
 */
function parseDirective(line) {
  var match = /^\s*##\s*(\S+)\s*(.*)/.exec(line);
  if (!match) return null;

  var _match = _slicedToArray(match, 3),
      name = _match[1],
      contents = _match[2];

  var parsed = { directive: name };
  if (contents.length) {
    contents = contents.replace(/\r?\n$/, '');
    parsed.value = contents;
  }

  // do a little additional parsing for sequence-region and genome-build directives
  if (name === 'sequence-region') {
    var c = contents.split(/\s+/, 3);
    parsed.seq_id = c[0];
    parsed.start = c[1].replace(/\D/g, '');
    parsed.end = c[2].replace(/\D/g, '');
  } else if (name === 'genome-build') {
    ;
    var _contents$split = contents.split(/\s+/, 2);

    var _contents$split2 = _slicedToArray(_contents$split, 2);

    parsed.source = _contents$split2[0];
    parsed.buildname = _contents$split2[1];
  }

  return parsed;
}

/**
 * Format an attributes object into a string suitable for the 9th column of GFF3.
 *
 * @param {Object} attrs
 */
function formatAttributes(attrs) {
  var attrOrder = [];
  (0, _keys2.default)(attrs).forEach(function (tag) {
    var val = attrs[tag];
    var valstring = void 0;
    if (val.hasOwnProperty('toString')) {
      valstring = escape(val.toString());
    } else if (Array.isArray(val.values)) {
      valstring = val.values.map(escape).join(',');
    } else if (Array.isArray(val)) {
      valstring = val.map(escape).join(',');
    } else {
      valstring = escape(val);
    }
    attrOrder.push(escape(tag) + '=' + valstring);
  });
  return attrOrder.length ? attrOrder.join(';') : '.';
}

var translateStrand = ['-', '.', '+'];

function _formatSingleFeature(f, seenFeature) {
  var attrString = f.attributes === null || f.attributes === undefined ? '.' : formatAttributes(f.attributes);

  var fields = [];
  for (var i = 0; i < 8; i += 1) {
    var val = f[fieldNames[i]];
    // deserialize strand
    if (i === 6) fields[i] = val === null || val === undefined ? '.' : translateStrand[val + 1] || val;else fields[i] = val === null || val === undefined ? '.' : escapeColumn(String(val));
  }
  fields[8] = attrString;

  var formattedString = fields.join('\t') + '\n';

  // if we have already output this exact feature, skip it
  if (seenFeature[formattedString]) {
    return '';
  }

  seenFeature[formattedString] = true;
  return formattedString;
}

function _formatFeature(feature, seenFeature) {
  if (Array.isArray(feature)) {
    return feature.map(function (f) {
      return _formatFeature(f, seenFeature);
    }).join('');
  }

  var strings = [_formatSingleFeature(feature, seenFeature)];['child_features', 'derived_features'].forEach(function (multiSlot) {
    if (feature[multiSlot]) {
      strings.push.apply(strings, _toConsumableArray(feature[multiSlot].map(function (f) {
        return _formatFeature(f, seenFeature);
      })));
    }
  });
  return strings.join('');
}

/**
 * Format a feature object or array of
 * feature objects into one or more lines of GFF3.
 *
 * @param {Object|Array[Object]} featureOrFeatures
 */
function formatFeature(featureOrFeatures) {
  var seen = {};
  return _formatFeature(featureOrFeatures, seen);
}

/**
 * Format a directive into a line of GFF3.
 *
 * @param {Object} directive
 * @returns {String}
 */
function formatDirective(directive) {
  var str = '##' + directive.directive;
  if (directive.value) str += ' ' + directive.value;
  str += '\n';
  return str;
}

/**
 * Format a comment into a GFF3 comment.
 * Yes I know this is just adding a # and a newline.
 *
 * @param {Object} comment
 * @returns {String}
 */
function formatComment(comment) {
  return '# ' + comment.comment + '\n';
}

/**
 * Format a sequence object as FASTA
 *
 * @param {Object} seq
 * @returns {String} formatted single FASTA sequence
 */
function formatSequence(seq) {
  return '>' + seq.id + (seq.description ? ' ' + seq.description : '') + '\n' + seq.sequence + '\n';
}

/**
 * Format a directive, comment, or feature,
 * or array of such items, into one or more lines of GFF3.
 *
 * @param {Object|Array} itemOrItems
 */
function formatItem(itemOrItems) {
  function formatSingleItem(item) {
    if (item[0] || item.attributes) return formatFeature(item);else if (item.directive) return formatDirective(item);else if (item.sequence) return formatSequence(item);else if (item.comment) return formatComment(item);
    return '# (invalid item found during format)\n';
  }

  if (_typical2.default.isArrayLike(itemOrItems)) {
    return (0, _map2.default)(itemOrItems, formatSingleItem);
  }
  return formatSingleItem(itemOrItems);
}