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

import com.oracle.js.parser.Token;
import com.oracle.js.parser.TokenType;
import com.oracle.js.parser.ErrorManager;
import com.oracle.js.parser.ParserException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.antlr.v4.runtime.ANTLRErrorListener;
import org.antlr.v4.runtime.Parser;
import org.antlr.v4.runtime.RecognitionException;
import org.antlr.v4.runtime.Recognizer;
import org.antlr.v4.runtime.atn.ATNConfigSet;
import org.antlr.v4.runtime.dfa.DFA;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.document.LineDocument;
import org.netbeans.api.editor.document.LineDocumentUtils;
import org.netbeans.api.lexer.Language;
import org.netbeans.api.lexer.TokenSequence;
import org.netbeans.modules.csl.api.Error;
import org.netbeans.modules.csl.api.Severity;
import org.netbeans.modules.css.lib.api.FilterableError;
import org.netbeans.modules.javascript2.editor.embedding.JsEmbeddingProvider;
import org.netbeans.modules.javascript2.lexer.api.JsTokenId;
import org.netbeans.modules.javascript2.lexer.api.LexUtilities;
import org.netbeans.modules.parsing.api.Snapshot;
import org.openide.filesystems.FileObject;

/**
 *
 * @author Petr Hejl, Petr Pisl
 */
public class JsErrorManager extends ErrorManager implements ANTLRErrorListener {

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

    private static final int MAX_MESSAGE_LENGTH = 100;

    private static final boolean SHOW_BADGES_EMBEDDED = Boolean.getBoolean(JsErrorManager.class.getName() + ".showBadgesEmbedded");;

    private static final Comparator<SimpleError> POSITION_COMPARATOR = new Comparator<SimpleError>() {

        @Override
        public int compare(SimpleError o1, SimpleError o2) {
            if (o1.getStartPosition() < o2.getStartPosition()) {
                return -1;
            }
            if (o1.getStartPosition() > o2.getStartPosition()) {
                return 1;
            }
            return 0;
        }
    };

    // message pattern is for example "index.html:2:16 Exepcted ;"
    private static final Pattern ERROR_MESSAGE_PATTERN = Pattern.compile(".*:\\d+:\\d+ (.*)", Pattern.DOTALL); // NOI18N

    // used to replace pointers from mesage such as
    // Expected ( but found else
    // else
    // ^
    // with this pattern we replace last two lines and related new lines
    private static final Pattern REPLACE_POINTER_PATTERN = Pattern.compile("(\\n)+.*\\n\\s*\\^\\s*"); // NOI18N

    /** Keyword from the error.message which identifies missing char in the JS source. */
    private static final String EXPECTED = "Expected"; //NOI18N

    private final Snapshot snapshot;

    private final Language<JsTokenId> language;

    private List<ParserError> parserErrors;

    private List<JsParserError> convertedErrors;

    private final static Map<String, JsTokenId> JS_TEXT_TOKENS = new HashMap<String, JsTokenId>();

    static {
        for (JsTokenId jsTokenId : JsTokenId.values()) {
            if (jsTokenId.fixedText() != null) {
                JS_TEXT_TOKENS.put(jsTokenId.fixedText(), jsTokenId);
            }
        }
    }

    public JsErrorManager(Snapshot snapshot, Language<JsTokenId> language) {
        this.snapshot = snapshot;
        this.language = language;
    }

    Error getMissingCurlyError() {
        if (parserErrors == null) {
            return null;
        }
        final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
        Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
                ? ParsingErrorFilter.getEnableFilterAction(file)
                : Collections.emptyList();
        FilterableError.SetFilterAction disableFilterAction = file != null
                ? ParsingErrorFilter.getDisableFilterAction(file)
                : null;
        for (ParserError error : parserErrors) {
            if (error.message != null
                    && (error.message.contains("Expected }") || error.message.contains("but found }"))) { // NOI18N
                return new JsParserError(error.toSimpleError(snapshot, language),
                        snapshot != null ? snapshot.getSource().getFileObject() : null,
                        Severity.ERROR, null, false, false, 
                        enableFilterAction, disableFilterAction);
            }
        }
        return null;
    }

