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

import org.codehaus.groovy.GroovyBugError;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.HashMap;

/**
 * Class for handling array access through invokedynamic using static callsite information
 * @author Jochen Theodorou
 * @since 2.5.0
 */
public class IndyArrayAccess {

    private static final MethodHandle notNegative,normalizeIndex;
    static {
        try {
            notNegative = MethodHandles.lookup().findStatic(IndyArrayAccess.class, "notNegative", MethodType.methodType(boolean.class, int.class));
            normalizeIndex = MethodHandles.lookup().findStatic(IndyArrayAccess.class, "normalizeIndex", MethodType.methodType(int.class, Object.class, int.class));
        } catch (ReflectiveOperationException e) {
            throw new GroovyBugError(e);
        }
    }

    private static final HashMap<Class,MethodHandle> getterMap, setterMap;
    static {
        getterMap = new HashMap<Class,MethodHandle>();
        Class[] classes = new Class[]{
                int[].class, byte[].class, short[].class, long[].class,
                double[].class, float[].class,
                boolean[].class, char[].class, Object[].class};
        for (Class arrayClass : classes) {
            MethodHandle handle = buildGetter(arrayClass);
            getterMap.put(arrayClass, handle);
        }
        setterMap = new HashMap<Class,MethodHandle>();
        for (Class arrayClass : classes) {
            MethodHandle handle = buildSetter(arrayClass);
            setterMap.put(arrayClass, handle);
        }
    }

    private static MethodHandle buildGetter(Class arrayClass) {
        MethodHandle get = MethodHandles.arrayElementGetter(arrayClass);
        MethodHandle fallback = MethodHandles.explicitCastArguments(get, get.type().changeParameterType(0, Object.class));

        fallback = MethodHandles.dropArguments(fallback, 2, int.class);
        MethodType reorderType = fallback.type().
                                    insertParameterTypes(0, int.class).
                                    dropParameterTypes(2,3);
        fallback = MethodHandles.permuteArguments(fallback, reorderType, 1, 0, 0);

        fallback = MethodHandles.foldArguments(fallback, normalizeIndex);
        fallback = MethodHandles.explicitCastArguments(fallback, get.type());

        MethodHandle guard = MethodHandles.dropArguments(notNegative, 0, arrayClass);
        MethodHandle handle = MethodHandles.guardWithTest(guard, get, fallback);
        return handle;
    }

    private static MethodHandle buildSetter(Class arrayClass){
        MethodHandle set = MethodHandles.arrayElementSetter(arrayClass);
        MethodHandle fallback = MethodHandles.explicitCastArguments(set, set.type().changeParameterType(0, Object.class));

        fallback = MethodHandles.dropArguments(fallback, 3, int.class);
        MethodType reorderType = fallback.type().
                                    insertParameterTypes(0, int.class).
                                    dropParameterTypes(4,5);
        fallback = MethodHandles.permuteArguments(fallback, reorderType, 1, 0, 3, 0);

        fallback = MethodHandles.foldArguments(fallback, normalizeIndex);
        fallback = MethodHandles.explicitCastArguments(fallback, set.type());

        MethodHandle guard = MethodHandles.dropArguments(notNegative, 0, arrayClass);
        MethodHandle handle = MethodHandles.guardWithTest(guard, set, fallback);
        return handle;
    }

    private static int getLength(Object array) {
        if (array instanceof Object[]) return ((Object[])array).length;
        if (array instanceof boolean[]) return ((boolean[])array).length;
        if (array instanceof byte[]) return ((byte[])array).length;
        if (array instanceof char[]) return ((char[])array).length;
        if (array instanceof short[]) return ((short[])array).length;
        if (array instanceof int[]) return ((int[])array).length;
        if (array instanceof long[]) return ((long[])array).length;
        if (array instanceof float[]) return ((float[])array).length;
        if (array instanceof double[]) return ((double[])array).length;
        return 0;
    }

    private static int normalizeIndex(Object array, int i) {
        int temp = i;
        int size = getLength(array);
        i += size;
        if (i < 0) {
            throw new ArrayIndexOutOfBoundsException("Negative array index [" + temp + "] too large for array size " + size);
        }
        return i;
    }

    public static boolean notNegative(int index) {
        return index>=0;
    }

    public static MethodHandle arrayGet(MethodType type) {
        Class key = type.parameterType(0);
        MethodHandle res = getterMap.get(key);
        if (res!=null) return res;
        res = buildGetter(key);
        res = MethodHandles.explicitCastArguments(res, type);
        return res;
    }

    public static MethodHandle arraySet(MethodType type) {
        Class key = type.parameterType(0);
        MethodHandle res = setterMap.get(key);
        if (res!=null) return res;
        res = buildSetter(key);
        res = MethodHandles.explicitCastArguments(res, type);
        return res;
    }
}
