/*
 * 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;

import java.util.Optional;
import java.util.function.Predicate;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.text.BadLocationException;
import javax.swing.text.Document;
import org.netbeans.api.editor.document.EditorDocumentUtils;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.api.editor.mimelookup.MimeRegistration;
import org.netbeans.api.editor.mimelookup.MimeRegistrations;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.Token;
import org.netbeans.api.lexer.TokenHierarchy;
import org.netbeans.api.lexer.TokenId;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.editor.BaseDocument;
import org.netbeans.editor.Utilities;
import org.netbeans.lib.editor.util.CharSequenceUtilities;
import org.netbeans.modules.csl.api.EditorOptions;
import org.netbeans.modules.csl.spi.GsfUtilities;
import org.netbeans.modules.editor.indent.api.IndentUtils;
import org.netbeans.modules.javascript2.editor.doc.JsDocumentationCompleter;
import org.netbeans.modules.javascript2.json.api.JsonOptionsQuery;
import org.netbeans.modules.javascript2.lexer.api.JsDocumentationTokenId;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.spi.editor.typinghooks.TypedBreakInterceptor;
import org.openide.util.Exceptions;

/**
 *
 * @author Petr Hejl
 */
public class JsTypedBreakInterceptor implements TypedBreakInterceptor {

    /**
     * When true, continue comments if you press return in a line comment
     * (that does not also have code on the same line).
     */
    static final boolean CONTINUE_COMMENTS = Boolean.getBoolean("js.cont.comment"); // NOI18N

    // unit testing
    static boolean completeDocumentation = true;

    private static final Logger LOGGER = Logger.getLogger(JsTypedBreakInterceptor.class.getName());

    private final Language<JsTokenId> language;

    private final Predicate<Document> comments;

    private final boolean multiLineLiterals;

    private CommentGenerator commentGenerator = null;

    public JsTypedBreakInterceptor(Language<JsTokenId> language, boolean comments, boolean multiLineLiterals) {
        this(language, (doc) -> comments, multiLineLiterals);
    }

    public JsTypedBreakInterceptor(Language<JsTokenId> language, Predicate<Document> comments, boolean multiLineLiterals) {
        this.language = language;
        this.comments = comments;
        this.multiLineLiterals = multiLineLiterals;
    }

    private boolean isInsertMatchingEnabled() {
        EditorOptions options = EditorOptions.get(language.mimeType());
        if (options != null) {
            return options.getMatchBrackets();
        }

        return true;
    }

