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

import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import freemarker.core.BugException;
import freemarker.template.TemplateModel;
import freemarker.template.TemplateModelException;
import freemarker.template.Version;
import freemarker.template.utility.ClassUtil;

/**
 * The argument types of a method call; usable as cache key.
 */
final class ArgumentTypes {
    
    /**
     * Conversion difficulty: Lowest; Java Reflection will do it automatically.
     */
    private static final int CONVERSION_DIFFICULTY_REFLECTION = 0;

    /**
     * Conversion difficulty: Java reflection API will won't convert it, FreeMarker has to do it.
     */
    private static final int CONVERSION_DIFFICULTY_FREEMARKER = 1;
    
    /**
     * Conversion difficulty: Highest; conversion is not possible.
     */
    private static final int CONVERSION_DIFFICULTY_IMPOSSIBLE = 2;

    /**
     * The types of the arguments; for varags this contains the exploded list (not the array). 
     */
    private final Class<?>[] types;
    
    private final boolean bugfixed;
    
    /**
     * @param args The actual arguments. A varargs argument should be present exploded, no as an array.
     * @param bugfixed Introduced in 2.3.21, sets this object to a mode that works well with {@link BeansWrapper}-s
     *      created with {@link Version} 2.3.21 or higher.
     */
    ArgumentTypes(Object[] args, boolean bugfixed) {
        int ln = args.length;
        Class<?>[] typesTmp = new Class[ln];
        for (int i = 0; i < ln; ++i) {
            Object arg = args[i];
            typesTmp[i] = arg == null
                    ? (bugfixed ? Null.class : Object.class)
                    : arg.getClass();
        }
        
        // `typesTmp` is used so the array is only modified before it's stored in the final `types` field (see JSR-133)
        types = typesTmp;  
        this.bugfixed = bugfixed;
    }
    
    @Override
    public int hashCode() {
        int hash = 0;
        for (int i = 0; i < types.length; ++i) {
            hash ^= types[i].hashCode();
        }
        return hash;
    }
    
    @Override
    public boolean equals(Object o) {
        if (o instanceof ArgumentTypes) {
            ArgumentTypes cs = (ArgumentTypes) o;
            if (cs.types.length != types.length) {
                return false;
            }
            for (int i = 0; i < types.length; ++i) {
                if (cs.types[i] != types[i]) {
                    return false;
                }
            }
            return true;
        }
        return false;
    }
    
    /**
     * @return Possibly {@link EmptyCallableMemberDescriptor#NO_SUCH_METHOD} or
     *         {@link EmptyCallableMemberDescriptor#AMBIGUOUS_METHOD}. 
     */
    MaybeEmptyCallableMemberDescriptor getMostSpecific(
            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
        LinkedList<CallableMemberDescriptor> applicables = getApplicables(memberDescs, varArg);
        if (applicables.isEmpty()) {
            return EmptyCallableMemberDescriptor.NO_SUCH_METHOD;
        }
        if (applicables.size() == 1) {
            return applicables.getFirst();
        }
        
        LinkedList<CallableMemberDescriptor> maximals = new LinkedList<>();
        for (CallableMemberDescriptor applicable : applicables) {
            boolean lessSpecific = false;
            for (Iterator<CallableMemberDescriptor> maximalsIter = maximals.iterator(); 
                maximalsIter.hasNext(); ) {
                CallableMemberDescriptor maximal = maximalsIter.next();
                final int cmpRes = compareParameterListPreferability(
                        applicable.getParamTypes(), maximal.getParamTypes(), varArg); 
                if (cmpRes > 0) {
                    maximalsIter.remove();
                } else if (cmpRes < 0) {
                    lessSpecific = true;
                }
            }
            if (!lessSpecific) {
                maximals.addLast(applicable);
            }
        }
        if (maximals.size() > 1) {
            return EmptyCallableMemberDescriptor.AMBIGUOUS_METHOD;
        }
        return maximals.getFirst();
    }

