/**
 * @fileoverview Rule to flag when the same variable is declared more then once.
 * @author Ilya Volodin
 */

'use strict';

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

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

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

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

    defaultOptions: [{ builtinGlobals: true }],

    docs: {
      description: 'Disallow variable redeclaration',
      recommended: true,
      url: 'https://eslint.org/docs/latest/rules/no-redeclare',
    },

    messages: {
      redeclared: "'{{id}}' is already defined.",
      redeclaredAsBuiltin:
        "'{{id}}' is already defined as a built-in global variable.",
      redeclaredBySyntax:
        "'{{id}}' is already defined by a variable declaration.",
    },

    schema: [
      {
        type: 'object',
        properties: {
          builtinGlobals: { type: 'boolean' },
        },
        additionalProperties: false,
      },
    ],
  },

  create(context) {
    const [{ builtinGlobals }] = context.options;
    const sourceCode = context.sourceCode;

    /**
     * Iterate declarations of a given variable.
     * @param {escope.variable} variable The variable object to iterate declarations.
     * @returns {IterableIterator<{type:string,node:ASTNode,loc:SourceLocation}>} The declarations.
     */
    function* iterateDeclarations(variable) {
      if (
        builtinGlobals &&
        (variable.eslintImplicitGlobalSetting === 'readonly' ||
          variable.eslintImplicitGlobalSetting === 'writable')
      ) {
        yield { type: 'builtin' };
      }

      for (const id of variable.identifiers) {
        yield { type: 'syntax', node: id, loc: id.loc };
      }

      if (variable.eslintExplicitGlobalComments) {
        for (const comment of variable.eslintExplicitGlobalComments) {
          yield {
            type: 'comment',
            node: comment,
            loc: astUtils.getNameLocationInGlobalDirectiveComment(
              sourceCode,
              comment,
              variable.name
            ),
          };
        }
      }
    }

    /**
     * Find variables in a given scope and flag redeclared ones.
     * @param {Scope} scope An eslint-scope scope object.
     * @returns {void}
     * @private
     */
    function findVariablesInScope(scope) {
      for (const variable of scope.variables) {
        const [declaration, ...extraDeclarations] =
          iterateDeclarations(variable);

        if (extraDeclarations.length === 0) {
          continue;
        }

        /*
         * If the type of a declaration is different from the type of
         * the first declaration, it shows the location of the first
         * declaration.
         */
        const detailMessageId =
          declaration.type === 'builtin' ?
            'redeclaredAsBuiltin'
          : 'redeclaredBySyntax';
        const data = { id: variable.name };

        // Report extra declarations.
        for (const { type, node, loc } of extraDeclarations) {
          const messageId =
            type === declaration.type ? 'redeclared' : detailMessageId;

          context.report({ node, loc, messageId, data });
        }
      }
    }

    /**
     * Find variables in the current scope.
     * @param {ASTNode} node The node of the current scope.
     * @returns {void}
     * @private
     */
    function checkForBlock(node) {
      const scope = sourceCode.getScope(node);

      /*
       * In ES5, some node type such as `BlockStatement` doesn't have that scope.
       * `scope.block` is a different node in such a case.
       */
      if (scope.block === node) {
        findVariablesInScope(scope);
      }
    }

    return {
      Program(node) {
        const scope = sourceCode.getScope(node);

        findVariablesInScope(scope);

        // Node.js or ES modules has a special scope.
        if (
          scope.type === 'global' &&
          scope.childScopes[0] &&
          // The special scope's block is the Program node.
          scope.block === scope.childScopes[0].block
        ) {
          findVariablesInScope(scope.childScopes[0]);
        }
      },

      FunctionDeclaration: checkForBlock,
      FunctionExpression: checkForBlock,
      ArrowFunctionExpression: checkForBlock,

      StaticBlock: checkForBlock,

      BlockStatement: checkForBlock,
      ForStatement: checkForBlock,
      ForInStatement: checkForBlock,
      ForOfStatement: checkForBlock,
      SwitchStatement: checkForBlock,
    };
  },
};