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

import freemarker.ext.beans.BeanModel;
import freemarker.ext.beans._BeansAPI;
import freemarker.template.TemplateBooleanModel;
import freemarker.template.TemplateCollectionModel;
import freemarker.template.TemplateDateModel;
import freemarker.template.TemplateException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.TemplateNumberModel;
import freemarker.template.TemplateScalarModel;
import freemarker.template.TemplateSequenceModel;

/**
 * Internally used static utilities for evaluation expressions.
 */
class EvalUtil {
    static final int CMP_OP_EQUALS = 1;
    static final int CMP_OP_NOT_EQUALS = 2;
    static final int CMP_OP_LESS_THAN = 3;
    static final int CMP_OP_GREATER_THAN = 4;
    static final int CMP_OP_LESS_THAN_EQUALS = 5;
    static final int CMP_OP_GREATER_THAN_EQUALS = 6;
    // If you add a new operator here, update the "compare" and "cmpOpToString" methods!
    
    // Prevents instantination.
    private EvalUtil() { }
    
    /**
     * @param expr {@code null} is allowed, but may results in less helpful error messages
     * @param env {@code null} is allowed, but may results in lower performance in classic-compatible mode
     */
    static String modelToString(TemplateScalarModel model, Expression expr, Environment env)
    throws TemplateModelException {
        String value = model.getAsString();
        if (value == null) {
            if (env == null) env = Environment.getCurrentEnvironment();
            if (env != null && env.isClassicCompatible()) {
                return "";
            } else {
                throw newModelHasStoredNullException(String.class, model, expr);
            }
        }
        return value;
    }
    
    /**
     * @param expr {@code null} is allowed, but may results in less helpful error messages
     */
    static Number modelToNumber(TemplateNumberModel model, Expression expr)
        throws TemplateModelException {
        Number value = model.getAsNumber();
        if (value == null) throw newModelHasStoredNullException(Number.class, model, expr);
        return value;
    }

    /**
     * @param expr {@code null} is allowed, but may results in less helpful error messages
     */
    static Date modelToDate(TemplateDateModel model, Expression expr)
        throws TemplateModelException {
        Date value = model.getAsDate();
        if (value == null) throw newModelHasStoredNullException(Date.class, model, expr);
        return value;
    }
    
    /** Signals the buggy case where we have a non-null model, but it wraps a null. */
    static TemplateModelException newModelHasStoredNullException(
            Class expected, TemplateModel model, Expression expr) {
        return new _TemplateModelException(expr,
                _TemplateModelException.modelHasStoredNullDescription(expected, model));
    }

    /**
     * Compares two expressions according the rules of the FTL comparator operators.
     * 
     * @param leftExp not {@code null}
     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
     * @param rightExp not {@code null}
     * @param env {@code null} is tolerated, but should be avoided
     */
    static boolean compare(
            Expression leftExp,
            int operator, String  operatorString,
            Expression rightExp,
            Expression defaultBlamed,
            Environment env) throws TemplateException {
        TemplateModel ltm = leftExp.eval(env);
        TemplateModel rtm = rightExp.eval(env);
        return compare(
                ltm, leftExp,
                operator, operatorString,
                rtm, rightExp,
                defaultBlamed, false,
                false, false, false,
                env);
    }
    
    /**
     * Compares values according the rules of the FTL comparator operators; if the {@link Expression}-s are
     * accessible, use {@link #compare(Expression, int, String, Expression, Expression, Environment)} instead, as
     * that gives better error messages.
     * 
     * @param leftValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}. 
     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
     * @param rightValue maybe {@code null}, which will usually cause the appropriate {@link TemplateException}.
     * @param env {@code null} is tolerated, but should be avoided
     */
    static boolean compare(
            TemplateModel leftValue, int operator, TemplateModel rightValue,
            Environment env) throws TemplateException {
        return compare(
                leftValue, null,
                operator, null,
                rightValue, null,
                null, false,
                false, false, false,
                env);
    }

    /**
     * Same as {@link #compare(TemplateModel, int, TemplateModel, Environment)}, but if the two types are incompatible,
     *     they are treated as non-equal instead of throwing an exception. Comparing dates of different types will
     *     still throw an exception, however.
     */
    static boolean compareLenient(
            TemplateModel leftValue, int operator, TemplateModel rightValue,
            Environment env) throws TemplateException {
        return compare(
                leftValue, null,
                operator, null,
                rightValue, null,
                null, false,
                true, false, false,
                env);
    }
    
    private static final String VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE
            = "value of the comparison is a date-like value where "
              + "it's not known if it's a date (no time part), time, or date-time, "
              + "and thus can't be used in a comparison.";
    
