/*
 *  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.Closure;
import groovy.lang.GString;
import groovy.lang.GroovyObject;
import groovy.util.ProxyGenerator;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.reflection.stdclasses.CachedSAMClass;
import org.codehaus.groovy.runtime.ConvertedClosure;
import org.codehaus.groovy.runtime.typehandling.DefaultTypeTransformation;
import org.codehaus.groovy.transform.trait.Traits;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * This class contains several transformers for used during method invocation.
 */
public class TypeTransformers {
    private static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();
    private static final MethodHandle
            TO_STRING, TO_BYTE, TO_INT, TO_LONG, TO_SHORT,
            TO_FLOAT, TO_DOUBLE, TO_BIG_INT, AS_ARRAY,
            TO_REFLECTIVE_PROXY, TO_GENERATED_PROXY, TO_SAMTRAIT_PROXY,
            DOUBLE_TO_BIG_DEC, DOUBLE_TO_BIG_DEC_WITH_CONVERSION, LONG_TO_BIG_DEC, BIG_INT_TO_BIG_DEC;

    static {
        try {
            TO_STRING = LOOKUP.findVirtual(Object.class, "toString", MethodType.methodType(String.class));
            TO_BYTE = LOOKUP.findVirtual(Number.class, "byteValue", MethodType.methodType(Byte.TYPE));
            TO_SHORT = LOOKUP.findVirtual(Number.class, "shortValue", MethodType.methodType(Short.TYPE));
            TO_INT = LOOKUP.findVirtual(Number.class, "intValue", MethodType.methodType(Integer.TYPE));
            TO_LONG = LOOKUP.findVirtual(Number.class, "longValue", MethodType.methodType(Long.TYPE));
            TO_FLOAT = LOOKUP.findVirtual(Number.class, "floatValue", MethodType.methodType(Float.TYPE));
            TO_DOUBLE = LOOKUP.findVirtual(Number.class, "doubleValue", MethodType.methodType(Double.TYPE));

            // BigDecimal conversion is done by using the double value
            DOUBLE_TO_BIG_DEC = LOOKUP.findConstructor(BigDecimal.class, MethodType.methodType(Void.TYPE, Double.TYPE));
            DOUBLE_TO_BIG_DEC_WITH_CONVERSION = MethodHandles.filterReturnValue(TO_DOUBLE, DOUBLE_TO_BIG_DEC);

            // BigDecimal conversion is done by using the double value
            LONG_TO_BIG_DEC = LOOKUP.findConstructor(BigDecimal.class, MethodType.methodType(Void.TYPE, Long.TYPE));

            // BigDecimal conversion is done by using the double value
            BIG_INT_TO_BIG_DEC = LOOKUP.findConstructor(BigDecimal.class, MethodType.methodType(Void.TYPE, BigInteger.class));


            // BigInteger conversion is done by using the string representation
            MethodHandle tmp = LOOKUP.findConstructor(BigInteger.class, MethodType.methodType(Void.TYPE, String.class));
            TO_BIG_INT = MethodHandles.filterReturnValue(TO_STRING, tmp);

            // generic array to array conversion
            AS_ARRAY = LOOKUP.findStatic(DefaultTypeTransformation.class, "asArray", MethodType.methodType(Object.class, Object.class, Class.class));

            // reflective proxy generation, since we need a ConvertedClosure but have only a normal Closure, we need to create that wrapper object as well
            MethodHandle newProxyInstance = LOOKUP.findStatic(Proxy.class, "newProxyInstance",
                    MethodType.methodType(Object.class, ClassLoader.class, Class[].class, InvocationHandler.class));
            MethodHandle newConvertedClosure = LOOKUP.findConstructor(ConvertedClosure.class, MethodType.methodType(Void.TYPE, Closure.class, String.class));
            // prepare target newProxyInstance for fold to drop additional arguments needed by newConvertedClosure
            MethodType newOrder = newProxyInstance.type().dropParameterTypes(2, 3);
            newOrder = newOrder.insertParameterTypes(0, InvocationHandler.class, Closure.class, String.class);
            tmp = MethodHandles.permuteArguments(newProxyInstance, newOrder, 3, 4, 0);
            // execute fold:
            TO_REFLECTIVE_PROXY = MethodHandles.foldArguments(tmp, newConvertedClosure.asType(newConvertedClosure.type().changeReturnType(InvocationHandler.class)));

            {
                // generated proxy using a map to store the closure
                MethodHandle map = LOOKUP.findStatic(Collections.class, "singletonMap",
                        MethodType.methodType(Map.class, Object.class, Object.class));
                newProxyInstance = LOOKUP.findVirtual(ProxyGenerator.class, "instantiateAggregateFromBaseClass",
                        MethodType.methodType(GroovyObject.class, Map.class, Class.class));
                newOrder = newProxyInstance.type().dropParameterTypes(1, 2);
                newOrder = newOrder.insertParameterTypes(0, Map.class, Object.class, Object.class);
                tmp = MethodHandles.permuteArguments(newProxyInstance, newOrder, 3, 0, 4);
                tmp = MethodHandles.foldArguments(tmp, map);
                TO_GENERATED_PROXY = tmp;
            }
            {
                // Trait SAM coercion generated proxy using a map to store the closure
                MethodHandle map = LOOKUP.findStatic(Collections.class, "singletonMap",
                        MethodType.methodType(Map.class, Object.class, Object.class));
                newProxyInstance = LOOKUP.findVirtual(ProxyGenerator.class, "instantiateAggregate",
                        MethodType.methodType(GroovyObject.class, Map.class, List.class));
                newOrder = newProxyInstance.type().dropParameterTypes(1, 2);
                newOrder = newOrder.insertParameterTypes(0, Map.class, Object.class, Object.class);
                tmp = MethodHandles.permuteArguments(newProxyInstance, newOrder, 3, 0, 4);
                tmp = MethodHandles.foldArguments(tmp, map);
                TO_SAMTRAIT_PROXY = tmp;
            }
        } catch (Exception e) {
            throw new GroovyBugError(e);
        }
    }

