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

import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;

import freemarker.template.Template;
import freemarker.template.TemplateException;
import freemarker.template.utility.SecurityUtilities;
import freemarker.template.utility.StringUtil;

/**
 * Parsing-time exception in a template (as opposed to a runtime exception, a {@link TemplateException}). This usually
 * signals syntactical/lexical errors.
 * 
 * Note that on JavaCC-level lexical errors throw {@link TokenMgrError} instead of this, however with the API-s that
 * most users use those will be wrapped into {@link ParseException}-s. 
 *
 * This is a modified version of file generated by JavaCC from FTL.jj.
 * You can modify this class to customize the error reporting mechanisms so long as the public interface
 * remains compatible with the original.
 * 
 * @see TokenMgrError
 */
public class ParseException extends IOException implements FMParserConstants {

    private static final String END_TAG_SYNTAX_HINT
            = "(Note that FreeMarker end-tags must have # or @ after the / character.)";

    /**
     * This is the last token that has been consumed successfully.  If
     * this object has been created due to a parse error, the token
     * following this token will (therefore) be the first error token.
     */
    public Token currentToken;

    private static volatile Boolean jbossToolsMode;

    private boolean messageAndDescriptionRendered;
    private String message;
    private String description; 

    public int columnNumber, lineNumber;
    public int endColumnNumber, endLineNumber;

    /**
     * Each entry in this array is an array of integers.  Each array
     * of integers represents a sequence of tokens (by their ordinal
     * values) that is expected at this point of the parse.
     */
    public int[][] expectedTokenSequences;

    /**
     * This is a reference to the "tokenImage" array of the generated
     * parser within which the parse error occurred.  This array is
     * defined in the generated ...Constants interface.
     */
    public String[] tokenImage;

    /**
     * The end of line string for this machine.
     */
    protected String eol = SecurityUtilities.getSystemProperty("line.separator", "\n");

    /** @deprecated Will be remove without replacement in 2.4. */
    @Deprecated
    protected boolean specialConstructor;  

    private String templateName;

    /**
     * This constructor is used by the method "generateParseException"
     * in the generated parser.  Calling this constructor generates
     * a new object of this type with the fields "currentToken",
     * "expectedTokenSequences", and "tokenImage" set.
     * This constructor calls its super class with the empty string
     * to force the "toString" method of parent class "Throwable" to
     * print the error message in the form:
     *     ParseException: &lt;result of getMessage&gt;
     */
    public ParseException(Token currentTokenVal,
            int[][] expectedTokenSequencesVal,
            String[] tokenImageVal
            ) {
        super("");
        currentToken = currentTokenVal;
        specialConstructor = true;
        expectedTokenSequences = expectedTokenSequencesVal;
        tokenImage = tokenImageVal;
        lineNumber = currentToken.next.beginLine;
        columnNumber = currentToken.next.beginColumn;
        endLineNumber = currentToken.next.endLine;
        endColumnNumber = currentToken.next.endColumn;
    }

    /**
     * The following constructors are for use by you for whatever
     * purpose you can think of.  Constructing the exception in this
     * manner makes the exception behave in the normal way - i.e., as
     * documented in the class "Throwable".  The fields "errorToken",
     * "expectedTokenSequences", and "tokenImage" do not contain
     * relevant information.  The JavaCC generated code does not use
     * these constructors.
     * 
     * @deprecated Use a constructor to which you pass description, template, and positions.
     */
    @Deprecated
    protected ParseException() {
        super();
    }

    /**
     * @deprecated Use a constructor to which you can also pass the template, and the end positions.
     */
    @Deprecated
    public ParseException(String description, int lineNumber, int columnNumber) {
        this(description, null, lineNumber, columnNumber, null);
    }

    /**
     * @since 2.3.21
     */
    public ParseException(String description, Template template,
            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber) {
        this(description, template, lineNumber, columnNumber, endLineNumber, endColumnNumber, null);      
    }

    /**
     * @since 2.3.21
     */
    public ParseException(String description, Template template,
            int lineNumber, int columnNumber, int endLineNumber, int endColumnNumber,
            Throwable cause) {
        this(description,
                template == null ? null : template.getSourceName(),
                        lineNumber, columnNumber,
                        endLineNumber, endColumnNumber,
                        cause);      
    }
    
    /**
     * @deprecated Use {@link #ParseException(String, Template, int, int, int, int)} instead, as IDE-s need the end
     * position of the error too.
     * @since 2.3.20
     */
    @Deprecated
    public ParseException(String description, Template template, int lineNumber, int columnNumber) {
        this(description, template, lineNumber, columnNumber, null);      
    }