    /**
     * @param leftExp {@code null} is allowed, but may results in less helpful error messages
     * @param operator one of the {@code COMP_OP_...} constants, like {@link #CMP_OP_EQUALS}.
     * @param operatorString can be null {@code null}; the actual operator used, used for more accurate error message.
     * @param rightExp {@code null} is allowed, but may results in less helpful error messages
     * @param defaultBlamed {@code null} allowed; the expression to which the error will point to if something goes
     *        wrong that is not specific to the left or right side expression, or if that expression is {@code null}.
     * @param typeMismatchMeansNotEqual If the two types are incompatible, they are treated as non-equal instead
     *     of throwing an exception. Comparing dates of different types will still throw an exception, however. 
     * @param leftNullReturnsFalse if {@code true}, a {@code null} left value will not cause exception, but make the
     *     expression {@code false}.  
     * @param rightNullReturnsFalse if {@code true}, a {@code null} right value will not cause exception, but make the
     *     expression {@code false}.  
     */
    static boolean compare(
            TemplateModel leftValue, Expression leftExp,
            int operator, String operatorString,
            TemplateModel rightValue, Expression rightExp,
            Expression defaultBlamed, boolean quoteOperandsInErrors,
            boolean typeMismatchMeansNotEqual,
            boolean leftNullReturnsFalse, boolean rightNullReturnsFalse,
            Environment env) throws TemplateException {
        if (leftValue == null) {
            if (env != null && env.isClassicCompatible()) {
                leftValue = TemplateScalarModel.EMPTY_STRING;
            } else {
                if (leftNullReturnsFalse) { 
                    return false;
                } else {
                    if (leftExp != null) {
                        throw InvalidReferenceException.getInstance(leftExp, env);
                    } else {
                        throw new _MiscTemplateException(defaultBlamed, env, 
                                    "The left operand of the comparison was undefined or null.");
                    }
                }
            }
        }

        if (rightValue == null) {
            if (env != null && env.isClassicCompatible()) {
                rightValue = TemplateScalarModel.EMPTY_STRING;
            } else {
                if (rightNullReturnsFalse) { 
                    return false;
                } else {
                    if (rightExp != null) {
                        throw InvalidReferenceException.getInstance(rightExp, env);
                    } else {
                        throw new _MiscTemplateException(defaultBlamed, env,
                                    "The right operand of the comparison was undefined or null.");
                    }
                }
            }
        }

        final int cmpResult;
        if (leftValue instanceof TemplateNumberModel && rightValue instanceof TemplateNumberModel) {
            Number leftNum = EvalUtil.modelToNumber((TemplateNumberModel) leftValue, leftExp);
            Number rightNum = EvalUtil.modelToNumber((TemplateNumberModel) rightValue, rightExp);
            ArithmeticEngine ae =
                    env != null
                        ? env.getArithmeticEngine()
                        : (leftExp != null
                            ? leftExp.getTemplate().getArithmeticEngine()
                            : ArithmeticEngine.BIGDECIMAL_ENGINE);
            try {
                cmpResult = ae.compareNumbers(leftNum, rightNum);
            } catch (RuntimeException e) {
                throw new _MiscTemplateException(defaultBlamed, e, env, new Object[]
                        { "Unexpected error while comparing two numbers: ", e });
            }
        } else if (leftValue instanceof TemplateDateModel && rightValue instanceof TemplateDateModel) {
            TemplateDateModel leftDateModel = (TemplateDateModel) leftValue;
            TemplateDateModel rightDateModel = (TemplateDateModel) rightValue;
            
            int leftDateType = leftDateModel.getDateType();
            int rightDateType = rightDateModel.getDateType();
            
            if (leftDateType == TemplateDateModel.UNKNOWN || rightDateType == TemplateDateModel.UNKNOWN) {
                String sideName;
                Expression sideExp;
                if (leftDateType == TemplateDateModel.UNKNOWN) {
                    sideName = "left";
                    sideExp = leftExp;
                } else {
                    sideName = "right";
                    sideExp = rightExp;
                }
                
                throw new _MiscTemplateException(sideExp != null ? sideExp : defaultBlamed, env,
                        "The ", sideName, " ", VALUE_OF_THE_COMPARISON_IS_UNKNOWN_DATE_LIKE);
            }
            
            if (leftDateType != rightDateType) {
                ;
                throw new _MiscTemplateException(defaultBlamed, env,
                        "Can't compare dates of different types. Left date type is ",
                        TemplateDateModel.TYPE_NAMES.get(leftDateType), ", right date type is ",
                        TemplateDateModel.TYPE_NAMES.get(rightDateType), ".");
            }

            Date leftDate = EvalUtil.modelToDate(leftDateModel, leftExp);
            Date rightDate = EvalUtil.modelToDate(rightDateModel, rightExp);
            cmpResult = leftDate.compareTo(rightDate);
        } else if (leftValue instanceof TemplateScalarModel && rightValue instanceof TemplateScalarModel) {
            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
                throw new _MiscTemplateException(defaultBlamed, env,
                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on string values.");
            }
            String leftString = EvalUtil.modelToString((TemplateScalarModel) leftValue, leftExp, env);
            String rightString = EvalUtil.modelToString((TemplateScalarModel) rightValue, rightExp, env);
            // FIXME NBC: Don't use the Collator here. That's locale-specific, but ==/!= should not be.
            cmpResult = env.getCollator().compare(leftString, rightString);
        } else if (leftValue instanceof TemplateBooleanModel && rightValue instanceof TemplateBooleanModel) {
            if (operator != CMP_OP_EQUALS && operator != CMP_OP_NOT_EQUALS) {
                throw new _MiscTemplateException(defaultBlamed, env,
                        "Can't use operator \"", cmpOpToString(operator, operatorString), "\" on boolean values.");
            }
            boolean leftBool = ((TemplateBooleanModel) leftValue).getAsBoolean();
            boolean rightBool = ((TemplateBooleanModel) rightValue).getAsBoolean();
            cmpResult = (leftBool ? 1 : 0) - (rightBool ? 1 : 0);
        } else if (env.isClassicCompatible()) {
            String leftSting = leftExp.evalAndCoerceToPlainText(env);
            String rightString = rightExp.evalAndCoerceToPlainText(env);
            cmpResult = env.getCollator().compare(leftSting, rightString);
        } else {
            if (typeMismatchMeansNotEqual) {
                if (operator == CMP_OP_EQUALS) {
                    return false;
                } else if (operator == CMP_OP_NOT_EQUALS) {
                    return true;
                }
                // Falls through
            }
            throw new _MiscTemplateException(defaultBlamed, env,
                    "Can't compare values of these types. ",
                    "Allowed comparisons are between two numbers, two strings, two dates, or two booleans.\n",
                    "Left hand operand ",
                    (quoteOperandsInErrors && leftExp != null
                            ? new Object[] { "(", new _DelayedGetCanonicalForm(leftExp), ") value " }
                            : (Object) ""),
                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(leftValue)), ".\n",
                    "Right hand operand ",
                    (quoteOperandsInErrors && rightExp != null
                            ? new Object[] { "(", new _DelayedGetCanonicalForm(rightExp), ") value " }
                            : (Object) ""),
                    "is ", new _DelayedAOrAn(new _DelayedFTLTypeDescription(rightValue)),
                    ".");
        }