    /**
     * Adds a type transformer applied at runtime.
     * This method handles transformations to String from GString,
     * array transformations and number based transformations
     */
    protected static MethodHandle addTransformer(MethodHandle handle, int pos, Object arg, Class<?> parameter) {
        MethodHandle transformer = null;
        if (arg instanceof GString) {
            transformer = TO_STRING;
        } else if (arg instanceof Closure) {
            transformer = createSAMTransform(arg, parameter);
        } else if (Number.class.isAssignableFrom(parameter)) {
            transformer = selectNumberTransformer(parameter, arg);
        } else if (parameter.isArray()) {
            transformer = MethodHandles.insertArguments(AS_ARRAY, 1, parameter);
        }
        if (transformer == null)
            throw new GroovyBugError("Unknown transformation for argument " + arg + " at position " + pos + " with " + arg.getClass() + " for parameter of type " + parameter);
        return applyUnsharpFilter(handle, pos, transformer);
    }

    /**
     * Creates a method handle that transforms the given Closure into the given
     * parameter type, if it is a SAM type.
     */
    private static MethodHandle createSAMTransform(Object closure, Class<?> parameter) {
        Method method = CachedSAMClass.getSAMMethod(parameter);
        if (method == null) return null;
        // TODO: have to think about how to optimize this!
        if (parameter.isInterface()) {
            if (Traits.isTrait(parameter)) {
                // the following code will basically do this:
                // Map<String,Closure> impl = Collections.singletonMap(method.getName(),arg);
                // return ProxyGenerator.INSTANCE.instantiateAggregate(impl,Collections.singletonList(clazz));
                // TO_SAMTRAIT_PROXY is a handle (Object,Object,ProxyGenerator,Class)GroovyObject
                // where the second object is the input closure, everything else
                // needs to be provided and is in remaining order: method name,
                // ProxyGenerator.INSTANCE and singletonList(parameter)
                MethodHandle ret = TO_SAMTRAIT_PROXY;
                ret = MethodHandles.insertArguments(ret, 2, ProxyGenerator.INSTANCE, Collections.singletonList(parameter));
                ret = MethodHandles.insertArguments(ret, 0, method.getName());
                return ret;
            }
            // the following code will basically do this:
            // return Proxy.newProxyInstance(
            //        parameter.getClassLoader(),
            //        new Class[]{parameter},
            //        new ConvertedClosure((Closure)closure, method.getName()));
            // TO_REFLECTIVE_PROXY will do that for us, though
            // input is the closure, the method name, the class loader and the
            // class array. All of that but the closure must be provided here.
            MethodHandle ret = TO_REFLECTIVE_PROXY;
            ret = MethodHandles.insertArguments(ret, 1, method.getName(), parameter.getClassLoader(), new Class[]{parameter});
            return ret;
        } else {
            // the following code will basically do this:
            //Map<String, Object> m = Collections.singletonMap(method.getName(), arg);
            //return ProxyGenerator.INSTANCE.
            //            instantiateAggregateFromBaseClass(m, parameter);
            // TO_GENERATED_PROXY is a handle (Object,Object,ProxyGenerator,Class)GroovyObject
            // where the second object is the input closure, everything else
            // needs to be provided and is in remaining order: method name,
            // ProxyGenerator.INSTANCE and parameter
            MethodHandle ret = TO_GENERATED_PROXY;
            ret = MethodHandles.insertArguments(ret, 2, ProxyGenerator.INSTANCE, parameter);
            ret = MethodHandles.insertArguments(ret, 0, method.getName());
            return ret;
        }
    }

    /**
     * Apply a transformer as filter.
     * The filter may not match exactly in the types. In this case needed
     * additional type transformations are done by {@link MethodHandle#asType(MethodType)}
     */
    public static MethodHandle applyUnsharpFilter(MethodHandle handle, int pos, MethodHandle transformer) {
        MethodType type = transformer.type();
        Class<?> given = handle.type().parameterType(pos);
        if (type.returnType() != given || type.parameterType(0) != given) {
            transformer = transformer.asType(MethodType.methodType(given, type.parameterType(0)));
        }
        return MethodHandles.filterArguments(handle, pos, transformer);
    }

    /**
     * Returns a transformer later applied as filter to transform one
     * number into another.
     */
    private static MethodHandle selectNumberTransformer(Class<?> param, Object arg) {
        param = TypeHelper.getWrapperClass(param);

        if (param == Byte.class) {
            return TO_BYTE;
        } else if (param == Character.class || param == Integer.class) {
            return TO_INT;
        } else if (param == Long.class) {
            return TO_LONG;
        } else if (param == Float.class) {
            return TO_FLOAT;
        } else if (param == Double.class) {
            return TO_DOUBLE;
        } else if (param == BigInteger.class) {
            return TO_BIG_INT;
        } else if (param == BigDecimal.class) {
            if (arg instanceof Double) {
                return DOUBLE_TO_BIG_DEC;
            } else if (arg instanceof Long) {
                return LONG_TO_BIG_DEC;
            } else if (arg instanceof BigInteger) {
                return BIG_INT_TO_BIG_DEC;
            } else {
                return DOUBLE_TO_BIG_DEC_WITH_CONVERSION;
            }
        } else if (param == Short.class) {
            return TO_SHORT;
        } else {
            return null;
        }
    }
}