    Error getMissingSemicolonError() {
        if (parserErrors == null) {
            return null;
        }
        final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
        Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
                ? ParsingErrorFilter.getEnableFilterAction(file)
                : Collections.emptyList();
        FilterableError.SetFilterAction disableFilterAction = file != null
                ? ParsingErrorFilter.getDisableFilterAction(file)
                : null;
        for (ParserError error : parserErrors) {
            if (error.message != null
                    && error.message.contains("Expected ;")) { // NOI18N
                return new JsParserError(error.toSimpleError(snapshot, language),
                        snapshot != null ? snapshot.getSource().getFileObject() : null,
                        Severity.ERROR, null, false, false,
                        enableFilterAction, disableFilterAction);
            }
        }
        return null;
    }

    public boolean isEmpty() {
        return parserErrors == null;
    }

    @Override
    public void error(ParserException e) {
        addParserError(new NashornParserError(e.getMessage(), e.getLineNumber(), e.getColumnNumber(), e.getToken()));
    }

    @Override
    public void error(String message) {
        LOGGER.log(Level.FINE, "Error {0}", message);
        addParserError(new NashornParserError(message));
    }

    @Override
    public void warning(ParserException e) {
        LOGGER.log(Level.FINE, null, e);
    }

    @Override
    public void warning(String message) {
        LOGGER.log(Level.FINE, "Warning {0}", message);
    }

    public List<? extends FilterableError> getErrors() {
        if (convertedErrors == null) {
            if (parserErrors == null) {
                convertedErrors = Collections.emptyList();
            } else {
                ArrayList<SimpleError> errors = new ArrayList<SimpleError>(parserErrors.size());
                for (ParserError error : parserErrors) {
                    errors.add(error.toSimpleError(snapshot, language));
                }
                Collections.sort(errors, POSITION_COMPARATOR);
                convertedErrors = convert(snapshot, errors);
            }
        }
        return Collections.unmodifiableList(convertedErrors);
    }

    JsErrorManager fillErrors(JsErrorManager original) {
        assert this.snapshot == original.snapshot : this.snapshot + ":" + original.snapshot;
        assert this.language == original.language : this.language + ":" + original.language;

        if (original.parserErrors != null) {
            this.parserErrors = new ArrayList<ParserError>(original.parserErrors);
        } else {
            this.parserErrors = null;
        }
        this.convertedErrors = null;
        return this;
    }

    private void addParserError(ParserError error) {
        convertedErrors = null;
        if (parserErrors == null) {
            parserErrors = new ArrayList<ParserError>();
        }
        parserErrors.add(error);
    }

    private static List<JsParserError> convert(Snapshot snapshot, List<SimpleError> errors) {
        // basically we are solwing showExplorerBadge attribute here
        List<JsParserError> ret = new ArrayList<JsParserError>(errors.size());
        final FileObject file = snapshot != null ? snapshot.getSource().getFileObject() : null;
        Collection<FilterableError.SetFilterAction> enableFilterAction = file != null
                ? ParsingErrorFilter.getEnableFilterAction(file)
                : Collections.emptyList();
        FilterableError.SetFilterAction disableFilterAction = file != null
                ? ParsingErrorFilter.getDisableFilterAction(file)
                : null;
        
        if (snapshot != null && BaseParserResult.isEmbedded(snapshot)) {
            int nextCorrect = -1;
            boolean afterGeneratedIdentifier = false;
            for (SimpleError error : errors) {
                boolean showInEditor = true;
                // if the error is in embedded code we ignore it
                // as we don't know what the other language will add
                int pos = snapshot.getOriginalOffset(error.getStartPosition());
                if (pos >= 0 && nextCorrect <= error.getStartPosition()
                        && !JsEmbeddingProvider.containsGeneratedIdentifier(error.getMessage())) {
                    TokenSequence<? extends JsTokenId> ts = LexUtilities.getJsPositionedSequence(
                            snapshot, error.getStartPosition());
                    if (ts != null && ts.movePrevious()) {
                        // check also a previous token - is it generated ?
                        org.netbeans.api.lexer.Token<? extends JsTokenId> token =
                                LexUtilities.findPreviousNonWsNonComment(ts);
                        if (JsEmbeddingProvider.containsGeneratedIdentifier(token.text().toString())) {
                            // usually we may expect a group of errors
                            // so we disable them until next } .... \n
                            nextCorrect = findNextCorrectOffset(ts, error.getStartPosition());
                            showInEditor = false;
                            afterGeneratedIdentifier = true;
                        } else if (afterGeneratedIdentifier && error.getMessage().indexOf(EXPECTED) != -1) {
                            // errors after generated identifiers can display farther - see issue #229985
                            String expected = getExpected(error.getMessage());
                            if ("eof".equals(expected)) { //NOI18N
                                // unexpected end of script, probably missing at some earlier place : ; } etc.
                                showInEditor = false;
                            } else {
                                JsTokenId expectedToken = getJsTokenFromString(expected);
                                ts.movePrevious();
                                org.netbeans.api.lexer.Token<? extends JsTokenId> previousNonWsToken = LexUtilities.findPreviousNonWsNonComment(ts);
                                if (expectedToken != null && expectedToken == previousNonWsToken.id()) {
                                    // char is available, doesn't show the error
                                    showInEditor = false;
                                }
                            }
                        }
                    }
                } else {
                    showInEditor = false;
                }
                ret.add(new JsParserError(error, file, Severity.ERROR, null, SHOW_BADGES_EMBEDDED, showInEditor, enableFilterAction, disableFilterAction));
            }
        } else {
            for (SimpleError error : errors) {
                ret.add(new JsParserError(error, file, Severity.ERROR, null, true, true, enableFilterAction, disableFilterAction));
            }
        }
        return ret;
    }

