/*
 * Copyright 2014 - Present Rafael Winterhalter
 *
 * Licensed 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 net.bytebuddy.benchmark;

import javassist.util.proxy.MethodFilter;
import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.benchmark.specimen.ExampleInterface;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.dynamic.scaffold.TypeValidation;
import net.bytebuddy.implementation.StubMethod;
import net.bytebuddy.pool.TypePool;
import net.sf.cglib.proxy.CallbackHelper;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.FixedValue;
import net.sf.cglib.proxy.NoOp;
import org.openjdk.jmh.annotations.*;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.TimeUnit;

import static net.bytebuddy.matcher.ElementMatchers.isDeclaredBy;
import static net.bytebuddy.matcher.ElementMatchers.none;

/**
 * <p>
 * This benchmark dynamically creates a class which implements {@link net.bytebuddy.benchmark.specimen.ExampleInterface}
 * which overrides all methods to invoke the direct super class's implementation. The benchmark furthermore creates an
 * instance of this class since some code generation frameworks rely on this property.
 * </p>
 * <p>
 * Note that this class defines all values that are accessed by benchmark methods as instance fields. This way, the JIT
 * compiler's capability of constant folding is limited in order to produce more comparable test results.
 * </p>
 */
@State(Scope.Thread)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MICROSECONDS)
public class ClassByImplementationBenchmark {

    /**
     * The base class to be subclassed in all benchmarks.
     */
    public static final Class<? extends ExampleInterface> BASE_CLASS = ExampleInterface.class;

    /**
     * The default reference value. By defining the default reference value as a string type instead of as an object
     * type, the field is inlined by the compiler, similar to the primitive values.
     */
    public static final String DEFAULT_REFERENCE_VALUE = null;

    /**
     * The default {@code boolean} value.
     */
    public static final boolean DEFAULT_BOOLEAN_VALUE = false;

    /**
     * The default {@code byte} value.
     */
    public static final byte DEFAULT_BYTE_VALUE = 0;

    /**
     * The default {@code short} value.
     */
    public static final short DEFAULT_SHORT_VALUE = 0;

    /**
     * The default {@code char} value.
     */
    public static final char DEFAULT_CHAR_VALUE = 0;

    /**
     * The default {@code int} value.
     */
    public static final int DEFAULT_INT_VALUE = 0;

    /**
     * The default {@code long} value.
     */
    public static final long DEFAULT_LONG_VALUE = 0L;

    /**
     * The default {@code float} value.
     */
    public static final float DEFAULT_FLOAT_VALUE = 0f;

    /**
     * The default {@code double} value.
     */
    public static final double DEFAULT_DOUBLE_VALUE = 0d;

    /**
     * The base class to be subclassed in all benchmarks.
     */
    private Class<? extends ExampleInterface> baseClass = BASE_CLASS;

    /**
     * The default reference value. By defining the default reference value as a string type instead of as an object
     * type, the field is inlined by the compiler, similar to the primitive values.
     */
    private String defaultReferenceValue = DEFAULT_REFERENCE_VALUE;

    /**
     * The default {@code boolean} value.
     */
    private boolean defaultBooleanValue = DEFAULT_BOOLEAN_VALUE;

    /**
     * The default {@code byte} value.
     */
    private byte defaultByteValue = DEFAULT_BYTE_VALUE;

    /**
     * The default {@code short} value.
     */
    private short defaultShortValue = DEFAULT_SHORT_VALUE;

    /**
     * The default {@code char} value.
     */
    private char defaultCharValue = DEFAULT_CHAR_VALUE;

    /**
     * The default {@code int} value.
     */
    private int defaultIntValue = DEFAULT_INT_VALUE;

    /**
     * The default {@code long} value.
     */
    private long defaultLongValue = DEFAULT_LONG_VALUE;

    /**
     * The default {@code float} value.
     */
    private float defaultFloatValue = DEFAULT_FLOAT_VALUE;

    /**
     * The default {@code double} value.
     */
    private double defaultDoubleValue = DEFAULT_DOUBLE_VALUE;

