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

/**
 * A subclass of {@link TemplateException} that says that an FTL expression has evaluated to {@code null} or it refers
 * to something that doesn't exist. At least in FreeMarker 2.3.x these two cases aren't distinguished.
 */
public class InvalidReferenceException extends TemplateException {

    static final InvalidReferenceException FAST_INSTANCE;
    static {
        Environment prevEnv = Environment.getCurrentEnvironment();
        try {
            Environment.setCurrentEnvironment(null);
            FAST_INSTANCE = new InvalidReferenceException(
                    "Invalid reference. Details are unavilable, as this should have been handled by an FTL construct. "
                    + "If it wasn't, that's problably a bug in FreeMarker.",
                    null);
        } finally {
            Environment.setCurrentEnvironment(prevEnv);
        }
    }
    
    private static final Object[] TIP = {
        "If the failing expression is known to legally refer to something that's sometimes null or missing, "
        + "either specify a default value like myOptionalVar!myDefault, or use ",
        "<#if myOptionalVar??>", "when-present", "<#else>", "when-missing", "</#if>",
        ". (These only cover the last step of the expression; to cover the whole expression, "
        + "use parenthesis: (myOptionalVar.foo)!myDefault, (myOptionalVar.foo)??"
    };

    private static final Object[] TIP_MISSING_ASSIGNMENT_TARGET = {
            "If the target variable is known to be legally null or missing sometimes, instead of something like ",
            "<#assign x += 1>", ", you could write ", "<#if x??>", "<#assign x += 1>", "</#if>",
            " or ", "<#assign x = (x!0) + 1>"
    };
    
    private static final String TIP_NO_DOLLAR =
            "Variable references must not start with \"$\", unless the \"$\" is really part of the variable name.";

    private static final String TIP_LAST_STEP_DOT =
            "It's the step after the last dot that caused this error, not those before it.";

    private static final String TIP_LAST_STEP_SQUARE_BRACKET =
            "It's the final [] step that caused this error, not those before it.";
    
    private static final String TIP_JSP_TAGLIBS =
            "The \"JspTaglibs\" variable isn't a core FreeMarker feature; "
            + "it's only available when templates are invoked through freemarker.ext.servlet.FreemarkerServlet"
            + " (or other custom FreeMarker-JSP integration solution).";
    
    /**
     * Creates and invalid reference exception that contains no information about what was missing or null.
     * As such, try to avoid this constructor.
     */
    public InvalidReferenceException(Environment env) {
        super("Invalid reference: The expression has evaluated to null or refers to something that doesn't exist.",
                env);
    }

    /**
     * Creates and invalid reference exception that contains no programmatically extractable information about the
     * blamed expression. As such, try to avoid this constructor, unless need to raise this expression from outside
     * the FreeMarker core.
     */
    public InvalidReferenceException(String description, Environment env) {
        super(description, env);
    }

    /**
     * This is the recommended constructor, but it's only used internally, and has no backward compatibility guarantees.
     * 
     * @param expression The expression that evaluates to missing or null. The last step of the expression should be
     *     the failing one, like in {@code goodStep.failingStep.furtherStep} it should only contain
     *     {@code goodStep.failingStep}.
     */
    InvalidReferenceException(_ErrorDescriptionBuilder description, Environment env, Expression expression) {
        super(null, env, expression, description);
    }

    /**
     * Use this whenever possible, as it returns {@link #FAST_INSTANCE} instead of creating a new instance, when
     * appropriate.
     */
    static InvalidReferenceException getInstance(Expression blamed, Environment env) {
        if (env != null && env.getFastInvalidReferenceExceptions()) {
            return FAST_INSTANCE;
        } else {
            if (blamed != null) {
                final _ErrorDescriptionBuilder errDescBuilder
                        = new _ErrorDescriptionBuilder("The following has evaluated to null or missing:").blame(blamed);
                if (endsWithDollarVariable(blamed)) {
                    errDescBuilder.tips(TIP_NO_DOLLAR, TIP);
                } else if (blamed instanceof Dot) {
                    final String rho = ((Dot) blamed).getRHO();
                    String nameFixTip = null;
                    if ("size".equals(rho)) {
                        nameFixTip = "To query the size of a collection or map use ?size, like myList?size";
                    } else if ("length".equals(rho)) {
                        nameFixTip = "To query the length of a string use ?length, like myString?size";
                    }
                    errDescBuilder.tips(
                            nameFixTip == null
                                    ? new Object[] { TIP_LAST_STEP_DOT, TIP }
                                    : new Object[] { TIP_LAST_STEP_DOT, nameFixTip, TIP });
                } else if (blamed instanceof DynamicKeyName) {
                    errDescBuilder.tips(TIP_LAST_STEP_SQUARE_BRACKET, TIP);
                } else if (blamed instanceof Identifier
                        && ((Identifier) blamed).getName().equals("JspTaglibs")) {
                    errDescBuilder.tips(TIP_JSP_TAGLIBS, TIP);
                } else {
                    errDescBuilder.tip(TIP);
                }
                return new InvalidReferenceException(errDescBuilder, env, blamed);
            } else {
                return new InvalidReferenceException(env);
            }
        }
    }
    
    /**
     * Used for assignments that use operators like {@code +=}, when the target variable was null/missing. 
     */
    static InvalidReferenceException getInstance(int scope, String missingAssignedVarName, String assignmentOperator,
            Environment env) {
        if (env != null && env.getFastInvalidReferenceExceptions()) {
            return FAST_INSTANCE;
        } else {
            final _ErrorDescriptionBuilder errDescBuilder = new _ErrorDescriptionBuilder(
                            "The target variable of the assignment, ",
                            new _DelayedJQuote(missingAssignedVarName),
                            ", was null or missing in the " + Assignment.scopeAsString(scope) + ", and the \"",
                            assignmentOperator, "\" operator must get its value from there before assigning to it."
                    );
            if (missingAssignedVarName.startsWith("$")) {
                errDescBuilder.tips(TIP_NO_DOLLAR, TIP_MISSING_ASSIGNMENT_TARGET);
            } else {
                errDescBuilder.tip(TIP_MISSING_ASSIGNMENT_TARGET);
            }
            return new InvalidReferenceException(errDescBuilder, env, null);
        }
    }

    private static boolean endsWithDollarVariable(Expression blame) {
        return blame instanceof Identifier && ((Identifier) blame).getName().startsWith("$")
                || blame instanceof Dot && ((Dot) blame).getRHO().startsWith("$");
    }
    
}
