package wicket.contrib.spring.injection;

import java.io.InvalidClassException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

import net.sf.cglib.proxy.Callback;
import net.sf.cglib.proxy.CallbackFilter;
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.FixedValue;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import net.sf.cglib.proxy.NoOp;

/**
 * Creates lazyproxies both JDK (interface) and CGLIB based.
 * @author chris
 *
 */
public class SerializableLazyProxyCreator
{

    private SerializableLazyProxyCreator()
    {
        super();
    }

    /**
     * main method returns a serializable Proxy which loads the lazaly the target through the ObjectResolver. 
     * Supports both interfaces (JDK Proxies) and classes (CGLIB) for creating the proxy.
     * @param proxyClass the class/interface the proxy should implement
     * @param or the ObjectResolver to load the proxied instance
     * @return
     */
    public static Object makeProxy(Class proxyClass, ObjectResolver or) {
        if (proxyClass == null) throw new NullPointerException("proxyClass");
        if (or == null) throw new NullPointerException("ObjectResolver");

        if (proxyClass.isInterface())
            return makeJDKProxy(proxyClass, or);
        else
            return makeCGLIBProxy(proxyClass, or);
    }
    
    public static boolean isLazyInitProxy(Object proxy){
        return proxy instanceof LazyInitProxy;
    }
    
    public static ObjectResolver getObjectResolver(Object proxy){
        if(!isLazyInitProxy(proxy))
            throw new IllegalArgumentException("The proxy is no LazyInitProxy");
        return ((LazyInitProxy)proxy).getLazyInitProxyObjectResolver();
    }

    // ///////////////////////////////
    // the jdk proxy

    private static Object makeJDKProxy(Class targetClass, ObjectResolver or) {

        InvocationHandler handler = new JDKInvocationHandler(or);
        return Proxy.newProxyInstance(Thread.currentThread()
                .getContextClassLoader(), new Class[] { targetClass,
                LazyInitProxy.class }, handler);
    }

