/**
 * @fileoverview Rule to flag use of parseInt without a radix argument
 * @author James Allardice
 */

"use strict";

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

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

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

const MODE_ALWAYS = "always",
    MODE_AS_NEEDED = "as-needed";

/**
 * Checks whether a given variable is shadowed or not.
 * @param {eslint-scope.Variable} variable A variable to check.
 * @returns {boolean} `true` if the variable is shadowed.
 */
function isShadowed(variable) {
    return variable.defs.length >= 1;
}

/**
 * Checks whether a given node is a MemberExpression of `parseInt` method or not.
 * @param {ASTNode} node A node to check.
 * @returns {boolean} `true` if the node is a MemberExpression of `parseInt`
 *      method.
 */
function isParseIntMethod(node) {
    return (
        node.type === "MemberExpression" &&
        !node.computed &&
        node.property.type === "Identifier" &&
        node.property.name === "parseInt"
    );
}

/**
 * Checks whether a given node is a valid value of radix or not.
 *
 * The following values are invalid.
 *
 * - A literal except numbers.
 * - undefined.
 * @param {ASTNode} radix A node of radix to check.
 * @returns {boolean} `true` if the node is valid.
 */
function isValidRadix(radix) {
    return !(
        (radix.type === "Literal" && typeof radix.value !== "number") ||
        (radix.type === "Identifier" && radix.name === "undefined")
    );
}

/**
 * Checks whether a given node is a default value of radix or not.
 * @param {ASTNode} radix A node of radix to check.
 * @returns {boolean} `true` if the node is the literal node of `10`.
 */
function isDefaultRadix(radix) {
    return radix.type === "Literal" && radix.value === 10;
}

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

module.exports = {
    meta: {
        type: "suggestion",

        docs: {
            description: "enforce the consistent use of the radix argument when using `parseInt()`",
            category: "Best Practices",
            recommended: false,
            url: "https://eslint.org/docs/rules/radix"
        },

        schema: [
            {
                enum: ["always", "as-needed"]
            }
        ]
    },

    create(context) {
        const mode = context.options[0] || MODE_ALWAYS;

        /**
         * Checks the arguments of a given CallExpression node and reports it if it
         * offends this rule.
         * @param {ASTNode} node A CallExpression node to check.
         * @returns {void}
         */
        function checkArguments(node) {
            const args = node.arguments;

            switch (args.length) {
                case 0:
                    context.report({
                        node,
                        message: "Missing parameters."
                    });
                    break;

                case 1:
                    if (mode === MODE_ALWAYS) {
                        context.report({
                            node,
                            message: "Missing radix parameter."
                        });
                    }
                    break;

                default:
                    if (mode === MODE_AS_NEEDED && isDefaultRadix(args[1])) {
                        context.report({
                            node,
                            message: "Redundant radix parameter."
                        });
                    } else if (!isValidRadix(args[1])) {
                        context.report({
                            node,
                            message: "Invalid radix parameter."
                        });
                    }
                    break;
            }
        }

        return {
            "Program:exit"() {
                const scope = context.getScope();
                let variable;

                // Check `parseInt()`
                variable = astUtils.getVariableByName(scope, "parseInt");
                if (!isShadowed(variable)) {
                    variable.references.forEach(reference => {
                        const node = reference.identifier;

                        if (astUtils.isCallee(node)) {
                            checkArguments(node.parent);
                        }
                    });
                }

                // Check `Number.parseInt()`
                variable = astUtils.getVariableByName(scope, "Number");
                if (!isShadowed(variable)) {
                    variable.references.forEach(reference => {
                        const node = reference.identifier.parent;

                        if (isParseIntMethod(node) && astUtils.isCallee(node)) {
                            checkArguments(node.parent);
                        }
                    });
                }
            }
        };
    }
};
