/***
 * Copyright (c) 2000-2011 INRIA, France Telecom
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the copyright holders nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodHandles.Lookup;
import java.lang.invoke.MethodType;
import java.lang.invoke.MutableCallSite;
import java.util.HashMap;

/**
 * There are 3 bootstrap methods: - one for the constant that are initialized
 * once the first time the bootstrap method is called and after always reuse the
 * same constant. It's almost equivalent to an LDC but here the constant are
 * stored in boxed form e.g a java.lang.Integer containing 0 instead of an int
 * containing 0.
 * 
 * - one for the unary operation 'not' and 'asBoolean', here the semantics is
 * hard coded, all primitive value are transformed as object by applying this
 * operation: (v == 0)? false: true
 * 
 * - one for the binary operation 'add', 'mul' and 'gt', here the semantics can
 * be changed by adding more static methods in {@link BinaryOps}. This bootstrap
 * method is a little more complex because it creates an inlining cache to avoid
 * to recompute the binary method to call if the type of the two arguments
 * doesn't change. Also, if the expression is used a lot and trigger the JIT, it
 * will be able to inline the code of the operation directly at callsite.
 * 
 * @author Remi Forax
 */
public class RT {
    /**
     * bootstrap method for constant
     */
    public static CallSite cst(Lookup lookup, String name, MethodType type,
            Object constant) {
        return new ConstantCallSite(MethodHandles.constant(Object.class,
                constant));
    }

    /**
     * bootstrap method for unary operation 'asBoolean' and 'not'
     */
    public static CallSite unary(Lookup lookup, String name, MethodType type) {
        MethodHandle target;
        if (name.equals("asBoolean")) {
            target = MethodHandles.explicitCastArguments(
                    MethodHandles.identity(Object.class),
                    MethodType.methodType(boolean.class, Object.class));
        } else { // "not"
            target = MethodHandles.explicitCastArguments(NOT,
                    MethodType.methodType(Object.class, Object.class));
        }
        return new ConstantCallSite(target);
    }

    /**
     * bootstrap method for binary operation 'add', 'mul' and 'gt'
     * 
     * This bootstrap method doesn't install the target method handle directly,
     * because we want to install an inlining cache and we can't create an
     * inlining cache without knowing the class of the arguments. So this method
     * first installs a method handle that will call
     * {@link BinaryOpCallSite#fallback(Object, Object)} and the fallback method
     * will be called with the arguments and thus can install the inlining
     * cache. Also, the fallback has to be bound to a specific callsite to be
     * able to change its target after the first call, this part is done in the
     * constructor of {@link BinaryOpCallSite}.
     */
    public static CallSite binary(Lookup lookup, String name, MethodType type) {
        BinaryOpCallSite callSite = new BinaryOpCallSite(name, type);
        callSite.setTarget(callSite.fallback);
        return callSite;
    }

    /**
     * Garbage class containing the method used to apply 'not' on a boolean. See
     * {@link RT#unary(Lookup, String, MethodType)}
     */
    public static class UnayOps {

        public static Object not(boolean b) {
            return !b;
        }
    }

    private static final MethodHandle NOT;

    static {
        try {
            NOT = MethodHandles.publicLookup().findStatic(UnayOps.class, "not",
                    MethodType.methodType(Object.class, boolean.class));
        } catch (ReflectiveOperationException e) {
            throw new LinkageError(e.getMessage(), e);
        }
    }

    /**
     * A specific callsite that will install an 'inlining cache'. Because we
     * don't know until runtime which method handle to call, the lookup
     * depending on the dynamic type of the argument will be done at runtime
     * when the method {@link #fallback(Object, Object)} is called. To avoid to
     * do this dynamic lookup at each call, the fallback install two guards in
     * front of dispatch call that will check if the arguments class change or
     * not. If the arguments class don't change, the previously computed method
     * handle will be called again. Otherwise, a new method handle will be
     * computed and two new guards will be installed.
     */
    static class BinaryOpCallSite extends MutableCallSite {

        private final String opName;

        final MethodHandle fallback;

        public BinaryOpCallSite(String opName, MethodType type) {
            super(type);
            this.opName = opName;
            this.fallback = FALLBACK.bindTo(this);
        }

        Object fallback(Object v1, Object v2) throws Throwable {
            // when you debug with this message
            // don't forget that && and || are lazy !!
            // System.out.println("fallback called with "+opName+'('+v1.getClass()+','+v2.getClass()+')');

            Class<? extends Object> class1 = v1.getClass();
            Class<? extends Object> class2 = v2.getClass();
            MethodHandle op = lookupBinaryOp(opName, class1, class2);

            // convert arguments
            MethodType type = type();
            MethodType opType = op.type();
            if (opType.parameterType(0) == String.class) {
                if (opType.parameterType(1) == String.class) {
                    op = MethodHandles.filterArguments(op, 0, TO_STRING,
                            TO_STRING);
                } else {
                    op = MethodHandles.filterArguments(op, 0, TO_STRING);
                    op = MethodHandles.explicitCastArguments(op, type);
                }
            } else {
                if (opType.parameterType(1) == String.class) {
                    op = MethodHandles.filterArguments(op, 1, TO_STRING);
                }
                op = MethodHandles.explicitCastArguments(op, type);
            }

            // prepare guard
            MethodHandle guard = MethodHandles.guardWithTest(TEST1
                    .bindTo(class1), MethodHandles.guardWithTest(
                    TEST2.bindTo(class2), op, fallback), fallback);

            // install the inlining cache
            setTarget(guard);
            return op.invokeWithArguments(v1, v2);
        }