    private static class JDKInvocationHandler implements InvocationHandler,
            Serializable
    {

        private final ObjectResolver _resolver;
        private transient Object _target;

        public JDKInvocationHandler(ObjectResolver resolver)
        {
            _resolver = resolver;
        }

        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            // special method checks
            if (isGetObjectResolverMethod(method)) return _resolver;

            // very simple implementations of equals/hashCode
            // ??is it enough
            if (isEqualsMethod(method))
                return args[0] == proxy ? Boolean.TRUE : Boolean.FALSE;
            if (isHashCodeMethod(method)) return new Integer(this.hashCode());

            // load the target
            if (_target == null) {
                _target = _resolver.resolveObject();
                if (_target == null)
                    throw new IllegalStateException(
                            "Could not load a target from ObjectResolver: "
                                    + _resolver.toString());
            }

            // do the invocation
            Object ret;
            try {
                ret = method.invoke(_target, args);
            } catch (IllegalArgumentException e) {
                throw new IllegalStateException("Tried to call [" + method
                        + "] on [" + _target + "] but could" + "not succeed:",
                        e);
            } catch (IllegalAccessException e) {
                throw e;
            } catch (InvocationTargetException e) {
                throw e.getTargetException();
            }

            // check to return the proxy in case the proxied return itself
            if (ret != null && ret == _target) ret = proxy;
            return ret;
        }
    }

    // ///////////////////////////
    // //////////////////////////
    // cglib proxies
    private static Object makeCGLIBProxy(Class targetClass, ObjectResolver or) {

        // make the calls backs
        Callback[] callbacks = { new CGLIBForwardInterceptor(or),
                new ExternalizeCallback(or, targetClass), NoOp.INSTANCE,
                new LazyInitProxyInterceptor(or) };

        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(targetClass);

        if (!hasExternalizeMethod(targetClass))
            enhancer.setInterfaces(new Class[] { IWriteReplace.class,
                    Serializable.class, LazyInitProxy.class });
        else
            enhancer.setInterfaces(new Class[] { Serializable.class, LazyInitProxy.class });

        enhancer.setCallbacks(callbacks);
        enhancer.setCallbackFilter(SINGLETON_CF);

        Object proxy = enhancer.create();
        return proxy;
    }

    private static ProxyCallbackFilter SINGLETON_CF = new ProxyCallbackFilter();

    private static class ProxyCallbackFilter implements CallbackFilter
    {
        public int accept(Method m) {
            if (isExternalizeMethod(m)) return 1;// externalize interceptor
            if (isToStringMethod(m)) return 0; // forward
            if (isObjectMethod(m)) return 2; // NoOp call to super class
                                                // (also for equals/hashCode)
            if (isGetObjectResolverMethod(m)) return 3;// return the
                                                        // ObjectResolver
            return 0;// all other forward
        }
    }

    // Callbacks
    private static class CGLIBForwardInterceptor implements MethodInterceptor
    {

        private final ObjectResolver _resolver;
        private transient Object _target;

        public CGLIBForwardInterceptor(ObjectResolver or)
        {
            _resolver = or;
        }

        public Object intercept(Object obj, Method m, Object[] args,
                MethodProxy proxy) throws Throwable {
            if (_target == null) {
                _target = _resolver.resolveObject();
                if (_target == null)
                    throw new IllegalStateException("ObjectResolver ["
                            + _resolver + "] did not provide a target");
            }
            Object ret = proxy.invoke(_target, args);
            if (ret != null && ret == _target) ret = obj;
            return ret;
        }

    }

    private static class LazyInitProxyInterceptor implements FixedValue
    {

        private final ObjectResolver _or;

        public LazyInitProxyInterceptor(ObjectResolver or)
        {
            _or = or;
        }

        public Object loadObject() throws Exception {
            return _or;
        }

    }

    private static class ExternalizeCallback implements MethodInterceptor
    {

        private final ObjectResolver _or;
        private final Class _targetClass;

        ExternalizeCallback(ObjectResolver or, Class targetClass)
        {
            _or = or;
            _targetClass = targetClass;
        }

        public Object intercept(Object arg0, Method arg1, Object[] arg2,
                MethodProxy arg3) throws Throwable {
            System.out.println("writeReplace()");
            ProxyReplacement ret = new ProxyReplacement(_or, _targetClass);
            return ret;
        }

    }

    public interface IWriteReplace
    {
        public Object writeReplace() throws ObjectStreamException;
    }

    static class ProxyReplacement implements Serializable
    {
        private final ObjectResolver _or;
        private final String _targetClass;

        ProxyReplacement(ObjectResolver or, Class target)
        {
            _or = or;
            _targetClass = target.getName();
        }

        public Object readResolve() throws ObjectStreamException {
            System.out.println("readResolve()");
            Class cl;
            try {
                cl = Thread.currentThread().getContextClassLoader().loadClass(_targetClass);
            } catch (ClassNotFoundException e) {
               throw new InvalidClassException(_targetClass,"Could not resolve class ["+_targetClass+"] from the context ClassLoader"); 
            }
            return makeProxy(cl, _or);
        }
    }

    // check for externalize method
    private static boolean hasExternalizeMethod(Class targetClass) {
        Class cl = targetClass;
        while (cl != null) {
            Method[] ms = cl.getDeclaredMethods();
            for (int i = 0; i < ms.length; i++) {
                Method m = ms[i];
                if (isExternalizeMethod(m)) {
                    return true;
                }
            }
            cl = cl.getSuperclass();
        }
        return false;
    }

    private static boolean isExternalizeMethod(Method m) {
        if ("writeReplace".equals(m.getName())
                && m.getParameterTypes().length == 0) {
            Class[] exs = m.getExceptionTypes();
            if (exs.length == 1 && exs[0].equals(ObjectStreamException.class))
                return true;
            else
                return false;
        }
        return false;
    }

    // ///////////////////////////////
    // Helpers for checking wheter a method is a certain method
    private static boolean isGetObjectResolverMethod(Method m) {
        if (LazyInitProxy.class.equals(m.getDeclaringClass())) return true;
        return false;
    }

    private static boolean isEqualsMethod(Method m) {
        if ("equals".equals(m.getName()) && m.getParameterTypes().length == 1
                && m.getParameterTypes()[0] == Object.class) return true;
        return false;
    }

    private static boolean isHashCodeMethod(Method m) {
        if ("hashCode".equals(m.getName()) && m.getParameterTypes().length == 0)
            return true;
        return false;
    }

    private static boolean isToStringMethod(Method m) {
        if ("toString".equals(m.getName()) && m.getParameterTypes().length == 0)
            return true;
        return false;
    }

    private static boolean isFinalizeMethod(Method m) {
        if ("finalize".equals(m.getName()) && m.getParameterTypes().length == 0)
            return true;
        return false;
    }

    private final static Map OBJECT_METHOD_NAMES = new HashMap();
    static {
        Method[] ms = Object.class.getDeclaredMethods();
        for (int i = 0; i < ms.length; i++) {
            Method m = ms[i];
            OBJECT_METHOD_NAMES.put(m.getName(), m.getParameterTypes());
        }
    }

    private static boolean isObjectMethod(Method m) {
        Class[] paras = (Class[]) OBJECT_METHOD_NAMES.get(m.getName());
        if (paras != null) {
            return Arrays.equals(paras, m.getParameterTypes());
        }
        return false;
    }

}
