/**
 * 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.apache.openejb.client.proxy;

import org.apache.openejb.client.ClientRuntimeException;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.util.LinkedHashSet;
import java.util.Properties;
import java.util.Set;

public class Jdk13ProxyFactory implements ProxyFactory {

    private final Class[] constructorParams = {java.lang.reflect.InvocationHandler.class};

    @Override
    public void init(final Properties props) {
        String version = "";
        final String badVersion = "1.3.0-";
        try {
            version = System.getProperty("java.vm.version");
        } catch (Exception e) {
            //Ignore
        }

        if (version.contains(badVersion)) {
            final String message = "" +
                "INCOMPATIBLE VM: \n\n" +
                "The Java Virtual Machine you are using contains a bug\n" +
                "in the proxy generation logic.  This bug has been    \n" +
                "documented by Sun and has been fixed in later VMs.   \n" +
                "Please download the latest 1.3 Virtual Machine.      \n" +
                "For more details see:                                \n" +
                "http://developer.java.sun.com/developer/bugParade/bugs/4346224.html\n  ";
            throw new ClientRuntimeException(message);
        }
    }

    @Override
    public InvocationHandler getInvocationHandler(final Object proxy) throws IllegalArgumentException {

        final Jdk13InvocationHandler handler = (Jdk13InvocationHandler) Proxy.getInvocationHandler(proxy);

        if (handler == null) {
            return null;
        }

        return handler.getInvocationHandler();
    }

    @Override
    public Object setInvocationHandler(final Object proxy, final InvocationHandler handler) throws IllegalArgumentException {

        final Jdk13InvocationHandler jdk13 = (Jdk13InvocationHandler) Proxy.getInvocationHandler(proxy);

        if (jdk13 == null) {
            throw new IllegalArgumentException("Proxy " + proxy + " unknown!");
        }

        return jdk13.setInvocationHandler(handler);
    }

    @Override
    public Class getProxyClass(final Class interfce) throws IllegalArgumentException {
        return Proxy.getProxyClass(interfce.getClassLoader(), new Class[]{interfce});
    }

    @Override
    public Class getProxyClass(final Class[] interfaces) throws IllegalArgumentException {
        if (interfaces.length < 1) {
            throw new IllegalArgumentException("There must be at least one interface to implement.");
        }

        return Proxy.getProxyClass(interfaces[0].getClassLoader(), interfaces);
    }

    @Override
    public boolean isProxyClass(final Class cl) {
        return Proxy.isProxyClass(cl);
    }

    @SuppressWarnings("unchecked")
    @Override
    public Object newProxyInstance(final Class proxyClass) throws IllegalArgumentException {
        if (!Proxy.isProxyClass(proxyClass)) {
            throw new IllegalArgumentException("This class is not a proxy.");
        }

        try {

            final Constructor cons = proxyClass.getConstructor(constructorParams);
            //noinspection RedundantArrayCreation
            return cons.newInstance(new Object[]{new Jdk13InvocationHandler()});

        } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException e) {
            throw new InternalError(e.toString());
        }
    }

    @Override
    public Object newProxyInstance(final Class interfce, final InvocationHandler h) throws IllegalArgumentException {

        final Jdk13InvocationHandler handler = new Jdk13InvocationHandler(h);

        return Proxy.newProxyInstance(interfce.getClassLoader(), new Class[]{interfce}, handler);
    }

    @Override
    public Object newProxyInstance(final Class[] interfaces, final InvocationHandler h) throws IllegalArgumentException {
        if (interfaces.length < 1) {
            throw new IllegalArgumentException("There must be at least one interface to implement.");
        }

        final Jdk13InvocationHandler handler = new Jdk13InvocationHandler(h);
        try {
            return Proxy.newProxyInstance(interfaces[0].getClassLoader(), interfaces, handler);
        } catch (IllegalArgumentException iae) {
            final ClassLoader reconciliatedCl = reconciliate(interfaces);
            try {
                reconciliatedCl.loadClass(interfaces[0].getName());
                return Proxy.newProxyInstance(reconciliatedCl, interfaces, handler);
            } catch (ClassNotFoundException e2) {
                throw iae;
            }
        }
    }

    private static ClassLoader reconciliate(final Class<?>... interfaces) {
        final Set<ClassLoader> classloaders = new LinkedHashSet<ClassLoader>();
        for (final Class<?> clazz : interfaces) {
            classloaders.add(clazz.getClassLoader());
        }
        return new MultipleClassLoadersClassLoader(classloaders.toArray(new ClassLoader[classloaders.size()]));
    }

    private static class MultipleClassLoadersClassLoader extends ClassLoader {

        private ClassLoader[] delegatingClassloaders;

        public MultipleClassLoadersClassLoader(final ClassLoader[] classLoaders) {
            super(classLoaders[0]);
            delegatingClassloaders = classLoaders;
        }

        @Override
        public Class<?> loadClass(final String name) throws ClassNotFoundException {
            ClassNotFoundException ex = null;
            for (final ClassLoader cl : delegatingClassloaders) {
                try {
                    return cl.loadClass(name);
                } catch (ClassNotFoundException cnfe) {
                    if (ex == null) {
                        ex = cnfe;
                    }
                }
            }
            throw ex;
        }
    }
}