    private static String getExpected(String errorMessage) {
        int expectedIndex = errorMessage.indexOf(EXPECTED);
        String afterExpected = errorMessage.substring(expectedIndex + 9);
        int indexOfSpace = afterExpected.indexOf(" "); //NOI18N
        return (indexOfSpace != -1) ? afterExpected.substring(0, indexOfSpace) : afterExpected;
    }

    public static JsTokenId getJsTokenFromString(String name) {
        return JS_TEXT_TOKENS.get(name);
    }

    private static int findNextCorrectOffset(TokenSequence<? extends JsTokenId> ts, int offset) {
        ts.move(offset);
        if (ts.moveNext()) {
            LexUtilities.findNextIncluding(ts, Collections.singletonList(JsTokenId.BRACKET_LEFT_CURLY));
            LexUtilities.findNextIncluding(ts, Collections.singletonList(JsTokenId.EOL));
        }
        return ts.offset();
    }

    //--- Antlr4 ---
    @Override
    public void syntaxError(
            final Recognizer<?, ?> recognizer,
            final Object offendingSymbol,
            final int line,
            final int charPositionInLine,
            final String msg,
            final RecognitionException e) {
        if (recognizer instanceof Parser) {
            //Recognizer can be either Parser or Lexer
            List<String> stack = ((Parser) recognizer).getRuleInvocationStack();
            Collections.reverse(stack);
        }
        addParserError(new AntlrParserError(msg, line, charPositionInLine, offendingSymbol));
    }

    @Override
    public void reportAmbiguity(Parser parser, DFA dfa, int i, int i1, boolean bln, BitSet bitset, ATNConfigSet atncs) {
        //Not important
    }

    @Override
    public void reportAttemptingFullContext(Parser parser, DFA dfa, int i, int i1, BitSet bitset, ATNConfigSet atncs) {
        //Not important
    }

    @Override
    public void reportContextSensitivity(Parser parser, DFA dfa, int i, int i1, int i2, ATNConfigSet atncs) {
        //Not important
    }

    static final class SimpleError {

        private final String message;
        private final boolean lineError;
        private final int startPosition;
        private final int endPosition;

        private SimpleError(
                final String message,
                final boolean lineError,
                final int startPosition,
                final int endPosition) {
            this.message = message;
            this.lineError = lineError;
            this.startPosition = startPosition;
            this.endPosition = endPosition;
        }

        public String getMessage() {
            return message;
        }

        public boolean isLineError() {
            return lineError;
        }

        public int getStartPosition() {
            return startPosition;
        }

        public int getEndPosition() {
            return endPosition;
        }
    }

    private abstract static class ParserError {
        final String message;
        final int line;
        final int column;

