/*
 *  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 org.codehaus.groovy.vmplugin.v8;

import groovy.lang.MetaMethod;
import org.codehaus.groovy.GroovyBugError;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

import static org.codehaus.groovy.runtime.DefaultGroovyMethods.toBigInteger;
import static org.codehaus.groovy.vmplugin.v8.IndyInterface.LOOKUP;
import static org.codehaus.groovy.vmplugin.v8.TypeHelper.isBigDecCategory;
import static org.codehaus.groovy.vmplugin.v8.TypeHelper.isDoubleCategory;
import static org.codehaus.groovy.vmplugin.v8.TypeHelper.isIntCategory;
import static org.codehaus.groovy.vmplugin.v8.TypeHelper.isLongCategory;
import static org.codehaus.groovy.vmplugin.v8.TypeHelper.replaceWithMoreSpecificType;

/**
 * This class contains math operations used by indy instead of the normal
 * meta method and call site caching system. The goal is to avoid boxing, thus
 * use primitive types for parameters and return types where possible.
 * WARNING: This class is for internal use only. Do not use it outside the
 * org.codehaus.groovy.vmplugin.v8 package of groovy-core.
 */
public class IndyMath {

    private static final MethodType
            IV = MethodType.methodType(Void.TYPE, int.class),
            II = MethodType.methodType(int.class, int.class),
            IIV = MethodType.methodType(Void.TYPE, int.class, int.class),
            III = MethodType.methodType(int.class, int.class, int.class),
            LV = MethodType.methodType(Void.TYPE, long.class),
            LL = MethodType.methodType(long.class, long.class),
            LLV = MethodType.methodType(Void.TYPE, long.class, long.class),
            LLL = MethodType.methodType(long.class, long.class, long.class),
            DV = MethodType.methodType(Void.TYPE, double.class),
            DD = MethodType.methodType(double.class, double.class),
            DDV = MethodType.methodType(Void.TYPE, double.class, double.class),
            DDD = MethodType.methodType(double.class, double.class, double.class),
            GV = MethodType.methodType(Void.TYPE, BigDecimal.class),
            GGV = MethodType.methodType(Void.TYPE, BigDecimal.class, BigDecimal.class),
            OOV = MethodType.methodType(Void.TYPE, Object.class, Object.class);

    private static void makeMapEntry(String method, MethodType[] keys, MethodType[] values) throws NoSuchMethodException, IllegalAccessException {
        Map<MethodType, MethodHandle> xMap = new HashMap<>();
        METHODS.put(method, xMap);
        for (int i = 0; i < keys.length; i++) {
            xMap.put(keys[i], LOOKUP.findStatic(IndyMath.class, method, values[i]));
        }
    }

    private static final Map<String, Map<MethodType, MethodHandle>> METHODS = new HashMap<>();

    static {
        try {

            MethodType[] keys = new MethodType[]{IIV, LLV, DDV};
            MethodType[] values = new MethodType[]{III, LLL, DDD};
            makeMapEntry("minus", keys, values);
            makeMapEntry("plus", keys, values);
            makeMapEntry("multiply", keys, values);

            keys = new MethodType[]{DDV};
            values = new MethodType[]{DDD};
            makeMapEntry("div", keys, values);

            keys = new MethodType[]{IV, LV, DV};
            values = new MethodType[]{II, LL, DD};
            makeMapEntry("next", keys, values);
            makeMapEntry("previous", keys, values);

            keys = new MethodType[]{IIV, LLV};
            values = new MethodType[]{III, LLL};
            makeMapEntry("remainder", keys, values);
            makeMapEntry("mod", keys, values);
            makeMapEntry("or", keys, values);
            makeMapEntry("xor", keys, values);
            makeMapEntry("and", keys, values);
            makeMapEntry("leftShift", keys, values);
            makeMapEntry("rightShift", keys, values);

        } catch (Exception e) {
            throw new GroovyBugError(e);
        }
    }

