/*
 * Copyright 2016 higherfrequencytrading.com
 *
 *  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.openhft.chronicle.wire;

import net.openhft.chronicle.bytes.MethodWriterBuilder;
import net.openhft.chronicle.bytes.MethodWriterInterceptor;
import net.openhft.chronicle.bytes.MethodWriterInvocationHandler;
import net.openhft.chronicle.bytes.MethodWriterListener;
import net.openhft.chronicle.core.Jvm;
import net.openhft.chronicle.core.io.Closeable;
import org.jetbrains.annotations.NotNull;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Supplier;

/*
 * Created by Peter Lawrey on 28/03/16.
 */
@SuppressWarnings({"rawtypes", "unchecked"})
public class VanillaMethodWriterBuilder<T> implements Supplier<T>, MethodWriterBuilder<T> {

    private final List<Class> interfaces = new ArrayList<>();
    private static final Map<Set<Class>, Class> setOfClassesToClassName = new ConcurrentHashMap<>();
    private final String packageName;
    private ClassLoader classLoader;
    @NotNull
    private final MethodWriterInvocationHandlerSupplier handlerSupplier;

    public VanillaMethodWriterBuilder(@NotNull Class<T> tClass, @NotNull Supplier<MethodWriterInvocationHandler> handlerSupplier) {
        packageName = tClass.getPackage().getName();
        interfaces.add(Closeable.class);
        interfaces.add(tClass);
        classLoader = tClass.getClassLoader();
        this.handlerSupplier = new MethodWriterInvocationHandlerSupplier(handlerSupplier);
    }

    private static AtomicLong proxyCount = new AtomicLong();

    @NotNull
    public MethodWriterBuilder<T> classLoader(ClassLoader classLoader) {
        this.classLoader = classLoader;
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> addInterface(Class additionalClass) {
        interfaces.add(additionalClass);
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> recordHistory(boolean recordHistory) {
        handlerSupplier.recordHistory(recordHistory);
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> methodWriterListener(MethodWriterListener methodWriterListener) {
        handlerSupplier.methodWriterListener(methodWriterListener);
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> methodWriterInterceptor(MethodWriterInterceptor methodWriterInterceptor) {
        handlerSupplier.methodWriterInterceptor(methodWriterInterceptor);
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> onClose(Closeable closeable) {
        handlerSupplier.onClose(closeable);
        return this;
    }

    @NotNull
    public MethodWriterBuilder<T> disableThreadSafe(boolean theadSafe) {
        handlerSupplier.disableThreadSafe(theadSafe);
        return this;
    }

    @NotNull
    public T build() {
        return get();
    }

    private Class<?> proxyClass;

    private static <T> Class generatedProxyClass(String packageName, Set<Class> interfaces) {
        return GeneratedProxyClass.from(packageName, interfaces, "Proxy" + proxyCount.incrementAndGet());
    }

    @NotNull
    @Override
    public T get() {
        if (proxyClass != null) {

            try {
                Constructor<T> constructor = (Constructor) proxyClass.getConstructor(MethodWriterInvocationHandlerSupplier.class);
                return (T) constructor.newInstance(handlerSupplier);
            } catch (Throwable e) {
                // do nothing and drop through
                if (Jvm.isDebug())
                    Jvm.debug().on(getClass(), e);
            }
        }

        try {
            // this will create proxy that does not suffer from the arg[] issue
            LinkedHashSet<Class> setOfInterfaces = new LinkedHashSet<>(interfaces);
            final Class<T> o = setOfClassesToClassName.computeIfAbsent(setOfInterfaces,
                    i -> VanillaMethodWriterBuilder.generatedProxyClass(packageName, i));
            if (o != null)
                return o.getConstructor(MethodWriterInvocationHandlerSupplier.class)
                        .newInstance(handlerSupplier);
            
        } catch (Throwable e) {
            // do nothing and drop through
            if (Jvm.isDebug())
                Jvm.debug().on(getClass(), e);
        }

        @NotNull Class[] interfacesArr = interfaces.toArray(new Class[interfaces.size()]);
        //noinspection unchecked
        return (T) Proxy.newProxyInstance(classLoader, interfacesArr, new CallSupplierInvocationHandler());
    }

    /**
     * A generic event treats the first argument and the eventName
     *
     * @param genericEvent name
     * @return this
     */
    public MethodWriterBuilder<T> genericEvent(String genericEvent) {
        handlerSupplier.genericEvent(genericEvent);
        return this;
    }

    public MethodWriterBuilder<T> useMethodIds(boolean useMethodIds) {
        handlerSupplier.useMethodIds(useMethodIds);
        return this;
    }

    class CallSupplierInvocationHandler implements InvocationHandler {
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            return handlerSupplier.get().invoke(proxy, method, args);
        }
    }

    public Class<?> proxyClass() {
        return proxyClass;
    }

    public MethodWriterBuilder<T> proxyClass(Class<?> proxyClass) {
        if (proxyClass.isInterface())
            throw new IllegalArgumentException("expecting a class rather than an interface, proxyClass=" + proxyClass);
        this.proxyClass = proxyClass;
        return this;
    }

}

@@ -1496,24 +1496,120 @@
 ilder%3CT%3E %7B%0A%0A
+    private static final boolean DISABLE_PROXY_GEN = Boolean.getBoolean(%22disableProxyCodegen%22);%0A
     private 
@@ -4001,17 +4001,16 @@
 null) %7B%0A
-%0A
         
@@ -4405,32 +4405,71 @@
    %7D%0A        %7D%0A%0A
+        if (! DISABLE_PROXY_GEN) %7B%0A    
         try %7B%0A  
@@ -4478,16 +4478,20 @@
         
+    
 // this 
@@ -4562,16 +4562,20 @@
         
+   
+ 
 LinkedHa
@@ -4634,24 +4634,28 @@
 nterfaces);%0A
+    
             
@@ -4750,16 +4750,20 @@
         
+ 
+   
 i -%3E Van
@@ -4820,24 +4820,28 @@
 eName, i));%0A
+    
             
@@ -4867,24 +4867,28 @@
             
+    
 return o.get
@@ -4968,16 +4968,20 @@
         
+  
+  
 .newInst
@@ -4995,35 +4995,33 @@
 ndlerSupplier);%0A
-   
+%0A
          %0A      
@@ -5013,22 +5013,16 @@
         
-%0A     
    %7D cat
@@ -5044,32 +5044,36 @@
 ) %7B%0A            
+ 
+   
 // do nothing an
@@ -5079,32 +5079,36 @@
 nd drop through%0A
+    
             if (
@@ -5130,32 +5130,36 @@
                 
+    
 Jvm.debug().on(g
@@ -5165,32 +5165,46 @@
 getClass(), e);%0A
+            %7D%0A
         %7D%0A%0A     

Allow proxy codegen to be disabled - IntelliJ too long class path can screw up the compiler