/*
 * Copyright (C) 2016 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

// There is no standard for tokenizer types in JavaScript ASTs.
// This currently assumes Esprima tokens, ranges, and comments.
// <http://esprima.org/demo/parse.html>

JSFormatter = class JSFormatter
{
    constructor(sourceText, sourceType, builder, indentString = "    ")
    {
        this._success = false;

        let tree = (function() {
            try {
                return esprima.parse(sourceText, {attachComment: true, range: true, tokens: true, sourceType});
            } catch {
                return null;
            }
        })();

        if (!tree)
            return;

        this._sourceText = sourceText;

        this._tokens = tree.tokens;
        this._tokensLength = this._tokens.length;
        this._tokenIndex = 0;

        this._lineEndings = sourceText.lineEndings();
        this._lineEndingsIndex = 0;

        this._builder = builder;
        if (!this._builder) {
            this._builder = new FormatterContentBuilder(indentString);
            this._builder.setOriginalLineEndings(this._lineEndings.slice());
        }

        let walker = new ESTreeWalker(this._before.bind(this), this._after.bind(this));
        walker.walk(tree);
        this._afterProgram(tree);
        this._builder.appendNewline();

        this._success = true;
    }

    // Static

    static isWhitespace(ch)
    {
        return isECMAScriptWhitespace(ch) || isECMAScriptLineTerminator(ch);
    }

    // Public

    get success() { return this._success; }

    get formattedText()
    {
        if (!this._success)
            return null;
        return this._builder.formattedContent;
    }

    get sourceMapData()
    {
        if (!this._success)
            return null;
        return this._builder.sourceMapData;
    }

    // Private

    _appendNewline(node, force)
    {
        if (!force && node.type !== "TemplateElement") {
            while (node = node.parent) {
                if (node.type === "TemplateLiteral")
                    return;
            }
        }

        this._builder.appendNewline(force);
    }

    _insertNewlinesBeforeToken(node, token)
    {
        let force = false;
        while (token.range[0] > this._lineEndings[this._lineEndingsIndex]) {
            this._appendNewline(node, force);
            this._lineEndingsIndex++;
            force = true;
        }
    }

    _insertComment(node, comment)
    {
        if (comment.type === "Line")
            this._builder.appendToken("//" + comment.value, comment.range[0]);
        else if (comment.type === "Block")
            this._builder.appendToken("/*" + comment.value + "*/", comment.range[0]);
        this._appendNewline(node);
        comment.__handled = true;
    }

    _insertSameLineTrailingComments(node)
    {
        let endOfLine = this._lineEndings[this._lineEndingsIndex];
        for (let comment of node.trailingComments) {
            if (comment.range[0] > endOfLine)
                break;
            this._builder.removeLastNewline();
            this._builder.appendSpace();
            this._insertComment(node, comment);
        }
    }

    _insertCommentsAndNewlines(node, comments)
    {
        for (let comment of comments) {
            // A previous node may have handled this as a trailing comment.
            if (comment.__handled)
                continue;

            // We expect the comment to be ahead of the last line.
            // But if it is ahead of the next line ending, then it
            // was preceded by an empty line. So include that.
            if (comment.range[0] > this._lineEndings[this._lineEndingsIndex + 1])
                this._builder.appendNewline(true);

            this._insertComment(node, comment);

            // Remove line endings for this comment.
            while (comment.range[1] > this._lineEndings[this._lineEndingsIndex])
                this._lineEndingsIndex++;
        }
    }

    _before(node)
    {
        if (!node.parent)
            return;

        // Handle the tokens before this node, so in the context of our parent node.
        while (this._tokenIndex < this._tokensLength && this._tokens[this._tokenIndex].range[0] < node.range[0]) {
            let token = this._tokens[this._tokenIndex++];
            this._insertNewlinesBeforeToken(node, token);
            this._handleTokenAtNode(token, node.parent);
        }

        if (node.leadingComments)
            this._insertCommentsAndNewlines(node, node.leadingComments);
    }

    _after(node)
    {
        // Handle any other tokens inside of this node before exiting.
        while (this._tokenIndex < this._tokensLength && this._tokens[this._tokenIndex].range[0] < node.range[1]) {
            let token = this._tokens[this._tokenIndex++];
            this._insertNewlinesBeforeToken(node, token);
            this._handleTokenAtNode(token, node);
        }

        this._exitNode(node);

        if (node.trailingComments)
            this._insertSameLineTrailingComments(node);
    }

    _isInForHeader(node)
    {
        let parent = node.parent;
        if (!parent)
            return false;

        return (parent.type === "ForStatement" || parent.type === "ForInStatement" || parent.type === "ForOfStatement") && node !== parent.body;
    }

    _isLikelyToHaveNewline(node)
    {
        let nodeType = node.type;
        return nodeType === "IfStatement"
            || nodeType === "ForStatement"
            || nodeType === "ForOfStatement"
            || nodeType === "ForInStatement"
            || nodeType === "WhileStatement"
            || nodeType === "DoWhileStatement"
            || nodeType === "SwitchStatement"
            || nodeType === "TryStatement"
            || nodeType === "FunctionDeclaration"
            || nodeType === "ClassDeclaration"
            || nodeType === "BlockStatement"
            || nodeType === "WithStatement";
    }

    _isRangeWhitespace(from, to)
    {
        let substring = this._sourceText.substring(from, to);
        for (let i = 0; i < substring.length; ++i) {
            if (!JSFormatter.isWhitespace(substring.charCodeAt(i)))
                return false;
        }

        return true;
    }

    _handleTokenAtNode(token, node)
    {
        let builder = this._builder;
        let nodeType = node.type;
        let tokenType = token.type;
        let tokenValue = token.value;
        let tokenOffset = token.range[0];

        // Very common types that just pass through.
        if (nodeType === "MemberExpression" || nodeType === "Literal" || nodeType === "ThisExpression" || nodeType === "UpdateExpression") {
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        // Most identifiers just pass through, but a few are special.
        if (nodeType === "Identifier") {
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenValue === "async" && node.parent.type === "Property" && node.parent.value.async && token.range[1] !== node.range[1])
                builder.appendSpace();
            return;
        }

        // Most newline handling is done with semicolons. However, we preserve
        // newlines so code relying on automatic semicolon insertion should
        // continue to work.
        if (tokenValue === ";") {
            // Avoid newlines for for loop header semicolons.
            if (nodeType === "ForStatement") {
                builder.appendToken(tokenValue, tokenOffset);
                // Do not include spaces in empty for loop header sections: for(;;)
                if (node.test || node.update) {
                    if (node.test && this._isRangeWhitespace(token.range[1], node.test.range[0]))
                        builder.appendSpace();
                    else if (node.update && this._isRangeWhitespace(token.range[1], node.update.range[0]))
                        builder.appendSpace();
                }
                return;
            }

            // Sometimes more specific nodes gets the semicolon inside a for loop header.
            // Avoid newlines. Example is a VariableDeclaration in: for (var a, b; ...; ...).
            if (this._isInForHeader(node)) {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }

            // Avoid newline for single statement arrow functions with semicolons.
            if (node.parent.type === "BlockStatement" && node.parent.body.length === 1 && node.parent.parent && node.parent.parent.type === "ArrowFunctionExpression") {
                builder.appendToken(tokenValue, tokenOffset);
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            this._appendNewline(node);
            return;
        }

        if (nodeType === "CallExpression" || nodeType === "ArrayExpression" || nodeType === "ArrayPattern" || nodeType === "ObjectPattern" || nodeType === "SequenceExpression") {
            if (tokenValue === ",") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "LogicalExpression" || nodeType === "BinaryExpression") {
            if (tokenValue !== "(" && tokenValue !== ")") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenType === "Keyword") {
                // in, instanceof, ...
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "BlockStatement") {
            let isSingleStatementArrowFunctionWithUnlikelyMultilineContent = node.parent.type === "ArrowFunctionExpression" && node.body.length === 1 && !this._isLikelyToHaveNewline(node.body[0]);
            if (tokenValue === "{") {
                // Class methods we put the opening brace on its own line.
                if (node.parent && node.parent.parent && node.parent.parent.type === "MethodDefinition" && node.body.length) {
                    this._appendNewline(node);
                    builder.appendToken(tokenValue, tokenOffset);
                    this._appendNewline(node);
                    builder.indent();
                    return;
                }
                builder.appendToken(tokenValue, tokenOffset);
                if (node.body.length && !isSingleStatementArrowFunctionWithUnlikelyMultilineContent)
                    this._appendNewline(node);
                builder.indent();
                return;
            }
            if (tokenValue === "}") {
                if (node.body.length && !isSingleStatementArrowFunctionWithUnlikelyMultilineContent)
                    this._appendNewline(node);
                builder.dedent();
                builder.appendToken(tokenValue, tokenOffset);
                return;
            }
            console.warn("Unexpected BlockStatement token", token);
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "VariableDeclaration") {
            if (tokenValue === ",") {
                if (this._isInForHeader(node)) {
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
                builder.appendToken(tokenValue, tokenOffset);
                this._appendNewline(node);
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            builder.appendSpace();
            // If this is a multiple variable declaration, indent.
            if (node.declarations.length > 1 && !node.__autoDedent) {
                builder.indent();
                node.__autoDedent = true;
            }
            return;
        }

        if (nodeType === "VariableDeclarator" || nodeType === "AssignmentExpression") {
            if (tokenType === "Punctuator") {
                let surroundWithSpaces = tokenValue !== "(" && tokenValue !== ")";
                if (surroundWithSpaces)
                    builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                if (surroundWithSpaces)
                    builder.appendSpace();
                return;
            }
            console.warn("Unexpected " + nodeType + " token", token);
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "IfStatement") {
            if (tokenType === "Keyword") {
                if (tokenValue === "else") {
                    if (node.__autoDedent) {
                        builder.dedent();
                        node.__autoDedent = false;
                    }
                    builder.appendSpace();
                    builder.appendToken(tokenValue, tokenOffset);

                    if (node.alternate && (node.alternate.type !== "BlockStatement" && node.alternate.type !== "IfStatement")) {
                        this._appendNewline(node);
                        builder.indent();
                        node.__autoDedent = true;
                    } else
                        builder.appendSpace();
                    return;
                }

                console.assert(tokenValue === "if", token);
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }

            // The last ')' in if(){}.
            if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.consequent.range[0])) {
                if (node.consequent.type === "BlockStatement") {
                    // The block will handle indenting.
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }

                builder.appendToken(tokenValue, tokenOffset);
                this._appendNewline(node);
                builder.indent();
                node.__autoDedent = true;
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ReturnStatement") {
            if (tokenValue === ";") {
                builder.appendToken(tokenValue, tokenOffset);
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            if (node.argument) {
                // Multi-line LogicalExpressions (&& and ||) are a common style of return
                // statement that benefits from indentation.
                if (node.argument.type === "LogicalExpression" && !node.__autoDedent) {
                    builder.indent();
                    node.__autoDedent = true;
                }
                builder.appendSpace();
            }
            return;
        }

        if (nodeType === "FunctionDeclaration" || nodeType === "FunctionExpression") {
            if (tokenType === "Keyword") {
                console.assert(tokenValue === "function", token);
                builder.appendToken(tokenValue, tokenOffset);
                if (node.id)
                    builder.appendSpace();
                return;
            }
            if (tokenType === "Punctuator") {
                if (tokenValue === "*") {
                    builder.removeLastWhitespace();
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
                builder.appendToken(tokenValue, tokenOffset);
                if (tokenValue === ")" || tokenValue === ",")
                    builder.appendSpace();
                return;
            }
            if (tokenType === "Identifier" && tokenValue === "async") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "WhileStatement" || nodeType === "WithStatement") {
            if (tokenValue === "while" || tokenValue === "with") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }

            // The last ')' in while(){} or with(){}.
            if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.body.range[0])) {
                if (node.body.type === "BlockStatement") {
                    // The block will handle indenting.
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }

                builder.appendToken(tokenValue, tokenOffset);
                this._appendNewline(node);
                builder.indent();
                node.__autoDedent = true;
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ForStatement" || nodeType === "ForOfStatement" || nodeType === "ForInStatement") {
            if (tokenValue === "for" || tokenValue === "await") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === "in" || tokenValue === "of") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }

            // The last ')' in for(){}.
            if (tokenValue === ")" && this._isRangeWhitespace(token.range[1], node.body.range[0])) {
                if (node.body.type === "BlockStatement") {
                    // The block will handle indenting.
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }

                builder.appendToken(tokenValue, tokenOffset);
                this._appendNewline(node);
                builder.indent();
                node.__autoDedent = true;
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "SwitchStatement") {
            if (tokenValue === "switch") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenType === "Punctuator") {
                if (tokenValue === ")") {
                    // FIXME: Would be nice to only add a space if this the ')' closing the discriminant: switch((1)){}
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
                if (tokenValue === "{") {
                    builder.appendToken(tokenValue, tokenOffset);
                    this._appendNewline(node);
                    return;
                }
                if (tokenValue === "}") {
                    this._appendNewline(node);
                    builder.appendToken(tokenValue, tokenOffset);
                    this._appendNewline(node);
                    return;
                }
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "SwitchCase") {
            if (tokenType === "Keyword") {
                if (tokenValue === "case") {
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
                if (tokenValue === "default") {
                    builder.appendToken(tokenValue, tokenOffset);
                    return;
                }
                console.warn("Unexpected SwitchCase Keyword token", token);
                builder.appendToken(tokenValue, tokenOffset);
                return;
            }

            if (tokenValue === ":") {
                builder.appendToken(tokenValue, tokenOffset);
                this._appendNewline(node);
                if (node.consequent.length) {
                    builder.indent();
                    node.__autoDedent = true;
                }
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "NewExpression") {
            if (tokenValue === "new") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === ",") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "DoWhileStatement") {
            if (tokenValue === "do") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === "while") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }

            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ThrowStatement") {
            if (tokenValue === "throw") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "UnaryExpression") {
            if (tokenType === "Keyword") {
                // typeof, instanceof, void
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ConditionalExpression") {
            if (tokenValue === "?" || tokenValue === ":") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ObjectExpression") {
            // FIXME: It would be nice to detect if node.properties is very short
            // and the node.properties themselves are very small then inline them
            // instead of always adding newlines. Objects like: {a}, {a:1} but
            // not objects like {a:function(){1;}}.
            if (tokenValue === "{") {
                builder.appendToken(tokenValue, tokenOffset);
                if (node.properties.length) {
                    this._appendNewline(node);
                    builder.indent();
                }
                return;
            }
            if (tokenValue === "}") {
                if (node.properties.length) {
                    this._appendNewline(node);
                    builder.dedent();
                }
                builder.appendToken(tokenValue, tokenOffset);
                return;
            }
            if (tokenValue === ",") {
                builder.appendToken(tokenValue, tokenOffset);
                if (node.properties.length)
                    this._appendNewline(node);
                else
                    builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ArrowFunctionExpression") {
            if (tokenType === "Punctuator") {
                if (tokenValue === "=>") {
                    builder.appendSpace();
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
                if (tokenValue === ",") {
                    builder.appendToken(tokenValue, tokenOffset);
                    builder.appendSpace();
                    return;
                }
            }
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenType === "Identifier" && tokenValue === "async")
                builder.appendSpace();
            return;
        }

        if (nodeType === "AwaitExpression") {
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenType === "Identifier" && tokenValue === "await")
                builder.appendSpace();
            return;
        }

        if (nodeType === "Property") {
            console.assert(tokenValue === ":" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async" || tokenValue === "*" || tokenValue === "[" || tokenValue === "]" || tokenValue === "(" || tokenValue === ")", token);
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenValue === ":" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async")
                builder.appendSpace();
            return;
        }

        if (nodeType === "MethodDefinition") {
            console.assert(tokenValue === "static" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async" || tokenValue === "*" || tokenValue === "[" || tokenValue === "]" || tokenValue === "(" || tokenValue === ")", token);
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenValue === "static" || tokenValue === "get" || tokenValue === "set" || tokenValue === "async")
                builder.appendSpace();
            return;
        }

        if (nodeType === "BreakStatement" || nodeType === "ContinueStatement") {
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenType === "Keyword" && node.label)
                builder.appendSpace();
            return;
        }

        if (nodeType === "LabeledStatement") {
            console.assert(tokenValue === ":", token);
            builder.appendToken(tokenValue, tokenOffset);
            builder.appendNewline();
            return;
        }

        if (nodeType === "TryStatement") {
            if (tokenValue === "try") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === "finally") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            console.warn("Unexpected TryStatement token", token);
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "CatchClause") {
            if (tokenValue === "catch") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === ")") {
                // The block will handle indenting.
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ClassExpression" || nodeType === "ClassDeclaration") {
            if (tokenValue === "class") {
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            if (tokenValue === "extends") {
                builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ClassBody") {
            if (tokenValue === "{") {
                if (node.parent.id)
                    builder.appendSpace();
                builder.appendToken(tokenValue, tokenOffset);
                if (node.body.length)
                    builder.appendNewline();
                builder.indent();
                return;
            }
            if (tokenValue === "}") {
                if (node.body.length)
                    builder.appendNewline();
                builder.dedent();
                builder.appendToken(tokenValue, tokenOffset);
                builder.appendNewline();
                return;
            }
            console.warn("Unexpected ClassBody token", token);
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "YieldExpression") {
            if (tokenType === "Keyword") {
                console.assert(tokenValue === "yield", token);
                builder.appendToken(tokenValue, tokenOffset);
                if (node.argument)
                    builder.appendSpace();
                return;
            }
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        if (nodeType === "ImportDeclaration" || nodeType === "ExportNamedDeclaration") {
            if (tokenValue === "}" || (tokenType === "Identifier" && tokenValue === "from"))
                builder.appendSpace();
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenValue !== "}")
                builder.appendSpace();
            return;
        }

        if (nodeType === "ExportSpecifier" || nodeType === "ImportSpecifier") {
            if (tokenType === "Identifier" && tokenValue === "as")
                builder.appendSpace();
            builder.appendToken(tokenValue, tokenOffset);
            builder.appendSpace();
            return;
        }

        if (nodeType === "ExportAllDeclaration" || nodeType === "ExportDefaultDeclaration" || nodeType === "ImportDefaultSpecifier" || nodeType === "ImportNamespaceSpecifier") {
            builder.appendToken(tokenValue, tokenOffset);
            if (tokenValue !== "(" && tokenValue !== ")")
                builder.appendSpace();
            return;
        }

        // Include these here so we get only get warnings about unhandled nodes.
        if (nodeType === "ExpressionStatement"
            || nodeType === "SpreadElement"
            || nodeType === "Super"
            || nodeType === "Import"
            || nodeType === "MetaProperty"
            || nodeType === "RestElement"
            || nodeType === "TemplateElement"
            || nodeType === "TemplateLiteral"
            || nodeType === "DebuggerStatement"
            || nodeType === "AssignmentPattern") {
            builder.appendToken(tokenValue, tokenOffset);
            return;
        }

        // Warn about possible unhandled types.
        console.warn(nodeType, tokenValue);

        // Fallback behavior in case there are unhandled types.
        builder.appendToken(tokenValue, tokenOffset);

        if (tokenType === "Keyword")
            builder.appendSpace();
    }

    _exitNode(node)
    {
        if (node.__autoDedent)
            this._builder.dedent();

        if (node.type === "BlockStatement") {
            if (node.parent) {
                // Newline after if(){}
                if (node.parent.type === "IfStatement" && (!node.parent.alternate || node.parent.consequent !== node)) {
                    this._appendNewline(node);
                    return;
                }
                // Newline after for(){}
                if (node.parent.type === "ForStatement" || node.parent.type === "ForOfStatement" || node.parent.type === "ForInStatement") {
                    this._appendNewline(node);
                    return;
                }
                // Newline after while(){}
                if (node.parent.type === "WhileStatement") {
                    this._appendNewline(node);
                    return;
                }
                // Newline after function(){}
                if (node.parent.type === "FunctionDeclaration") {
                    this._appendNewline(node);
                    return;
                }
                // Newline after catch block in try{}catch(e){}
                if (node.parent.type === "CatchClause" && !node.parent.parent.finalizer) {
                    this._appendNewline(node);
                    return;
                }
                // Newline after finally block in try{}catch(e){}finally{}
                if (node.parent.type === "TryStatement" && node.parent.finalizer && node.parent.finalizer === node) {
                    this._appendNewline(node);
                    return;
                }
                // Newline after class body methods in class {method(){}}
                if (node.parent.parent && node.parent.parent.type === "MethodDefinition") {
                    this._appendNewline(node);
                    return;
                }
                // Newline after anonymous block inside a block or program.
                if (node.parent.type === "BlockStatement" || node.parent.type === "Program") {
                    this._appendNewline(node);
                    return;
                }
            }
            return;
        }
    }

    _afterProgram(programNode)
    {
        if (!programNode)
            return;

        console.assert(programNode.type === "Program");

        // If a program ends with comments, Esprima puts those
        // comments on the last node of the body. However, if
        // a program is entirely comments, then they are
        // leadingComments on the program node.

        if (programNode.body.length) {
            let lastNode = programNode.body[programNode.body.length - 1];
            if (lastNode.trailingComments)
                this._insertCommentsAndNewlines(lastNode, lastNode.trailingComments);
        } else {
            if (programNode.leadingComments)
                this._insertCommentsAndNewlines(programNode, programNode.leadingComments);
            console.assert(!programNode.trailingComments);
        }
    }
};

JSFormatter.SourceType = {
    Script: "script",
    Module: "module",
};
