'use strict';

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

var _lodash = require('lodash');

var _lodash2 = _interopRequireDefault(_lodash);

var _utilities = require('../utilities');

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 Array.from(arr); } }

var defaults = {
  caseSensitive: true,
  natural: false
};

var schema = [{
  enum: ['asc', 'desc'],
  type: 'string'
}, {
  additionalProperties: false,
  properties: {
    caseSensitive: {
      type: 'boolean'
    },
    natural: {
      type: 'boolean'
    }
  },
  type: 'object'
}];

/**
 * Functions to compare the order of two strings
 *
 * Based on a similar function from eslint's sort-keys rule.
 * https://github.com/eslint/eslint/blob/master/lib/rules/sort-keys.js
 *
 * @private
 */
var isValidOrders = {
  asc(str1, str2) {
    return str1 <= str2;
  },
  ascI(str1, str2) {
    return str1.toLowerCase() <= str2.toLowerCase();
  },
  ascIN(str1, str2) {
    return isValidOrders.naturalCompare(str1.toLowerCase(), str2.toLowerCase()) <= 0;
  },
  ascN(str1, str2) {
    return isValidOrders.naturalCompare(str1, str2) <= 0;
  },
  desc(str1, str2) {
    return isValidOrders.asc(str2, str1);
  },
  descI(str1, str2) {
    return isValidOrders.ascI(str2, str1);
  },
  descIN(str1, str2) {
    return isValidOrders.ascIN(str2, str1);
  },
  descN(str1, str2) {
    return isValidOrders.ascN(str2, str1);
  },
  naturalCompare(str1, str2) {
    return str1.localeCompare(str2, 'en-US', { numeric: true });
  }
};

var generateOrderedList = function generateOrderedList(context, sort, properties) {
  var source = context.getSourceCode();

  var items = properties.map(function (property) {
    var name = (0, _utilities.getParameterName)(property, context);

    var commentsBefore = source.getCommentsBefore(property);
    var startIndex = commentsBefore.length > 0 ? commentsBefore[0].start : property.start;

    if (property.type === 'ObjectTypeSpreadProperty' || !property.value) {
      // NOTE: It could but currently does not fix recursive generic type arguments in GenericTypeAnnotation within ObjectTypeSpreadProperty.

      // Maintain everything between the start of property including leading comments and the nextPunctuator `,` or `}`:
      var nextPunctuator = source.getTokenAfter(property, {
        filter: function filter(token) {
          return token.type === 'Punctuator';
        }
      });
      var beforePunctuator = source.getTokenBefore(nextPunctuator, {
        includeComments: true
      });
      var text = source.getText().substring(startIndex, beforePunctuator.end);

      return [property, text];
    }

    var colonToken = source.getTokenBefore(property.value, {
      filter: function filter(token) {
        return token.value === ':';
      }
    });

    // Preserve all code until the colon verbatim:
    var key = source.getText().substring(startIndex, colonToken.start);
    var value = void 0;

    if (property.value.type === 'ObjectTypeAnnotation') {
      // eslint-disable-next-line no-use-before-define
      value = ' ' + generateFix(property.value, context, sort);
    } else {
      // NOTE: It could but currently does not fix recursive generic type arguments in GenericTypeAnnotation.

      // Maintain everything between the `:` and the next Punctuator `,` or `}`:
      var _nextPunctuator = source.getTokenAfter(property, {
        filter: function filter(token) {
          return token.type === 'Punctuator';
        }
      });
      var _beforePunctuator = source.getTokenBefore(_nextPunctuator, {
        includeComments: true
      });
      var _text = source.getText().substring(colonToken.end, _beforePunctuator.end);

      value = _text;
    }

    return [property, name, key, value];
  });

  var itemGroups = [[]];
  var itemGroupIndex = 0;
  items.forEach(function (item) {
    if (item[0].type === 'ObjectTypeSpreadProperty') {
      ++itemGroupIndex;
      itemGroups[itemGroupIndex] = [item];
      ++itemGroupIndex;
      itemGroups[itemGroupIndex] = [];
    } else {
      itemGroups[itemGroupIndex].push(item);
    }
  });

  var orderedList = [];
  itemGroups.forEach(function (itemGroup) {
    if (itemGroup[0] && itemGroup[0].type !== 'ObjectTypeSpreadProperty') {
      itemGroup.sort(function (first, second) {
        return sort(first[1], second[1]) ? -1 : 1;
      });
    }
    orderedList.push.apply(orderedList, _toConsumableArray(itemGroup.map(function (item) {
      if (item.length === 2) {
        return item[1];
      }

      return item[2] + ':' + item[3];
    })));
  });

  return orderedList;
};

var generateFix = function generateFix(node, context, sort) {
  // this could be done much more cleanly in ESLint >=4
  // as we can apply multiple fixes. That also means we can
  // maintain code style in a much nicer way
  var nodeText = void 0;
  var newTypes = generateOrderedList(context, sort, node.properties);
  var source = context.getSourceCode(node);

  var originalSubstring = source.getText(node);

  nodeText = originalSubstring;

  node.properties.forEach(function (property, index) {
    var nextPunctuator = source.getTokenAfter(property, {
      filter: function filter(token) {
        return token.type === 'Punctuator';
      }
    });
    var beforePunctuator = source.getTokenBefore(nextPunctuator, {
      includeComments: true
    });
    var commentsBefore = source.getCommentsBefore(property);
    var startIndex = commentsBefore.length > 0 ? commentsBefore[0].start : property.start;
    var subString = source.getText().substring(startIndex, beforePunctuator.end);

    nodeText = nodeText.replace(subString, '$' + index);
  });

  newTypes.forEach(function (item, index) {
    nodeText = nodeText.replace('$' + index, item);
  });

  return nodeText;
};

var create = function create(context) {
  var order = _lodash2.default.get(context, ['options', 0], 'asc');

  var _$get = _lodash2.default.get(context, ['options', 1], defaults),
      natural = _$get.natural,
      caseSensitive = _$get.caseSensitive;

  var insensitive = caseSensitive === false;

  var prev = void 0;
  var checkKeyOrder = function checkKeyOrder(node) {
    prev = null;

    _lodash2.default.forEach(node.properties, function (identifierNode) {
      var current = (0, _utilities.getParameterName)(identifierNode, context);
      var last = prev;

      // keep track of the last token
      prev = current || last;

      if (!last || !current) {
        return;
      }

      var isValidOrder = isValidOrders[order + (insensitive ? 'I' : '') + (natural ? 'N' : '')];

      if (isValidOrder(last, current) === false) {
        context.report({
          data: {
            current,
            insensitive: insensitive ? 'insensitive ' : '',
            last,
            natural: natural ? 'natural ' : '',
            order
          },
          fix(fixer) {
            var nodeText = generateFix(node, context, isValidOrder);

            return fixer.replaceText(node, nodeText);
          },
          loc: identifierNode.loc,
          message: 'Expected type annotations to be in {{natural}}{{insensitive}}{{order}}ending order. "{{current}}" should be before "{{last}}".',
          node: identifierNode
        });
      }
    });
  };

  return {
    ObjectTypeAnnotation: checkKeyOrder
  };
};

exports.default = {
  create,
  schema
};
module.exports = exports.default;