/**
 * @fileoverview Rule that warns when identifier names are shorter or longer
 * than the values provided in configuration.
 * @author Burak Yigit Kaya aka BYK
 */

'use strict';

//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------

const { getGraphemeCount } = require('../shared/string-utils');
const {
  getModuleExportName,
  isImportAttributeKey,
} = require('./utils/ast-utils');

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

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
  meta: {
    type: 'suggestion',

    defaultOptions: [
      {
        exceptionPatterns: [],
        exceptions: [],
        min: 2,
        properties: 'always',
      },
    ],

    docs: {
      description: 'Enforce minimum and maximum identifier lengths',
      recommended: false,
      frozen: true,
      url: 'https://eslint.org/docs/latest/rules/id-length',
    },

    schema: [
      {
        type: 'object',
        properties: {
          min: {
            type: 'integer',
          },
          max: {
            type: 'integer',
          },
          exceptions: {
            type: 'array',
            uniqueItems: true,
            items: {
              type: 'string',
            },
          },
          exceptionPatterns: {
            type: 'array',
            uniqueItems: true,
            items: {
              type: 'string',
            },
          },
          properties: {
            enum: ['always', 'never'],
          },
        },
        additionalProperties: false,
      },
    ],
    messages: {
      tooShort: "Identifier name '{{name}}' is too short (< {{min}}).",
      tooShortPrivate: "Identifier name '#{{name}}' is too short (< {{min}}).",
      tooLong: "Identifier name '{{name}}' is too long (> {{max}}).",
      tooLongPrivate: "Identifier name #'{{name}}' is too long (> {{max}}).",
    },
  },

  create(context) {
    const [options] = context.options;
    const { max: maxLength = Infinity, min: minLength } = options;
    const properties = options.properties !== 'never';
    const exceptions = new Set(options.exceptions);
    const exceptionPatterns = options.exceptionPatterns.map(
      (pattern) => new RegExp(pattern, 'u')
    );
    const reportedNodes = new Set();

    /**
     * Checks if a string matches the provided exception patterns
     * @param {string} name The string to check.
     * @returns {boolean} if the string is a match
     * @private
     */
    function matchesExceptionPattern(name) {
      return exceptionPatterns.some((pattern) => pattern.test(name));
    }

    const SUPPORTED_EXPRESSIONS = {
      MemberExpression:
        properties &&
        function (parent) {
          return (
            !parent.computed &&
            // regular property assignment
            ((parent.parent.left === parent &&
              parent.parent.type === 'AssignmentExpression') ||
              // or the last identifier in an ObjectPattern destructuring
              (parent.parent.type === 'Property' &&
                parent.parent.value === parent &&
                parent.parent.parent.type === 'ObjectPattern' &&
                parent.parent.parent.parent.left === parent.parent.parent))
          );
        },
      AssignmentPattern(parent, node) {
        return parent.left === node;
      },
      VariableDeclarator(parent, node) {
        return parent.id === node;
      },
      Property(parent, node) {
        if (parent.parent.type === 'ObjectPattern') {
          const isKeyAndValueSame = parent.value.name === parent.key.name;

          return (
            (!isKeyAndValueSame && parent.value === node) ||
            (isKeyAndValueSame && parent.key === node && properties)
          );
        }
        return (
          properties &&
          !isImportAttributeKey(node) &&
          !parent.computed &&
          parent.key.name === node.name
        );
      },
      ImportSpecifier(parent, node) {
        return (
          parent.local === node &&
          getModuleExportName(parent.imported) !==
            getModuleExportName(parent.local)
        );
      },
      ImportDefaultSpecifier: true,
      ImportNamespaceSpecifier: true,
      RestElement: true,
      FunctionExpression: true,
      ArrowFunctionExpression: true,
      ClassDeclaration: true,
      FunctionDeclaration: true,
      MethodDefinition: true,
      PropertyDefinition: true,
      CatchClause: true,
      ArrayPattern: true,
    };

    return {
      [['Identifier', 'PrivateIdentifier']](node) {
        const name = node.name;
        const parent = node.parent;

        const nameLength = getGraphemeCount(name);

        const isShort = nameLength < minLength;
        const isLong = nameLength > maxLength;

        if (
          !(isShort || isLong) ||
          exceptions.has(name) ||
          matchesExceptionPattern(name)
        ) {
          return; // Nothing to report
        }

        const isValidExpression = SUPPORTED_EXPRESSIONS[parent.type];

        /*
         * We used the range instead of the node because it's possible
         * for the same identifier to be represented by two different
         * nodes, with the most clear example being shorthand properties:
         * { foo }
         * In this case, "foo" is represented by one node for the name
         * and one for the value. The only way to know they are the same
         * is to look at the range.
         */
        if (
          isValidExpression &&
          !reportedNodes.has(node.range.toString()) &&
          (isValidExpression === true || isValidExpression(parent, node))
        ) {
          reportedNodes.add(node.range.toString());

          let messageId = isShort ? 'tooShort' : 'tooLong';

          if (node.type === 'PrivateIdentifier') {
            messageId += 'Private';
          }

          context.report({
            node,
            messageId,
            data: { name, min: minLength, max: maxLength },
          });
        }
      },
    };
  },
};