/**
 * @fileoverview Enforce return after a callback.
 * @author Jamund Ferguson
 * @copyright 2015 Jamund Ferguson. All rights reserved.
 */
"use strict";

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

module.exports = function(context) {

    var callbacks = context.options[0] || ["callback", "cb", "next"];

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

    /**
     * Find the closest parent matching a list of types.
     * @param {ASTNode} node The node whose parents we are searching
     * @param {Array} types The node types to match
     * @returns {ASTNode} The matched node or undefined.
     */
    function findClosestParentOfType(node, types) {
        if (!node.parent) {
            return null;
        }
        if (types.indexOf(node.parent.type) === -1) {
            return findClosestParentOfType(node.parent, types);
        }
        return node.parent;
    }

    /**
     * Check to see if a CallExpression is in our callback list.
     * @param {ASTNode} node The node to check against our callback names list.
     * @returns {Boolean} Whether or not this function matches our callback name.
     */
    function isCallback(node) {
        return node.callee.type === "Identifier" && callbacks.indexOf(node.callee.name) > -1;
    }

    /**
     * Determines whether or not the callback is part of a callback expression.
     * @param {ASTNode} node The callback node
     * @param {ASTNode} parentNode The expression node
     * @returns {boolean} Whether or not this is part of a callback expression
     */
    function isCallbackExpression(node, parentNode) {

        // ensure the parent node exists and is an expression
        if (!parentNode || parentNode.type !== "ExpressionStatement") {
            return false;
        }

        // cb()
        if (parentNode.expression === node) {
            return true;
        }

        // special case for cb && cb() and similar
        if (parentNode.expression.type === "BinaryExpression" || parentNode.expression.type === "LogicalExpression") {
            if (parentNode.expression.right === node) {
                return true;
            }
        }
    }

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    return {
        "CallExpression": function(node) {

            // if we"re not a callback we can return
            if (!isCallback(node)) {
                return;
            }

            // find the closest block, return or loop
            var closestBlock = findClosestParentOfType(node, ["BlockStatement", "ReturnStatement", "ArrowFunctionExpression"]) || {},
                lastItem, parentType;

            // if our parent is a return we know we're ok
            if (closestBlock.type === "ReturnStatement" ) {
                return;
            }

            // arrow functions don't always have blocks and implicitly return
            if (closestBlock.type === "ArrowFunctionExpression") {
                return;
            }

            // block statements are part of functions and most if statements
            if (closestBlock.type === "BlockStatement") {

                // find the last item in the block
                lastItem = closestBlock.body[closestBlock.body.length - 1];

                // if the callback is the last thing in a block that might be ok
                if (isCallbackExpression(node, lastItem)) {

                    parentType = closestBlock.parent.type;

                    // but only if the block is part of a function
                    if (parentType === "FunctionExpression" ||
                        parentType === "FunctionDeclaration" ||
                        parentType === "ArrowFunctionExpression"
                    ) {
                        return;
                    }

                }

                // ending a block with a return is also ok
                if (lastItem.type === "ReturnStatement") {

                    // but only if the callback is immediately before
                    if (isCallbackExpression(node, closestBlock.body[closestBlock.body.length - 2])) {
                        return;
                    }
                }

            }

            // as long as you're the child of a function at this point you should be asked to return
            if (findClosestParentOfType(node, ["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"])) {
                context.report(node, "Expected return with your callback function.");
            }

        }

    };
};

module.exports.schema = [{
    type: "array",
    items: { type: "string" }
}];
