package tc.oc.evil;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Method;
import java.util.concurrent.ExecutionException;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import tc.oc.commons.core.reflect.MethodResolver;
import tc.oc.commons.core.reflect.Methods;

public class LibCGDecoratorGenerator implements DecoratorGenerator {

    private static class CGMeta<T, D extends Decorator<T>> extends DecoratorGenerator.Meta<T, D> {
        final Enhancer enhancer;

        public CGMeta(Class<T> type, Class<D> decorator, Class implementation, Enhancer enhancer) {
            super(type, decorator, implementation);
            this.enhancer = enhancer;
        }

        @Override
        public D newInstance() throws Exception {
            return (D) enhancer.create();
        }

        @Override
        public D newInstance(Class[] parameterTypes, Object[] arguments) throws Exception {
            return (D) enhancer.create(parameterTypes, arguments);
        }
    }

    private static class Callback<T, D extends Decorator<T>> implements MethodInterceptor {
        final Class<T> base;
        final Class<D> decorator;
        final MethodResolver resolver;
        final Cache<Method, Boolean> isDecorated = CacheBuilder.newBuilder().build();
        final Cache<Method, MethodHandle> methodHandles = CacheBuilder.newBuilder().build();

        private Callback(Class<T> base, Class<D> decorator) {
            this.base = base;
            this.decorator = decorator;
            this.resolver = new MethodResolver(decorator);
        }

        boolean isDecorated(Method method) throws ExecutionException {
            final Boolean yes = isDecorated.getIfPresent(method);
            if(yes != null) return yes;

            synchronized(isDecorated) {
                return isDecorated.get(method, () -> {
                    // This may look simple, but it was absurdly difficult to get right.
                    // There are a lot of subtleties involved in method dispatch.
                    //
                    // Note, for example, that we completely ignore the declaring class
                    // of all methods involved. The only thing that matters is the
                    // name and signature of the method. This is absolutely necessary
                    // in order to handle all cases properly.

                    // First of all, make sure the delegate() method always goes to the decorator.
                    if("delegate".equals(method.getName()) &&
                       method.getParameterTypes().length == 0) return true;

                    // Look for a matching method in the base class (which may be abstract).
                    // If the method is completely absent from the base, send it to the decorator.
                    final Method baseMethod = Methods.accessibleMethod(base, method);
                    if(baseMethod == null) return true;

                    // Now look for a callable match in the decorator. If we find one,
                    // and it's different from the base method, and it doesn't come from
                    // an interface, then assume it's an override and call it.
                    //
                    // To be consistent with Java, we don't allow interface default methods
                    // to override methods in the base class.
                    final Method decoMethod = Methods.callableMethod(decorator, method);
                    return decoMethod != null &&
                           !decoMethod.equals(baseMethod) &&
                           !decoMethod.getDeclaringClass().isInterface();
                });
            }
        }

        @Override
        public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
            if(isDecorated(method)) {
                // Decorated method
                return proxy.invokeSuper(obj, args);
            } else {
                final T t = ((Decorator<T>) obj).delegate();
                if(method.getDeclaringClass().isInstance(t)) {
                    // Forwarded method
                    return proxy.invoke(t, args);
                } else {
                    // Forwarded method shadowed by an interface method in the decorator.
                    //
                    // This can happen if the decorator implements an interface that the
                    // base class doesn't, and that interface contains a method that shadows
                    // one on the base class. Java would allow the method to be called on the
                    // base anyway, but MethodProxy refuses to invoke it on something that
                    // is not assignable to the method's declaring type. So, unfortunately,
                    // we have to fall back to the JDK to handle this case.
                    return methodHandles.get(method, () ->
                        resolver.virtualHandle(t.getClass(), method).bindTo(t)
                    ).invokeWithArguments(args);
                }
            }
        }
    }

    @Override
    public <T, D extends Decorator<T>> DecoratorGenerator.Meta<T, D> implement(Class<T> type, Class<D> decorator) {
        final Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(decorator);
        enhancer.setCallbackType(MethodInterceptor.class);
        final Class<? extends D> impl = enhancer.createClass();
        enhancer.setCallback(new Callback<>(type, decorator));
        return new CGMeta(type, decorator, impl, enhancer);
    }
}