/**
 * @fileoverview Specify the maximum number of statements allowed per line.
 * @author Kenneth Williams
 */
"use strict";

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

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

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

/** @type {import('../shared/types').Rule} */
module.exports = {
    meta: {
        type: "layout",

        docs: {
            description: "Enforce a maximum number of statements allowed per line",
            recommended: false,
            url: "https://eslint.org/docs/rules/max-statements-per-line"
        },

        schema: [
            {
                type: "object",
                properties: {
                    max: {
                        type: "integer",
                        minimum: 1,
                        default: 1
                    }
                },
                additionalProperties: false
            }
        ],
        messages: {
            exceed: "This line has {{numberOfStatementsOnThisLine}} {{statements}}. Maximum allowed is {{maxStatementsPerLine}}."
        }
    },

    create(context) {

        const sourceCode = context.getSourceCode(),
            options = context.options[0] || {},
            maxStatementsPerLine = typeof options.max !== "undefined" ? options.max : 1;

        let lastStatementLine = 0,
            numberOfStatementsOnThisLine = 0,
            firstExtraStatement;

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

        const SINGLE_CHILD_ALLOWED = /^(?:(?:DoWhile|For|ForIn|ForOf|If|Labeled|While)Statement|Export(?:Default|Named)Declaration)$/u;

        /**
         * Reports with the first extra statement, and clears it.
         * @returns {void}
         */
        function reportFirstExtraStatementAndClear() {
            if (firstExtraStatement) {
                context.report({
                    node: firstExtraStatement,
                    messageId: "exceed",
                    data: {
                        numberOfStatementsOnThisLine,
                        maxStatementsPerLine,
                        statements: numberOfStatementsOnThisLine === 1 ? "statement" : "statements"
                    }
                });
            }
            firstExtraStatement = null;
        }

        /**
         * Gets the actual last token of a given node.
         * @param {ASTNode} node A node to get. This is a node except EmptyStatement.
         * @returns {Token} The actual last token.
         */
        function getActualLastToken(node) {
            return sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
        }

        /**
         * Addresses a given node.
         * It updates the state of this rule, then reports the node if the node violated this rule.
         * @param {ASTNode} node A node to check.
         * @returns {void}
         */
        function enterStatement(node) {
            const line = node.loc.start.line;

            /*
             * Skip to allow non-block statements if this is direct child of control statements.
             * `if (a) foo();` is counted as 1.
             * But `if (a) foo(); else foo();` should be counted as 2.
             */
            if (SINGLE_CHILD_ALLOWED.test(node.parent.type) &&
                node.parent.alternate !== node
            ) {
                return;
            }

            // Update state.
            if (line === lastStatementLine) {
                numberOfStatementsOnThisLine += 1;
            } else {
                reportFirstExtraStatementAndClear();
                numberOfStatementsOnThisLine = 1;
                lastStatementLine = line;
            }

            // Reports if the node violated this rule.
            if (numberOfStatementsOnThisLine === maxStatementsPerLine + 1) {
                firstExtraStatement = firstExtraStatement || node;
            }
        }

        /**
         * Updates the state of this rule with the end line of leaving node to check with the next statement.
         * @param {ASTNode} node A node to check.
         * @returns {void}
         */
        function leaveStatement(node) {
            const line = getActualLastToken(node).loc.end.line;

            // Update state.
            if (line !== lastStatementLine) {
                reportFirstExtraStatementAndClear();
                numberOfStatementsOnThisLine = 1;
                lastStatementLine = line;
            }
        }

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

        return {
            BreakStatement: enterStatement,
            ClassDeclaration: enterStatement,
            ContinueStatement: enterStatement,
            DebuggerStatement: enterStatement,
            DoWhileStatement: enterStatement,
            ExpressionStatement: enterStatement,
            ForInStatement: enterStatement,
            ForOfStatement: enterStatement,
            ForStatement: enterStatement,
            FunctionDeclaration: enterStatement,
            IfStatement: enterStatement,
            ImportDeclaration: enterStatement,
            LabeledStatement: enterStatement,
            ReturnStatement: enterStatement,
            SwitchStatement: enterStatement,
            ThrowStatement: enterStatement,
            TryStatement: enterStatement,
            VariableDeclaration: enterStatement,
            WhileStatement: enterStatement,
            WithStatement: enterStatement,
            ExportNamedDeclaration: enterStatement,
            ExportDefaultDeclaration: enterStatement,
            ExportAllDeclaration: enterStatement,

            "BreakStatement:exit": leaveStatement,
            "ClassDeclaration:exit": leaveStatement,
            "ContinueStatement:exit": leaveStatement,
            "DebuggerStatement:exit": leaveStatement,
            "DoWhileStatement:exit": leaveStatement,
            "ExpressionStatement:exit": leaveStatement,
            "ForInStatement:exit": leaveStatement,
            "ForOfStatement:exit": leaveStatement,
            "ForStatement:exit": leaveStatement,
            "FunctionDeclaration:exit": leaveStatement,
            "IfStatement:exit": leaveStatement,
            "ImportDeclaration:exit": leaveStatement,
            "LabeledStatement:exit": leaveStatement,
            "ReturnStatement:exit": leaveStatement,
            "SwitchStatement:exit": leaveStatement,
            "ThrowStatement:exit": leaveStatement,
            "TryStatement:exit": leaveStatement,
            "VariableDeclaration:exit": leaveStatement,
            "WhileStatement:exit": leaveStatement,
            "WithStatement:exit": leaveStatement,
            "ExportNamedDeclaration:exit": leaveStatement,
            "ExportDefaultDeclaration:exit": leaveStatement,
            "ExportAllDeclaration:exit": leaveStatement,
            "Program:exit": reportFirstExtraStatementAndClear
        };
    }
};
