/**
 * @fileoverview Disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken
 * @author Teddy Katz
 */

'use strict';

const utils = require('../utils');

// ------------------------------------------------------------------------------
// Rule Definition
// ------------------------------------------------------------------------------

module.exports = {
  meta: {
    docs: {
      description: 'disallow unnecessary calls to sourceCode.getFirstToken and sourceCode.getLastToken',
      category: 'Rules',
      recommended: true,
    },
    type: 'suggestion',
    fixable: 'code',
    schema: [],
  },

  create (context) {
    const sourceCode = context.getSourceCode();

    // ----------------------------------------------------------------------
    // Helpers
    // ----------------------------------------------------------------------

    /**
     * Determines whether a second argument to getFirstToken or getLastToken changes the output of the function.
     * This occurs when the second argument exists and is not an object literal, or has keys other than `includeComments`.
     * @param {ASTNode} arg The second argument to `sourceCode.getFirstToken` or `sourceCode.getLastToken`
     * @returns {boolean} `true` if the argument affects the output of getFirstToken or getLastToken
     */
    function affectsGetTokenOutput (arg) {
      if (!arg) {
        return false;
      }
      if (arg.type !== 'ObjectExpression') {
        return true;
      }
      return arg.properties.length >= 2 || (
        arg.properties.length === 1 && (
          utils.getKeyName(arg.properties[0]) !== 'includeComments' ||
          arg.properties[0].value.type !== 'Literal'
        ));
    }

    /**
     * Determines whether a node is a MemberExpression that accesses the `range` property
     * @param {ASTNode} node The node
     * @returns {boolean} `true` if the node is a MemberExpression that accesses the `range` property
     */
    function isRangeAccess (node) {
      return node.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'range';
    }

    /**
     * Determines whether a MemberExpression accesses the `start` property (either `.range[0]` or `.start`).
     * Note that this will also work correctly if the `.range` MemberExpression is passed.
     * @param {ASTNode} memberExpression The MemberExpression node to check
     * @returns {boolean} `true` if this node accesses either `.range[0]` or `.start`
     */
    function isStartAccess (memberExpression) {
      if (isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression') {
        return isStartAccess(memberExpression.parent);
      }
      return (
        (memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'start') ||
        (
          memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 0 &&
          isRangeAccess(memberExpression.object)
        )
      );
    }

    /**
     * Determines whether a MemberExpression accesses the `start` property (either `.range[1]` or `.end`).
     * Note that this will also work correctly if the `.range` MemberExpression is passed.
     * @param {ASTNode} memberExpression The MemberExpression node to check
     * @returns {boolean} `true` if this node accesses either `.range[1]` or `.end`
     */
    function isEndAccess (memberExpression) {
      if (isRangeAccess(memberExpression) && memberExpression.parent.type === 'MemberExpression') {
        return isEndAccess(memberExpression.parent);
      }
      return (
        (memberExpression.property.type === 'Identifier' && memberExpression.property.name === 'end') ||
        (
          memberExpression.computed && memberExpression.property.type === 'Literal' && memberExpression.property.value === 1 &&
          isRangeAccess(memberExpression.object)
        )
      );
    }

    // ----------------------------------------------------------------------
    // Public
    // ----------------------------------------------------------------------

    return {
      'Program:exit' (ast) {
        Array.from(utils.getSourceCodeIdentifiers(context, ast))
          .filter(identifier => identifier.parent.type === 'MemberExpression' &&
            identifier.parent.object === identifier &&
            identifier.parent.property.type === 'Identifier' &&
            identifier.parent.parent.type === 'CallExpression' &&
            identifier.parent === identifier.parent.parent.callee &&
            identifier.parent.parent.arguments.length <= 2 &&
            !affectsGetTokenOutput(identifier.parent.parent.arguments[1]) &&
            identifier.parent.parent.parent.type === 'MemberExpression' &&
            identifier.parent.parent === identifier.parent.parent.parent.object && (
            (isStartAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getFirstToken') ||
              (isEndAccess(identifier.parent.parent.parent) && identifier.parent.property.name === 'getLastToken'))
          )
          .forEach(identifier => {
            const fullRangeAccess = isRangeAccess(identifier.parent.parent.parent)
              ? identifier.parent.parent.parent.parent
              : identifier.parent.parent.parent;
            const replacementText = sourceCode.text.slice(fullRangeAccess.range[0], identifier.parent.parent.range[0]) +
              sourceCode.getText(identifier.parent.parent.arguments[0]) +
              sourceCode.text.slice(identifier.parent.parent.range[1], fullRangeAccess.range[1]);
            context.report({
              node: identifier.parent.parent,
              message: "Use '{{replacementText}}' instead.",
              data: { replacementText },
              fix (fixer) {
                return fixer.replaceText(identifier.parent.parent, sourceCode.getText(identifier.parent.parent.arguments[0]));
              },
            });
          });
      },
    };
  },
};
