/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.netbeans.modules.javascript2.editor.formatter;

import com.oracle.js.parser.ir.AccessNode;
import com.oracle.js.parser.ir.BinaryNode;
import com.oracle.js.parser.ir.Block;
import com.oracle.js.parser.ir.BlockStatement;
import com.oracle.js.parser.ir.CallNode;
import com.oracle.js.parser.ir.CaseNode;
import com.oracle.js.parser.ir.CatchNode;
import com.oracle.js.parser.ir.Expression;
import com.oracle.js.parser.ir.ForNode;
import com.oracle.js.parser.ir.FunctionNode;
import com.oracle.js.parser.ir.IdentNode;
import com.oracle.js.parser.ir.IfNode;
import com.oracle.js.parser.ir.LexicalContext;
import com.oracle.js.parser.ir.LiteralNode;
import com.oracle.js.parser.ir.LoopNode;
import com.oracle.js.parser.ir.Node;
import com.oracle.js.parser.ir.ObjectNode;
import com.oracle.js.parser.ir.PropertyNode;
import com.oracle.js.parser.ir.Statement;
import com.oracle.js.parser.ir.SwitchNode;
import com.oracle.js.parser.ir.TernaryNode;
import com.oracle.js.parser.ir.TryNode;
import com.oracle.js.parser.ir.UnaryNode;
import com.oracle.js.parser.ir.VarNode;
import com.oracle.js.parser.ir.WhileNode;
import com.oracle.js.parser.ir.WithNode;
import com.oracle.js.parser.ir.visitor.NodeVisitor;
import com.oracle.js.parser.TokenType;
import com.oracle.js.parser.ir.ClassElement;
import com.oracle.js.parser.ir.ClassNode;
import com.oracle.js.parser.ir.ExportNode;
import com.oracle.js.parser.ir.ExpressionStatement;
import com.oracle.js.parser.ir.ImportNode;
import com.oracle.js.parser.ir.JsxAttributeNode;
import com.oracle.js.parser.ir.JsxElementNode;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.Set;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;

/**
 *
 * @author Petr Hejl
 */
public class JsFormatVisitor extends NodeVisitor {

    private static final Set<TokenType> UNARY_TYPES = EnumSet.noneOf(TokenType.class);

    static {
        Collections.addAll(UNARY_TYPES, TokenType.ADD, TokenType.SUB,
                TokenType.BIT_NOT, TokenType.NOT,
                TokenType.INCPOSTFIX, TokenType.INCPREFIX,
                TokenType.DECPOSTFIX, TokenType.DECPREFIX);
    }

    private final TokenSequence<? extends JsTokenId> ts;

    private final FormatTokenStream tokenStream;

    private final int formatFinish;

    private final TokenUtils tokenUtils;

    public JsFormatVisitor(FormatTokenStream tokenStream, TokenSequence<? extends JsTokenId> ts, int formatFinish) {
        super(new LexicalContext());
        this.ts = ts;
        this.tokenStream = tokenStream;
        this.formatFinish = formatFinish;
        this.tokenUtils = new TokenUtils(ts, tokenStream, formatFinish);
    }

    @Override
    public boolean enterBlock(Block block) {
        if (isScript(block) || !isVirtual(block)) {
            if (isScript(block)) {
                handleBlockContent(block, true);
            } else {
                handleStandardBlock(block);
            }
        }

        if (isScript(block) || !isVirtual(block)) {
            return false;
        } else {
            return super.enterBlock(block);
        }
    }

    @Override
    public Node leaveBlock(Block block) {
        if (isScript(block)
                || !isVirtual(block)) {
            return null;
        } else {
            return super.leaveBlock(block);
        }
    }

    @Override
    public boolean enterCaseNode(CaseNode caseNode) {
        List<Statement> nodes = caseNode.getStatements();
        if (nodes.size() == 1) {
            Statement node = nodes.get(0);
            if (node instanceof BlockStatement) {
                return super.enterCaseNode(caseNode);
            }
        }

        if (nodes.size() >= 1) {
            // indentation mark
            FormatToken formatToken = tokenUtils.getPreviousToken(getStart(nodes.get(0)), JsTokenId.OPERATOR_COLON, true);
            if (formatToken != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            }

            // put indentation mark
            formatToken = getCaseEndToken(getStart(nodes.get(0)), getFinish(nodes.get(nodes.size() - 1)));
            if (formatToken != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
            }

            handleBlockContent(nodes, true);
        }
        return false;
    }