    /**
     * The zero-length of the class loader's URL.
     */
    private int urlLength = 0;

    /**
     * Creates a new class loader. By using a fresh class loader for each creation, we avoid name space issues.
     * A class loader's creation is part of the benchmark but since any test creates a class loader exactly once,
     * the benchmark remains valid.
     *
     * @return A new class loader.
     */
    private ClassLoader newClassLoader() {
        return new URLClassLoader(new URL[urlLength]);
    }

    /**
     * A description of {@link ClassByExtensionBenchmark#baseClass}.
     */
    private TypeDescription baseClassDescription;

    /**
     * Sets up this benchmark.
     */
    @Setup
    public void setup() {
        baseClassDescription = TypePool.Default.ofSystemLoader().describe(baseClass.getName()).resolve();
    }

    /**
     * Creates a baseline for the benchmark.
     *
     * @return A simple object that is not transformed.
     */
    @Benchmark
    public ExampleInterface baseline() {
        return new ExampleInterface() {
            public boolean method(boolean arg) {
                return false;
            }

            public byte method(byte arg) {
                return 0;
            }

            public short method(short arg) {
                return 0;
            }

            public int method(int arg) {
                return 0;
            }

            public char method(char arg) {
                return 0;
            }

            public long method(long arg) {
                return 0;
            }

            public float method(float arg) {
                return 0;
            }

            public double method(double arg) {
                return 0;
            }

            public Object method(Object arg) {
                return null;
            }

            public boolean[] method(boolean arg1, boolean arg2, boolean arg3) {
                return null;
            }

            public byte[] method(byte arg1, byte arg2, byte arg3) {
                return null;
            }

            public short[] method(short arg1, short arg2, short arg3) {
                return null;
            }

            public int[] method(int arg1, int arg2, int arg3) {
                return null;
            }

            public char[] method(char arg1, char arg2, char arg3) {
                return null;
            }

            public long[] method(long arg1, long arg2, long arg3) {
                return null;
            }

            public float[] method(float arg1, float arg2, float arg3) {
                return null;
            }

            public double[] method(double arg1, double arg2, double arg3) {
                return null;
            }

            public Object[] method(Object arg1, Object arg2, Object arg3) {
                return null;
            }
        };
    }

    /**
     * Performs a benchmark of an interface implementation using Byte Buddy.
     *
     * @return The created instance, in order to avoid JIT removal.
     * @throws java.lang.Exception If the reflective invocation causes an exception.
     */
    @Benchmark
    public ExampleInterface benchmarkByteBuddy() throws Exception {
        return new ByteBuddy()
                .with(TypeValidation.DISABLED)
                .ignore(none())
                .subclass(baseClass)
                .method(isDeclaredBy(baseClass)).intercept(StubMethod.INSTANCE)
                .make()
                .load(newClassLoader(), ClassLoadingStrategy.Default.INJECTION)
                .getLoaded()
                .getDeclaredConstructor()
                .newInstance();
    }

    /**
     * Performs a benchmark of an interface implementation using Byte Buddy. This benchmark uses a type pool to compare against
     * usage of the reflection API.
     *
     * @return The created instance, in order to avoid JIT removal.
     * @throws java.lang.Exception If the reflective invocation causes an exception.
     */
    @Benchmark
    public ExampleInterface benchmarkByteBuddyWithTypePool() throws Exception {
        return (ExampleInterface) new ByteBuddy()
                .with(TypeValidation.DISABLED)
                .ignore(none())
                .subclass(baseClassDescription)
                .method(isDeclaredBy(baseClassDescription)).intercept(StubMethod.INSTANCE)
                .make()
                .load(newClassLoader(), ClassLoadingStrategy.Default.INJECTION)
                .getLoaded()
                .getDeclaredConstructor()
                .newInstance();
    }

