/*
 * Copyright (C) 2010 The Android Open Source Project
 *
 * 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 libcore.java.lang.reflect;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import junit.framework.TestCase;

/**
 * This class creates another class loader to load multiple copies of various
 * classes into the VM at once. Then it verifies that reflection resolves the
 * class names using the correct class loader.
 */
public final class ClassLoaderReflectionTest extends TestCase {

    /*
     * Each of these class instances points to a different copy of the class
     * than the one in the application class loader!
     */
    private Class<?> aClass;
    private Class<?> aListClass;
    private Class<?> bClass;
    private Class<?> bStringClass;
    private Class<?> cClass;
    private Class<?> dClass;
    private Class<?> eClass;
    private Class<?> fClass;

    @Override protected void setUp() throws Exception {
        String prefix = ClassLoaderReflectionTest.class.getName();
        ClassLoader loader = twoCopiesClassLoader(prefix, getClass().getClassLoader());
        aClass = loader.loadClass(prefix + "$A");
        bClass = loader.loadClass(prefix + "$B");
        cClass = loader.loadClass(prefix + "$C");
        dClass = loader.loadClass(prefix + "$D");
        eClass = loader.loadClass(prefix + "$E");
        fClass = loader.loadClass(prefix + "$F");
        aListClass = loader.loadClass(prefix + "$AList");
        bStringClass = loader.loadClass(prefix + "$BString");
    }

    public void testLoadOneClassInTwoClassLoadersSimultaneously() throws Exception {
        assertEquals(aClass.getName(), A.class.getName());
        assertNotSame(aClass, A.class);
    }

    public void testField() throws Exception {
        assertEquals(aClass, aListClass.getDeclaredField("field").getType());
    }

    /**
     * http://code.google.com/p/android/issues/detail?id=10111
     */
    public void testGenericSuperclassParameter() throws Exception {
        assertParameterizedType(aListClass.getGenericSuperclass(), ArrayList.class, aClass);
    }

    public void testGenericSuperclassRawType() throws Exception {
        assertParameterizedType(bStringClass.getGenericSuperclass(), bClass, String.class);
    }

    public void testTypeParameters() throws Exception {
        TypeVariable<? extends Class<?>>[] typeVariables = cClass.getTypeParameters();
        assertEquals(2, typeVariables.length);
        assertTypeVariable(typeVariables[0], "K", String.class);
        assertTypeVariable(typeVariables[1], "V", aClass);
    }

    public void testGenericInterfaces() throws Exception {
        Type[] types = eClass.getGenericInterfaces();
        assertEquals(2, types.length);
        // TODO: this test incorrectly assumes that interfaces will be returned in source order!
        assertParameterizedType(types[0], Callable.class, aClass);
        assertParameterizedType(types[1], dClass, aClass);
    }

    public void testFieldGenericType() throws Exception {
        Field bString = fClass.getDeclaredField("bString");
        assertParameterizedType(bString.getGenericType(), bClass, String.class);
        Field listA = fClass.getDeclaredField("listA");
        assertParameterizedType(listA.getGenericType(), List.class, aClass);
    }

    public void testConstructorGenericType() throws Exception {
        Constructor<?> constructor = fClass.getDeclaredConstructors()[0];
        Type[] parameters = constructor.getGenericParameterTypes();
        assertParameterizedType(parameters[0], bClass, String.class);
        assertParameterizedType(parameters[1], List.class, aClass);
    }

    public void testMethodGenericReturnType() throws Exception {
        Method method = fClass.getDeclaredMethod("method", bClass, List.class);
        assertParameterizedType(method.getGenericReturnType(), bClass, String.class);
    }

    public void testMethodGenericParameterTypes() throws Exception {
        Method method = fClass.getDeclaredMethod("method", bClass, List.class);
        Type[] types = method.getGenericParameterTypes();
        assertEquals(2, types.length);
        assertParameterizedType(types[0], bClass, String.class);
        assertParameterizedType(types[1], List.class, aClass);
    }

    static class A {}
    static class B<T> {
        T field;
    }
    static class C<K extends String, V extends A> {}
    interface D<T> {}
    class E implements Callable<A>, D<A> {
        public A call() throws Exception {
            return null;
        }
    }
    class F {
        B<String> bString;
        List<A> listA;
        F(B<String> parameter, List<A> anotherParameter) {}
        B<String> method(B<String> parameter, List<A> anotherParameter) {
            return null;
        }
    }
    static class AList extends ArrayList<A> {
        A field;
    }
    static class BString extends B<String> {}

    /**
     * Returns a class loader that permits multiple copies of the same class to
     * be loaded into the same VM at the same time. This loads classes using the
     * same classpath as the application class loader.
     *
     * @param prefix the prefix of classes that can be loaded by both the
     *     returned class loader and the application class loader.
     */
    private ClassLoader twoCopiesClassLoader(final String prefix, ClassLoader parent)
            throws IOException, InterruptedException {

        /*
         * To load two copies of a given class in the VM, we end up creating two
         * new class loaders: a bridge class loader and a leaf class loader.
         *
         * The bridge class loader is a child of the application class loader.
         * It never loads any classes. All it does is decide when to delegate to
         * the application class loader (which has a copy of everything) and
         * when to fail.
         *
         * The leaf class loader is a child of the bridge class loader. It
         * uses the same classpath as the application class loader. It loads
         * anything that its parent failed on.
         */

        ClassLoader bridge = new ClassLoader(parent) {
            @Override protected Class<?> loadClass(String className, boolean resolve)
                    throws ClassNotFoundException {
                if (className.startsWith(prefix)) {
                    /* throwing will cause the child class loader to load the class. */
                    throw new ClassNotFoundException();
                } else {
                    return super.loadClass(className, resolve);
                }
            }
        };

        try {
            // first try to create a PathClassLoader for a dalvik VM...
            String classPath = System.getProperty("java.class.path");
            return (ClassLoader) Class.forName("dalvik.system.PathClassLoader")
                    .getConstructor(String.class, ClassLoader.class)
                    .newInstance(classPath, bridge);
        } catch (Exception ignored) {
        }

        // fall back to a URLClassLoader on a JVM
        List<URL> classpath = new ArrayList<URL>();
        classpath.addAll(classpathToUrls("java.class.path"));
        classpath.addAll(classpathToUrls("sun.boot.class.path"));
        return new URLClassLoader(classpath.toArray(new URL[classpath.size()]), bridge);
    }

    private List<URL> classpathToUrls(String propertyName) throws MalformedURLException {
        String classpath = System.getProperty(propertyName);
        List<URL> result = new ArrayList<URL>();
        for (String pathElement : classpath.split(File.pathSeparator)) {
            result.add(new File(pathElement).toURI().toURL());
        }
        return result;
    }

    private void assertParameterizedType(Type actual, Type raw, Type... args) {
        assertTrue(actual.toString(), actual instanceof ParameterizedType);
        ParameterizedType parameterizedType = (ParameterizedType) actual;
        assertEquals(raw, parameterizedType.getRawType());
        assertEquals(Arrays.<Type>asList(args),
                Arrays.asList(parameterizedType.getActualTypeArguments()));
    }

    private void assertTypeVariable(TypeVariable actual, String name, Type... bounds) {
        assertEquals(name, actual.getName());
        assertEquals(Arrays.<Type>asList(bounds), Arrays.asList(actual.getBounds()));
    }
}