    /**
     * Tells if among the parameter list of two methods, which one fits this argument list better.
     * This method assumes that the parameter lists are applicable to this argument lists; if that's not ensured,
     * what the result will be is undefined.
     * 
     * <p>This method behaves differently in {@code bugfixed}-mode (used when a {@link BeansWrapper} is created with
     * incompatible improvements set to 2.3.21 or higher). Below we describe the bugfixed behavior only. 
     *  
     * <p>The decision is made by comparing the preferability of each parameter types of the same position in a loop.
     * At the end, the parameter list with the more preferred parameters will be the preferred one. If both parameter
     * lists has the same amount of preferred parameters, the one that has the first (lower index) preferred parameter
     * is the preferred one. Otherwise the two parameter list are considered to be equal in terms of preferability.
     * 
     * <p>If there's no numerical conversion involved, the preferability of two parameter types is decided on how
     * specific their types are. For example, {@code String} is more specific than {@link Object} (because
     * {@code Object.class.isAssignableFrom(String.class)}-s), and so {@code String} is preferred. Primitive
     * types are considered to be more specific than the corresponding boxing class (like {@code boolean} is more
     * specific than {@code Boolean}, because the former can't store {@code null}). The preferability decision gets
     * trickier when there's a possibility of numerical conversion from the actual argument type to the type of some of
     * the parameters. If such conversion is only possible for one of the competing parameter types, that parameter
     * automatically wins. If it's possible for both, {@link OverloadedNumberUtil#getArgumentConversionPrice} will
     * be used to calculate the conversion "price", and the parameter type with lowest price wins. There are also
     * a twist with array-to-list and list-to-array conversions; we try to avoid those, so the parameter where such
     * conversion isn't needed will always win.
     * 
     * @param paramTypes1 The parameter types of one of the competing methods
     * @param paramTypes2 The parameter types of the other competing method
     * @param varArg Whether these competing methods are varargs methods. 
     * @return More than 0 if the first parameter list is preferred, less then 0 if the other is preferred,
     *        0 if there's no decision 
     */
    int compareParameterListPreferability(Class<?>[] paramTypes1, Class<?>[] paramTypes2, boolean varArg) {
        final int argTypesLen = types.length; 
        final int paramTypes1Len = paramTypes1.length;
        final int paramTypes2Len = paramTypes2.length;
        //assert varArg || paramTypes1Len == paramTypes2Len;
        
        if (bugfixed) {
            int paramList1WeakWinCnt = 0;
            int paramList2WeakWinCnt = 0;
            int paramList1WinCnt = 0;
            int paramList2WinCnt = 0;
            int paramList1StrongWinCnt = 0;
            int paramList2StrongWinCnt = 0;
            int paramList1VeryStrongWinCnt = 0;
            int paramList2VeryStrongWinCnt = 0;
            int firstWinerParamList = 0;
            for (int i = 0; i < argTypesLen; i++) {
                final Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
                final Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
                
                final int winerParam;  // 1 => paramType1; -1 => paramType2; 0 => draw
                if (paramType1 == paramType2) {
                    winerParam = 0;
                } else {
                    final Class<?> argType = types[i];
                    final boolean argIsNum = Number.class.isAssignableFrom(argType);
                    
                    final int numConvPrice1;
                    if (argIsNum && ClassUtil.isNumerical(paramType1)) {
                        final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
                                ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; 
                        numConvPrice1 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType1);
                    } else {
                        numConvPrice1 = Integer.MAX_VALUE;
                    }
                    // numConvPrice1 is Integer.MAX_VALUE if either:
                    // - argType and paramType1 aren't both numerical
                    // - FM doesn't know some of the numerical types, or the conversion between them is not allowed    
                    
                    final int numConvPrice2;
                    if (argIsNum && ClassUtil.isNumerical(paramType2)) {
                        final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
                                ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2; 
                        numConvPrice2 = OverloadedNumberUtil.getArgumentConversionPrice(argType, nonPrimParamType2);
                    } else {
                        numConvPrice2 = Integer.MAX_VALUE;
                    }
                    
                    if (numConvPrice1 == Integer.MAX_VALUE) {
                        if (numConvPrice2 == Integer.MAX_VALUE) {  // No numerical conversions anywhere
                            // List to array conversions (unwrapping sometimes makes a List instead of an array)
                            if (List.class.isAssignableFrom(argType)
                                    && (paramType1.isArray() || paramType2.isArray())) {
                                if (paramType1.isArray()) {
                                    if (paramType2.isArray()) {  // both paramType1 and paramType2 are arrays
                                        int r = compareParameterListPreferability_cmpTypeSpecificty(
                                                paramType1.getComponentType(), paramType2.getComponentType());
                                        // Because we don't know if the List items are instances of the component
                                        // type or not, we prefer the safer choice, which is the more generic array:
                                        if (r > 0) {
                                            winerParam = 2;
                                            paramList2StrongWinCnt++;
                                        } else if (r < 0) {
                                            winerParam = 1;
                                            paramList1StrongWinCnt++;
                                        } else {
                                            winerParam = 0;
                                        }
                                    } else {  // paramType1 is array, paramType2 isn't
                                        // Avoid List to array conversion if the other way makes any sense:
                                        if (Collection.class.isAssignableFrom(paramType2)) {
                                            winerParam = 2;
                                            paramList2StrongWinCnt++;
                                        } else {
                                            winerParam = 1;
                                            paramList1WeakWinCnt++;
                                        }
                                    }
                                } else {  // paramType2 is array, paramType1 isn't
                                    // Avoid List to array conversion if the other way makes any sense:
                                    if (Collection.class.isAssignableFrom(paramType1)) {
                                        winerParam = 1;
                                        paramList1StrongWinCnt++;
                                    } else {
                                        winerParam = 2;
                                        paramList2WeakWinCnt++;
                                    }
                                }
                            } else if (argType.isArray()
                                    && (List.class.isAssignableFrom(paramType1)
                                            || List.class.isAssignableFrom(paramType2))) {
                                // Array to List conversions (unwrapping sometimes makes an array instead of a List)
                                if (List.class.isAssignableFrom(paramType1)) {
                                    if (List.class.isAssignableFrom(paramType2)) {
                                        // Both paramType1 and paramType2 extends List
                                        winerParam = 0;
                                    } else {
                                        // Only paramType1 extends List
                                        winerParam = 2;
                                        paramList2VeryStrongWinCnt++;
                                    }
                                } else {
                                    // Only paramType2 extends List
                                    winerParam = 1;
                                    paramList1VeryStrongWinCnt++;
                                }
                            } else {  // No list to/from array conversion
                                final int r = compareParameterListPreferability_cmpTypeSpecificty(
                                        paramType1, paramType2);
                                if (r > 0) {
                                    winerParam = 1;
                                    if (r > 1) {
                                        paramList1WinCnt++;
                                    } else {
                                        paramList1WeakWinCnt++;
                                    }
                                } else if (r < 0) {
                                    winerParam = -1;
                                    if (r < -1) {
                                        paramList2WinCnt++;
                                    } else {
                                        paramList2WeakWinCnt++;
                                    }
                                } else {
                                    winerParam = 0;
                                }
                            }
                        } else {  // No num. conv. of param1, num. conv. of param2
                            winerParam = -1;
                            paramList2WinCnt++;
                        }
                    } else if (numConvPrice2 == Integer.MAX_VALUE) {  // Num. conv. of param1, not of param2
                        winerParam = 1;
                        paramList1WinCnt++;
                    } else {  // Num. conv. of both param1 and param2
                        if (numConvPrice1 != numConvPrice2) {
                            if (numConvPrice1 < numConvPrice2) {
                                winerParam = 1;
                                if (numConvPrice1 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
                                        && numConvPrice2 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
                                    paramList1StrongWinCnt++;
                                } else {
                                    paramList1WinCnt++;
                                }
                            } else {
                                winerParam = -1;
                                if (numConvPrice2 < OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE
                                        && numConvPrice1 > OverloadedNumberUtil.BIG_MANTISSA_LOSS_PRICE) {
                                    paramList2StrongWinCnt++;
                                } else {
                                    paramList2WinCnt++;
                                }
                            }
                        } else {
                            winerParam = (paramType1.isPrimitive() ? 1 : 0) - (paramType2.isPrimitive() ? 1 : 0);
                            if (winerParam == 1) paramList1WeakWinCnt++;
                            else if (winerParam == -1) paramList2WeakWinCnt++;
                        }
                    }
                }  // when paramType1 != paramType2
                
                if (firstWinerParamList == 0 && winerParam != 0) {
                    firstWinerParamList = winerParam; 
                }
            }  // for each parameter types
            
            if (paramList1VeryStrongWinCnt != paramList2VeryStrongWinCnt) {
                return paramList1VeryStrongWinCnt - paramList2VeryStrongWinCnt;
            } else if (paramList1StrongWinCnt != paramList2StrongWinCnt) {
                return paramList1StrongWinCnt - paramList2StrongWinCnt;
            } else if (paramList1WinCnt != paramList2WinCnt) {
                return paramList1WinCnt - paramList2WinCnt;
            } else if (paramList1WeakWinCnt != paramList2WeakWinCnt) {
                return paramList1WeakWinCnt - paramList2WeakWinCnt;
            } else if (firstWinerParamList != 0) {  // paramList1WinCnt == paramList2WinCnt
                return firstWinerParamList;
            } else { // still undecided
                if (varArg) {
                    if (paramTypes1Len == paramTypes2Len) {
                        // If we had a 0-length varargs array in both methods, we also compare the types at the
                        // index of the varargs parameter, like if we had a single varargs argument. However, this
                        // time we don't have an argument type, so we can only decide based on type specificity:
                        if (argTypesLen == paramTypes1Len - 1) {
                            Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, argTypesLen, true);
                            Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, argTypesLen, true);
                            if (ClassUtil.isNumerical(paramType1) && ClassUtil.isNumerical(paramType2)) {
                                int r = OverloadedNumberUtil.compareNumberTypeSpecificity(paramType1, paramType2);
                                if (r != 0) return r;
                                // falls through
                            }
                            return compareParameterListPreferability_cmpTypeSpecificty(paramType1, paramType2);
                        } else {
                            return 0;
                        }
                    } else {
                        // The method with more fixed parameters wins:
                        return paramTypes1Len - paramTypes2Len;
                    }
                } else {
                    return 0;
                }
            }
        } else { // non-bugfixed (backward-compatible) mode
            boolean paramTypes1HasAMoreSpecific = false;
            boolean paramTypes2HasAMoreSpecific = false;
            for (int i = 0; i < paramTypes1Len; ++i) {
                Class<?> paramType1 = getParamType(paramTypes1, paramTypes1Len, i, varArg);
                Class<?> paramType2 = getParamType(paramTypes2, paramTypes2Len, i, varArg);
                if (paramType1 != paramType2) {
                    paramTypes1HasAMoreSpecific = 
                        paramTypes1HasAMoreSpecific
                        || _MethodUtil.isMoreOrSameSpecificParameterType(paramType1, paramType2, false, 0) != 0;
                    paramTypes2HasAMoreSpecific = 
                        paramTypes2HasAMoreSpecific
                        || _MethodUtil.isMoreOrSameSpecificParameterType(paramType2, paramType1, false, 0) != 0;
                }
            }
            
            if (paramTypes1HasAMoreSpecific) {
                return paramTypes2HasAMoreSpecific ? 0 : 1;
            } else if (paramTypes2HasAMoreSpecific) {
                return -1;
            } else {
                return 0;
            }
        }
    }
    
    /**
     * Trivial comparison of type specificities; unaware of numerical conversions. 
     * 
     * @return Less-than-0, 0, or more-than-0 depending on which side is more specific. The absolute value is 1 if
     *     the difference is only in primitive VS non-primitive, more otherwise.
     */
    private int compareParameterListPreferability_cmpTypeSpecificty(
            final Class<?> paramType1, final Class<?> paramType2) {
        // The more specific (smaller) type wins.
        
        final Class<?> nonPrimParamType1 = paramType1.isPrimitive()
                ? ClassUtil.primitiveClassToBoxingClass(paramType1) : paramType1; 
        final Class<?> nonPrimParamType2 = paramType2.isPrimitive()
                ? ClassUtil.primitiveClassToBoxingClass(paramType2) : paramType2;
                
        if (nonPrimParamType1 == nonPrimParamType2) {
            if (nonPrimParamType1 != paramType1) {
                if (nonPrimParamType2 != paramType2) {
                    return 0;  // identical prim. types; shouldn't ever be reached
                } else {
                    return 1;  // param1 is prim., param2 is non prim.
                }
            } else if (nonPrimParamType2 != paramType2) {
                return -1;  // param1 is non-prim., param2 is prim.
            } else {
                return 0;  // identical non-prim. types
            }
        } else if (nonPrimParamType2.isAssignableFrom(nonPrimParamType1)) {
            return 2;
        } else if (nonPrimParamType1.isAssignableFrom(nonPrimParamType2)) {
            return -2;
        } if (nonPrimParamType1 == Character.class && nonPrimParamType2.isAssignableFrom(String.class)) {
            return 2;  // A character is a 1 long string in FTL, so we pretend that it's a String subtype.
        } if (nonPrimParamType2 == Character.class && nonPrimParamType1.isAssignableFrom(String.class)) {
            return -2;
        } else {
            return 0;  // unrelated types
        }
    }

    private static Class<?> getParamType(Class<?>[] paramTypes, int paramTypesLen, int i, boolean varArg) {
        return varArg && i >= paramTypesLen - 1
                ? paramTypes[paramTypesLen - 1].getComponentType()
                : paramTypes[i];
    }
    
    /**
     * Returns all methods that are applicable to actual
     * parameter types represented by this ArgumentTypes object.
     */
    LinkedList<CallableMemberDescriptor> getApplicables(
            List<ReflectionCallableMemberDescriptor> memberDescs, boolean varArg) {
        LinkedList<CallableMemberDescriptor> applicables = new LinkedList<>();
        for (ReflectionCallableMemberDescriptor memberDesc : memberDescs) {
            int difficulty = isApplicable(memberDesc, varArg);
            if (difficulty != CONVERSION_DIFFICULTY_IMPOSSIBLE) {
                if (difficulty == CONVERSION_DIFFICULTY_REFLECTION) {
                    applicables.add(memberDesc);
                } else if (difficulty == CONVERSION_DIFFICULTY_FREEMARKER) {
                    applicables.add(new SpecialConversionCallableMemberDescriptor(memberDesc));
                } else {
                    throw new BugException();
                }
            }
        }
        return applicables;
    }
    
    /**
     * Returns if the supplied method is applicable to actual
     * parameter types represented by this ArgumentTypes object, also tells
     * how difficult that conversion is.
     * 
     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
     */
    private int isApplicable(ReflectionCallableMemberDescriptor memberDesc, boolean varArg) {
        final Class<?>[] paramTypes = memberDesc.getParamTypes(); 
        final int cl = types.length;
        final int fl = paramTypes.length - (varArg ? 1 : 0);
        if (varArg) {
            if (cl < fl) {
                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
            }
        } else {
            if (cl != fl) {
                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
            }
        }
        
        int maxDifficulty = 0;
        for (int i = 0; i < fl; ++i) {
            int difficulty = isMethodInvocationConvertible(paramTypes[i], types[i]);
            if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
            }
            if (maxDifficulty < difficulty) {
                maxDifficulty = difficulty;
            }
        }
        if (varArg) {
            Class<?> varArgParamType = paramTypes[fl].getComponentType();
            for (int i = fl; i < cl; ++i) {
                int difficulty = isMethodInvocationConvertible(varArgParamType, types[i]); 
                if (difficulty == CONVERSION_DIFFICULTY_IMPOSSIBLE) {
                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
                }
                if (maxDifficulty < difficulty) {
                    maxDifficulty = difficulty;
                }
            }
        }
        return maxDifficulty;
    }

    /**
     * Determines whether a type is convertible to another type via 
     * method invocation conversion, and if so, what kind of conversion is needed.
     * It treates the object type counterpart of primitive types as if they were the primitive types
     * (that is, a Boolean actual parameter type matches boolean primitive formal type). This behavior
     * is because this method is used to determine applicable methods for 
     * an actual parameter list, and primitive types are represented by 
     * their object duals in reflective method calls.
     * @param formal the parameter type to which the actual 
     * parameter type should be convertible; possibly a primitive type
     * @param actual the argument type; not a primitive type, maybe {@link Null}.
     * 
     * @return One of the <tt>CONVERSION_DIFFICULTY_...</tt> constants.
     */
    private int isMethodInvocationConvertible(final Class<?> formal, final Class<?> actual) {
        // Check for identity or widening reference conversion
        if (formal.isAssignableFrom(actual) && actual != CharacterOrString.class) {
            return CONVERSION_DIFFICULTY_REFLECTION;
        } else if (bugfixed) {
            final Class<?> formalNP;
            if (formal.isPrimitive()) {
                if (actual == Null.class) {
                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
                }
                
                formalNP = ClassUtil.primitiveClassToBoxingClass(formal);
                if (actual == formalNP) {
                    // Character and char, etc.
                    return CONVERSION_DIFFICULTY_REFLECTION;
                }
            } else {  // formal is non-primitive
                if (actual == Null.class) {
                    return CONVERSION_DIFFICULTY_REFLECTION;
                }
                
                formalNP = formal;
            }
            if (Number.class.isAssignableFrom(actual) && Number.class.isAssignableFrom(formalNP)) {
                return OverloadedNumberUtil.getArgumentConversionPrice(actual, formalNP) == Integer.MAX_VALUE
                        ? CONVERSION_DIFFICULTY_IMPOSSIBLE : CONVERSION_DIFFICULTY_REFLECTION;
            } else if (formal.isArray()) {
                // BeansWrapper method/constructor calls convert from List to array automatically
                return List.class.isAssignableFrom(actual)
                        ? CONVERSION_DIFFICULTY_FREEMARKER : CONVERSION_DIFFICULTY_IMPOSSIBLE;
            } else if (actual.isArray() && formal.isAssignableFrom(List.class)) {
                // BeansWrapper method/constructor calls convert from array to List automatically
                return CONVERSION_DIFFICULTY_FREEMARKER;
            } else if (actual == CharacterOrString.class
                    && (formal.isAssignableFrom(String.class)
                            || formal.isAssignableFrom(Character.class) || formal == char.class)) {
                return CONVERSION_DIFFICULTY_FREEMARKER;
            } else {
                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
            }
        } else { // if !bugfixed
            // This non-bugfixed (backward-compatible, pre-2.3.21) branch:
            // - Doesn't convert *to* non-primitive numerical types (unless the argument is a BigDecimal).
            //   (This is like in Java language, which also doesn't coerce to non-primitive numerical types.) 
            // - Doesn't support BigInteger conversions
            // - Doesn't support NumberWithFallbackType-s and CharacterOrString-s. Those are only produced in bugfixed
            //   mode anyway.
            // - Doesn't support conversion between array and List
            if (formal.isPrimitive()) {
                // Check for boxing with widening primitive conversion. Note that 
                // actual parameters are never primitives.
                // It doesn't do the same with boxing types... that was a bug.
                if (formal == Boolean.TYPE) {
                    return actual == Boolean.class
                            ? CONVERSION_DIFFICULTY_REFLECTION : CONVERSION_DIFFICULTY_IMPOSSIBLE;
                } else if (formal == Double.TYPE && 
                        (actual == Double.class || actual == Float.class || 
                         actual == Long.class || actual == Integer.class || 
                         actual == Short.class || actual == Byte.class)) {
                     return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (formal == Integer.TYPE && 
                        (actual == Integer.class || actual == Short.class || 
                         actual == Byte.class)) {
                     return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (formal == Long.TYPE && 
                        (actual == Long.class || actual == Integer.class || 
                         actual == Short.class || actual == Byte.class)) {
                     return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (formal == Float.TYPE && 
                        (actual == Float.class || actual == Long.class || 
                         actual == Integer.class || actual == Short.class || 
                         actual == Byte.class)) {
                     return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (formal == Character.TYPE) {
                    return actual == Character.class
                            ? CONVERSION_DIFFICULTY_REFLECTION : CONVERSION_DIFFICULTY_IMPOSSIBLE;
                } else if (formal == Byte.TYPE && actual == Byte.class) {
                    return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (formal == Short.TYPE &&
                   (actual == Short.class || actual == Byte.class)) {
                    return CONVERSION_DIFFICULTY_REFLECTION;
                } else if (BigDecimal.class.isAssignableFrom(actual) && ClassUtil.isNumerical(formal)) {
                    // Special case for BigDecimals as we deem BigDecimal to be
                    // convertible to any numeric type - either object or primitive.
                    // This can actually cause us trouble as this is a narrowing 
                    // conversion, not widening. 
                    return CONVERSION_DIFFICULTY_REFLECTION;
                } else {
                    return CONVERSION_DIFFICULTY_IMPOSSIBLE;
                }
            } else {
                return CONVERSION_DIFFICULTY_IMPOSSIBLE;
            }
        }
    }
    
    /**
     * Symbolizes the class of null (it's missing from Java).
     */
    private static class Null {
        
        // Can't be instantiated
        private Null() { }
        
    }
    
    /**
     * Used instead of {@link ReflectionCallableMemberDescriptor} when the method is only applicable
     * ({@link #isApplicable}) with conversion that Java reflection won't do. It delegates to a
     * {@link ReflectionCallableMemberDescriptor}, but it adds the necessary conversions to the invocation methods. 
     */
    private static final class SpecialConversionCallableMemberDescriptor extends CallableMemberDescriptor {
        
        private final ReflectionCallableMemberDescriptor callableMemberDesc;

        SpecialConversionCallableMemberDescriptor(ReflectionCallableMemberDescriptor callableMemberDesc) {
            this.callableMemberDesc = callableMemberDesc;
        }

        @Override
        TemplateModel invokeMethod(BeansWrapper bw, Object obj, Object[] args) throws TemplateModelException,
                InvocationTargetException, IllegalAccessException {
            convertArgsToReflectionCompatible(bw, args);
            return callableMemberDesc.invokeMethod(bw, obj, args);
        }

        @Override
        Object invokeConstructor(BeansWrapper bw, Object[] args) throws IllegalArgumentException,
                InstantiationException, IllegalAccessException, InvocationTargetException, TemplateModelException {
            convertArgsToReflectionCompatible(bw, args);
            return callableMemberDesc.invokeConstructor(bw, args);
        }

        @Override
        String getDeclaration() {
            return callableMemberDesc.getDeclaration();
        }

        @Override
        boolean isConstructor() {
            return callableMemberDesc.isConstructor();
        }

        @Override
        boolean isStatic() {
            return callableMemberDesc.isStatic();
        }

        @Override
        boolean isVarargs() {
            return callableMemberDesc.isVarargs();
        }

        @Override
        Class<?>[] getParamTypes() {
            return callableMemberDesc.getParamTypes();
        }
        
        @Override
        String getName() {
            return callableMemberDesc.getName();
        }

        private void convertArgsToReflectionCompatible(BeansWrapper bw, Object[] args) throws TemplateModelException {
            Class<?>[] paramTypes = callableMemberDesc.getParamTypes();
            int ln = paramTypes.length;
            for (int i = 0; i < ln; i++) {
                Class<?> paramType = paramTypes[i];
                final Object arg = args[i];
                if (arg == null) continue;
                
                // Handle conversion between List and array types, in both directions. Java reflection won't do such
                // conversion, so we have to.
                // Most reflection-incompatible conversions were already addressed by the unwrapping. The reason
                // this one isn't is that for overloaded methods the hint of a given parameter position is often vague,
                // so we may end up with a List even if some parameter types at that position are arrays (remember, we
                // have to chose one unwrapping target type, despite that we have many possible overloaded methods), or
                // the other way around (that happens when AdapterTemplateMoldel returns an array).
                // Later, the overloaded method selection will assume that a List argument is applicable to an array
                // parameter, and that an array argument is applicable to a List parameter, so we end up with this
                // situation.
                if (paramType.isArray() && arg instanceof List) {
                   args[i] = bw.listToArray((List<?>) arg, paramType, null);
                }
                if (arg.getClass().isArray() && paramType.isAssignableFrom(List.class)) {
                    args[i] = bw.arrayToList(arg);
                }
                
                // Handle the conversion from CharacterOrString to Character or String:
                if (arg instanceof CharacterOrString) {
                    if (paramType == Character.class || paramType == char.class
                            || (!paramType.isAssignableFrom(String.class)
                                    && paramType.isAssignableFrom(Character.class))) {
                        args[i] = Character.valueOf(((CharacterOrString) arg).getAsChar());
                    } else {
                        args[i] = ((CharacterOrString) arg).getAsString();
                    }
                }
            }
        }

    }
    
}