        public static boolean test1(Class<?> v1Class, Object v1, Object v2) {
            return v1.getClass() == v1Class;
        }

        public static boolean test2(Class<?> v2Class, Object v1, Object v2) {
            return v2.getClass() == v2Class;
        }

        private static final MethodHandle TO_STRING;

        private static final MethodHandle TEST1;

        private static final MethodHandle TEST2;

        private static final MethodHandle FALLBACK;

        static {
            Lookup lookup = MethodHandles.lookup();
            try {
                TO_STRING = lookup.findVirtual(Object.class, "toString",
                        MethodType.methodType(String.class));
                MethodType testType = MethodType.methodType(boolean.class,
                        Class.class, Object.class, Object.class);
                TEST1 = lookup.findStatic(BinaryOpCallSite.class, "test1",
                        testType);
                TEST2 = lookup.findStatic(BinaryOpCallSite.class, "test2",
                        testType);
                FALLBACK = lookup.findVirtual(BinaryOpCallSite.class,
                        "fallback", MethodType.genericMethodType(2));
            } catch (ReflectiveOperationException e) {
                throw new LinkageError(e.getMessage(), e);
            }
        }
    }

    /**
     * Garbage class that contains the raw operations used for binary
     * operations. All methods must be static returns an Object and takes the
     * same type for the two parameter types.
     * 
     * See {@link RT#lookupBinaryOp(String, Class, Class)} for more info.
     */
    public static class BinaryOps {

        public static Object add(int v1, int v2) {
            return v1 + v2;
        }

        public static Object add(double v1, double v2) {
            return v1 + v2;
        }

        public static Object add(String v1, String v2) {
            return v1 + v2;
        }

        public static Object mul(int v1, int v2) {
            return v1 * v2;
        }

        public static Object mul(double v1, double v2) {
            return v1 * v2;
        }

        public static Object gt(int v1, int v2) {
            return v1 > v2;
        }

        public static Object gt(double v1, double v2) {
            return v1 > v2;
        }

        public static Object gt(String v1, String v2) {
            return v1.compareTo(v2) > 0;
        }
    }

    /**
     * Select a most specific method among the ones defined in
     * {@link RT.BinaryOps}. The algorithm first find the most specific subtype
     * between class1 and class2. The order of the types is defined in
     * {@link RT#RANK_MAP}: Boolean < Byte < Short < Character < Integer < Long
     * < Float < Double < String then the algorithm lookup in
     * {@link RT.BinaryOps} to find a method with the name opName taking as
     * argument the primitive corresponding to the most specific subtype. If no
     * such method exist, the algorithm retry but looking for a method with a
     * more specific type (using the same order). The result of the lookup is
     * cached in {@link RT#BINARY_CACHE} to avoid to avoid to do a lookup (a
     * reflective call) on the same method twice.
     */
    static MethodHandle lookupBinaryOp(String opName, Class<?> class1,
            Class<?> class2) {
        int rank = Math.max(RANK_MAP.get(class1), RANK_MAP.get(class2));
        String mangledName = opName + rank;
        MethodHandle mh = BINARY_CACHE.get(mangledName);
        if (mh != null) {
            return mh;
        }

        for (; rank < PRIMITIVE_ARRAY.length;) {
            Class<?> primitive = PRIMITIVE_ARRAY[rank];
            try {
                mh = MethodHandles.publicLookup().findStatic(
                        BinaryOps.class,
                        opName,
                        MethodType.methodType(Object.class, primitive,
                                primitive));
            } catch (NoSuchMethodException e) {
                rank = rank + 1;
                continue;
            } catch (IllegalAccessException e) {
                throw new LinkageError(e.getMessage(), e);
            }

            BINARY_CACHE.put(mangledName, mh);
            return mh;
        }
        throw new LinkageError("unknown operation " + opName + " ("
                + class1.getName() + ',' + class2.getName() + ')');
    }

    private static final HashMap<Class<?>, Integer> RANK_MAP;

    private static final Class<?>[] PRIMITIVE_ARRAY;

    private static final HashMap<String, MethodHandle> BINARY_CACHE;

    static {
        Class<?>[] primitives = new Class<?>[] { boolean.class, byte.class,
                short.class, char.class, int.class, long.class, float.class,
                double.class, String.class };
        Class<?>[] wrappers = new Class<?>[] { Boolean.class, Byte.class,
                Short.class, Character.class, Integer.class, Long.class,
                Float.class, Double.class, String.class };
        HashMap<Class<?>, Integer> rankMap = new HashMap<Class<?>, Integer>();
        for (int i = 0; i < wrappers.length; i++) {
            rankMap.put(wrappers[i], i);
        }

        RANK_MAP = rankMap;
        PRIMITIVE_ARRAY = primitives;
        BINARY_CACHE = new HashMap<String, MethodHandle>();
    }
}
