/**
 * @fileoverview Rule to require newlines before `return` statement
 * @author Kai Cataldo
 * @deprecated in ESLint v4.0.0
 */
"use strict";

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

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

        docs: {
            description: "Require an empty line before `return` statements",
            recommended: false,
            url: "https://eslint.org/docs/rules/newline-before-return"
        },

        fixable: "whitespace",
        schema: [],
        messages: {
            expected: "Expected newline before return statement."
        },

        deprecated: true,
        replacedBy: ["padding-line-between-statements"]
    },

    create(context) {
        const sourceCode = context.getSourceCode();

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

        /**
         * Tests whether node is preceded by supplied tokens
         * @param {ASTNode} node node to check
         * @param {Array} testTokens array of tokens to test against
         * @returns {boolean} Whether or not the node is preceded by one of the supplied tokens
         * @private
         */
        function isPrecededByTokens(node, testTokens) {
            const tokenBefore = sourceCode.getTokenBefore(node);

            return testTokens.includes(tokenBefore.value);
        }

        /**
         * Checks whether node is the first node after statement or in block
         * @param {ASTNode} node node to check
         * @returns {boolean} Whether or not the node is the first node after statement or in block
         * @private
         */
        function isFirstNode(node) {
            const parentType = node.parent.type;

            if (node.parent.body) {
                return Array.isArray(node.parent.body)
                    ? node.parent.body[0] === node
                    : node.parent.body === node;
            }

            if (parentType === "IfStatement") {
                return isPrecededByTokens(node, ["else", ")"]);
            }
            if (parentType === "DoWhileStatement") {
                return isPrecededByTokens(node, ["do"]);
            }
            if (parentType === "SwitchCase") {
                return isPrecededByTokens(node, [":"]);
            }
            return isPrecededByTokens(node, [")"]);

        }

        /**
         * Returns the number of lines of comments that precede the node
         * @param {ASTNode} node node to check for overlapping comments
         * @param {number} lineNumTokenBefore line number of previous token, to check for overlapping comments
         * @returns {number} Number of lines of comments that precede the node
         * @private
         */
        function calcCommentLines(node, lineNumTokenBefore) {
            const comments = sourceCode.getCommentsBefore(node);
            let numLinesComments = 0;

            if (!comments.length) {
                return numLinesComments;
            }

            comments.forEach(comment => {
                numLinesComments++;

                if (comment.type === "Block") {
                    numLinesComments += comment.loc.end.line - comment.loc.start.line;
                }

                // avoid counting lines with inline comments twice
                if (comment.loc.start.line === lineNumTokenBefore) {
                    numLinesComments--;
                }

                if (comment.loc.end.line === node.loc.start.line) {
                    numLinesComments--;
                }
            });

            return numLinesComments;
        }

        /**
         * Returns the line number of the token before the node that is passed in as an argument
         * @param {ASTNode} node The node to use as the start of the calculation
         * @returns {number} Line number of the token before `node`
         * @private
         */
        function getLineNumberOfTokenBefore(node) {
            const tokenBefore = sourceCode.getTokenBefore(node);
            let lineNumTokenBefore;

            /**
             * Global return (at the beginning of a script) is a special case.
             * If there is no token before `return`, then we expect no line
             * break before the return. Comments are allowed to occupy lines
             * before the global return, just no blank lines.
             * Setting lineNumTokenBefore to zero in that case results in the
             * desired behavior.
             */
            if (tokenBefore) {
                lineNumTokenBefore = tokenBefore.loc.end.line;
            } else {
                lineNumTokenBefore = 0; // global return at beginning of script
            }

            return lineNumTokenBefore;
        }

        /**
         * Checks whether node is preceded by a newline
         * @param {ASTNode} node node to check
         * @returns {boolean} Whether or not the node is preceded by a newline
         * @private
         */
        function hasNewlineBefore(node) {
            const lineNumNode = node.loc.start.line;
            const lineNumTokenBefore = getLineNumberOfTokenBefore(node);
            const commentLines = calcCommentLines(node, lineNumTokenBefore);

            return (lineNumNode - lineNumTokenBefore - commentLines) > 1;
        }

        /**
         * Checks whether it is safe to apply a fix to a given return statement.
         *
         * The fix is not considered safe if the given return statement has leading comments,
         * as we cannot safely determine if the newline should be added before or after the comments.
         * For more information, see: https://github.com/eslint/eslint/issues/5958#issuecomment-222767211
         * @param {ASTNode} node The return statement node to check.
         * @returns {boolean} `true` if it can fix the node.
         * @private
         */
        function canFix(node) {
            const leadingComments = sourceCode.getCommentsBefore(node);
            const lastLeadingComment = leadingComments[leadingComments.length - 1];
            const tokenBefore = sourceCode.getTokenBefore(node);

            if (leadingComments.length === 0) {
                return true;
            }

            /*
             * if the last leading comment ends in the same line as the previous token and
             * does not share a line with the `return` node, we can consider it safe to fix.
             * Example:
             * function a() {
             *     var b; //comment
             *     return;
             * }
             */
            if (lastLeadingComment.loc.end.line === tokenBefore.loc.end.line &&
                lastLeadingComment.loc.end.line !== node.loc.start.line) {
                return true;
            }

            return false;
        }

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

        return {
            ReturnStatement(node) {
                if (!isFirstNode(node) && !hasNewlineBefore(node)) {
                    context.report({
                        node,
                        messageId: "expected",
                        fix(fixer) {
                            if (canFix(node)) {
                                const tokenBefore = sourceCode.getTokenBefore(node);
                                const newlines = node.loc.start.line === tokenBefore.loc.end.line ? "\n\n" : "\n";

                                return fixer.insertTextBefore(node, newlines);
                            }
                            return null;
                        }
                    });
                }
            }
        };
    }
};