        ParserError(String message, int line, int column) {
            if (message.length() > MAX_MESSAGE_LENGTH) {
                int index = message.indexOf('\n', MAX_MESSAGE_LENGTH);
                this.message = message.substring(0, (index < MAX_MESSAGE_LENGTH && index > 0) ? index : MAX_MESSAGE_LENGTH);
                LOGGER.log(Level.FINE, "Too long error message {0}", message);
            } else {
                this.message = message;
            }
            this.line = line;
            this.column = column;
        }
        abstract SimpleError toSimpleError(@NonNull Snapshot snapshot, @NonNull Language<JsTokenId> language);
    }

    private static final class NashornParserError extends ParserError {

        final long token;

        NashornParserError(String message, int line, int column, long token) {
            super(message, line, column);
            this.token = token;
        }

        NashornParserError(String message, long token) {
            this(message, -1, -1, token);
        }

        NashornParserError(String message) {
            this(message, -1, -1, -1);
        }

        @Override
        SimpleError toSimpleError(
                @NonNull final Snapshot snapshot,
                @NonNull final Language<JsTokenId> language) {
            String message = this.message;
            int offset = -1;
            Matcher matcher = ERROR_MESSAGE_PATTERN.matcher(message);
            if (matcher.matches()) {
                message = matcher.group(1);
            }
            message = REPLACE_POINTER_PATTERN.matcher(message).replaceAll(""); // NOI18N

            if (this.token > 0) {
                offset = Token.descPosition(this.token);
                if (Token.descType(this.token) == TokenType.EOF
                        && snapshot.getOriginalOffset(offset) == -1) {

                    int realOffset = -1;
                    TokenSequence<? extends JsTokenId> ts =
                            LexUtilities.getPositionedSequence(snapshot, offset, language);
                    while (ts.movePrevious()) {
                        if (snapshot.getOriginalOffset(ts.offset()) > 0) {
                            realOffset = ts.offset() + ts.token().length() - 1;
                            break;
                        }
                    }

                    if (realOffset > 0) {
                        offset = realOffset;
                    }
                }
            } else if (this.line == -1 && this.column == -1) {
                // is this still used ?
                String parts[] = this.message.split(":");
    //            if (parts.length > 4) {
    //                message = parts[4];
    //                int index = message.indexOf('\n');
    //                if (index > 0) {
    //                    message = message.substring(0, index);
    //                }
    //
    //            }
                if (parts.length > 3) {
                    try {
                        offset = Integer.parseInt(parts[3]);
                    } catch (NumberFormatException nfe) {
                        // do nothing
                    }
                }
            }
            return new SimpleError(message, true, offset, offset+1);
        }
    }

    private static final class AntlrParserError extends ParserError {
        
        final Object token;

        public AntlrParserError(String message, int line, int column, Object token) {
            super(message, line, column);
            this.token = token;
        }

        @Override
        SimpleError toSimpleError(Snapshot snapshot, Language<JsTokenId> language) {
            String message = this.message;
            LineDocument doc = (LineDocument)snapshot.getSource().getDocument(false);
            if (doc == null) {
                LOGGER.log(Level.WARNING, "No document found");
                return new SimpleError(message, false, 0, 0);
            }
            int lineOffset = LineDocumentUtils.getLineStartFromIndex((LineDocument)snapshot.getSource().getDocument(false), this.line - 1);
            int offset = lineOffset + this.column;
            if (offset > -1 && offset < snapshot.getText().length()
                    && snapshot.getOriginalOffset(offset) == -1) {

                int realOffset = -1;
                TokenSequence<? extends JsTokenId> ts
                        = LexUtilities.getPositionedSequence(snapshot, offset, language);
                while (ts.movePrevious()) {
                    if (snapshot.getOriginalOffset(ts.offset()) > 0) {
                        realOffset = ts.offset() + ts.token().length() - 1;
                        break;
                    }
                }

                if (realOffset > 0) {
                    offset = realOffset;
                }
            }
            int endOffset = -1;
            if (offset >= 0) {
                endOffset = offset + 1;
                if (token instanceof org.antlr.v4.runtime.Token) {
                    org.antlr.v4.runtime.Token t = (org.antlr.v4.runtime.Token)token;
                    int len = t.getStopIndex() - t.getStartIndex();
                    if (len > 0) {
                        endOffset = offset + len + 1;
                    }
                }
            }

            return new SimpleError(
                    message,
                    false,
                    offset,
                    endOffset);
        }
    }
}
