/**
 * @fileoverview Rule to check for the usage of var.
 * @author Jamund Ferguson
 */

"use strict";

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

const SCOPE_NODE_TYPE = /^(?:Program|BlockStatement|SwitchStatement|ForStatement|ForInStatement|ForOfStatement)$/;

/**
 * Gets the scope node which directly contains a given node.
 *
 * @param {ASTNode} node - A node to get. This is a `VariableDeclaration` or
 *      an `Identifier`.
 * @returns {ASTNode} A scope node. This is one of `Program`, `BlockStatement`,
 *      `SwitchStatement`, `ForStatement`, `ForInStatement`, and
 *      `ForOfStatement`.
 */
function getScopeNode(node) {
    while (node) {
        if (SCOPE_NODE_TYPE.test(node.type)) {
            return node;
        }

        node = node.parent;
    }

    /* istanbul ignore next : unreachable */
    return null;
}

/**
 * Checks whether a given variable is redeclared or not.
 *
 * @param {escope.Variable} variable - A variable to check.
 * @returns {boolean} `true` if the variable is redeclared.
 */
function isRedeclared(variable) {
    return variable.defs.length >= 2;
}

/**
 * Checks whether a given variable is used from outside of the specified scope.
 *
 * @param {ASTNode} scopeNode - A scope node to check.
 * @returns {Function} The predicate function which checks whether a given
 *      variable is used from outside of the specified scope.
 */
function isUsedFromOutsideOf(scopeNode) {

    /**
     * Checks whether a given reference is inside of the specified scope or not.
     *
     * @param {escope.Reference} reference - A reference to check.
     * @returns {boolean} `true` if the reference is inside of the specified
     *      scope.
     */
    function isOutsideOfScope(reference) {
        const scope = scopeNode.range;
        const id = reference.identifier.range;

        return id[0] < scope[0] || id[1] > scope[1];
    }

    return function(variable) {
        return variable.references.some(isOutsideOfScope);
    };
}

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

module.exports = {
    meta: {
        docs: {
            description: "require `let` or `const` instead of `var`",
            category: "ECMAScript 6",
            recommended: false
        },

        schema: [],
        fixable: "code"
    },

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

        /**
         * Checks whether it can fix a given variable declaration or not.
         * It cannot fix if the following cases:
         *
         * - A variable is declared on a SwitchCase node.
         * - A variable is redeclared.
         * - A variable is used from outside the scope.
         *
         * ## A variable is declared on a SwitchCase node.
         *
         * If this rule modifies 'var' declarations on a SwitchCase node, it
         * would generate the warnings of 'no-case-declarations' rule. And the
         * 'eslint:recommended' preset includes 'no-case-declarations' rule, so
         * this rule doesn't modify those declarations.
         *
         * ## A variable is redeclared.
         *
         * The language spec disallows redeclarations of `let` declarations.
         * Those variables would cause syntax errors.
         *
         * ## A variable is used from outside the scope.
         *
         * The language spec disallows accesses from outside of the scope for
         * `let` declarations. Those variables would cause reference errors.
         *
         * @param {ASTNode} node - A variable declaration node to check.
         * @returns {boolean} `true` if it can fix the node.
         */
        function canFix(node) {
            const variables = context.getDeclaredVariables(node);
            const scopeNode = getScopeNode(node);

            return !(
                node.parent.type === "SwitchCase" ||
                variables.some(isRedeclared) ||
                variables.some(isUsedFromOutsideOf(scopeNode))
            );
        }

        /**
         * Reports a given variable declaration node.
         *
         * @param {ASTNode} node - A variable declaration node to report.
         * @returns {void}
         */
        function report(node) {
            const varToken = sourceCode.getFirstToken(node);

            context.report({
                node,
                message: "Unexpected var, use let or const instead.",

                fix(fixer) {
                    if (canFix(node)) {
                        return fixer.replaceText(varToken, "let");
                    }
                    return null;
                }
            });
        }

        return {
            VariableDeclaration(node) {
                if (node.kind === "var") {
                    report(node);
                }
            }
        };
    }
};
