/*
 *  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.GroovySystem;
import org.apache.groovy.util.SystemUtil;
import org.codehaus.groovy.GroovyBugError;
import org.codehaus.groovy.reflection.ClassInfo;
import org.codehaus.groovy.runtime.GeneratedClosure;

import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.invoke.MutableCallSite;
import java.lang.invoke.SwitchPoint;
import java.util.Map;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * Bytecode level interface for bootstrap methods used by invokedynamic.
 * This class provides a logging ability by using the boolean system property
 * groovy.indy.logging. Other than that this class contains the
 * interfacing methods with bytecode for invokedynamic as well as some helper
 * methods and classes.
 */
public class IndyInterface {
    private static final long INDY_OPTIMIZE_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.optimize.threshold", 1_000L);
    private static final long INDY_FALLBACK_THRESHOLD = SystemUtil.getLongSafe("groovy.indy.fallback.threshold", 1_000L);
    private static final long INDY_FALLBACK_CUTOFF = SystemUtil.getLongSafe("groovy.indy.fallback.cutoff", 100L);

    /**
     * Flags for method and property calls.
     */
    public static final int SAFE_NAVIGATION=1, THIS_CALL=2, GROOVY_OBJECT=4, IMPLICIT_THIS=8, SPREAD_CALL=16, UNCACHED_CALL=32;

    private static final MethodHandleWrapper NULL_METHOD_HANDLE_WRAPPER = MethodHandleWrapper.getNullMethodHandleWrapper();

    /**
     * Enum for easy differentiation between call types.
     */
    public enum CallType {
        /**
         * Method invocation type.
         */
        METHOD("invoke"),
        /**
         * Constructor invocation type.
         */
        INIT("init"),
        /**
         * Get property invocation type.
         */
        GET("getProperty"),
        /**
         * Set property invocation type.
         */
        SET("setProperty"),
        /**
         * Cast invocation type.
         */
        CAST("cast"),
        /**
         * Interface method invocation type.
         */
        INTERFACE("interface");

        private static final Map<String, CallType> NAME_CALLTYPE_MAP = Stream.of(CallType.values())
            .collect(Collectors.toUnmodifiableMap(CallType::getCallSiteName, Function.identity()));

        /**
         * The call site type name.
         */
        private final String name;

        CallType(String callSiteName) {
            this.name = callSiteName;
        }

        /**
         * Returns the name of the call site type
         */
        public String getCallSiteName() {
            return name;
        }

        public static CallType fromCallSiteName(String callSiteName) {
            return NAME_CALLTYPE_MAP.get(callSiteName);
        }

        public int getOrderNumber() {
            return ordinal();
        }
    }

    /**
     * Logger.
     */
    protected static final Logger LOG;
    /**
     * Indicates if indy logging is enabled.
     */
    protected static final boolean LOG_ENABLED;

    static {
        boolean enableLogger = false;

        LOG = Logger.getLogger(IndyInterface.class.getName());

        try {
            if (Boolean.getBoolean("groovy.indy.logging")) {
                LOG.setLevel(Level.ALL);
                enableLogger = true;
            }
        } catch (SecurityException e) {
            // Allow security managers to prevent system property access
        }

        LOG_ENABLED = enableLogger;
    }

    /**
     * LOOKUP constant used for example in unreflect calls
     */
    public static final MethodHandles.Lookup LOOKUP = MethodHandles.lookup();

    /**
     * shared invoker for cached method handles
     */
    private static final MethodHandle CACHED_INVOKER = MethodHandles.exactInvoker(MethodType.methodType(Object.class, Object[].class));

    /**
     * handle for the fromCacheHandle method
     */
    private static final MethodHandle FROM_CACHE_HANDLE_METHOD;

    /**
     * handle for the selectMethodHandle method
     */
    private static final MethodHandle SELECT_METHOD_HANDLE_METHOD;

    static {
        try {
            MethodType mt = MethodType.methodType(MethodHandle.class, CacheableCallSite.class, Class.class, String.class, int.class, Boolean.class, Boolean.class, Boolean.class, Object.class, Object[].class);

            FROM_CACHE_HANDLE_METHOD = LOOKUP.findStatic(IndyInterface.class, "fromCacheHandle", mt);
            SELECT_METHOD_HANDLE_METHOD = LOOKUP.findStatic(IndyInterface.class, "selectMethodHandle", mt);
        } catch (Exception e) {
            throw new GroovyBugError(e);
        }
    }

    protected static SwitchPoint switchPoint = new SwitchPoint();

    static {
        GroovySystem.getMetaClassRegistry().addMetaClassRegistryChangeEventListener(cmcu -> invalidateSwitchPoints());
    }

