/**
 * @fileoverview Rule to enforce grouped require statements for Node.JS
 * @author Raphael Pigulla
 */

"use strict";

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

module.exports = function(context) {

    var grouping = false,
        allowCall = false,
        options = context.options[0];

    if (typeof options === "object") {
        grouping = options.grouping;
        allowCall = options.allowCall;
    } else {
        grouping = !!options;
    }

    /**
     * Returns the list of built-in modules.
     *
     * @returns {string[]} An array of built-in Node.js modules.
     */
    function getBuiltinModules() {
        // This list is generated using `require("repl")._builtinLibs.concat('repl').sort()`
        // This particular list is as per nodejs v0.12.2 and iojs v0.7.1
        return [
            "assert", "buffer", "child_process", "cluster", "crypto",
            "dgram", "dns", "domain", "events", "fs", "http", "https",
            "net", "os", "path", "punycode", "querystring", "readline",
            "repl", "smalloc", "stream", "string_decoder", "tls", "tty",
            "url", "util", "v8", "vm", "zlib"
        ];
    }

    var BUILTIN_MODULES = getBuiltinModules();

    var DECL_REQUIRE = "require",
        DECL_UNINITIALIZED = "uninitialized",
        DECL_OTHER = "other";

    var REQ_CORE = "core",
        REQ_FILE = "file",
        REQ_MODULE = "module",
        REQ_COMPUTED = "computed";

    /**
     * Determines the type of a declaration statement.
     * @param {ASTNode} initExpression The init node of the VariableDeclarator.
     * @returns {string} The type of declaration represented by the expression.
     */
    function getDeclarationType(initExpression) {
        if (!initExpression) {
            // "var x;"
            return DECL_UNINITIALIZED;
        }

        if (initExpression.type === "CallExpression" &&
            initExpression.callee.type === "Identifier" &&
            initExpression.callee.name === "require"
        ) {
            // "var x = require('util');"
            return DECL_REQUIRE;
        } else if (allowCall &&
            initExpression.type === "CallExpression" &&
            initExpression.callee.type === "CallExpression"
        ) {
            // "var x = require('diagnose')('sub-module');"
            return getDeclarationType(initExpression.callee);
        } else if (initExpression.type === "MemberExpression") {
            // "var x = require('glob').Glob;"
            return getDeclarationType(initExpression.object);
        }

        // "var x = 42;"
        return DECL_OTHER;
    }

    /**
     * Determines the type of module that is loaded via require.
     * @param {ASTNode} initExpression The init node of the VariableDeclarator.
     * @returns {string} The module type.
     */
    function inferModuleType(initExpression) {
        if (initExpression.type === "MemberExpression") {
            // "var x = require('glob').Glob;"
            return inferModuleType(initExpression.object);
        } else if (initExpression.arguments.length === 0) {
            // "var x = require();"
            return REQ_COMPUTED;
        }

        var arg = initExpression.arguments[0];

        if (arg.type !== "Literal" || typeof arg.value !== "string") {
            // "var x = require(42);"
            return REQ_COMPUTED;
        }

        if (BUILTIN_MODULES.indexOf(arg.value) !== -1) {
            // "var fs = require('fs');"
            return REQ_CORE;
        } else if (/^\.{0,2}\//.test(arg.value)) {
            // "var utils = require('./utils');"
            return REQ_FILE;
        } else {
            // "var async = require('async');"
            return REQ_MODULE;
        }
    }

    /**
     * Check if the list of variable declarations is mixed, i.e. whether it
     * contains both require and other declarations.
     * @param {ASTNode} declarations The list of VariableDeclarators.
     * @returns {boolean} True if the declarations are mixed, false if not.
     */
    function isMixed(declarations) {
        var contains = {};

        declarations.forEach(function(declaration) {
            var type = getDeclarationType(declaration.init);
            contains[type] = true;
        });

        return !!(
            contains[DECL_REQUIRE] &&
            (contains[DECL_UNINITIALIZED] || contains[DECL_OTHER])
        );
    }

    /**
     * Check if all require declarations in the given list are of the same
     * type.
     * @param {ASTNode} declarations The list of VariableDeclarators.
     * @returns {boolean} True if the declarations are grouped, false if not.
     */
    function isGrouped(declarations) {
        var found = {};

        declarations.forEach(function(declaration) {
            if (getDeclarationType(declaration.init) === DECL_REQUIRE) {
                found[inferModuleType(declaration.init)] = true;
            }
        });

        return Object.keys(found).length <= 1;
    }


    return {

        "VariableDeclaration": function(node) {

            if (isMixed(node.declarations)) {
                context.report(
                    node,
                    "Do not mix 'require' and other declarations."
                );
            } else if (grouping && !isGrouped(node.declarations)) {
                context.report(
                    node,
                    "Do not mix core, module, file and computed requires."
                );
            }
        }
    };

};

module.exports.schema = [
    {
        "oneOf": [
            {
                "type": "boolean"
            },
            {
                "type": "object",
                "properties": {
                    "grouping": {
                        "type": "boolean"
                    },
                    "allowCall": {
                        "type": "boolean"
                    }
                },
                "additionalProperties": false
            }
        ]
    }
];