    @Override
    public boolean enterJsxElementNode(JsxElementNode jsxElementNode) {
        FormatToken jsxToken = tokenStream.getToken(getStart(jsxElementNode));
        if (jsxToken != null) {
            assert jsxToken.getId() == JsTokenId.JSX_TEXT : jsxToken.toString() + " from " + jsxElementNode.toString();
            if (jsxToken.getText().toString().startsWith("<")) {
                FormatToken jsxTokenPrev = jsxToken.previous();
                if (jsxTokenPrev != null) {
                    TokenUtils.appendTokenAfterLastVirtual(jsxTokenPrev, FormatToken.forFormat(FormatToken.Kind.BEFORE_JSX_BLOCK_START), true);
                }
            }

            jsxToken = tokenUtils.getPreviousToken(getFinish(jsxElementNode) - 1, JsTokenId.JSX_TEXT);
            if (jsxToken != null && jsxToken.getText().toString().endsWith(">")) {
                assert jsxToken.getId() == JsTokenId.JSX_TEXT : jsxToken;
                TokenUtils.appendTokenAfterLastVirtual(jsxToken, FormatToken.forFormat(FormatToken.Kind.AFTER_JSX_BLOCK_END), true);
            }
        }

        for (Expression e : jsxElementNode.getChildren()) {
            if (!(e instanceof LiteralNode) && !(e instanceof JsxElementNode)) {
                // assignmentExpression
                int start = getStart(e);
                FormatToken token = tokenUtils.getPreviousToken(start, JsTokenId.JSX_EXP_BEGIN);
                if (token != null) {
                    TokenUtils.appendToken(token, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
                }
                int finish = getFinish(e);
                token = tokenUtils.getNextToken(finish, JsTokenId.JSX_EXP_END);
                if (token != null) {
                    token = tokenUtils.getPreviousNonWhiteToken(token.getOffset(),
                            start, JsTokenId.JSX_EXP_END, true);
                    if (token != null) {
                        TokenUtils.appendToken(token, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
                    }
                }
            }
        }
        return super.enterJsxElementNode(jsxElementNode);
    }

    @Override
    public boolean enterJsxAttributeNode(JsxAttributeNode jsxAttributeNode) {
        Expression e = jsxAttributeNode.getValue();
        if (e != null && !(e instanceof LiteralNode) && !(e instanceof JsxElementNode)) {
            // assignmentExpression or unaryNode
            int start = getStart(e);
            FormatToken token = tokenUtils.getPreviousToken(start, JsTokenId.JSX_EXP_BEGIN);
            if (token != null) {
                TokenUtils.appendToken(token, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            }
            int finish = getFinish(e);
            token = tokenUtils.getNextToken(finish, JsTokenId.JSX_EXP_END);
            if (token != null) {
                token = tokenUtils.getPreviousNonWhiteToken(token.getOffset(),
                        start, JsTokenId.JSX_EXP_END, true);
                if (token != null) {
                    TokenUtils.appendToken(token, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
                }
            }
        }
        return super.enterJsxAttributeNode(jsxAttributeNode);
    }

    @Override
    public boolean enterWhileNode(WhileNode whileNode) {
        if (whileNode.isDoWhile()) {
            // within parens spaces
            int leftStart;
            Block body = whileNode.getBody();
//            if (isVirtual(body)) {
//                // unfortunately due to condition at the end of do-while
//                // we have to care about virtual block
//                List<Statement> statements = body.getStatements();
//                leftStart = getFinish(statements.get(statements.size() - 1));
//            } else {
                leftStart = getFinish(whileNode.getBody());
//            }
            markSpacesWithinParentheses(whileNode, leftStart, getFinish(whileNode),
                    FormatToken.Kind.AFTER_WHILE_PARENTHESIS, FormatToken.Kind.BEFORE_WHILE_PARENTHESIS);

            // mark space before left brace
            markSpacesBeforeBrace(whileNode.getBody(), FormatToken.Kind.BEFORE_DO_BRACE);

            FormatToken whileToken = tokenUtils.getPreviousToken(getFinish(whileNode), JsTokenId.KEYWORD_WHILE);
            if (whileToken != null) {
                FormatToken beforeWhile = whileToken.previous();
                if (beforeWhile != null) {
                    tokenUtils.appendToken(beforeWhile, FormatToken.forFormat(FormatToken.Kind.BEFORE_WHILE_KEYWORD));
                }
            }
            if (handleLoop(whileNode, FormatToken.Kind.AFTER_DO_START)) {
                return false;
            }

            markEndCurlyBrace(whileNode.getBody());
            return super.enterWhileNode(whileNode);
        } else {
            // within parens spaces
            markSpacesWithinParentheses(whileNode, getStart(whileNode), getStart(whileNode.getBody()),
                    FormatToken.Kind.AFTER_WHILE_PARENTHESIS, FormatToken.Kind.BEFORE_WHILE_PARENTHESIS);

            // mark space before left brace
            markSpacesBeforeBrace(whileNode.getBody(), FormatToken.Kind.BEFORE_WHILE_BRACE);

            if (handleLoop(whileNode, FormatToken.Kind.AFTER_WHILE_START)) {
                return false;
            }

            markEndCurlyBrace(whileNode.getBody());
            return super.enterWhileNode(whileNode);
        }
    }

    @Override
    public boolean enterForNode(ForNode forNode) {
        // within parens spaces
        markSpacesWithinParentheses(forNode, getStart(forNode), getStart(forNode.getBody()),
                FormatToken.Kind.AFTER_FOR_PARENTHESIS, FormatToken.Kind.BEFORE_FOR_PARENTHESIS);

        // mark space before left brace
        markSpacesBeforeBrace(forNode.getBody(), FormatToken.Kind.BEFORE_FOR_BRACE);

        if (!forNode.isForEach() && !forNode.isForIn()) {
            Node init = forNode.getInit();
            Node test = forNode.getTest();

            FormatToken formatToken;

            // unfortunately init and test may be null
            if (init != null) {
                formatToken = tokenUtils.getNextToken(getFinish(init), JsTokenId.OPERATOR_SEMICOLON);
            } else {
                formatToken = tokenUtils.getNextToken(getStart(forNode), JsTokenId.OPERATOR_SEMICOLON,
                        getStart(forNode.getBody()));
            }
            if (formatToken != null && test != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken,
                        FormatToken.forFormat(FormatToken.Kind.BEFORE_FOR_TEST));
            }

            if (test != null) {
                formatToken = tokenUtils.getNextToken(getFinish(forNode.getTest()), JsTokenId.OPERATOR_SEMICOLON);
            } else {
                // we use the position of init semicolon
                int start = formatToken != null ? formatToken.getOffset() + 1 : getStart(forNode);
                formatToken = tokenUtils.getNextToken(start, JsTokenId.OPERATOR_SEMICOLON,
                                            getStart(forNode.getBody()));
            }
            if (formatToken != null && forNode.getModify() != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken,
                        FormatToken.forFormat(FormatToken.Kind.BEFORE_FOR_MODIFY));
            }
        }
        if (handleLoop(forNode, FormatToken.Kind.AFTER_FOR_START)) {
            return false;
        }

        markEndCurlyBrace(forNode.getBody());
        return super.enterForNode(forNode);
    }

    @Override
    public boolean enterIfNode(IfNode ifNode) {
        ifNode.getTest().accept(this);

        // within parens spaces
        markSpacesWithinParentheses(ifNode, getStart(ifNode), getStart(ifNode.getPass()),
                FormatToken.Kind.AFTER_IF_PARENTHESIS, FormatToken.Kind.BEFORE_IF_PARENTHESIS);

        // pass block
        Block body = ifNode.getPass();
        // mark space before left brace
        markSpacesBeforeBrace(body, FormatToken.Kind.BEFORE_IF_BRACE);

        if (isVirtual(body)) {
            handleVirtualBlock(body, FormatToken.Kind.AFTER_IF_START);
        } else {
            enterBlock(body);
            markEndCurlyBrace(body);
        }

        // fail block
        body = ifNode.getFail();
        if (body != null) {
            if (isVirtual(body)) {
                // do the standard block related things
                List<Statement> statements = body.getStatements();
                // there might be no statements when code is broken
                if (!statements.isEmpty() && (statements.get(0) instanceof IfNode)) {
                    // we mark else if statement here
                    handleVirtualBlock(body, FormatToken.Kind.ELSE_IF_INDENTATION_INC,
                            FormatToken.Kind.ELSE_IF_INDENTATION_DEC, FormatToken.Kind.ELSE_IF_AFTER_BLOCK_START, true);
                } else {
                    // mark space before left brace
                    markSpacesBeforeBrace(body, FormatToken.Kind.BEFORE_ELSE_BRACE);

                    handleVirtualBlock(body, FormatToken.Kind.AFTER_ELSE_START);
                }
            } else {
                // mark space before left brace
                markSpacesBeforeBrace(body, FormatToken.Kind.BEFORE_ELSE_BRACE);

                enterBlock(body);
                markEndCurlyBrace(body);
            }
        }

        return false;
    }

    @Override
    public Node leaveIfNode(IfNode ifNode) {
        return null;
    }

    @Override
    public boolean enterWithNode(WithNode withNode) {
        // within parens spaces
        markSpacesWithinParentheses(withNode, getStart(withNode), getStart(withNode.getBody()),
                FormatToken.Kind.AFTER_WITH_PARENTHESIS, FormatToken.Kind.BEFORE_WITH_PARENTHESIS);

        Block body = withNode.getBody();

        // mark space before left brace
        markSpacesBeforeBrace(body, FormatToken.Kind.BEFORE_WITH_BRACE);

        if (isVirtual(body)) {
            handleVirtualBlock(body, FormatToken.Kind.AFTER_WITH_START);
            return false;
        }

        markEndCurlyBrace(body);
        return super.enterWithNode(withNode);
    }

    @Override
    public boolean enterFunctionNode(FunctionNode functionNode) {
        if (functionNode.isModule()) {
            functionNode.visitImports(this);
            functionNode.visitExports(this);
        }

//        if (functionNode.isClassConstructor() && !"constructor".equals(functionNode.getIdent().getName())) { // NOI18N
//            // generated constructor
//            return false;
//        }

        Block body = functionNode.getBody();
        // default parameters are stored as assignments inside the function
        // body block - the real block is just behind it
        if (body.isParameterBlock()) {
            List<Statement> statements = body.getStatements();
            if (!statements.isEmpty()) {
                Statement last = statements.get(statements.size() - 1);
                if (last instanceof BlockStatement) {
                    body = ((BlockStatement) last).getBlock();
                }
            }
        }
        if (isVirtual(body) && functionNode.getKind() == FunctionNode.Kind.ARROW) {
            Token nonEmpty = tokenUtils.getPreviousNonEmptyToken(getStart(body));
            if (nonEmpty != null) {
                FormatToken token = tokenStream.getToken(ts.offset());
                if (token != null) {
                    TokenUtils.appendTokenAfterLastVirtual(token, FormatToken.forFormat(FormatToken.Kind.BEFORE_ARROW_BLOCK));
                }
            }

            handleVirtualBlock(body, FormatToken.Kind.INDENTATION_INC, FormatToken.Kind.INDENTATION_DEC, null, false);

            nonEmpty = tokenUtils.getPreviousNonEmptyToken(getFinish(body));
            if (nonEmpty != null) {
                FormatToken token = tokenStream.getToken(ts.offset());
                if (token != null) {
                    TokenUtils.appendTokenAfterLastVirtual(token, FormatToken.forFormat(FormatToken.Kind.AFTER_ARROW_BLOCK));
                }
            }
        } else {
            enterBlock(body);
        }

        if (functionNode.isProgram()) {
            return false;
        }

        int start = getStart(functionNode);
        if (functionNode.getKind() == FunctionNode.Kind.ARROW) {
            FormatToken left = tokenUtils.getNextToken(start, JsTokenId.BRACKET_LEFT_PAREN, start);
            FormatToken leftParen = left;
            if (left == null) {
                // single parameter arrow without parenthesis
                left = tokenUtils.getNextToken(start, JsTokenId.IDENTIFIER, start);
            }
            if (left != null) {
                FormatToken previous = left.previous();
                if (previous != null) {
                    TokenUtils.appendToken(previous, FormatToken.forFormat(
                            FormatToken.Kind.BEFORE_ARROW_FUNCTION_DECLARATION));
                }
            }

            handleFunctionParameters(functionNode, leftParen);
        } else {
            // the star * is not multiplication (binary operator)
            // FIXME should this be solved in lexer?
            if (functionNode.getKind() == FunctionNode.Kind.GENERATOR) {
                FormatToken star = tokenUtils.getNextToken(start, JsTokenId.OPERATOR_MULTIPLICATION);
                if (star != null) {
                    FormatToken prev = star.previous();
                    if (prev != null && (prev.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR
                            || prev.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR_WRAP)) {
                        tokenStream.removeToken(prev);
                    }
                    prev = star.previous();
                    if (prev != null && (prev.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR
                            || prev.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR_WRAP)) {
                        tokenStream.removeToken(prev);
                    }

                    FormatToken next = star.next();
                    if (next != null && (next.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR
                            || next.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR_WRAP)) {
                        tokenStream.removeToken(next);
                    }
                    next = star.next();
                    if (next != null && (next.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR
                            || next.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR_WRAP)) {
                        tokenStream.removeToken(next);
                    }
                }
            }

            FormatToken leftParen = tokenUtils.getNextToken(start, JsTokenId.BRACKET_LEFT_PAREN, getStart(body));
            if (leftParen != null) {
                FormatToken previous = leftParen.previous();
                if (previous != null) {
                    TokenUtils.appendToken(previous, FormatToken.forFormat(
                            functionNode.isAnonymous()
                                    ? FormatToken.Kind.BEFORE_ANONYMOUS_FUNCTION_DECLARATION
                                    : FormatToken.Kind.BEFORE_FUNCTION_DECLARATION));
                }

                handleFunctionParameters(functionNode, leftParen);

                // mark left brace of block - this works if function node
                // start offset is offset of the left brace
                FormatToken leftBrace = tokenUtils.getNextToken(getStart(functionNode),
                        JsTokenId.BRACKET_LEFT_CURLY, getFinish(functionNode));
                if (leftBrace != null) {
                    previous = leftBrace.previous();
                    if (previous != null) {
                        TokenUtils.appendToken(previous, FormatToken.forFormat(
                                FormatToken.Kind.BEFORE_FUNCTION_DECLARATION_BRACE));
                    }
                }

                if (functionNode.isStatement() && !functionNode.isAnonymous()) {
                    FormatToken rightBrace = tokenUtils.getPreviousToken(getFinish(functionNode),
                            JsTokenId.BRACKET_RIGHT_CURLY,
                            leftBrace != null ? leftBrace.getOffset() : start);
                    if (rightBrace != null) {
                        TokenUtils.appendToken(rightBrace, FormatToken.forFormat(
                                FormatToken.Kind.AFTER_STATEMENT));
                    }
                }

                markEndCurlyBrace(functionNode);
            }

        }
        return false;
    }

    @Override
    public Node leaveFunctionNode(FunctionNode functionNode) {
        leaveBlock(functionNode.getBody());

        return null;
    }

    @Override
    public boolean enterImportNode(ImportNode importNode) {
        int finish = getFinish(importNode);
        FormatToken token = tokenUtils.getNextToken(finish, JsTokenId.OPERATOR_SEMICOLON);
        if (token != null) {
            // we treat the import as statement
            TokenUtils.appendTokenAfterLastVirtual(token, FormatToken.forFormat(FormatToken.Kind.AFTER_STATEMENT));
        }
        return false;
    }

    @Override
    public boolean enterExportNode(ExportNode exportNode) {
        int finish = getFinish(exportNode);
        FormatToken token = tokenUtils.getNextToken(finish, JsTokenId.OPERATOR_SEMICOLON);
        if (token != null) {
            // we treat the import as statement
            TokenUtils.appendTokenAfterLastVirtual(token, FormatToken.forFormat(FormatToken.Kind.AFTER_STATEMENT));
        }
        // the complex export nodes are included in top level function body anyway
        // so we do not want to to visit further
        if (exportNode.isDefault()) {
            return super.enterExportNode(exportNode);
        }
        return false;
    }

    @Override
    public boolean enterCallNode(CallNode callNode) {
        FormatToken leftBrace = tokenUtils.getNextToken(getFinish(callNode.getFunction()),
                JsTokenId.BRACKET_LEFT_PAREN, getFinish(callNode));
        if (leftBrace != null) {
            FormatToken previous = leftBrace.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_FUNCTION_CALL));
            }

            // mark the within parenthesis places

            // remove original paren marks
            FormatToken mark = leftBrace.next();
            assert mark != null && mark.getKind() == FormatToken.Kind.AFTER_LEFT_PARENTHESIS : mark;
            tokenStream.removeToken(mark);

            // there is -1 as on the finish position may be some outer paren
            // so we really need the position precisely
            int stopMark = getStart(callNode);
            // lets calculate stop mark precisely to not to catch on arguments
            // parens in broken source
            List<Expression> args = callNode.getArgs();
            if (!args.isEmpty()) {
                stopMark = getFinish(args.get(args.size() - 1));
            }
            FormatToken rightBrace = tokenUtils.getPreviousToken(getFinish(callNode) - 1,
                    JsTokenId.BRACKET_RIGHT_PAREN, stopMark);
            if (rightBrace != null) {
                previous = TokenUtils.findVirtualToken(rightBrace,
                        FormatToken.Kind.BEFORE_RIGHT_PARENTHESIS, true);

                // this might happen for sanitization inserted paren
                if (previous != null) {
                    tokenStream.removeToken(previous);
                }
            }

            // place the new marks
            if (!callNode.getArgs().isEmpty()) {
                TokenUtils.appendToken(leftBrace, FormatToken.forFormat(
                        FormatToken.Kind.AFTER_FUNCTION_CALL_PARENTHESIS));

                if (rightBrace != null) {
                    previous = rightBrace.previous();
                    if (previous != null) {
                        TokenUtils.appendToken(previous, FormatToken.forFormat(
                                FormatToken.Kind.BEFORE_FUNCTION_CALL_PARENTHESIS));
                    }
                }
            }

            // place function arguments marks
            for (Node arg : callNode.getArgs()) {
                FormatToken argToken = tokenUtils.getNextToken(getStart(arg), null);
                if (argToken != null) {
                    FormatToken beforeArg = argToken.previous();
                    if (beforeArg != null) {
                        TokenUtils.appendToken(beforeArg,
                                FormatToken.forFormat(FormatToken.Kind.BEFORE_FUNCTION_CALL_ARGUMENT));
                    }
                }
            }
        }
        handleFunctionCallChain(callNode);