    /**
     * Callback for constant metaclass update change
     */
    protected static void invalidateSwitchPoints() {
        if (LOG_ENABLED) {
            LOG.info("invalidating switch point");
        }

        synchronized (IndyInterface.class) {
            SwitchPoint old = switchPoint;
            switchPoint = new SwitchPoint();
            SwitchPoint.invalidateAll(new SwitchPoint[]{old});
        }
    }

    /**
     * Bootstrap method for method calls from Groovy-compiled code with indy.
     *
     * @param caller   - the caller
     * @param callType - the type of call
     * @param type     - the parameter(s) and return type specification
     * @param name     - the real method name
     * @param flags    - call flags <ul>
     *                   <li>{@value #SAFE_NAVIGATION} is the flag value for safe navigation; see {@link #SAFE_NAVIGATION}</li>
     *                   <li>{@value #THIS_CALL} is the flag value for a call on this; see {@link #THIS_CALL}</li>
     *                   <li>{@value #SPREAD_CALL} is the flag value for a spread call; see {@link #SPREAD_CALL}</li>
     *                   </ul>
     * @since 2.1.0
     */
    public static CallSite bootstrap(final MethodHandles.Lookup caller, final String callType, final MethodType type, final String name, final int flags) {
        CallType ct = CallType.fromCallSiteName(callType);
        if (null == ct) throw new GroovyBugError("Unknown call type: " + callType);

        int callID = ct.getOrderNumber();
        boolean safe       = (flags & SAFE_NAVIGATION) != 0;
        boolean thisCall   = (flags & THIS_CALL      ) != 0;
        boolean spreadCall = (flags & SPREAD_CALL    ) != 0;

        // first produce a dummy call site, since indy doesn't give the runtime types;
        // the site then changes to the target when INDY_OPTIMIZE_THRESHOLD is reached
        // that does the method selection including the direct call to the real method
        var mc = new CacheableCallSite(type, caller);
        Class<?> sender = caller.lookupClass();
        if (thisCall) {
            while (GeneratedClosure.class.isAssignableFrom(sender)) {
                sender = sender.getEnclosingClass(); // GROOVY-2433
            }
        }
        // make an adapter for method selection, i.e. get cached method handle (fast path) or fall back
        MethodHandle mh = makeBootHandle(mc, sender, name, callID, type, safe, thisCall, spreadCall, FROM_CACHE_HANDLE_METHOD);
        mc.setTarget(mh);
        mc.setDefaultTarget(mh);
        mc.setFallbackTarget(makeFallBack(mc, sender, name, callID, type, safe, thisCall, spreadCall));

        return mc;
    }

    /**
     * Makes a fallback method for an invalidated method selection.
     */
    protected static MethodHandle makeFallBack(MutableCallSite mc, Class<?> sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall) {
        return makeBootHandle(mc, sender, name, callID, type, safeNavigation, thisCall, spreadCall, SELECT_METHOD_HANDLE_METHOD);
    }

    private static MethodHandle makeBootHandle(MutableCallSite mc, Class<?> sender, String name, int callID, MethodType type, boolean safeNavigation, boolean thisCall, boolean spreadCall, MethodHandle fromCacheOrSelectMethod) {
        final Object dummyReceiver = 1;
        // Step 1: bind site-constant arguments
        MethodHandle boundHandle = MethodHandles.insertArguments(
            fromCacheOrSelectMethod,
            0, // insert start index
            mc,
            sender,
            name,
            callID,
            safeNavigation,
            thisCall,
            spreadCall,
            dummyReceiver
        );
        // boundHandle: (Object receiver, Object[] arguments) ? MethodHandle

        // Step 2: fold into the shared invoker (MethodHandle, Object[]) ? Object
        MethodHandle bootHandle = MethodHandles.foldArguments(
            CACHED_INVOKER, // (MethodHandle, Object[]) ? Object
            boundHandle  // (Object, Object[]) ? MethodHandle
        );
        // bootHandle: (Object receiver, Object[] arguments) ? Object

        // Step 3: adapt to call site type: collect all arguments into Object[] and then asType
        bootHandle = bootHandle.asCollector(Object[].class, type.parameterCount()).asType(type);

        return bootHandle;
    }

    private static class FallbackSupplier {
        private final CacheableCallSite callSite;
        private final Class<?> sender;
        private final String methodName;
        private final int callID;
        private final Boolean safeNavigation;
        private final Boolean thisCall;
        private final Boolean spreadCall;
        private final Object dummyReceiver;
        private final Object[] arguments;
        private MethodHandleWrapper result;

        FallbackSupplier(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
            this.callSite = callSite;
            this.sender = sender;
            this.methodName = methodName;
            this.callID = callID;
            this.safeNavigation = safeNavigation;
            this.thisCall = thisCall;
            this.spreadCall = spreadCall;
            this.dummyReceiver = dummyReceiver;
            this.arguments = arguments;
        }

