/**
 * @fileoverview Disallows or enforces spaces inside of parentheses.
 * @author Jonathan Rajavuori
 * @deprecated in ESLint v8.53.0
 */
'use strict';

const astUtils = require('./utils/ast-utils');

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

/** @type {import('../types').Rule.RuleModule} */
module.exports = {
  meta: {
    deprecated: {
      message: 'Formatting rules are being moved out of ESLint core.',
      url: 'https://eslint.org/blog/2023/10/deprecating-formatting-rules/',
      deprecatedSince: '8.53.0',
      availableUntil: '10.0.0',
      replacedBy: [
        {
          message:
            'ESLint Stylistic now maintains deprecated stylistic core rules.',
          url: 'https://eslint.style/guide/migration',
          plugin: {
            name: '@stylistic/eslint-plugin-js',
            url: 'https://eslint.style/packages/js',
          },
          rule: {
            name: 'space-in-parens',
            url: 'https://eslint.style/rules/js/space-in-parens',
          },
        },
      ],
    },
    type: 'layout',

    docs: {
      description: 'Enforce consistent spacing inside parentheses',
      recommended: false,
      url: 'https://eslint.org/docs/latest/rules/space-in-parens',
    },

    fixable: 'whitespace',

    schema: [
      {
        enum: ['always', 'never'],
      },
      {
        type: 'object',
        properties: {
          exceptions: {
            type: 'array',
            items: {
              enum: ['{}', '[]', '()', 'empty'],
            },
            uniqueItems: true,
          },
        },
        additionalProperties: false,
      },
    ],

    messages: {
      missingOpeningSpace: 'There must be a space after this paren.',
      missingClosingSpace: 'There must be a space before this paren.',
      rejectedOpeningSpace: 'There should be no space after this paren.',
      rejectedClosingSpace: 'There should be no space before this paren.',
    },
  },

  create(context) {
    const ALWAYS = context.options[0] === 'always',
      exceptionsArrayOptions =
        (context.options[1] && context.options[1].exceptions) || [],
      options = {};

    let exceptions;

    if (exceptionsArrayOptions.length) {
      options.braceException = exceptionsArrayOptions.includes('{}');
      options.bracketException = exceptionsArrayOptions.includes('[]');
      options.parenException = exceptionsArrayOptions.includes('()');
      options.empty = exceptionsArrayOptions.includes('empty');
    }

    /**
     * Produces an object with the opener and closer exception values
     * @returns {Object} `openers` and `closers` exception values
     * @private
     */
    function getExceptions() {
      const openers = [],
        closers = [];

      if (options.braceException) {
        openers.push('{');
        closers.push('}');
      }

      if (options.bracketException) {
        openers.push('[');
        closers.push(']');
      }

      if (options.parenException) {
        openers.push('(');
        closers.push(')');
      }

      if (options.empty) {
        openers.push(')');
        closers.push('(');
      }

      return {
        openers,
        closers,
      };
    }

    //--------------------------------------------------------------------------
    // Helpers
    //--------------------------------------------------------------------------
    const sourceCode = context.sourceCode;

    /**
     * Determines if a token is one of the exceptions for the opener paren
     * @param {Object} token The token to check
     * @returns {boolean} True if the token is one of the exceptions for the opener paren
     */
    function isOpenerException(token) {
      return exceptions.openers.includes(token.value);
    }

    /**
     * Determines if a token is one of the exceptions for the closer paren
     * @param {Object} token The token to check
     * @returns {boolean} True if the token is one of the exceptions for the closer paren
     */
    function isCloserException(token) {
      return exceptions.closers.includes(token.value);
    }

    /**
     * Determines if an opening paren is immediately followed by a required space
     * @param {Object} openingParenToken The paren token
     * @param {Object} tokenAfterOpeningParen The token after it
     * @returns {boolean} True if the opening paren is missing a required space
     */
    function openerMissingSpace(openingParenToken, tokenAfterOpeningParen) {
      if (
        sourceCode.isSpaceBetweenTokens(
          openingParenToken,
          tokenAfterOpeningParen
        )
      ) {
        return false;
      }

      if (
        !options.empty &&
        astUtils.isClosingParenToken(tokenAfterOpeningParen)
      ) {
        return false;
      }

      if (ALWAYS) {
        return !isOpenerException(tokenAfterOpeningParen);
      }
      return isOpenerException(tokenAfterOpeningParen);
    }

    /**
     * Determines if an opening paren is immediately followed by a disallowed space
     * @param {Object} openingParenToken The paren token
     * @param {Object} tokenAfterOpeningParen The token after it
     * @returns {boolean} True if the opening paren has a disallowed space
     */
    function openerRejectsSpace(openingParenToken, tokenAfterOpeningParen) {
      if (
        !astUtils.isTokenOnSameLine(openingParenToken, tokenAfterOpeningParen)
      ) {
        return false;
      }

      if (tokenAfterOpeningParen.type === 'Line') {
        return false;
      }

      if (
        !sourceCode.isSpaceBetweenTokens(
          openingParenToken,
          tokenAfterOpeningParen
        )
      ) {
        return false;
      }

      if (ALWAYS) {
        return isOpenerException(tokenAfterOpeningParen);
      }
      return !isOpenerException(tokenAfterOpeningParen);
    }

    /**
     * Determines if a closing paren is immediately preceded by a required space
     * @param {Object} tokenBeforeClosingParen The token before the paren
     * @param {Object} closingParenToken The paren token
     * @returns {boolean} True if the closing paren is missing a required space
     */
    function closerMissingSpace(tokenBeforeClosingParen, closingParenToken) {
      if (
        sourceCode.isSpaceBetweenTokens(
          tokenBeforeClosingParen,
          closingParenToken
        )
      ) {
        return false;
      }

      if (
        !options.empty &&
        astUtils.isOpeningParenToken(tokenBeforeClosingParen)
      ) {
        return false;
      }

      if (ALWAYS) {
        return !isCloserException(tokenBeforeClosingParen);
      }
      return isCloserException(tokenBeforeClosingParen);
    }

    /**
     * Determines if a closer paren is immediately preceded by a disallowed space
     * @param {Object} tokenBeforeClosingParen The token before the paren
     * @param {Object} closingParenToken The paren token
     * @returns {boolean} True if the closing paren has a disallowed space
     */
    function closerRejectsSpace(tokenBeforeClosingParen, closingParenToken) {
      if (
        !astUtils.isTokenOnSameLine(tokenBeforeClosingParen, closingParenToken)
      ) {
        return false;
      }

      if (
        !sourceCode.isSpaceBetweenTokens(
          tokenBeforeClosingParen,
          closingParenToken
        )
      ) {
        return false;
      }

      if (ALWAYS) {
        return isCloserException(tokenBeforeClosingParen);
      }
      return !isCloserException(tokenBeforeClosingParen);
    }

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

    return {
      Program: function checkParenSpaces(node) {
        exceptions = getExceptions();
        const tokens = sourceCode.tokensAndComments;

        tokens.forEach((token, i) => {
          const prevToken = tokens[i - 1];
          const nextToken = tokens[i + 1];

          // if token is not an opening or closing paren token, do nothing
          if (
            !astUtils.isOpeningParenToken(token) &&
            !astUtils.isClosingParenToken(token)
          ) {
            return;
          }

          // if token is an opening paren and is not followed by a required space
          if (token.value === '(' && openerMissingSpace(token, nextToken)) {
            context.report({
              node,
              loc: token.loc,
              messageId: 'missingOpeningSpace',
              fix(fixer) {
                return fixer.insertTextAfter(token, ' ');
              },
            });
          }

          // if token is an opening paren and is followed by a disallowed space
          if (token.value === '(' && openerRejectsSpace(token, nextToken)) {
            context.report({
              node,
              loc: {
                start: token.loc.end,
                end: nextToken.loc.start,
              },
              messageId: 'rejectedOpeningSpace',
              fix(fixer) {
                return fixer.removeRange([token.range[1], nextToken.range[0]]);
              },
            });
          }

          // if token is a closing paren and is not preceded by a required space
          if (token.value === ')' && closerMissingSpace(prevToken, token)) {
            context.report({
              node,
              loc: token.loc,
              messageId: 'missingClosingSpace',
              fix(fixer) {
                return fixer.insertTextBefore(token, ' ');
              },
            });
          }

          // if token is a closing paren and is preceded by a disallowed space
          if (token.value === ')' && closerRejectsSpace(prevToken, token)) {
            context.report({
              node,
              loc: {
                start: prevToken.loc.end,
                end: token.loc.start,
              },
              messageId: 'rejectedClosingSpace',
              fix(fixer) {
                return fixer.removeRange([prevToken.range[1], token.range[0]]);
              },
            });
          }
        });
      },
    };
  },
};