        return super.enterCallNode(callNode);
    }

    @Override
    public boolean enterClassNode(ClassNode classNode) {
        handleDecorators(classNode.getDecorators());

        Expression heritage = classNode.getClassHeritage();
        if (heritage != null) {
            heritage.accept(this);
            FormatToken extendsToken = tokenUtils.getPreviousToken(getStart(heritage), JsTokenId.KEYWORD_EXTENDS, getStart(classNode));
            if (extendsToken != null) {
                FormatToken token = extendsToken.previous();
                if (token != null) {
                    TokenUtils.appendToken(token, FormatToken.forFormat(FormatToken.Kind.BEFORE_CLASS_EXTENDS));
                }
            }
        }

        // indentation mark
        FormatToken formatToken = tokenUtils.getNextToken(heritage != null ? getFinish(heritage) : getStart(classNode),
                JsTokenId.BRACKET_LEFT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_CLASS_START));

            FormatToken previous = formatToken.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(
                        FormatToken.Kind.BEFORE_CLASS_DECLARATION_BRACE));
            }
        }

        PropertyNode constructor = classNode.getConstructor();
        if (constructor != null) {
            // generated default constructor has range equal to class
            if (constructor.getStart() != classNode.getStart()
                    || constructor.getFinish() != classNode.getFinish()) {
                handleDecorators(constructor.getDecorators());
                handleClassElement(constructor, getStart(constructor));
            }
        }
        for (Node property : classNode.getClassElements()) {
            PropertyNode propertyNode = (PropertyNode) property;
            handleDecorators(propertyNode.getDecorators());
            handleClassElement(propertyNode, getStart(propertyNode));
        }

        // put indentation mark after non white token preceeding curly bracket
        formatToken = tokenUtils.getPreviousNonWhiteToken(getFinish(classNode) - 1,
                getStart(classNode), JsTokenId.BRACKET_RIGHT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.BEFORE_CLASS_END));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
        }

        markEndCurlyBrace(classNode);

        return false;
    }

    @Override
    public boolean enterObjectNode(ObjectNode objectNode) {
        // indentation mark
        FormatToken formatToken = tokenUtils.getPreviousToken(getStart(objectNode), JsTokenId.BRACKET_LEFT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_LEFT_BRACE));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_OBJECT_START));
            FormatToken previous = formatToken.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_OBJECT));
            }
        }

        int objectFinish = getFinish(objectNode);
        for (Node property : objectNode.getElements()) {
            property.accept(this);

            PropertyNode propertyNode = (PropertyNode) property;
            if (propertyNode.getGetter() != null) {
                FunctionNode getter = (FunctionNode) propertyNode.getGetter();
                markPropertyFinish(getFinish(getter), objectFinish, false);
            }
            if (propertyNode.getSetter() != null) {
                FunctionNode setter = (FunctionNode) propertyNode.getSetter();
                markPropertyFinish(getFinish(setter), objectFinish, false);
            }

            // mark property end
            markPropertyFinish(getFinish(property), objectFinish, true);
        }

        // put indentation mark after non white token preceeding curly bracket
        formatToken = tokenUtils.getPreviousNonWhiteToken(getFinish(objectNode) - 1,
                getStart(objectNode), JsTokenId.BRACKET_RIGHT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.BEFORE_OBJECT_END));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.BEFORE_RIGHT_BRACE));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
        }

        return false;
    }

    @Override
    public boolean enterPropertyNode(PropertyNode propertyNode) {
        FormatToken colon = tokenUtils.getNextToken(getFinish(propertyNode.getKey()),
                JsTokenId.OPERATOR_COLON, getFinish(propertyNode));
        if (colon != null) {
            TokenUtils.appendToken(colon, FormatToken.forFormat(FormatToken.Kind.AFTER_PROPERTY_OPERATOR));
            FormatToken before = colon.previous();
            if (before != null) {
                TokenUtils.appendTokenAfterLastVirtual(before, FormatToken.forFormat(FormatToken.Kind.BEFORE_PROPERTY_OPERATOR));
            }
        }
        return super.enterPropertyNode(propertyNode);
    }

    @Override
    public boolean enterSwitchNode(SwitchNode switchNode) {
        // within parens spaces
        markSpacesWithinParentheses(switchNode);

        // mark space before left brace
        markSpacesBeforeBrace(switchNode);

        FormatToken formatToken = tokenUtils.getNextToken(getStart(switchNode), JsTokenId.BRACKET_LEFT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_BLOCK_START));
        }

        List<CaseNode> nodes = new ArrayList<>(switchNode.getCases());
        if (switchNode.getDefaultCase() != null) {
            nodes.add(switchNode.getDefaultCase());
        }

        for (CaseNode caseNode : nodes) {
            int index = getFinish(caseNode);
            List<Statement> statements = caseNode.getStatements();
            if (!statements.isEmpty()) {
                index = getStart(statements.get(0));
            }

            formatToken = tokenUtils.getPreviousToken(index, JsTokenId.OPERATOR_COLON);
            if (formatToken != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_CASE));
            }
        }

        // put indentation mark after non white token preceeding curly bracket
        formatToken = tokenUtils.getPreviousNonWhiteToken(getFinish(switchNode),
                getStart(switchNode), JsTokenId.BRACKET_RIGHT_CURLY, true);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
        }

        markEndCurlyBrace(switchNode);
        return super.enterSwitchNode(switchNode);
    }

    @Override
    public boolean enterUnaryNode(UnaryNode unaryNode) {
        TokenType type = unaryNode.tokenType();
        if (UNARY_TYPES.contains(type)) {
            if (TokenType.DECPOSTFIX.equals(type) || TokenType.INCPOSTFIX.equals(type)) {
                FormatToken formatToken = tokenUtils.getPreviousToken(getFinish(unaryNode),
                        TokenType.DECPOSTFIX.equals(type) ? JsTokenId.OPERATOR_DECREMENT : JsTokenId.OPERATOR_INCREMENT);

                if (formatToken != null) {
                    formatToken = formatToken.previous();
                    if (formatToken != null) {
                        TokenUtils.appendToken(formatToken,
                                FormatToken.forFormat(FormatToken.Kind.BEFORE_UNARY_OPERATOR));
                    }
                }
            } else {
                FormatToken formatToken = tokenUtils.getNextToken(getStart(unaryNode), null);

                // may be null when we are out of formatted area
                if (formatToken != null) {
                    // remove around binary operator tokens added during token
                    // stream creation
                    if (TokenType.ADD.equals(type) || TokenType.SUB.equals(type)) {
                        assert formatToken.getId() == JsTokenId.OPERATOR_PLUS
                                    || formatToken.getId() == JsTokenId.OPERATOR_MINUS : formatToken;
                        // we remove blindly inserted binary op markers
                        FormatToken toRemove = TokenUtils.findVirtualToken(formatToken,
                                FormatToken.Kind.BEFORE_BINARY_OPERATOR, true);
                        assert toRemove != null
                                && toRemove.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR : toRemove;
                        tokenStream.removeToken(toRemove);
                        toRemove = TokenUtils.findVirtualToken(formatToken,
                                FormatToken.Kind.BEFORE_BINARY_OPERATOR_WRAP, true);
                        assert toRemove != null
                                && toRemove.getKind() == FormatToken.Kind.BEFORE_BINARY_OPERATOR_WRAP : toRemove;
                        tokenStream.removeToken(toRemove);

                        toRemove = TokenUtils.findVirtualToken(formatToken,
                                FormatToken.Kind.AFTER_BINARY_OPERATOR, false);
                        assert toRemove != null
                                && toRemove.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR : toRemove;
                        tokenStream.removeToken(toRemove);
                        toRemove = TokenUtils.findVirtualToken(formatToken,
                                FormatToken.Kind.AFTER_BINARY_OPERATOR_WRAP, false);
                        assert toRemove != null
                                && toRemove.getKind() == FormatToken.Kind.AFTER_BINARY_OPERATOR_WRAP : toRemove;
                        tokenStream.removeToken(toRemove);
                    }

                    TokenUtils.appendToken(formatToken,
                            FormatToken.forFormat(FormatToken.Kind.AFTER_UNARY_OPERATOR));
                }
            }
        }

        return super.enterUnaryNode(unaryNode);
    }

    @Override
    public boolean enterTernaryNode(TernaryNode ternaryNode) {
        int start = getFinish(ternaryNode.getTest());
        FormatToken question = tokenUtils.getNextToken(start, JsTokenId.OPERATOR_TERNARY);
        if (question != null) {
            FormatToken previous = question.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_TERNARY_OPERATOR));
                TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_TERNARY_OPERATOR_WRAP));
            }
            TokenUtils.appendToken(question, FormatToken.forFormat(FormatToken.Kind.AFTER_TERNARY_OPERATOR));
            TokenUtils.appendToken(question, FormatToken.forFormat(FormatToken.Kind.AFTER_TERNARY_OPERATOR_WRAP));
            FormatToken colon = tokenUtils.getPreviousToken(getStart(ternaryNode.getFalseExpression()), JsTokenId.OPERATOR_COLON);
            if (colon != null) {
                previous = colon.previous();
                if (previous != null) {
                    TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_TERNARY_OPERATOR));
                    TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_TERNARY_OPERATOR_WRAP));
                }
                TokenUtils.appendToken(colon, FormatToken.forFormat(FormatToken.Kind.AFTER_TERNARY_OPERATOR));
                TokenUtils.appendToken(colon, FormatToken.forFormat(FormatToken.Kind.AFTER_TERNARY_OPERATOR_WRAP));
            }
        }

        return super.enterTernaryNode(ternaryNode);
    }

    @Override
    public boolean enterCatchNode(CatchNode catchNode) {
        // within parens spaces
        markSpacesWithinParentheses(catchNode, getStart(catchNode), getStart(catchNode.getBody()),
                FormatToken.Kind.AFTER_CATCH_PARENTHESIS, FormatToken.Kind.BEFORE_CATCH_PARENTHESIS);

        // mark space before left brace
        markSpacesBeforeBrace(catchNode.getBody(), FormatToken.Kind.BEFORE_CATCH_BRACE);

        markEndCurlyBrace(catchNode.getBody());
        return super.enterCatchNode(catchNode);
    }

    @Override
    public boolean enterTryNode(TryNode tryNode) {
        // mark space before left brace
        markSpacesBeforeBrace(tryNode.getBody(), FormatToken.Kind.BEFORE_TRY_BRACE);

        Block finallyBody = tryNode.getFinallyBody();
        if (finallyBody != null) {
            // mark space before finally left brace
            markSpacesBeforeBrace(tryNode.getFinallyBody(), FormatToken.Kind.BEFORE_FINALLY_BRACE);
        }

        markEndCurlyBrace(tryNode.getBody());
        markEndCurlyBrace(finallyBody);
        return super.enterTryNode(tryNode);
    }

    @Override
    public boolean enterLiteralNode(LiteralNode literalNode) {
        Object value = literalNode.getValue();
        if (value instanceof Node[]) {
            int start = getStart(literalNode);
            int finish = getFinish(literalNode);
            FormatToken leftBracket = tokenUtils.getNextToken(start, JsTokenId.BRACKET_LEFT_BRACKET, finish);
            if (leftBracket != null) {
                if (leftBracket.previous() != null) {
                    // mark beginning of the array (see issue #250150)
                    TokenUtils.appendToken(leftBracket.previous(), FormatToken.forFormat(FormatToken.Kind.BEFORE_ARRAY));
                }
                TokenUtils.appendToken(leftBracket, FormatToken.forFormat(FormatToken.Kind.AFTER_ARRAY_LITERAL_START));
                TokenUtils.appendToken(leftBracket, FormatToken.forFormat(FormatToken.Kind.AFTER_ARRAY_LITERAL_BRACKET));
                TokenUtils.appendToken(leftBracket, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
                FormatToken rightBracket = tokenUtils.getPreviousToken(finish - 1, JsTokenId.BRACKET_RIGHT_BRACKET, start + 1);
                if (rightBracket != null) {
                    FormatToken previous = rightBracket.previous();
                    if (previous != null) {
                        TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_ARRAY_LITERAL_END));
                        TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_ARRAY_LITERAL_BRACKET));
                        TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
                    }
                }
            }

            if (literalNode.isArray()) {
                Node[] items = ((LiteralNode.ArrayLiteralNode) literalNode).getValue();
                if (items != null && items.length > 0) {
                    int prevItemFinish = start;
                    for (int i = 1; i < items.length; i++) {
                        Node prevItem = items[i - 1];
                        if (prevItem != null) {
                            prevItemFinish = getFinish(prevItem);
                        }
                        FormatToken comma = tokenUtils.getNextToken(prevItemFinish, JsTokenId.OPERATOR_COMMA, finish);
                        if (comma != null) {
                            prevItemFinish = comma.getOffset();
                            TokenUtils.appendTokenAfterLastVirtual(comma,
                                    FormatToken.forFormat(FormatToken.Kind.AFTER_ARRAY_LITERAL_ITEM));
                        }
                    }
                }
            }
        }

        return super.enterLiteralNode(literalNode);
    }

    @Override
    public boolean enterVarNode(VarNode varNode) {
        if (varNode.isExport() || varNode.isDestructuring()) {
            return false;
        }
        int finish = getFinish(varNode) - 1;
        Token nextToken = tokenUtils.getNextNonEmptyToken(finish);
        if (nextToken != null && nextToken.id() == JsTokenId.OPERATOR_COMMA) {
            FormatToken formatToken = tokenStream.getToken(ts.offset());
            if (formatToken != null) {
                FormatToken next = formatToken.next();
                assert next != null && next.getKind() == FormatToken.Kind.AFTER_COMMA : next;
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_VAR_DECLARATION));
            }
        }

        return super.enterVarNode(varNode);
    }

    @Override
    public boolean enterClassElement(ClassElement propertyNode) {
        FormatToken colon = tokenUtils.getNextToken(getFinish(propertyNode.getKey()),
                JsTokenId.OPERATOR_COLON, getFinish(propertyNode));
        if (colon != null) {
            TokenUtils.appendToken(colon, FormatToken.forFormat(FormatToken.Kind.AFTER_PROPERTY_OPERATOR));
            FormatToken before = colon.previous();
            if (before != null) {
                TokenUtils.appendTokenAfterLastVirtual(before, FormatToken.forFormat(FormatToken.Kind.BEFORE_PROPERTY_OPERATOR));
            }
        }
        return super.enterPropertyNode(propertyNode);
    }

    // handles both standard and arrow functions
    private void handleFunctionParameters(FunctionNode functionNode, FormatToken leftParen) {
        if (leftParen != null) {
            // remove original paren marks
            FormatToken mark = leftParen.next();
            assert mark != null && mark.getKind() == FormatToken.Kind.AFTER_LEFT_PARENTHESIS : mark;
            tokenStream.removeToken(mark);

            FormatToken rightParen = tokenUtils.getPreviousToken(getStart(functionNode.getBody()),
                    JsTokenId.BRACKET_RIGHT_PAREN, leftParen.getOffset());
            if (rightParen != null) {
                FormatToken previous = rightParen.previous();
                assert previous != null && previous.getKind() == FormatToken.Kind.BEFORE_RIGHT_PARENTHESIS : previous;
                tokenStream.removeToken(previous);
            }

            // place the new marks
            if (!functionNode.getParameters().isEmpty()) {
                TokenUtils.appendToken(leftParen, FormatToken.forFormat(
                        FormatToken.Kind.AFTER_FUNCTION_DECLARATION_PARENTHESIS));

                if (rightParen != null) {
                    FormatToken previous = rightParen.previous();
                    if (previous != null) {
                        TokenUtils.appendToken(previous, FormatToken.forFormat(
                                FormatToken.Kind.BEFORE_FUNCTION_DECLARATION_PARENTHESIS));
                    }
                }
            }
        }

        // place function parameters marks
        for (IdentNode param : functionNode.getParameters()) {
            FormatToken paramToken = tokenUtils.getNextToken(getStart(param), JsTokenId.IDENTIFIER);
            if (paramToken != null) {
                // there might be "a, ...z" for example so we want the mark before the rest
                // parameter
                Token previousNonEmpty = tokenUtils.getPreviousNonEmptyToken(paramToken.getOffset());
                if (previousNonEmpty != null && previousNonEmpty.id() == JsTokenId.OPERATOR_REST) {
                    paramToken = tokenStream.getToken(ts.offset());
                }

                FormatToken beforeIdent = paramToken.previous();
                if (beforeIdent != null) {
                    TokenUtils.appendToken(beforeIdent,
                            FormatToken.forFormat(FormatToken.Kind.BEFORE_FUNCTION_DECLARATION_PARAMETER));
                }
            }
        }
    }

    private void handleFunctionCallChain(CallNode callNode) {
        Node function = callNode.getFunction();
        if (function instanceof AccessNode) {
            Node base = ((AccessNode) function).getBase();
            if (base instanceof CallNode) {
                CallNode chained = (CallNode) base;
                int finish = getFinish(chained);
                FormatToken formatToken = tokenUtils.getNextToken(finish, JsTokenId.OPERATOR_DOT);
                if (formatToken != null) {
                    TokenUtils.appendTokenAfterLastVirtual(formatToken,
                            FormatToken.forFormat(FormatToken.Kind.AFTER_CHAIN_CALL_DOT));
                    formatToken = formatToken.previous();
                    if (formatToken != null) {
                        TokenUtils.appendToken(formatToken, FormatToken.forFormat(FormatToken.Kind.BEFORE_CHAIN_CALL_DOT));
                    }
                }
            }
        }
    }

    private boolean handleLoop(LoopNode loopNode, FormatToken.Kind afterStart) {
        Block body = loopNode.getBody();
        if (isVirtual(body)) {
            handleVirtualBlock(body, afterStart);
            return true;
        }
        return false;
    }

    private void handleStandardBlock(Block block) {
        handleBlockContent(block, true);

        // indentation mark & block start
        FormatToken formatToken = tokenUtils.getPreviousToken(getStart(block), JsTokenId.BRACKET_LEFT_CURLY, true);
        if (formatToken != null && !isScript(block)) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_INC));
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_BLOCK_START));
        }

        // put indentation mark after non white token preceeding curly bracket
        // XXX optimize ?
        formatToken = tokenUtils.getNextToken(getFinish(block) - 1, JsTokenId.BRACKET_RIGHT_CURLY);
        if (formatToken != null) {
            formatToken = tokenUtils.getPreviousNonWhiteToken(formatToken.getOffset(),
                    getStart(block), JsTokenId.BRACKET_RIGHT_CURLY, true);
            if (formatToken != null && !isScript(block)) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.INDENTATION_DEC));
            }
        }
    }

    private void handleVirtualBlock(Block block, FormatToken.Kind afterBlock) {
        handleVirtualBlock(block, FormatToken.Kind.INDENTATION_INC, FormatToken.Kind.INDENTATION_DEC,
                afterBlock, true);
    }

    private void handleVirtualBlock(Block block, FormatToken.Kind indentationInc,
            FormatToken.Kind indentationDec, FormatToken.Kind afterBlock, boolean markStatements) {

        assert isVirtual(block) : block;

        boolean assertsEnabled = false;
        assert assertsEnabled = true;
        if (assertsEnabled) {
            if (block.getStatements().size() > 1) {
                int count = 0;
                // there may be multiple var statements due to the comma
                // separated vars translated to multiple statements in ast
                for (Node node : block.getStatements()) {
                    if (!(node instanceof VarNode)) {
                        count++;
                    }
                }
                assert count <= 1;
            }
        }

        if (block.getStart() >= block.getFinish()/*block.getStatements().isEmpty()*/) {
            return;
        }

        handleBlockContent(block, markStatements);

        //Node statement = block.getStatements().get(0);

        // indentation mark & block start
        Token token = tokenUtils.getPreviousNonEmptyToken(getStart(block));

        if (token != null) {
            FormatToken formatToken = tokenStream.getToken(ts.offset());
            if (!isScript(block)) {
                if (formatToken == null && ts.offset() <= formatFinish) {
                    formatToken = tokenStream.getTokens().get(0);
                }
                if (formatToken != null) {
                    TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(indentationInc));
                    if (afterBlock != null) {
                        TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(afterBlock));
                    }
                }
            }
        }

        // put indentation mark after non white token
        int finish = getFinish(block);
        // empty statement has start == finish
        FormatToken formatToken = tokenUtils.getPreviousToken(
                block.getStart() < finish ? finish - 1 : finish, null, true);
        if (formatToken != null && !isScript(block)) {
            while (formatToken != null && (formatToken.getKind() == FormatToken.Kind.EOL
                    || formatToken.getKind() == FormatToken.Kind.WHITESPACE
                    || formatToken.getKind() == FormatToken.Kind.LINE_COMMENT
                    || formatToken.getKind() == FormatToken.Kind.BLOCK_COMMENT
                    || formatToken.getKind() == FormatToken.Kind.DOC_COMMENT)) {
                formatToken = formatToken.previous();
            }
            if (block.getStatementCount() == 0 && block.getStart() < block.getFinish()) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_STATEMENT));
            }
            TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(indentationDec));
        }
    }

    private void handleBlockContent(Block block, boolean markStatements) {
        handleBlockContent(block.getStatements(), markStatements);
    }

    private void handleBlockContent(List<Statement> statements, boolean markStatements) {
        // statements
        boolean destructuring = false;
        for (int i = 0; i < statements.size(); i++) {
            Node statement = statements.get(i);
            statement.accept(this);

            if (markStatements) {
                int start = getStart(statement);
                int finish = getFinish(statement);

                /*
                 * What do we solve here? Unfortunately nashorn parses single
                 * var statement as (possibly) multiple VarNodes. For example:
                 * var a=1,b=2; is parsed to two VarNodes. The first covering a=1,
                 * the second b=2. So we iterate subsequent VarNodes searching the
                 * last one and the proper finish token.
                 */
                if (statement instanceof VarNode) {
                    if (((VarNode) statement).isDestructuring()) {
                        destructuring = true;
                        continue;
                    }
                    if (!isDeclaration((VarNode) statement)) {
                        int index = i + 1;
                        Node lastVarNode = statement;

                        while (i + 1 < statements.size()) {
                            Node next = statements.get(++i);
                            if (!(next instanceof VarNode) || isDeclaration((VarNode) next)) {
                                i--;
                                break;
                            } else {
                                Token token = tokenUtils.getPreviousNonEmptyToken(getStart(next));
                                if (token != null && (JsTokenId.KEYWORD_VAR == token.id()
                                        || JsTokenId.KEYWORD_CONST == token.id()
                                        || JsTokenId.RESERVED_LET == token.id())) {
                                    i--;
                                    break;
                                }
                            }
                            lastVarNode = next;
                        }

                        for (int j = index; j < i + 1; j++) {
                            Node skipped = statements.get(j);
                            skipped.accept(this);
                        }

                        finish = getFinish(lastVarNode);
                    }
                }

                int searchOffset = start < finish ? finish - 1 : finish;
                // if it is destructuring the finish is at the end
                if (statement instanceof ExpressionStatement && destructuring) {
                    searchOffset = finish;
                }
                destructuring = false;
                FormatToken formatToken = tokenUtils.getPreviousToken(searchOffset, null);
                while (formatToken != null && (formatToken.getKind() == FormatToken.Kind.EOL
                        || formatToken.getKind() == FormatToken.Kind.WHITESPACE
                        || formatToken.getKind() == FormatToken.Kind.LINE_COMMENT
                        || formatToken.getKind() == FormatToken.Kind.BLOCK_COMMENT
                        || formatToken.getKind() == FormatToken.Kind.DOC_COMMENT)) {
                    formatToken = formatToken.previous();
                }
                if (formatToken != null) {
                    TokenUtils.appendTokenAfterLastVirtual(formatToken,
                            FormatToken.forFormat(FormatToken.Kind.AFTER_STATEMENT), true);
                }
            }
        }
    }

    private void handleClassElement(PropertyNode property, int start) {
        property.accept(this);

        PropertyNode propertyNode = (PropertyNode) property;
        if (propertyNode.getGetter() != null) {
            FunctionNode getter = (FunctionNode) propertyNode.getGetter();
            markClassElementFinish(getStart(getter), getFinish(getter), start,
                    false, propertyNode.getGetter());
        }
        if (propertyNode.getSetter() != null) {
            FunctionNode setter = (FunctionNode) propertyNode.getSetter();
            markClassElementFinish(getStart(setter), getFinish(setter), start,
                    false, propertyNode.getSetter());
        }

        // mark property end
        markClassElementFinish(getStart(property), getFinish(property), start,
                true, propertyNode.getValue());
    }

    private void handleDecorators(List<Expression> decorators) {
        for (Expression decorator : decorators) {
            FormatToken formatToken = tokenUtils.getPreviousNonWhiteToken(getFinish(decorator) - 1, -1, null, true);
            if (formatToken != null) {
                TokenUtils.appendTokenAfterLastVirtual(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_DECORATOR));
            }
        }
    }

    private void markSpacesWithinParentheses(SwitchNode node) {
        int leftStart = getStart(node);

        // the { has to be there for switch
        FormatToken token = tokenUtils.getNextToken(leftStart, JsTokenId.BRACKET_LEFT_CURLY, getFinish(node));
        if (token != null) {
            markSpacesWithinParentheses(node, leftStart, token.getOffset(),
                    FormatToken.Kind.AFTER_SWITCH_PARENTHESIS, FormatToken.Kind.BEFORE_SWITCH_PARENTHESIS);
        }
    }

    private void markSpacesBeforeBrace(SwitchNode node) {
        int leftStart = getStart(node);

        // the { has to be there for switch
        FormatToken token = tokenUtils.getNextToken(leftStart, JsTokenId.BRACKET_LEFT_CURLY, getFinish(node));
        if (token != null) {
            FormatToken previous = token.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(FormatToken.Kind.BEFORE_SWITCH_BRACE));
            }
        }
    }

    /**
     * Method putting formatting tokens for within parenthesis rule. Note
     * that this method may be more secure as it can search for the left paren
     * from start of the node and for the right from the body of the node
     * avoiding possibly wrong offset of expressions/conditions.
     *
     * @param outerNode the node we are marking, such as if, while, with
     * @param leftStart from where to start search to the right for the left paren
     * @param rightStart from where to start search to the left for the right paren
     * @param leftMark where to stop searching for the left paren
     * @param rightMark where to stop searching for the right paren
     */
    private void markSpacesWithinParentheses(Node outerNode, int leftStart,
            int rightStart, FormatToken.Kind leftMark, FormatToken.Kind rightMark) {

        FormatToken leftParen = tokenUtils.getNextToken(leftStart,
                JsTokenId.BRACKET_LEFT_PAREN, getFinish(outerNode));
        if (leftParen != null) {
            FormatToken mark = leftParen.next();
            assert mark != null && mark.getKind() == FormatToken.Kind.AFTER_LEFT_PARENTHESIS : mark;
            if (mark.getKind() == FormatToken.Kind.AFTER_LEFT_PARENTHESIS) {
                tokenStream.removeToken(mark);
            }

            TokenUtils.appendToken(leftParen, FormatToken.forFormat(leftMark));
            FormatToken rightParen = tokenUtils.getPreviousToken(rightStart,
                    JsTokenId.BRACKET_RIGHT_PAREN, getStart(outerNode));
            if (rightParen != null) {
                FormatToken previous = rightParen.previous();
                assert previous != null && previous.getKind() == FormatToken.Kind.BEFORE_RIGHT_PARENTHESIS : previous;
                if (previous.getKind() == FormatToken.Kind.BEFORE_RIGHT_PARENTHESIS) {
                    tokenStream.removeToken(previous);
                }

                previous = rightParen.previous();
                if (previous != null) {
                    TokenUtils.appendToken(previous, FormatToken.forFormat(rightMark));
                }
            }
        }
    }

    private void markSpacesBeforeBrace(Block block, FormatToken.Kind mark) {
        FormatToken brace = tokenUtils.getPreviousToken(getStart(block), null,
                getStart(block) - 1);
        if (brace != null) {
            FormatToken previous = brace.previous();
            if (previous != null) {
                TokenUtils.appendToken(previous, FormatToken.forFormat(mark));
            }
        }
    }

    private void markPropertyFinish(int finish, int objectFinish, boolean checkDuplicity) {
        FormatToken formatToken = tokenUtils.getNextToken(finish, JsTokenId.OPERATOR_COMMA, objectFinish - 1);
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken,
                    FormatToken.forFormat(FormatToken.Kind.AFTER_PROPERTY), checkDuplicity);
        }
    }

    private void markClassElementFinish(int start, int finish, int classFinish,
            boolean checkDuplicity, Expression value) {

        FormatToken formatToken;
        if (value instanceof FunctionNode) {
            // method
            formatToken = tokenUtils.getPreviousToken(finish, JsTokenId.BRACKET_RIGHT_CURLY, classFinish);
        } else {
            // property
            formatToken = tokenUtils.getPreviousToken(start < finish ? finish - 1 : finish, null);
            while (formatToken != null && (formatToken.getKind() == FormatToken.Kind.EOL
                    || formatToken.getKind() == FormatToken.Kind.WHITESPACE
                    || formatToken.getKind() == FormatToken.Kind.LINE_COMMENT
                    || formatToken.getKind() == FormatToken.Kind.BLOCK_COMMENT
                    || formatToken.getKind() == FormatToken.Kind.DOC_COMMENT)) {
                formatToken = formatToken.previous();
            }
        }
        if (formatToken != null) {
            TokenUtils.appendTokenAfterLastVirtual(formatToken,
                    FormatToken.forFormat(FormatToken.Kind.AFTER_ELEMENT), checkDuplicity);
        }
    }

    private void markEndCurlyBrace(Node node) {
        if (node != null) {
            FormatToken formatToken = tokenStream.getToken(getFinish(node) - 1);
            if (formatToken != null) {
                while (formatToken.isVirtual()) {
                    formatToken = formatToken.previous();
                    if (formatToken == null) {
                        return;
                    }
                }
                if (formatToken.getId() == JsTokenId.BRACKET_RIGHT_CURLY) {
                    TokenUtils.appendToken(formatToken, FormatToken.forFormat(FormatToken.Kind.AFTER_END_BRACE));
                }
            }
        }
    }

    /**
     * Finds the next non empty token first and then move back to non whitespace
     * token.
     *
     * @param block case block
     * @return format token
     */
    private FormatToken getCaseEndToken(int start, int finish) {
        ts.move(finish);

        if (!ts.moveNext() && !ts.movePrevious()) {
            return null;
        }

        Token ret = null;
        while (ts.moveNext()) {
            Token token = ts.token();
            if ((token.id() != JsTokenId.BLOCK_COMMENT && token.id() != JsTokenId.DOC_COMMENT
                && token.id() != JsTokenId.LINE_COMMENT && token.id() != JsTokenId.EOL
                && token.id() != JsTokenId.WHITESPACE)) {
                ret = token;
                break;
            }
        }

        if (ret != null) {
            while (ts.movePrevious() && ts.offset() >= start) {
                Token current = ts.token();
                if (current.id() != JsTokenId.WHITESPACE) {
                    ret = current;
                    break;
                }
            }

            return tokenUtils.getFallback(ts.offset(), true);
        }
        return null;
    }

    private int getStart(Node node) {
        // unfortunately in binary node the token represents operator
        // so string fix would not work
        if (node instanceof BinaryNode) {
            return getStart((BinaryNode) node);
        }
        if (node instanceof FunctionNode) {
            return getFunctionStart((FunctionNode) node);
        }
        // All this magic is because nashorn nodes and tokens don't contain the
        // quotes for string. Due to this we call this method to add 1 to start
        // in case it is string literal.
        int start = node.getStart();
        long firstToken = node.getToken();
        TokenType type = com.oracle.js.parser.Token.descType(firstToken);
        if (type.equals(TokenType.STRING) || type.equals(TokenType.ESCSTRING)) {
            ts.move(start - 1);
            if (ts.moveNext()) {
                Token<? extends JsTokenId> token = ts.token();
                if (token.id() == JsTokenId.STRING_BEGIN) {
                    start--;
                }
            }
        }

        return start;
    }

    private int getStart(BinaryNode node) {
        return getStart(node.lhs());
    }

    private static int getFunctionStart(FunctionNode node) {
        return com.oracle.js.parser.Token.descPosition(node.getFirstToken());
    }

    private int getFinish(Node node) {
        // we are fixing the wrong finish offset here
        // only function node has last token
        if (node instanceof FunctionNode) {
            FunctionNode function = (FunctionNode) node;
            if (node.getStart() == node.getFinish()) {
                long lastToken = function.getLastToken();
                TokenType type = com.oracle.js.parser.Token.descType(lastToken);
                int finish;
                if (type == TokenType.EOL) {
                    // when eol token length just stores line number
                    finish = com.oracle.js.parser.Token.descPosition(lastToken);
                } else {
                    finish = com.oracle.js.parser.Token.descPosition(lastToken)
                            + com.oracle.js.parser.Token.descLength(lastToken);
                }
                // check if it is a string
                if (com.oracle.js.parser.Token.descType(lastToken).equals(TokenType.STRING)) {
                    finish++;
                }
                return finish;
            } else {
                return node.getFinish();
            }
        } else if (node instanceof Block) {
            // XXX in truffle the function body finish is at last statement
            FunctionNode fn = lc.getCurrentFunction();
            if (fn != null) {
                if (fn.getBody() == node) {
                    return getFinish(fn);
                }
            }
        } else if (node instanceof VarNode) {
            VarNode var = (VarNode) node;
            if (var.getInit() instanceof ClassNode) {
                return getFinish(var.getInit());
            }
            Token token = tokenUtils.getNextNonEmptyToken(getFinishFixed(node) - 1);
            if (token != null && JsTokenId.OPERATOR_SEMICOLON == token.id()) {
                return ts.offset() + 1;
            } else {
                return getFinishFixed(node);
            }
        }

        return getFinishFixed(node);
    }

    private int getFinishFixed(Node node) {
        // All this magic is because nashorn nodes and tokens don't contain the
        // quotes for string. Due to this we call this method to add 1 to finish
        // in case it is string literal.
        int finish = node.getFinish();
        ts.move(finish);
        if (!ts.moveNext()) {
            return finish;
        }
        Token<? extends JsTokenId> token = ts.token();
        if (token.id() == JsTokenId.STRING_END || token.id() == JsTokenId.TEMPLATE_END) {
            return finish + 1;
        }

        return finish;
    }

    private boolean isScript(Block node) {
        if(!node.isFunctionBody()) {
            return false;
        }
        FunctionNode functionNode = getLexicalContext().getCurrentFunction();
        return functionNode != null && functionNode.isProgram();
    }

    private boolean isVirtual(Block block) {
        return block.getStart() == block.getFinish()
                    || com.oracle.js.parser.Token.descType(block.getToken()) != TokenType.LBRACE
                    || block.isCatchBlock();
    }

    private boolean isDeclaration(VarNode varNode) {
        if (varNode.isFunctionDeclaration() || varNode.isExport() || varNode.isDestructuring()) {
            return true;
        }
        if (varNode.getInit() instanceof ClassNode) {
            IdentNode cIdent = ((ClassNode) varNode.getInit()).getIdent();
            IdentNode vIdent = varNode.getName();
            // this is artificial var node for simple class declaration
            if (cIdent != null
                    && cIdent.getStart() == vIdent.getStart()
                    && cIdent.getFinish() == vIdent.getFinish()) {
                return true;
            }
        }
        return false;
    }
}
