/**
 * @fileoverview Rule to flag block statements that do not use the one true brace style
 * @author Ian Christian Myers
 */

"use strict";

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

module.exports = function(context) {
    var style = context.options[0] || "1tbs";
    var params = context.options[1] || {};

    var OPEN_MESSAGE = "Opening curly brace does not appear on the same line as controlling statement.",
        OPEN_MESSAGE_ALLMAN = "Opening curly brace appears on the same line as controlling statement.",
        BODY_MESSAGE = "Statement inside of curly braces should be on next line.",
        CLOSE_MESSAGE = "Closing curly brace does not appear on the same line as the subsequent block.",
        CLOSE_MESSAGE_SINGLE = "Closing curly brace should be on the same line as opening curly brace or on the line after the previous block.",
        CLOSE_MESSAGE_STROUSTRUP_ALLMAN = "Closing curly brace appears on the same line as the subsequent block.";

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

    /**
     * Determines if a given node is a block statement.
     * @param {ASTNode} node The node to check.
     * @returns {boolean} True if the node is a block statement, false if not.
     * @private
     */
    function isBlock(node) {
        return node && node.type === "BlockStatement";
    }

    /**
     * Check if the token is an punctuator with a value of curly brace
     * @param {object} token - Token to check
     * @returns {boolean} true if its a curly punctuator
     * @private
     */
    function isCurlyPunctuator(token) {
        return token.value === "{" || token.value === "}";
    }

    /**
     * Binds a list of properties to a function that verifies that the opening
     * curly brace is on the same line as its controlling statement of a given
     * node.
     * @param {...string} The properties to check on the node.
     * @returns {Function} A function that will perform the check on a node
     * @private
     */
    function checkBlock() {
        var blockProperties = arguments;

        return function(node) {
            [].forEach.call(blockProperties, function(blockProp) {
                var block = node[blockProp], previousToken, curlyToken, curlyTokenEnd, curlyTokensOnSameLine;

                if (!isBlock(block)) {
                    return;
                }

                previousToken = context.getTokenBefore(block);
                curlyToken = context.getFirstToken(block);
                curlyTokenEnd = context.getLastToken(block);
                curlyTokensOnSameLine = curlyToken.loc.start.line === curlyTokenEnd.loc.start.line;

                if (style !== "allman" && previousToken.loc.start.line !== curlyToken.loc.start.line) {
                    context.report(node, OPEN_MESSAGE);
                } else if (style === "allman" && previousToken.loc.start.line === curlyToken.loc.start.line && !params.allowSingleLine) {
                    context.report(node, OPEN_MESSAGE_ALLMAN);
                }

                if (!block.body.length || curlyTokensOnSameLine && params.allowSingleLine) {
                    return;
                }

                if (curlyToken.loc.start.line === block.body[0].loc.start.line) {
                    context.report(block.body[0], BODY_MESSAGE);
                } else if (curlyTokenEnd.loc.start.line === block.body[block.body.length - 1].loc.start.line) {
                    context.report(block.body[block.body.length - 1], CLOSE_MESSAGE_SINGLE);
                }
            });
        };
    }

    /**
     * Enforces the configured brace style on IfStatements
     * @param {ASTNode} node An IfStatement node.
     * @returns {void}
     * @private
     */
    function checkIfStatement(node) {
        var tokens,
            alternateIsBlock = false,
            alternateIsIfBlock = false;

        checkBlock("consequent", "alternate")(node);

        if (node.alternate) {

            alternateIsBlock = isBlock(node.alternate);
            alternateIsIfBlock = node.alternate.type === "IfStatement" && isBlock(node.alternate.consequent);

            if (alternateIsBlock || alternateIsIfBlock) {
                tokens = context.getTokensBefore(node.alternate, 2);

                if (style === "1tbs") {
                    if (tokens[0].loc.start.line !== tokens[1].loc.start.line &&
                        node.consequent.type === "BlockStatement" &&
                        isCurlyPunctuator(tokens[0]) ) {
                        context.report(node.alternate, CLOSE_MESSAGE);
                    }
                } else if (tokens[0].loc.start.line === tokens[1].loc.start.line) {
                    context.report(node.alternate, CLOSE_MESSAGE_STROUSTRUP_ALLMAN);
                }
            }

        }
    }

    /**
     * Enforces the configured brace style on TryStatements
     * @param {ASTNode} node A TryStatement node.
     * @returns {void}
     * @private
     */
    function checkTryStatement(node) {
        var tokens;

        checkBlock("block", "finalizer")(node);

        if (isBlock(node.finalizer)) {
            tokens = context.getTokensBefore(node.finalizer, 2);
            if (style === "1tbs") {
                if (tokens[0].loc.start.line !== tokens[1].loc.start.line) {
                    context.report(node.finalizer, CLOSE_MESSAGE);
                }
            } else if (tokens[0].loc.start.line === tokens[1].loc.start.line) {
                context.report(node.finalizer, CLOSE_MESSAGE_STROUSTRUP_ALLMAN);
            }
        }
    }

    /**
     * Enforces the configured brace style on CatchClauses
     * @param {ASTNode} node A CatchClause node.
     * @returns {void}
     * @private
     */
    function checkCatchClause(node) {
        var previousToken = context.getTokenBefore(node),
            firstToken = context.getFirstToken(node);

        checkBlock("body")(node);

        if (isBlock(node.body)) {
            if (style === "1tbs") {
                if (previousToken.loc.start.line !== firstToken.loc.start.line) {
                    context.report(node, CLOSE_MESSAGE);
                }
            } else {
                if (previousToken.loc.start.line === firstToken.loc.start.line) {
                    context.report(node, CLOSE_MESSAGE_STROUSTRUP_ALLMAN);
                }
            }
        }
    }

    /**
     * Enforces the configured brace style on SwitchStatements
     * @param {ASTNode} node A SwitchStatement node.
     * @returns {void}
     * @private
     */
    function checkSwitchStatement(node) {
        var tokens;
        if (node.cases && node.cases.length) {
            tokens = context.getTokensBefore(node.cases[0], 2);
        } else {
            tokens = context.getLastTokens(node, 3);
        }

        if (style !== "allman" && tokens[0].loc.start.line !== tokens[1].loc.start.line) {
            context.report(node, OPEN_MESSAGE);
        } else if (style === "allman" && tokens[0].loc.start.line === tokens[1].loc.start.line) {
            context.report(node, OPEN_MESSAGE_ALLMAN);
        }
    }

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

    return {
        "FunctionDeclaration": checkBlock("body"),
        "FunctionExpression": checkBlock("body"),
        "ArrowFunctionExpression": checkBlock("body"),
        "IfStatement": checkIfStatement,
        "TryStatement": checkTryStatement,
        "CatchClause": checkCatchClause,
        "DoWhileStatement": checkBlock("body"),
        "WhileStatement": checkBlock("body"),
        "WithStatement": checkBlock("body"),
        "ForStatement": checkBlock("body"),
        "ForInStatement": checkBlock("body"),
        "ForOfStatement": checkBlock("body"),
        "SwitchStatement": checkSwitchStatement
    };

};

module.exports.schema = [
    {
        "enum": ["1tbs", "stroustrup", "allman"]
    },
    {
        "type": "object",
        "properties": {
            "allowSingleLine": {
                "type": "boolean"
            }
        },
        "additionalProperties": false
    }
];
