/*
 * Copyright (C) 2015 The Android Open Source Project
 *
 * Licensed 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 android.databinding.tool.reflection;

import android.databinding.Bindable;

import java.util.List;

public abstract class ModelMethod {
    public abstract ModelClass getDeclaringClass();

    public abstract ModelClass[] getParameterTypes();

    public abstract String getName();

    public abstract ModelClass getReturnType(List<ModelClass> args);

    public abstract boolean isVoid();

    public abstract boolean isPublic();

    public abstract boolean isStatic();

    public abstract boolean isAbstract();

    /**
     * @return whether or not this method has been given the {@link Bindable} annotation.
     */
    public abstract boolean isBindable();

    /**
     * Since when this method is available. Important for Binding expressions so that we don't
     * call non-existing APIs when setting UI.
     *
     * @return The SDK_INT where this method was added. If it is not a framework method, should
     * return 1.
     */
    public abstract int getMinApi();

    /**
     * Returns the JNI description of the method which can be used to lookup it in SDK.
     * @see TypeUtil
     */
    public abstract String getJniDescription();

    /**
     * @return true if the final parameter is a varargs parameter.
     */
    public abstract boolean isVarArgs();

    /**
     * @param args The arguments to the method
     * @return Whether the arguments would be accepted as parameters to this method.
     */
    public boolean acceptsArguments(List<ModelClass> args) {
        boolean isVarArgs = isVarArgs();
        ModelClass[] parameterTypes = getParameterTypes();
        if ((!isVarArgs && args.size() != parameterTypes.length) ||
                (isVarArgs && args.size() < parameterTypes.length - 1)) {
            return false; // The wrong number of parameters
        }
        boolean parametersMatch = true;
        for (int i = 0; i < args.size(); i++) {
            ModelClass parameterType = getParameter(i, parameterTypes);
            ModelClass arg = args.get(i);
            if (!parameterType.isAssignableFrom(arg) && !isImplicitConversion(arg, parameterType)) {
                parametersMatch = false;
                break;
            }
        }
        return parametersMatch;
    }

    public boolean isBetterArgMatchThan(ModelMethod other, List<ModelClass> args) {
        final ModelClass[] parameterTypes = getParameterTypes();
        final ModelClass[] otherParameterTypes = other.getParameterTypes();
        for (int i = 0; i < args.size(); i++) {
            final ModelClass arg = args.get(i);
            final ModelClass thisParameter = getParameter(i, parameterTypes);
            final ModelClass thatParameter = other.getParameter(i, otherParameterTypes);
            final int diff = compareParameter(arg, thisParameter, thatParameter);
            if (diff != 0) {
                return diff < 0;
            }
        }
        return false;
    }

    private ModelClass getParameter(int index, ModelClass[] parameterTypes) {
        int normalParamCount = isVarArgs() ? parameterTypes.length - 1 : parameterTypes.length;
        if (index < normalParamCount) {
            return parameterTypes[index];
        } else {
            return parameterTypes[parameterTypes.length - 1].getComponentType();
        }
    }

    private static int compareParameter(ModelClass arg, ModelClass thisParameter,
            ModelClass thatParameter) {
        if (thatParameter.equals(arg)) {
            return 1;
        } else if (thisParameter.equals(arg)) {
            return -1;
        } else if (isBoxingConversion(thatParameter, arg)) {
            return 1;
        } else if (isBoxingConversion(thisParameter, arg)) {
            // Boxing/unboxing is second best
            return -1;
        } else {
            int argConversionLevel = getImplicitConversionLevel(arg);
            if (argConversionLevel != -1) {
                int oldConversionLevel = getImplicitConversionLevel(thatParameter);
                int newConversionLevel = getImplicitConversionLevel(thisParameter);
                if (newConversionLevel != -1 &&
                        (oldConversionLevel == -1 || newConversionLevel < oldConversionLevel)) {
                    return -1;
                } else if (oldConversionLevel != -1) {
                    return 1;
                }
            }
            // Look for more exact match
            if (thatParameter.isAssignableFrom(thisParameter)) {
                return -1;
            }
        }
        return 0; // no difference
    }

    public static boolean isBoxingConversion(ModelClass class1, ModelClass class2) {
        if (class1.isPrimitive() != class2.isPrimitive()) {
            return (class1.box().equals(class2.box()));
        } else {
            return false;
        }
    }

    public static int getImplicitConversionLevel(ModelClass primitive) {
        if (primitive == null) {
            return -1;
        } else if (primitive.isByte()) {
            return 0;
        } else if (primitive.isChar()) {
            return 1;
        } else if (primitive.isShort()) {
            return 2;
        } else if (primitive.isInt()) {
            return 3;
        } else if (primitive.isLong()) {
            return 4;
        } else if (primitive.isFloat()) {
            return 5;
        } else if (primitive.isDouble()) {
            return 6;
        } else {
            return -1;
        }
    }

    public static boolean isImplicitConversion(ModelClass from, ModelClass to) {
        if (from != null && to != null && from.isPrimitive() && to.isPrimitive()) {
            if (from.isBoolean() || to.isBoolean() || to.isChar()) {
                return false;
            }
            int fromConversionLevel = getImplicitConversionLevel(from);
            int toConversionLevel = getImplicitConversionLevel(to);
            return fromConversionLevel < toConversionLevel;
        } else {
            return false;
        }
    }
}