    /**
     * Choose a method to replace the originally chosen metaMethod to have a
     * more efficient call path.
     */
    public static boolean chooseMathMethod(Selector info, MetaMethod metaMethod) {
        Map<MethodType, MethodHandle> xmap = METHODS.get(info.name);
        if (xmap == null) return false;

        MethodType type = replaceWithMoreSpecificType(info.args, info.targetType);
        type = widenOperators(type);

        MethodHandle handle = xmap.get(type);
        if (handle == null) return false;

        info.handle = handle;
        return true;
    }

    /**
     * Widens the operators. For math operations like a+b we generally
     * execute them using a conversion to certain types. If a for example
     * is an int and b a byte, we do the operation using integer math. This
     * method gives a simplified MethodType that contains the two operators
     * with this widening according to Groovy rules applied. That means both
     * parameters in the MethodType will have the same type.
     */
    private static MethodType widenOperators(MethodType mt) {
        final int parameterCount = mt.parameterCount();
        if (parameterCount == 2) {
            Class<?> leftType = mt.parameterType(0);
            Class<?> rightType = mt.parameterType(1);

            if (isIntCategory(leftType) && isIntCategory(rightType)) return IIV;
            if (isLongCategory(leftType) && isLongCategory(rightType)) return LLV;
            if (isBigDecCategory(leftType) && isBigDecCategory(rightType)) return GGV;
            if (isDoubleCategory(leftType) && isDoubleCategory(rightType)) return DDV;

            return OOV;
        } else if (parameterCount == 1) {
            Class<?> leftType = mt.parameterType(0);
            if (isIntCategory(leftType)) return IV;
            if (isLongCategory(leftType)) return LV;
            if (isBigDecCategory(leftType)) return GV;
            if (isDoubleCategory(leftType)) return DV;
        }
        return mt;
    }

    // math methods used by indy

    // int x int
    public static int plus(int a, int b) {
        return a + b;
    }

    public static int minus(int a, int b) {
        return a - b;
    }

    public static int multiply(int a, int b) {
        return a * b;
    }

    public static int remainder(int a, int b) {
        return a % b;
    }

    public static int mod(int a, int b) {
        return toBigInteger(a).mod(toBigInteger(b)).intValue();
    }

    public static int or(int a, int b) {
        return a | b;
    }

    public static int xor(int a, int b) {
        return a ^ b;
    }

    public static int and(int a, int b) {
        return a & b;
    }

    public static int leftShift(int a, int b) {
        return a << b;
    }

    public static int rightShift(int a, int b) {
        return a >> b;
    }

    // long x long
    public static long plus(long a, long b) {
        return a + b;
    }

    public static long minus(long a, long b) {
        return a - b;
    }

    public static long multiply(long a, long b) {
        return a * b;
    }

    public static long remainder(long a, long b) {
        return a % b;
    }

    public static long mod(long a, long b) {
        return toBigInteger(a).mod(toBigInteger(b)).longValue();
    }

    public static long or(long a, long b) {
        return a | b;
    }

    public static long xor(long a, long b) {
        return a ^ b;
    }

    public static long and(long a, long b) {
        return a & b;
    }

    public static long leftShift(long a, long b) {
        return a << b;
    }

    public static long rightShift(long a, long b) {
        return a >> b;
    }

    // double x double
    public static double plus(double a, double b) {
        return a + b;
    }

    public static double minus(double a, double b) {
        return a - b;
    }

    public static double multiply(double a, double b) {
        return a * b;
    }

    public static double div(double a, double b) {
        return a / b;
    }

    // next & previous
    public static int next(int i) {
        return i + 1;
    }

    public static long next(long l) {
        return l + 1;
    }

    public static double next(double d) {
        return d + 1;
    }

    public static int previous(int i) {
        return i - 1;
    }

    public static long previous(long l) {
        return l - 1;
    }

    public static double previous(double d) {
        return d - 1;
    }

    /*
     further operations to be handled here maybe:
    a / b a.div(b) (if one is double, return double, otherwise BD)
    a[b]    a.getAt(b)
    a[b] = c    a.putAt(b, c)
    */
}
