/**
 * @fileoverview Rule to check for "block scoped" variables by binding context
 * @author Matt DuVall <http://www.mattduvall.com>
 */
"use strict";

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

module.exports = function(context) {

    var scopeStack = [];

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

    /**
     * Determines whether an identifier is in declaration position or is a non-declaration reference.
     * @param {ASTNode} id The identifier.
     * @param {ASTNode} parent The identifier's parent AST node.
     * @returns {Boolean} true when the identifier is in declaration position.
     */
    function isDeclaration(id, parent) {
        switch (parent.type) {
            case "FunctionDeclaration":
            case "FunctionExpression":
                return parent.params.indexOf(id) > -1 || id === parent.id;

            case "VariableDeclarator":
                return id === parent.id;

            case "CatchClause":
                return id === parent.param;

            default:
                return false;
        }
    }

    /**
     * Determines whether an identifier is in property position.
     * @param {ASTNode} id The identifier.
     * @param {ASTNode} parent The identifier's parent AST node.
     * @returns {Boolean} true when the identifier is in property position.
     */
    function isProperty(id, parent) {
        switch (parent.type) {
            case "MemberExpression":
                return id === parent.property && !parent.computed;

            case "Property":
                return id === parent.key;

            default:
                return false;
        }
    }

    /**
     * Pushes a new scope object on the scope stack.
     * @returns {void}
     */
    function pushScope() {
        scopeStack.push([]);
    }

    /**
     * Removes the topmost scope object from the scope stack.
     * @returns {void}
     */
    function popScope() {
        scopeStack.pop();
    }

    /**
     * Declares the given names in the topmost scope object.
     * @param {[String]} names A list of names to declare.
     * @returns {void}
     */
    function declare(names) {
        [].push.apply(scopeStack[scopeStack.length - 1], names);
    }

    //--------------------------------------------------------------------------
    // Public API
    //--------------------------------------------------------------------------

    /**
     * Declares all relevant identifiers for module imports.
     * @param {ASTNode} node The AST node representing an import.
     * @returns {void}
     * @private
     */
    function declareImports(node) {
        declare([node.local.name]);

        if (node.imported && node.imported.name !== node.local.name) {
            declare([node.imported.name]);
        }
    }

    /**
     * Declares all relevant identifiers for classes.
     * @param {ASTNode} node The AST node representing a class.
     * @returns {void}
     * @private
     */
    function declareClass(node) {

        if (node.id) {
            declare([node.id.name]);
        }

        pushScope();
    }

    /**
     * Declares all relevant identifiers for classes.
     * @param {ASTNode} node The AST node representing a class.
     * @returns {void}
     * @private
     */
    function declareClassMethod(node) {
        pushScope();

        declare([node.key.name]);
    }

    /**
     * Add declarations based on the type of node being passed.
     * @param {ASTNode} node The node containing declarations.
     * @returns {void}
     * @private
     */
    function declareByNodeType(node) {

        var declarations = [];

        switch (node.type) {
            case "Identifier":
                declarations.push(node.name);
                break;

            case "ObjectPattern":
                node.properties.forEach(function(property) {
                    declarations.push(property.key.name);
                    if (property.value) {
                        declarations.push(property.value.name);
                    }
                });
                break;

            case "ArrayPattern":
                node.elements.forEach(function(element) {
                    if (element) {
                        declarations.push(element.name);
                    }
                });
                break;

            case "AssignmentPattern":
                declareByNodeType(node.left);
                break;

            case "RestElement":
                declareByNodeType(node.argument);
                break;

            // no default
        }

        declare(declarations);

    }

    /**
     * Adds declarations of the function parameters and pushes the scope
     * @param {ASTNode} node The node containing declarations.
     * @returns {void}
     * @private
     */
    function functionHandler(node) {
        pushScope();

        node.params.forEach(function(param) {
            declareByNodeType(param);
        });

        declare(node.rest ? [node.rest.name] : []);
        declare(["arguments"]);
    }

    /**
     * Adds declaration of the function name in its parent scope then process the function
     * @param {ASTNode} node The node containing declarations.
     * @returns {void}
     * @private
     */
    function functionDeclarationHandler(node) {
        declare(node.id ? [node.id.name] : []);
        functionHandler(node);
    }

    /**
     * Process function declarations and declares its name in its own scope
     * @param {ASTNode} node The node containing declarations.
     * @returns {void}
     * @private
     */
    function functionExpressionHandler(node) {
        functionHandler(node);
        declare(node.id ? [node.id.name] : []);
    }

    function variableDeclarationHandler(node) {
        node.declarations.forEach(function(declaration) {
            declareByNodeType(declaration.id);
        });

    }

    return {
        "Program": function() {
            var scope = context.getScope();
            scopeStack = [scope.variables.map(function(v) {
                return v.name;
            })];

            // global return creates another scope
            if (context.ecmaFeatures.globalReturn) {
                scope = scope.childScopes[0];
                scopeStack.push(scope.variables.map(function(v) {
                    return v.name;
                }));
            }
        },

        "ImportSpecifier": declareImports,
        "ImportDefaultSpecifier": declareImports,
        "ImportNamespaceSpecifier": declareImports,

        "BlockStatement": function(node) {
            var statements = node.body;
            pushScope();
            statements.forEach(function(stmt) {
                if (stmt.type === "VariableDeclaration") {
                    variableDeclarationHandler(stmt);
                } else if (stmt.type === "FunctionDeclaration") {
                    declare([stmt.id.name]);
                }
            });
        },

        "VariableDeclaration": function (node) {
            variableDeclarationHandler(node);
        },

        "BlockStatement:exit": popScope,

        "CatchClause": function(node) {
            pushScope();
            declare([node.param.name]);
        },
        "CatchClause:exit": popScope,

        "FunctionDeclaration": functionDeclarationHandler,
        "FunctionDeclaration:exit": popScope,

        "ClassDeclaration": declareClass,
        "ClassDeclaration:exit": popScope,

        "ClassExpression": declareClass,
        "ClassExpression:exit": popScope,

        "MethodDefinition": declareClassMethod,
        "MethodDefinition:exit": popScope,

        "FunctionExpression": functionExpressionHandler,
        "FunctionExpression:exit": popScope,

        // Arrow functions cannot have names
        "ArrowFunctionExpression": functionHandler,
        "ArrowFunctionExpression:exit": popScope,

        "ForStatement": function() {
            pushScope();
        },
        "ForStatement:exit": popScope,

        "ForInStatement": function() {
            pushScope();
        },
        "ForInStatement:exit": popScope,

        "ForOfStatement": function() {
            pushScope();
        },
        "ForOfStatement:exit": popScope,

        "Identifier": function(node) {
            var ancestor = context.getAncestors().pop();
            if (isDeclaration(node, ancestor) || isProperty(node, ancestor) || ancestor.type === "LabeledStatement") {
                return;
            }

            for (var i = 0, l = scopeStack.length; i < l; i++) {
                if (scopeStack[i].indexOf(node.name) > -1) {
                    return;
                }
            }

            context.report(node, "\"" + node.name + "\" used outside of binding context.");
        }
    };

};

module.exports.schema = [];