    @Override
    public void insert(MutableContext context) throws BadLocationException {
        BaseDocument doc = (BaseDocument) context.getDocument();
        TokenHierarchy<BaseDocument> tokenHierarchy = TokenHierarchy.get(doc);
        int offset = context.getCaretOffset();

        int lineBegin = Utilities.getRowStart(doc, offset);
        int lineEnd = Utilities.getRowEnd(doc, offset);

        if (lineBegin == offset && lineEnd == offset) {
            // Pressed return on a blank newline - do nothing
            return;
        }

        TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence(
                tokenHierarchy, offset, language);

        if (ts == null) {
            return;
        }

        ts.move(offset);

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

        Token<? extends JsTokenId> token = ts.token();
        JsTokenId id = token.id();

        // Insert a missing }
        if (!id.isError() && isInsertMatchingEnabled() && !isDocToken(id) && isAddRightBrace(doc, offset)) {
            int indent = GsfUtilities.getLineIndent(doc, offset);

            int afterLastNonWhite = Utilities.getRowLastNonWhite(doc, offset);

            // We've either encountered a further indented line, or a line that doesn't
            // look like the end we're after, so insert a matching end.
            StringBuilder sb = new StringBuilder();
            int carretOffset = 0;
            int curlyOffset = getUnbalancedCurlyOffset(doc, offset);
            if (offset > afterLastNonWhite) {
                
                sb.append("\n"); // XXX On Windows, do \r\n?
                sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc)));
                carretOffset = sb.length();
                sb.append("\n"); // NOI18N
                if (curlyOffset >= 0) {
                    sb.append(IndentUtils.createIndentString(doc, getCurlyIndent(doc, curlyOffset)));
                } else {
                    sb.append(IndentUtils.createIndentString(doc, indent));
                }
                sb.append("}"); // NOI18N
            } else {
                boolean insert[] = {true};
                int end = getRowOrBlockEnd(doc, offset, insert);
                if (insert[0]) {
                    // I'm inserting a newline in the middle of a sentence, such as the scenario in #118656
                    // I should insert the end AFTER the text on the line
                    String restOfLine = doc.getText(offset,
                            Math.min(end, Utilities.getRowEnd(doc, afterLastNonWhite)) - offset);
                    sb.append("\n"); // XXX On Windows, do \r\n?
                    sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc)));
                    // right brace must be included into the correct context - issue #219683
                    carretOffset = sb.length();

                    sb.append(restOfLine); // NOI18N
                    sb.append("\n"); // NOI18N
                    if (curlyOffset >= 0) {
                        sb.append(IndentUtils.createIndentString(doc, getCurlyIndent(doc, curlyOffset)));
                    } else {
                        sb.append(IndentUtils.createIndentString(doc, indent));
                    }
                    sb.append("}"); // NOI18N
                    doc.remove(offset, restOfLine.length());
                }
                
            }

            if (sb.length() > 0) {
                context.setText(sb.toString(), 0, carretOffset);
            }
            return;
        }

        if (id.isError()) {
            // See if it's a block comment opener
            String text = token.text().toString();
            if (comments.test(doc) && text.startsWith("/*") ) {
                int indent = GsfUtilities.getLineIndent(doc, ts.offset());
                StringBuilder sb = new StringBuilder();
                sb.append("\n"); // NOI18N
                sb.append(IndentUtils.createIndentString(doc, indent));
                sb.append(" * "); // NOI18N
                int carretOffset = sb.length();
                sb.append("\n"); // NOI18N
                sb.append(IndentUtils.createIndentString(doc, indent));
                sb.append(" */"); // NOI18N

                if (text.startsWith("/**")) {
                    // setup comment generator
                    commentGenerator = new CommentGenerator(offset + carretOffset, indent + 1);
                }
                context.setText(sb.toString(), 0, carretOffset);
                return;
            }
        }

        if (multiLineLiterals) {
            if (id == JsTokenId.STRING ||
                    (id == JsTokenId.STRING_END) && offset < ts.offset()+ts.token().length()) {
                // Instead of splitting a string "foobar" into "foo"+"bar", just insert a \ instead!
                //int indent = GsfUtilities.getLineIndent(doc, offset);
                //int delimiterOffset = id == JsTokenId.STRING_END ? ts.offset() : ts.offset()-1;
                //char delimiter = doc.getText(delimiterOffset,1).charAt(0);
                //doc.insertString(offset, delimiter + " + " + delimiter, null);
                //caret.setDot(offset+3);
                //return offset + 5 + indent;
                String str = "\\\n";    //NOI18N
                if (id != JsTokenId.STRING || offset > ts.offset()) {
                    str = "\\n\\\n";    //NOI18N
                    if (offset - ts.offset() < ts.token().length()) {
                        String text = ts.token().text().toString();
                        text = text.substring(0, offset - ts.offset());
                        if(text.endsWith("\\n\\")) {    //NOI18N
                            str = "\n\\n\\";    //NOI18N
                        }
                    }
                }
                context.setText(str, -1, str.length());
                return;
            }
            if (id == JsTokenId.TEMPLATE ||
                    (id == JsTokenId.TEMPLATE_END) && offset < ts.offset()+ts.token().length()) {
                // Instead of indenting it to the previous line as below just insert a newline and finish!
                String str = "\n"; //NOI18N
                context.setText(str, -1, str.length());
                return;
            }



            if (id == JsTokenId.REGEXP ||
                    (id == JsTokenId.REGEXP_END) && offset < ts.offset()+ts.token().length()) {
                // Instead of splitting a string "foobar" into "foo"+"bar", just insert a \ instead!
                //int indent = GsfUtilities.getLineIndent(doc, offset);
                //doc.insertString(offset, "/ + /", null);
                //caret.setDot(offset+3);
                //return offset + 5 + indent;
                String str = (id != JsTokenId.REGEXP || offset > ts.offset()) ? "\\n\\\n"  : "\\\n";
                context.setText(str, -1, str.length());
                return;
            }
        } else {
            final int indent = GsfUtilities.getLineIndent(doc, offset);
            final StringBuilder sb = new StringBuilder();
            sb.append("\n"); // NOI18N
            sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc)));
            final int carretOffset = sb.length();
            context.setText(sb.toString(), 0, carretOffset);
            return;
        }

        // Special case: since I do hash completion, if you try to type
        //     y = Thread.start {
        //         code here
        //     }
        // you end up with
        //     y = Thread.start {|}
        // If you hit newline at this point, you end up with
        //     y = Thread.start {
        //     |}
        // which is not as helpful as it would be if we were not doing hash-matching
        // (in that case we'd notice the brace imbalance, and insert the closing
        // brace on the line below the insert position, and indent properly.
        // Catch this scenario and handle it properly.
        if ((id == JsTokenId.BRACKET_RIGHT_CURLY || id == JsTokenId.BRACKET_RIGHT_BRACKET) && offset > 0) {
            Token<? extends JsTokenId> prevToken = LexUtilities.getToken(doc, offset - 1, language);
            if (prevToken != null) {
                JsTokenId prevTokenId = prevToken.id();
                if (id == JsTokenId.BRACKET_RIGHT_CURLY && prevTokenId == JsTokenId.BRACKET_LEFT_CURLY ||
                        id == JsTokenId.BRACKET_RIGHT_BRACKET && prevTokenId == JsTokenId.BRACKET_LEFT_BRACKET) {
                    int indent = GsfUtilities.getLineIndent(doc, offset);
                    StringBuilder sb = new StringBuilder();
                    // XXX On Windows, do \r\n?
                    sb.append("\n"); // NOI18N
                    sb.append(IndentUtils.createIndentString(doc, indent + IndentUtils.indentLevelSize(doc)));
                    int carretOffset = sb.length();
                    sb.append("\n"); // NOI18N
                    sb.append(IndentUtils.createIndentString(doc, indent));

                    // should we reindent it automatically ?
                    context.setText(sb.toString(), 0, carretOffset);
                    return;
                }
            }
        }

        if (!comments.test(doc)) {
            return;
        }
        if (id == JsTokenId.WHITESPACE) {
            // Pressing newline in the whitespace before a comment
            // should be identical to pressing newline with the caret
            // at the beginning of the comment
            int begin = Utilities.getRowFirstNonWhite(doc, offset);
            if (begin != -1 && offset < begin) {
                ts.move(begin);
                if (ts.moveNext()) {
                    id = ts.token().id();
                    if (id == JsTokenId.LINE_COMMENT) {
                        offset = begin;
                    }
                }
            }
        }

        if ((id == JsTokenId.BLOCK_COMMENT || id == JsTokenId.DOC_COMMENT)
                && offset > ts.offset() && offset < ts.offset()+ts.token().length()) {
            // Continue *'s
            int begin = Utilities.getRowFirstNonWhite(doc, offset);
            int end = Utilities.getRowEnd(doc, offset)+1;
            if (begin == -1) {
                begin = end;
            }
            String line = doc.getText(begin, end-begin);
            boolean isBlockStart = line.startsWith("/*") || (begin != -1 && begin < ts.offset());
            if (isBlockStart || line.startsWith("*")) {
                int indent = GsfUtilities.getLineIndent(doc, offset);
                StringBuilder sb = new StringBuilder("\n");
                if (isBlockStart) {
                    indent++;
                }
                int carretPosition = 0;
                sb.append(IndentUtils.createIndentString(doc, indent));
                if (isBlockStart) {
                    // First comment should be propertly indented
                    sb.append("* "); //NOI18N
                    carretPosition = sb.length();

                    TokenSequence<? extends JsDocumentationTokenId> jsDocTS =
                            LexUtilities.getJsDocumentationTokenSequence(tokenHierarchy, offset);
                    if (jsDocTS != null) {
                        if (!hasCommentEnd(jsDocTS)) {
                            // setup comment generator
                            commentGenerator = new CommentGenerator(offset + carretPosition, indent);
                            // append end of the comment
                            sb.append("\n").append(IndentUtils.createIndentString(doc, indent)).append("*/"); //NOI18N
                        }
                    }
                } else {
                    // Copy existing indentation inside the block
                    sb.append("*"); //NOI18N
                    int afterStar = isBlockStart ? begin+2 : begin+1;
                    line = doc.getText(afterStar, Utilities.getRowEnd(doc, afterStar)-afterStar);
                    for (int i = 0; i < line.length(); i++) {
                        char c = line.charAt(i);
                        if (c == ' ' || c == '\t') { //NOI18N
                            sb.append(c);
                        } else {
                            break;
                        }
                    }
                    carretPosition = sb.length();
                }

                if (offset == begin && offset > 0) {
                    context.setText(sb.toString(), -1, sb.length());
                    return;
                }
                context.setText(sb.toString(), -1, carretPosition);
                return;
            }
        }

        boolean isComment = id == JsTokenId.LINE_COMMENT;
        if (id == JsTokenId.EOL) {
            if (ts.movePrevious() && ts.token().id() == JsTokenId.LINE_COMMENT) {
                //ts.moveNext();
                isComment = true;
            }
        }

        if (isComment) {
            // Only do this if the line only contains comments OR if there is content to the right on this line,
            // or if the next line is a comment!

            boolean continueComment = false;
            int begin = Utilities.getRowFirstNonWhite(doc, offset);

            // We should only continue comments if the previous line had a comment
            // (and a comment from the beginning, not a trailing comment)
            boolean previousLineWasComment = false;
            boolean nextLineIsComment = false;
            int rowStart = Utilities.getRowStart(doc, offset);
            if (rowStart > 0) {
                int prevBegin = Utilities.getRowFirstNonWhite(doc, rowStart - 1);
                if (prevBegin != -1) {
                    Token<? extends JsTokenId> firstToken = LexUtilities.getToken(
                            doc, prevBegin, language);
                    if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) {
                        previousLineWasComment = true;
                    }
                }
            }
            int rowEnd = Utilities.getRowEnd(doc, offset);
            if (rowEnd < doc.getLength()) {
                int nextBegin = Utilities.getRowFirstNonWhite(doc, rowEnd + 1);
                if (nextBegin != -1) {
                    Token<? extends JsTokenId> firstToken = LexUtilities.getToken(
                            doc, nextBegin, language);
                    if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) {
                        nextLineIsComment = true;
                    }
                }
            }

            // See if we have more input on this comment line (to the right
            // of the inserted newline); if so it's a "split" operation on
            // the comment
            if (previousLineWasComment || nextLineIsComment
                    || (offset > ts.offset() && offset < ts.offset() + ts.token().length())) {
                if (ts.offset() + token.length() > offset + 1) {
                    // See if the remaining text is just whitespace
                    String trailing = doc.getText(offset, Utilities.getRowEnd(doc, offset) - offset);
                    if (trailing.trim().length() != 0) {
                        continueComment = true;
                    }
                } else if (CONTINUE_COMMENTS) {
                    // See if the "continue comments" options is turned on, and this is a line that
                    // contains only a comment (after leading whitespace)
                    Token<? extends JsTokenId> firstToken = LexUtilities.getToken(
                            doc, begin, language);
                    if (firstToken.id() == JsTokenId.LINE_COMMENT) {
                        continueComment = true;
                    }
                }
                if (!continueComment) {
                    // See if the next line is a comment; if so we want to continue
                    // comments editing the middle of the comment
                    int nextLine = Utilities.getRowEnd(doc, offset) + 1;
                    if (nextLine < doc.getLength()) {
                        int nextLineFirst = Utilities.getRowFirstNonWhite(doc, nextLine);
                        if (nextLineFirst != -1) {
                            Token<? extends JsTokenId> firstToken = LexUtilities.getToken(
                                    doc, nextLineFirst, language);
                            if (firstToken != null && firstToken.id() == JsTokenId.LINE_COMMENT) {
                                continueComment = true;
                            }
                        }
                    }
                }
            }

            if (continueComment) {
                // Line comments should continue
                int indent = GsfUtilities.getLineIndent(doc, offset);
                StringBuilder sb = new StringBuilder();
                if (offset != begin || offset <= 0) {
                    sb.append("\n");
                }
                sb.append(IndentUtils.createIndentString(doc, indent));
                sb.append("//"); // NOI18N
                // Copy existing indentation
                int afterSlash = begin + 2;
                String line = doc.getText(afterSlash, Utilities.getRowEnd(doc, afterSlash) - afterSlash);
                for (int i = 0; i < line.length(); i++) {
                    char c = line.charAt(i);
                    if (c == ' ' || c == '\t') {
                        sb.append(c);
                    } else {
                        break;
                    }
                }

                if (offset == begin && offset > 0) {
                    int caretPosition = sb.length();
                    sb.append("\n");
                    context.setText(sb.toString(), -1, caretPosition);
                    return;
                }
                context.setText(sb.toString(), -1, sb.length());
                return;
            }
        }
        
        // Just indent the line properly
        int indentSize = getNextLineIndentation(doc, offset);
        if (indentSize > 0) {
            StringBuilder sb = new StringBuilder("\n"); // NOI18N
            sb.append(IndentUtils.createIndentString(doc, indentSize));
            context.setText(sb.toString(), -1, sb.length());
        }
    }

    @Override
    public void afterInsert(Context context) throws BadLocationException {
        if (completeDocumentation && commentGenerator != null) {
            JsDocumentationCompleter.generateCompleteComment(
                    (BaseDocument) context.getDocument(),
                    commentGenerator.getOffset(),
                    commentGenerator.getIndent());
            commentGenerator = null;
        }
    }

    @Override
    public boolean beforeInsert(Context context) throws BadLocationException {
        return false;
    }

    @Override
    public void cancelled(Context context) {
    }

    /**
     * Computes the indentation of the next line (after the line break).
     * 
     * @param doc document
     * @param offset current offset
     * @return indentation size
     * @throws BadLocationException 
     */
    private int getNextLineIndentation(BaseDocument doc, int offset) throws BadLocationException {
        int indent = GsfUtilities.getLineIndent(doc, offset);
        int currentOffset = offset;
        while (currentOffset > 0) {
            if (!Utilities.isRowEmpty(doc, currentOffset) && !Utilities.isRowWhite(doc, currentOffset)
                    && !isCommentOnlyLine(doc, currentOffset, language)) {
                indent = GsfUtilities.getLineIndent(doc, currentOffset);
                int parenBalance = getLineBalance(doc, currentOffset,
                        JsTokenId.BRACKET_LEFT_PAREN, JsTokenId.BRACKET_RIGHT_PAREN);
                if (parenBalance < 0) {
                    break;
                }
                int curlyBalance = getLineBalance(doc, currentOffset,
                        JsTokenId.BRACKET_LEFT_CURLY, JsTokenId.BRACKET_RIGHT_CURLY);
                if (curlyBalance > 0) {
                    indent += IndentUtils.indentLevelSize(doc);
                }
                return indent;
            }
            currentOffset = Utilities.getRowStart(doc, currentOffset) - 1;
        }

        return indent;
    }

    /**
     * From Java.
     *
     * Resolve whether pairing right curly should be added automatically
     * at the caret position or not.
     * <br>
     * There must be only whitespace or line comment or block comment
     * between the caret position
     * and the left brace and the left brace must be on the same line
     * where the caret is located.
     * <br>
     * The caret must not be "contained" in the opened block comment token.
     *
     * @param doc document in which to operate.
     * @param caretOffset offset of the caret.
     * @return true if a right brace '}' should be added
     *  or false if not.
     */
    private boolean isAddRightBrace(BaseDocument doc, int caretOffset) throws BadLocationException {
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence(doc, caretOffset, language);
        if (ts == null) {
            return false;
        }

        // get balance from start index of the tokenSequence
        ts.moveIndex(0);
        if (!ts.moveNext()) {
            return false;
        }

        int balance = 0;
        boolean balancedAfter = false;

        do {
            Token t = ts.token();

            if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) {
                balance++;
            } else if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
                balance--;
            }
        } while (ts.offset() < caretOffset && ts.moveNext());

        for (TokenSequenceIterator tsi = new TokenSequenceIterator(TokenHierarchy.get(doc).tokenSequenceList(ts.languagePath(), caretOffset, doc.getLength()), false); tsi.hasMore();) {
            TokenSequence<?> sq = tsi.getSequence();
            Token t = sq.token();

            if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) {
                balance++;
            } else if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
                balance--;
            }
            if (balance == 0
                    && (t.id() == JsTokenId.BRACKET_LEFT_CURLY || t.id() == JsTokenId.BRACKET_RIGHT_CURLY)) {
                balancedAfter = true;
                break;
            }
        }

        if (balance < 0) {
            return false;
        }

        int caretRowStartOffset = org.netbeans.editor.Utilities.getRowStart(doc, caretOffset);
        ts = LexUtilities.getPositionedSequence(doc, caretOffset, language);
        if (ts == null) {
            return false;
        }
        if (ts.offset() == caretOffset && !ts.movePrevious()) {
            return false;
        }
        boolean first = true;
        do {
            if (ts.offset() < caretRowStartOffset) {
                return false;
            }
            JsTokenId id = ts.token().id();
            switch (id) {
                case WHITESPACE:
                case LINE_COMMENT:
                    break;
                case BLOCK_COMMENT:
                case DOC_COMMENT:
                    if (first && caretOffset > ts.offset() && caretOffset < ts.offset() + ts.token().length()) {
                        // Caret contained within block comment -> do not add anything
                        return false;
                    }
                    break; // Skip
                case BRACKET_LEFT_CURLY:
                    return !balancedAfter;
                default:
                    return false;
            }
            first = false;
        } while (ts.movePrevious());
        return false;
    }

    /**
     * From Java.
     * 
     * Returns position of the first unpaired closing paren/brace/bracket from the caretOffset
     * till the end of caret row. If there is no such element, position after the last non-white
     * character on the caret row is returned.
     */
    private int getRowOrBlockEnd(BaseDocument doc, int caretOffset, boolean[] insert) throws BadLocationException {
        int rowEnd = org.netbeans.editor.Utilities.getRowLastNonWhite(doc, caretOffset);
        if (rowEnd == -1 || caretOffset >= rowEnd) {
            return caretOffset;
        }
        rowEnd += 1;
        int parenBalance = 0;
        int braceBalance = 0;
        int bracketBalance = 0;
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getPositionedSequence(
                doc, caretOffset, language);
        if (ts == null) {
            return caretOffset;
        }
        while (ts.offset() < rowEnd) {
            JsTokenId id = ts.token().id();
            switch (id) {
                case OPERATOR_SEMICOLON:
                    return ts.offset() + 1;
                case OPERATOR_COMMA:
                    return ts.offset();
                case BRACKET_LEFT_PAREN:
                    parenBalance++;
                    break;
                case BRACKET_RIGHT_PAREN:
                    if (parenBalance-- == 0) {
                        return ts.offset();
                    }
                    break;
                case BRACKET_LEFT_CURLY:
                    braceBalance++;
                    break;
                case BRACKET_RIGHT_CURLY:
                    if (braceBalance-- == 0) {
                        return ts.offset();
                    }
                    break;
                case BRACKET_LEFT_BRACKET:
                    bracketBalance++;
                    break;
                case BRACKET_RIGHT_BRACKET:
                    if (bracketBalance-- == 0) {
                        return ts.offset();
                    }
                    break;
            }
            if (!ts.moveNext()) {
                // this might happen in embedded case - line is not at the end
                // but there are no more tokens - for example <script>function foo() {</script>
                if ((caretOffset - ts.offset()) == 1
                        && (bracketBalance == 1 || parenBalance == 1 || braceBalance == 1)) {
                    return caretOffset;
                }
                break;
            }
        }

        insert[0] = false;
        return rowEnd;
    }

    private int getUnbalancedCurlyOffset(BaseDocument doc, int offset) throws BadLocationException {
        TokenSequence<? extends JsTokenId> ts = LexUtilities.getPositionedSequence(
                doc, offset, language);
        if (ts == null) {
            return -1;
        }

        int balance = 0;
        while (ts.movePrevious()) {
            Token t = ts.token();

            if (t.id() == JsTokenId.BRACKET_RIGHT_CURLY) {
                balance++;
            } else if (t.id() == JsTokenId.BRACKET_LEFT_CURLY) {
                balance--;
                if (balance < 0) {
                    return ts.offset();
                }
            }
        }
        return -1;
    }

    private int getCurlyIndent(BaseDocument doc, int offset) {
        try {
            int lineStart = Utilities.getRowStart(doc, offset, 0);
            TokenSequence<? extends JsTokenId> ts = LexUtilities.getTokenSequence(
                    doc, lineStart, language);

            int prevLineStart = -1;
            if (ts != null) {
                do {
                    ts.move(lineStart);
                    if (!ts.moveNext()) {
                        if (prevLineStart >= 0) {
                            return IndentUtils.lineIndent(doc, lineStart);
                        } else {
                            return GsfUtilities.getLineIndent(doc, offset);
                        }
                    }

                    Token<? extends JsTokenId> token = LexUtilities.findNextNonWsNonComment(ts);
                    Token<? extends JsTokenId> nextToken = LexUtilities.findNextNonWsNonComment(ts);
                    if (!LexUtilities.isBinaryOperator(token.id(), nextToken.id())) {
                        ts.move(lineStart);
                        if (!ts.movePrevious()) {
                            return IndentUtils.lineIndent(doc, lineStart);
                        }
                        nextToken = token;
                        token = LexUtilities.findPreviousNonWsNonComment(ts);
                        if (!LexUtilities.isBinaryOperator(token.id(), nextToken.id())) {
                            return IndentUtils.lineIndent(doc, lineStart);
                        }
                    }
                    prevLineStart = lineStart;
                    lineStart = Utilities.getRowStart(doc, lineStart, -1);
                } while (lineStart > 0);

                if (lineStart <= 0) {
                    return IndentUtils.lineIndent(doc, lineStart);
                }
            }
        } catch (BadLocationException ex) {
            LOGGER.log(Level.INFO, null, ex);
        }
        return GsfUtilities.getLineIndent(doc, offset);
    }

    private boolean isDocToken(JsTokenId id) {
        return id == JsTokenId.BLOCK_COMMENT || id == JsTokenId.DOC_COMMENT;
    }

    private static boolean hasCommentEnd(TokenSequence ts) {
        while (ts.moveNext()) {
            Token<JsDocumentationTokenId> token = ts.token();
            if (token.id() == JsDocumentationTokenId.COMMENT_END) {
                return true;
            } else if (CharSequenceUtilities.endsWith(token.text(), "/")) { //NOI18N
                if (ts.moveNext()) {
                    Token<JsDocumentationTokenId> nextToken = ts.token();
                    if (CharSequenceUtilities.textEquals(nextToken.text(), "/")) { //NOI18N
                        ts.movePrevious();
                        continue;
                    } else if (nextToken.id() == JsDocumentationTokenId.ASTERISK) {
                        return false;
                    }
                }
            }
        }
        return false;
    }
    
    /**
     * Return true iff the line for the given offset is a JavaScript comment line.
     * This will return false for lines that contain comments (even when the
     * offset is within the comment portion) but also contain code.
     */
    private static boolean isCommentOnlyLine(BaseDocument doc, int offset, Language<JsTokenId> language)
            throws BadLocationException {

        int begin = Utilities.getRowFirstNonWhite(doc, offset);

        if (begin == -1) {
            return false; // whitespace only
        }

        Token<? extends JsTokenId> token = LexUtilities.getToken(doc, begin, language);
        if (token != null) {
            return token.id() == JsTokenId.LINE_COMMENT;
        }

        return false;
    }

    /** Compute the balance of begin/end tokens on the line */
    private static int getLineBalance(BaseDocument doc, int offset, TokenId up, TokenId down) {
        try {
            int begin = Utilities.getRowStart(doc, offset);
            int end = Utilities.getRowEnd(doc, offset);

            TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsTokenSequence(doc, begin);
            if (ts == null) {
                return 0;
            }

            ts.move(begin);

            if (!ts.moveNext()) {
                return 0;
            }

            int balance = 0;

            do {
                Token<? extends JsTokenId> token = ts.token();
                TokenId id = token.id();

                if (id == up) {
                    balance++;
                } else if (id == down) {
                    balance--;
                }
            } while (ts.moveNext() && (ts.offset() <= end));

            return balance;
        } catch (BadLocationException ble) {
            Exceptions.printStackTrace(ble);

            return 0;
        }
    }

    @MimeRegistrations({
        @MimeRegistration(mimeType = JsTokenId.JAVASCRIPT_MIME_TYPE, service = TypedBreakInterceptor.Factory.class),
        @MimeRegistration(mimeType = JsDocumentationTokenId.MIME_TYPE, service = TypedBreakInterceptor.Factory.class)
    })
    public static class JsFactory implements TypedBreakInterceptor.Factory {

        @Override
        public TypedBreakInterceptor createTypedBreakInterceptor(MimePath mimePath) {
            return new JsTypedBreakInterceptor(JsTokenId.javascriptLanguage(), true, true);
        }

    }

    @MimeRegistration(mimeType = JsTokenId.JSON_MIME_TYPE, service = TypedBreakInterceptor.Factory.class)
    public static class JsonFactory implements TypedBreakInterceptor.Factory {

        @Override
        public TypedBreakInterceptor createTypedBreakInterceptor(MimePath mimePath) {
            return new JsTypedBreakInterceptor(
                    JsTokenId.jsonLanguage(),
                    (doc) -> {
                        return Optional.ofNullable(EditorDocumentUtils.getFileObject(doc))
                                .map((fo) -> JsonOptionsQuery.getOptions(fo).isCommentSupported())
                                .orElse(false);
                    },
                    false);
        }

    }
    
    private static class CommentGenerator {

        private final int offset;
        private final int indent;

        public CommentGenerator(int offset, int indent) {
            this.offset = offset;
            this.indent = indent;
        }

        public int getIndent() {
            return indent;
        }

        public int getOffset() {
            return offset;
        }

    }
}