        MethodHandleWrapper get() {
            if (null == result) {
                result = fallback(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);
            }

            return result;
        }
    }

    /**
     * Get the cached methodHandle. if the related methodHandle is not found in the inline cache, cache and return it.
     * @deprecated Use the new bootHandle-based approach instead.
     */
    @Deprecated
    public static Object fromCache(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
        MethodHandle mh = fromCacheHandle(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);
        return mh.invokeExact(arguments);
    }

    /**
     * Get the cached methodHandle. if the related methodHandle is not found in the inline cache, cache and return it.
     */
    private static MethodHandle fromCacheHandle(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
        FallbackSupplier fallbackSupplier = new FallbackSupplier(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);

        MethodHandleWrapper mhw;
        if (bypassCache(spreadCall, arguments)) {
            mhw = NULL_METHOD_HANDLE_WRAPPER;
        } else {
            Object receiver = arguments[0];
            String receiverClassName = receiver != null ? receiver.getClass().getName() : "org.codehaus.groovy.runtime.NullObject";

            mhw = callSite.getAndPut(receiverClassName, (theName) -> {
                MethodHandleWrapper fallback = fallbackSupplier.get();
                if (fallback.isCanSetTarget()) return fallback;
                return NULL_METHOD_HANDLE_WRAPPER;
            });
        }

        if (mhw == NULL_METHOD_HANDLE_WRAPPER) {
            mhw = fallbackSupplier.get();
        }

        if (mhw.isCanSetTarget() && (callSite.getTarget() != mhw.getTargetMethodHandle()) && (mhw.getLatestHitCount() > INDY_OPTIMIZE_THRESHOLD)) {
            if (callSite.getFallbackRound().get() > INDY_FALLBACK_CUTOFF) {
                if (callSite.getTarget() != callSite.getDefaultTarget()) {
                    // reset the call site target to default forever to avoid JIT deoptimization storm further
                    callSite.setTarget(callSite.getDefaultTarget());
                }
            } else {
                callSite.setTarget(mhw.getTargetMethodHandle());
                if (LOG_ENABLED) LOG.info("call site target set, preparing outside invocation");
            }

            mhw.resetLatestHitCount();
        }

        return mhw.getCachedMethodHandle();
    }

    private static boolean bypassCache(Boolean spreadCall, Object[] arguments) {
        if (spreadCall) return true;
        Object receiver = arguments[0];
        return receiver != null && ClassInfo.getClassInfo(receiver.getClass()).hasPerInstanceMetaClasses();
    }

    /**
     * Core method for indy method selection using runtime types.
     * @deprecated Use the new bootHandle-based approach instead.
     */
    @Deprecated
    public static Object selectMethod(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
        MethodHandle mh = selectMethodHandle(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);
        return mh.invokeExact(arguments);
    }

    /**
     * Core method for indy method selection using runtime types.
     */
    private static MethodHandle selectMethodHandle(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) throws Throwable {
        MethodHandleWrapper mhw = fallback(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, dummyReceiver, arguments);

        MethodHandle defaultTarget = callSite.getDefaultTarget();
        long fallbackCount = callSite.incrementFallbackCount();
        if ((fallbackCount > INDY_FALLBACK_THRESHOLD) && (callSite.getTarget() != defaultTarget)) {
            callSite.setTarget(defaultTarget);
            if (LOG_ENABLED) LOG.info("call site target reset to default, preparing outside invocation");
            callSite.resetFallbackCount();
        }

        if (callSite.getTarget() == defaultTarget) {
            // correct the stale methodHandle in the inline cache of callsite
            // it is important but impacts the performance somehow when cache misses frequently
            Object receiver = arguments[0];
            String key = receiver != null ? receiver.getClass().getName() : "org.codehaus.groovy.runtime.NullObject";
            callSite.put(key, mhw);
        }

        return mhw.getCachedMethodHandle();
    }

    private static MethodHandleWrapper fallback(CacheableCallSite callSite, Class<?> sender, String methodName, int callID, Boolean safeNavigation, Boolean thisCall, Boolean spreadCall, Object dummyReceiver, Object[] arguments) {
        Selector selector = Selector.getSelector(callSite, sender, methodName, callID, safeNavigation, thisCall, spreadCall, arguments);
        selector.setCallSiteTarget();

        return new MethodHandleWrapper(
                selector.handle.asSpreader(Object[].class, arguments.length).asType(MethodType.methodType(Object.class, Object[].class)),
                selector.handle,
                selector.cache
        );
    }

    /**
     * @since 2.5.0
     */
    public static CallSite staticArrayAccess(MethodHandles.Lookup lookup, String name, MethodType type) {
        if (type.parameterCount() == 2) {
            return new ConstantCallSite(IndyArrayAccess.arrayGet(type));
        } else {
            return new ConstantCallSite(IndyArrayAccess.arraySet(type));
        }
    }
}