        switch (operator) {
            case CMP_OP_EQUALS: return cmpResult == 0;
            case CMP_OP_NOT_EQUALS: return cmpResult != 0;
            case CMP_OP_LESS_THAN: return cmpResult < 0;
            case CMP_OP_GREATER_THAN: return cmpResult > 0;
            case CMP_OP_LESS_THAN_EQUALS: return cmpResult <= 0;
            case CMP_OP_GREATER_THAN_EQUALS: return cmpResult >= 0;
            default: throw new BugException("Unsupported comparator operator code: " + operator);
        }
    }

    private static String cmpOpToString(int operator, String operatorString) {
        if (operatorString != null) {
            return operatorString;
        } else {
            switch (operator) {
                case CMP_OP_EQUALS: return "equals";
                case CMP_OP_NOT_EQUALS: return "not-equals";
                case CMP_OP_LESS_THAN: return "less-than";
                case CMP_OP_GREATER_THAN: return "greater-than";
                case CMP_OP_LESS_THAN_EQUALS: return "less-than-equals";
                case CMP_OP_GREATER_THAN_EQUALS: return "greater-than-equals";
                default: return "???";
            }
        }
    }

    /**
     * Converts a value to plain text {@link String}, or a {@link TemplateMarkupOutputModel} if that's what the
     * {@link TemplateValueFormat} involved produces.
     * 
     * @param seqTip
     *            Tip to display if the value type is not coercable, but it's sequence or collection.
     * 
     * @return Never {@code null}
     * @throws TemplateException 
     */
    static Object coerceModelToStringOrMarkup(TemplateModel tm, Expression exp, String seqTip, Environment env)
            throws TemplateException {
        return coerceModelToStringOrMarkup(tm, exp, false, seqTip, env);
    }
    
    /**
     * @return {@code null} if the {@code returnNullOnNonCoercableType} parameter is {@code true}, and the coercion is
     *         not possible, because of the type is not right for it.
     * 
     * @see #coerceModelToStringOrMarkup(TemplateModel, Expression, String, Environment)
     */
    static Object coerceModelToStringOrMarkup(
            TemplateModel tm, Expression exp, boolean returnNullOnNonCoercableType, String seqTip, Environment env)
            throws TemplateException {
        if (tm instanceof TemplateNumberModel) {
            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
            try {
                return assertFormatResultNotNull(format.format(tnm));
            } catch (TemplateValueFormatException e) {
                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
            }
        } else if (tm instanceof TemplateDateModel) {
            TemplateDateModel tdm = (TemplateDateModel) tm;
            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
            try {
                return assertFormatResultNotNull(format.format(tdm));
            } catch (TemplateValueFormatException e) {
                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
            }
        } else if (tm instanceof TemplateMarkupOutputModel) {
            return tm;
        } else { 
            return coerceModelToTextualCommon(tm, exp, seqTip, true, returnNullOnNonCoercableType, env);
        }
    }

    /**
     * Like {@link #coerceModelToStringOrMarkup(TemplateModel, Expression, String, Environment)}, but gives error
     * if the result is markup. This is what you normally use where markup results can't be used.
     *
     * @param seqTip
     *            Tip to display if the value type is not coercable, but it's sequence or collection.
     * 
     * @return Never {@code null}
     */
    static String coerceModelToStringOrUnsupportedMarkup(
            TemplateModel tm, Expression exp, String seqTip, Environment env)
            throws TemplateException {
        if (tm instanceof TemplateNumberModel) {
            TemplateNumberModel tnm = (TemplateNumberModel) tm; 
            TemplateNumberFormat format = env.getTemplateNumberFormat(exp, false);
            try {
                return ensureFormatResultString(format.format(tnm), exp, env);
            } catch (TemplateValueFormatException e) {
                throw MessageUtil.newCantFormatNumberException(format, exp, e, false);
            }
        } else if (tm instanceof TemplateDateModel) {
            TemplateDateModel tdm = (TemplateDateModel) tm;
            TemplateDateFormat format = env.getTemplateDateFormat(tdm, exp, false);
            try {
                return ensureFormatResultString(format.format(tdm), exp, env);
            } catch (TemplateValueFormatException e) {
                throw MessageUtil.newCantFormatDateException(format, exp, e, false);
            }
        } else { 
            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
        }
    }

    /**
     * Converts a value to plain text {@link String}, even if the {@link TemplateValueFormat} involved normally produces
     * markup. This should be used rarely, where the user clearly intend to use the plain text variant of the format.
     * 
     * @param seqTip
     *            Tip to display if the value type is not coercable, but it's sequence or collection.
     * 
     * @return Never {@code null}
     */
    static String coerceModelToPlainText(TemplateModel tm, Expression exp, String seqTip,
            Environment env) throws TemplateException {
        if (tm instanceof TemplateNumberModel) {
            return assertFormatResultNotNull(env.formatNumberToPlainText((TemplateNumberModel) tm, exp, false));
        } else if (tm instanceof TemplateDateModel) {
            return assertFormatResultNotNull(env.formatDateToPlainText((TemplateDateModel) tm, exp, false));
        } else {
            return coerceModelToTextualCommon(tm, exp, seqTip, false, false, env);
        }
    }

    /**
     * @param tm
     *            If {@code null} that's an exception, unless we are in classic compatible mode.
     * 
     * @param supportsTOM
     *            Whether the caller {@code coerceModelTo...} method could handle a {@link TemplateMarkupOutputModel}.
     *            
     * @return Never {@code null}
     */
    private static String coerceModelToTextualCommon(
            TemplateModel tm, Expression exp, String seqHint, boolean supportsTOM, boolean returnNullOnNonCoercableType,
            Environment env)
            throws TemplateModelException, InvalidReferenceException, TemplateException,
                    NonStringOrTemplateOutputException, NonStringException {
        if (tm instanceof TemplateScalarModel) {
            return modelToString((TemplateScalarModel) tm, exp, env);
        } else if (tm == null) {
            if (env.isClassicCompatible()) {
                return "";
            } else {
                if (exp != null) {
                    throw InvalidReferenceException.getInstance(exp, env);
                } else {
                    throw new InvalidReferenceException(
                            "Null/missing value (no more informatoin avilable)",
                            env);
                }
            }
        } else if (tm instanceof TemplateBooleanModel) {
            // This should be before TemplateScalarModel, but automatic boolean-to-string is only non-error since 2.3.20
            // (and before that when classic_compatible was true), so to keep backward compatibility we couldn't insert
            // this before TemplateScalarModel.
            boolean booleanValue = ((TemplateBooleanModel) tm).getAsBoolean();
            int compatMode = env.getClassicCompatibleAsInt();
            if (compatMode == 0) {
                return env.formatBoolean(booleanValue, false);
            } else {
                if (compatMode == 1) {
                    return booleanValue ? MiscUtil.C_TRUE : "";
                } else if (compatMode == 2) {
                    if (tm instanceof BeanModel) {
                        // In 2.1, bean-wrapped booleans where strings, so that has overridden the boolean behavior: 
                        return _BeansAPI.getAsClassicCompatibleString((BeanModel) tm);
                    } else {
                        return booleanValue ? MiscUtil.C_TRUE : "";
                    }
                } else {
                    throw new BugException("Unsupported classic_compatible variation: " + compatMode);
                }
            }
        } else {
            if (env.isClassicCompatible() && tm instanceof BeanModel) {
                return _BeansAPI.getAsClassicCompatibleString((BeanModel) tm);
            }
            if (returnNullOnNonCoercableType) {
                return null;
            }
            if (seqHint != null && (tm instanceof TemplateSequenceModel || tm instanceof TemplateCollectionModel)) {
                if (supportsTOM) {
                    throw new NonStringOrTemplateOutputException(exp, tm, seqHint, env);
                } else {
                    throw new NonStringException(exp, tm, seqHint, env);
                }
            } else {
                if (supportsTOM) {
                    throw new NonStringOrTemplateOutputException(exp, tm, env);
                } else {
                    throw new NonStringException(exp, tm, env);
                }
            }
        }
    }

    private static String ensureFormatResultString(Object formatResult, Expression exp, Environment env)
            throws NonStringException {
        if (formatResult instanceof String) { 
            return (String) formatResult;
        }
        
        assertFormatResultNotNull(formatResult);
        
        TemplateMarkupOutputModel mo = (TemplateMarkupOutputModel) formatResult;
        _ErrorDescriptionBuilder desc = new _ErrorDescriptionBuilder(
                "Value was formatted to convert it to string, but the result was markup of ouput format ",
                new _DelayedJQuote(mo.getOutputFormat()), ".")
                .tip("Use value?string to force formatting to plain text.")
                .blame(exp);
        throw new NonStringException(null, desc);
    }

    static String assertFormatResultNotNull(String r) {
        if (r != null) {
            return r;
        }
        throw new NullPointerException("TemplateValueFormatter result can't be null");
    }

    static Object assertFormatResultNotNull(Object r) {
        if (r != null) {
            return r;
        }
        throw new NullPointerException("TemplateValueFormatter result can't be null");
    }

    static TemplateMarkupOutputModel concatMarkupOutputs(TemplateObject parent, TemplateMarkupOutputModel leftMO,
            TemplateMarkupOutputModel rightMO) throws TemplateException {
        MarkupOutputFormat leftOF = leftMO.getOutputFormat();
        MarkupOutputFormat rightOF = rightMO.getOutputFormat();
        if (rightOF != leftOF) {
            String rightPT;
            String leftPT;
            if ((rightPT = rightOF.getSourcePlainText(rightMO)) != null) {
                return leftOF.concat(leftMO, leftOF.fromPlainTextByEscaping(rightPT));
            } else if ((leftPT = leftOF.getSourcePlainText(leftMO)) != null) {
                return rightOF.concat(rightOF.fromPlainTextByEscaping(leftPT), rightMO);
            } else {
                Object[] message = { "Concatenation left hand operand is in ", new _DelayedToString(leftOF),
                        " format, while the right hand operand is in ", new _DelayedToString(rightOF),
                        ". Conversion to common format wasn't possible." };
                if (parent instanceof Expression) {
                    throw new _MiscTemplateException((Expression) parent, message);
                } else {
                    throw new _MiscTemplateException(message);
                }
            }
        } else {
            return leftOF.concat(leftMO, rightMO);
        }
    }

    /**
     * Returns an {@link ArithmeticEngine} even if {@code env} is {@code null}, because we are in parsing phase.
     */
    static ArithmeticEngine getArithmeticEngine(Environment env, TemplateObject tObj) {
        return env != null
                ? env.getArithmeticEngine()
                : tObj.getTemplate().getParserConfiguration().getArithmeticEngine();
    }
    
}