    /**
     * @deprecated Use {@link #ParseException(String, Template, int, int, int, int, Throwable)} instead, as IDE-s need
     * the end position of the error too.
     * @since 2.3.20
     */
    @Deprecated
    public ParseException(String description, Template template, int lineNumber, int columnNumber, Throwable cause) {
        this(description,
                template == null ? null : template.getSourceName(),
                        lineNumber, columnNumber,
                        0, 0,
                        cause);      
    }

    /**
     * @since 2.3.20
     */
    public ParseException(String description, Template template, Token tk) {
        this(description, template, tk, null);
    }

    /**
     * @since 2.3.20
     */
    public ParseException(String description, Template template, Token tk, Throwable cause) {
        this(description,
                template == null ? null : template.getSourceName(),
                        tk.beginLine, tk.beginColumn,
                        tk.endLine, tk.endColumn,
                        cause);
    }

    /**
     * @since 2.3.20
     */
    public ParseException(String description, TemplateObject tobj) {
        this(description, tobj, null);
    }

    /**
     * @since 2.3.20
     */
    public ParseException(String description, TemplateObject tobj, Throwable cause) {
        this(description,
                tobj.getTemplate() == null ? null : tobj.getTemplate().getSourceName(),
                        tobj.beginLine, tobj.beginColumn,
                        tobj.endLine, tobj.endColumn,
                        cause);
    }

    private ParseException(String description, String templateName,
            int lineNumber, int columnNumber,
            int endLineNumber, int endColumnNumber,
            Throwable cause) {
        super(description);  // but we override getMessage, so it will be different
        try {
            this.initCause(cause);
        } catch (Exception e) {
            // Suppressed; we can't do more
        }
        this.description = description; 
        this.templateName = templateName;
        this.lineNumber = lineNumber;
        this.columnNumber = columnNumber;
        this.endLineNumber = endLineNumber;
        this.endColumnNumber = endColumnNumber;
    }

    /**
     * Should be used internally only; sets the name of the template that contains the error.
     * This is needed as the constructor that JavaCC automatically calls doesn't pass in the template, so we
     * set it somewhere later in an exception handler. 
     */
    public void setTemplateName(String templateName) {
        this.templateName = templateName;
        synchronized (this) {
            messageAndDescriptionRendered = false;
            message = null;
        }
    }

    /**
     * Returns the error location plus the error description.
     * 
     * @see #getDescription()
     * @see #getTemplateName()
     * @see #getLineNumber()
     * @see #getColumnNumber()
     */
    @Override
    public String getMessage() {
        synchronized (this) {
            if (messageAndDescriptionRendered) return message;
        }
        renderMessageAndDescription();
        synchronized (this) {
            return message;
        }
    }

    private String getDescription() {
        synchronized (this) {
            if (messageAndDescriptionRendered) return description;
        }
        renderMessageAndDescription();
        synchronized (this) {
            return description;
        }
    }
    
    /**
     * Returns the description of the error without error location or source quotations, or {@code null} if there's no
     * description available. This is useful in editors (IDE-s) where the error markers and the editor window itself
     * already carry this information, so it's redundant the repeat in the error dialog.
     */
    public String getEditorMessage() {
        return getDescription();
    }

    /**
     * Returns the name (template-root relative path) of the template whose parsing was failed.
     * Maybe {@code null} if this is a non-stored template. 
     * 
     * @since 2.3.20
     */
    public String getTemplateName() {
        return templateName;
    }

    /**
     * 1-based line number of the failing section, or 0 is the information is not available.
     */
    public int getLineNumber() {
        return lineNumber;
    }

    /**
     * 1-based column number of the failing section, or 0 is the information is not available.
     */
    public int getColumnNumber() {
        return columnNumber;
    }

    /**
     * 1-based line number of the last line that contains the failing section, or 0 if the information is not available.
     * 
     * @since 2.3.21
     */
    public int getEndLineNumber() {
        return endLineNumber;
    }

    /**
     * 1-based column number of the last character of the failing section, or 0 if the information is not available.
     * Note that unlike with Java string API-s, this column number is inclusive.
     * 
     * @since 2.3.21
     */
    public int getEndColumnNumber() {
        return endColumnNumber;
    }

    private void renderMessageAndDescription() {
        String desc = getOrRenderDescription();

        String prefix;
        if (!isInJBossToolsMode()) {
            prefix = "Syntax error "
                    + _MessageUtil.formatLocationForSimpleParsingError(templateName, lineNumber, columnNumber)
                    + ":\n";  
        } else {
            prefix = "[col. " + columnNumber + "] ";
        }

        String msg = prefix + desc;
        desc = msg.substring(prefix.length());  // so we reuse the backing char[]

        synchronized (this) {
            message = msg;
            description = desc;
            messageAndDescriptionRendered = true;
        }
    }