    /**
     * Performs a benchmark of an interface implementation using cglib.
     *
     * @return The created instance, in order to avoid JIT removal.
     */
    @Benchmark
    public ExampleInterface benchmarkCglib() {
        Enhancer enhancer = new Enhancer();
        enhancer.setUseCache(false);
        enhancer.setClassLoader(newClassLoader());
        enhancer.setSuperclass(baseClass);
        CallbackHelper callbackHelper = new CallbackHelper(Object.class, new Class[]{baseClass}) {
            protected Object getCallback(Method method) {
                if (method.getDeclaringClass() == baseClass) {
                    return new FixedValue() {
                        public Object loadObject() {
                            return null;
                        }
                    };
                } else {
                    return NoOp.INSTANCE;
                }
            }
        };
        enhancer.setCallbackFilter(callbackHelper);
        enhancer.setCallbacks(callbackHelper.getCallbacks());
        return (ExampleInterface) enhancer.create();
    }

    /**
     * Performs a benchmark of an interface implementation using javassist proxies.
     *
     * @return The created instance, in order to avoid JIT removal.
     * @throws java.lang.Exception If the reflective invocation causes an exception.
     */
    @Benchmark
    public ExampleInterface benchmarkJavassist() throws Exception {
        ProxyFactory proxyFactory = new ProxyFactory() {
            protected ClassLoader getClassLoader() {
                return newClassLoader();
            }
        };
        proxyFactory.setUseCache(false);
        proxyFactory.setUseWriteReplace(false);
        proxyFactory.setSuperclass(Object.class);
        proxyFactory.setInterfaces(new Class<?>[]{baseClass});
        proxyFactory.setFilter(new MethodFilter() {
            public boolean isHandled(Method method) {
                return true;
            }
        });
        @SuppressWarnings("unchecked")
        Object instance = proxyFactory.createClass().getDeclaredConstructor().newInstance();
        ((javassist.util.proxy.Proxy) instance).setHandler(new MethodHandler() {
            public Object invoke(Object self,
                                 Method thisMethod,
                                 Method proceed,
                                 Object[] args) throws Throwable {
                Class<?> returnType = thisMethod.getReturnType();
                if (returnType.isPrimitive()) {
                    if (returnType == boolean.class) {
                        return defaultBooleanValue;
                    } else if (returnType == byte.class) {
                        return defaultByteValue;
                    } else if (returnType == short.class) {
                        return defaultShortValue;
                    } else if (returnType == char.class) {
                        return defaultCharValue;
                    } else if (returnType == int.class) {
                        return defaultIntValue;
                    } else if (returnType == long.class) {
                        return defaultLongValue;
                    } else if (returnType == float.class) {
                        return defaultFloatValue;
                    } else {
                        return defaultDoubleValue;
                    }
                } else {
                    return defaultReferenceValue;
                }
            }
        });
        return (ExampleInterface) instance;
    }

    /**
     * Performs a benchmark of an interface implementation using the Java Class Library's utilities.
     *
     * @return The created instance, in order to avoid JIT removal.
     * @throws java.lang.Exception If the reflective invocation causes an exception.
     */
    @Benchmark
    public ExampleInterface benchmarkJdkProxy() throws Exception {
        return (ExampleInterface) Proxy.newProxyInstance(newClassLoader(),
                new Class<?>[]{baseClass},
                new InvocationHandler() {
                    public Object invoke(Object proxy, Method method, Object[] args) {
                        Class<?> returnType = method.getReturnType();
                        if (returnType.isPrimitive()) {
                            if (returnType == boolean.class) {
                                return defaultBooleanValue;
                            } else if (returnType == byte.class) {
                                return defaultByteValue;
                            } else if (returnType == short.class) {
                                return defaultShortValue;
                            } else if (returnType == char.class) {
                                return defaultCharValue;
                            } else if (returnType == int.class) {
                                return defaultIntValue;
                            } else if (returnType == long.class) {
                                return defaultLongValue;
                            } else if (returnType == float.class) {
                                return defaultFloatValue;
                            } else {
                                return defaultDoubleValue;
                            }
                        } else {
                            return defaultReferenceValue;
                        }
                    }
                }
        );
    }
}