    private boolean isInJBossToolsMode() {
        if (jbossToolsMode == null) {
            try {
                jbossToolsMode = Boolean.valueOf(
                        ParseException.class.getClassLoader().toString().indexOf(
                                "[org.jboss.ide.eclipse.freemarker:") != -1);
            } catch (Throwable e) {
                jbossToolsMode = Boolean.FALSE;
            }
        }
        return jbossToolsMode.booleanValue();
    }

    /**
     * Returns the description of the error without the error location, or {@code null} if there's no description
     * available.
     */
    private String getOrRenderDescription() {
        synchronized (this) {
            if (description != null) return description;  // When we already have it from the constructor
        }

        if (currentToken == null) {
            return null;
        }

        Token unexpectedTok = currentToken.next;

        if (unexpectedTok.kind == EOF) {
            Set<String> endTokenDescs = getExpectedEndTokenDescs();
            return "Unexpected end of file reached."
                    + (endTokenDescs.size() == 0
                            ? ""
                            : " You have an unclosed " + joinWithAnds(endTokenDescs)
                                    + ". Check if the FreeMarker end-tags are present, and aren't malformed. "
                                    + END_TAG_SYNTAX_HINT);
        }

        int maxExpectedTokenSequenceLength = 0;
        for (int i = 0; i < expectedTokenSequences.length; i++) {
            int[] expectedTokenSequence = expectedTokenSequences[i];
            if (maxExpectedTokenSequenceLength < expectedTokenSequence.length) {
                maxExpectedTokenSequenceLength = expectedTokenSequence.length;
            }
        }

        StringBuilder tokenErrDesc = new StringBuilder();
        tokenErrDesc.append("Encountered ");
        boolean encounteredEndTag = false;
        for (int i = 0; i < maxExpectedTokenSequenceLength; i++) {
            if (i != 0) {
                tokenErrDesc.append(" ");
            }
            if (unexpectedTok.kind == 0) {
                tokenErrDesc.append(tokenImage[0]);
                break;
            }

            String image = unexpectedTok.image;
            if (i == 0) {
                if (image.startsWith("</") || image.startsWith("[/")) {
                    encounteredEndTag = true;
                }
            }
            tokenErrDesc.append(StringUtil.jQuote(image));
            unexpectedTok = unexpectedTok.next;
        }
        Set<String> expectedEndTokenDescs;
        int unexpTokKind = currentToken.next.kind;
        if (getIsEndToken(unexpTokKind) || unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
            expectedEndTokenDescs = new LinkedHashSet<>(getExpectedEndTokenDescs());
            if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
                // If <\#if> was expected, yet #else or #elseif wasn't, then this isn't nesting related problem.
                expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(END_IF));
            } else {
                expectedEndTokenDescs.remove(getEndTokenDescIfIsEndToken(unexpTokKind));
            }
        } else {
            expectedEndTokenDescs = Collections.emptySet();
        }
        // Generate more helpful error message if this was a nesting related problem:
        if (!expectedEndTokenDescs.isEmpty()) {
            if (unexpTokKind == ELSE || unexpTokKind == ELSE_IF) {
                tokenErrDesc.append(", which can only be used where an #if");
                if (unexpTokKind == ELSE) {
                    tokenErrDesc.append(" or #list");
                }
                tokenErrDesc.append(" could be closed");
            }
            tokenErrDesc.append(", but at this place only ");
            tokenErrDesc.append(expectedEndTokenDescs.size() > 1 ? "these" : "this");
            tokenErrDesc.append(" can be closed: ");
            boolean first = true;
            for (String expectedEndTokenDesc : expectedEndTokenDescs) {
                if (!first) {
                    tokenErrDesc.append(", ");
                } else {
                    first = false;
                }
                tokenErrDesc.append(
                        !expectedEndTokenDesc.startsWith("\"")
                                ? StringUtil.jQuote(expectedEndTokenDesc)
                                : expectedEndTokenDesc);
            }
            tokenErrDesc.append(".");
            if (encounteredEndTag) {
                tokenErrDesc.append(" This usually because of wrong nesting of FreeMarker directives, like a "
                        + "missed or malformed end-tag somewhere. " + END_TAG_SYNTAX_HINT);
            }
            tokenErrDesc.append(eol);
            tokenErrDesc.append("Was ");
        } else {
            tokenErrDesc.append(", but was ");
        }

        if (expectedTokenSequences.length == 1) {
            tokenErrDesc.append("expecting pattern:");
        } else {
            tokenErrDesc.append("expecting one of these patterns:");
        }
        tokenErrDesc.append(eol);

        for (int i = 0; i < expectedTokenSequences.length; i++) {
            if (i != 0) {
                tokenErrDesc.append(eol);
            }
            tokenErrDesc.append("    ");
            int[] expectedTokenSequence = expectedTokenSequences[i];
            for (int j = 0; j < expectedTokenSequence.length; j++) {
                if (j != 0) {
                    tokenErrDesc.append(' ');
                }
                tokenErrDesc.append(tokenImage[expectedTokenSequence[j]]);
            }
        }

        return tokenErrDesc.toString();
    }

    /**
     * Returns the descriptions end-tags (or expression closing tokens) that we could have at this point.
     * This is for generating error messages.
     */
    private Set<String> getExpectedEndTokenDescs() {
        Set<String> endTokenDescs = new LinkedHashSet<>();
        for (int i = 0; i < expectedTokenSequences.length; i++) {
            int[] sequence = expectedTokenSequences[i];
            for (int j = 0; j < sequence.length; j++) {
                int token = sequence[j];
                String endTokenDesc = getEndTokenDescIfIsEndToken(token);
                if (endTokenDesc != null) {
                    endTokenDescs.add(endTokenDesc);
                }
            }
        }
        return endTokenDescs;
    }

    private boolean getIsEndToken(int token) {
        return getEndTokenDescIfIsEndToken(token) != null;
    }

    private String getEndTokenDescIfIsEndToken(int token) {
        String endTokenDesc = null;
        switch (token) {
        case END_FOREACH:
            endTokenDesc = "#foreach";
            break;
        case END_LIST:
            endTokenDesc = "#list";
            break;
        case END_SEP:
            endTokenDesc = "#sep";
            break;
        case END_ITEMS:
            endTokenDesc = "#items";
            break;
        case END_SWITCH:
            endTokenDesc = "#switch";
            break;
        case END_IF:
            endTokenDesc = "#if";
            break;
        case END_COMPRESS:
            endTokenDesc = "#compress";
            break;
        case END_MACRO:
        case END_FUNCTION:
            endTokenDesc = "#macro or #function";
            break;
        case END_TRANSFORM:
            endTokenDesc = "#transform";
            break;
        case END_ESCAPE:
            endTokenDesc = "#escape";
            break;
        case END_NOESCAPE:
            endTokenDesc = "#noescape";
            break;
        case END_ASSIGN:
        case END_GLOBAL:
        case END_LOCAL:
            endTokenDesc = "#assign or #local or #global";
            break;
        case END_ATTEMPT:
            endTokenDesc = "#attempt";
            break;
        case CLOSING_CURLY_BRACKET:
            endTokenDesc = "\"{\"";
            break;
        case CLOSE_BRACKET:
            endTokenDesc = "\"[\"";
            break;
        case CLOSE_PAREN:
            endTokenDesc = "\"(\"";
            break;
        case UNIFIED_CALL_END:
            endTokenDesc = "@...";
            break;
        }
        return endTokenDesc;
    }

    private String joinWithAnds(Collection<String> strings) {
        StringBuilder sb = new StringBuilder();
        for (String s : strings) {
            if (sb.length() != 0) {
                sb.append(" and ");
            }
            sb.append(s);
        }
        return sb.toString();
    }

    /**
     * Used to convert raw characters to their escaped version
     * when these raw version cannot be used as part of an ASCII
     * string literal.
     */
    protected String add_escapes(String str) {
        StringBuilder retval = new StringBuilder();
        char ch;
        for (int i = 0; i < str.length(); i++) {
            switch (str.charAt(i))
            {
            case 0 :
                continue;
            case '\b':
                retval.append("\\b");
                continue;
            case '\t':
                retval.append("\\t");
                continue;
            case '\n':
                retval.append("\\n");
                continue;
            case '\f':
                retval.append("\\f");
                continue;
            case '\r':
                retval.append("\\r");
                continue;
            case '\"':
                retval.append("\\\"");
                continue;
            case '\'':
                retval.append("\\\'");
                continue;
            case '\\':
                retval.append("\\\\");
                continue;
            default:
                if ((ch = str.charAt(i)) < 0x20 || ch > 0x7e) {
                    String s = "0000" + Integer.toString(ch, 16);
                    retval.append("\\u" + s.substring(s.length() - 4, s.length()));
                } else {
                    retval.append(ch);
                }
                continue;
            }
        }
        return retval.toString();
    }

}
