/*
 * Copyright (C) 2016 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.
 */

import java.lang.ref.WeakReference;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;

public class Main {
    public static void main(String[] args) throws Exception {
        try {
            // Check if we're running dalvik or RI.
            Class<?> class_loader_class = Class.forName("dalvik.system.PathClassLoader");
            System.loadLibrary(args[0]);
        } catch (ClassNotFoundException e) {
            usingRI = true;
            // Add expected JNI_OnLoad log line to match expected-stdout.txt.
            System.out.println("JNI_OnLoad called");
        }

        testClearDexCache();
        testMultiDex();
        testRacyLoader();
        testRacyLoader2();
        testMisbehavingLoader();
        testRacyMisbehavingLoader();
        testRacyMisbehavingLoader2();
    }

    private static void testClearDexCache() throws Exception {
        DelegatingLoader delegating_loader = createDelegatingLoader();
        Class<?> helper = delegating_loader.loadClass("Helper1");

        WeakReference<Class<?>> weak_test1 = wrapHelperGet(helper);
        changeInner(delegating_loader);
        clearResolvedTypes(helper);
        Runtime.getRuntime().gc();
        WeakReference<Class<?>> weak_test2 = wrapHelperGet(helper);
        Runtime.getRuntime().gc();

        Class<?> test1 = weak_test1.get();
        if (test1 == null) {
            System.out.println("test1 disappeared");
        }
        Class<?> test2 = weak_test2.get();
        if (test2 == null) {
            System.out.println("test2 disappeared");
        }
        if (test1 != test2) {
            System.out.println("test1 != test2");
        }

        System.out.println("testClearDexCache done");
    }

    private static void testMultiDex() throws Exception {
        DelegatingLoader delegating_loader = createDelegatingLoader();

        Class<?> helper1 = delegating_loader.loadClass("Helper1");
        WeakReference<Class<?>> weak_test1 = wrapHelperGet(helper1);

        changeInner(delegating_loader);

        Class<?> helper2 = delegating_loader.loadClass("Helper2");
        WeakReference<Class<?>> weak_test2 = wrapHelperGet(helper2);

        Runtime.getRuntime().gc();

        Class<?> test1 = weak_test1.get();
        if (test1 == null) {
            System.out.println("test1 disappeared");
        }
        Class<?> test2 = weak_test2.get();
        if (test2 == null) {
            System.out.println("test2 disappeared");
        }
        if (test1 != test2) {
            System.out.println("test1 != test2");
        }

        System.out.println("testMultiDex done");
    }

    private static void testMisbehavingLoader() throws Exception {
        ClassLoader system_loader = ClassLoader.getSystemClassLoader();
        DefiningLoader defining_loader = new DefiningLoader(system_loader);
        MisbehavingLoader misbehaving_loader =
            new MisbehavingLoader(system_loader, defining_loader);
        Class<?> helper = misbehaving_loader.loadClass("Helper1");

        try {
            WeakReference<Class<?>> weak_test = wrapHelperGet(helper);
        } catch (InvocationTargetException ite) {
            String message = ite.getCause().getMessage();
            if (usingRI && "Test".equals(message)) {
              // Replace RI message with dalvik message to match expected-stdout.txt.
              message = "Initiating class loader of type " +
                  misbehaving_loader.getClass().getName() +
                  " returned class Helper2 instead of Test.";
            }
            System.out.println(ite.getCause().getClass().getName() + ": " + message);
        }
        System.out.println("testMisbehavingLoader done");
    }

    private static void testRacyLoader() throws Exception {
        final ClassLoader system_loader = ClassLoader.getSystemClassLoader();

        final Thread[] threads = new Thread[4];
        final Object[] results = new Object[threads.length];

        final RacyLoader racy_loader = new RacyLoader(system_loader, threads.length);
        final Class<?> helper1 = racy_loader.loadClass("Helper1");
        skipVerification(helper1);  // Avoid class loading during verification.

        for (int i = 0; i != threads.length; ++i) {
            final int my_index = i;
            Thread t = new Thread() {
                public void run() {
                    try {
                        Method get = helper1.getDeclaredMethod("get");
                        results[my_index] = get.invoke(null);
                    } catch (InvocationTargetException ite) {
                        results[my_index] = ite.getCause();
                    } catch (Throwable t) {
                        results[my_index] = t;
                    }
                }
            };
            t.start();
            threads[i] = t;
        }
        for (Thread t : threads) {
            t.join();
        }
        dumpResultStats(results, 1);
        System.out.println("testRacyLoader done");
    }

    private static void testRacyLoader2() throws Exception {
        final ClassLoader system_loader = ClassLoader.getSystemClassLoader();

        final Thread[] threads = new Thread[4];
        final Object[] results = new Object[threads.length];

        final RacyLoader racy_loader = new RacyLoader(system_loader, threads.length);
        final Class<?> helper1 = racy_loader.loadClass("Helper1");
        skipVerification(helper1);  // Avoid class loading during verification.
        final Class<?> helper3 = racy_loader.loadClass("Helper3");
        skipVerification(helper3);  // Avoid class loading during verification.

        for (int i = 0; i != threads.length; ++i) {
            final int my_index = i;
            Thread t = new Thread() {
                public void run() {
                    try {
                        Class<?> helper = (my_index < threads.length / 2) ? helper1 : helper3;
                        Method get = helper.getDeclaredMethod("get");
                        results[my_index] = get.invoke(null);
                    } catch (InvocationTargetException ite) {
                        results[my_index] = ite.getCause();
                    } catch (Throwable t) {
                        results[my_index] = t;
                    }
                }
            };
            t.start();
            threads[i] = t;
        }
        for (Thread t : threads) {
            t.join();
        }
        dumpResultStats(results, 2);
        System.out.println("testRacyLoader2 done");
    }

    private static void testRacyMisbehavingLoader() throws Exception {
        final ClassLoader system_loader = ClassLoader.getSystemClassLoader();

        final Thread[] threads = new Thread[4];
        final Object[] results = new Object[threads.length];

        final RacyMisbehavingLoader racy_loader =
            new RacyMisbehavingLoader(system_loader, threads.length, false);
        final Class<?> helper1 = racy_loader.loadClass("RacyMisbehavingHelper");
        skipVerification(helper1);  // Avoid class loading during verification.

        for (int i = 0; i != threads.length; ++i) {
            final int my_index = i;
            Thread t = new Thread() {
                public void run() {
                    try {
                        Method get = helper1.getDeclaredMethod("get");
                        results[my_index] = get.invoke(null);
                    } catch (InvocationTargetException ite) {
                        results[my_index] = ite.getCause();
                    } catch (Throwable t) {
                        results[my_index] = t;
                    }
                }
            };
            t.start();
            threads[i] = t;
        }
        for (Thread t : threads) {
            t.join();
        }
        dumpResultStats(results, 1);
        System.out.println("testRacyMisbehavingLoader done");
    }

    private static void testRacyMisbehavingLoader2() throws Exception {
        final ClassLoader system_loader = ClassLoader.getSystemClassLoader();

        final Thread[] threads = new Thread[4];
        final Object[] results = new Object[threads.length];

        final RacyMisbehavingLoader racy_loader =
            new RacyMisbehavingLoader(system_loader, threads.length, true);
        final Class<?> helper1 = racy_loader.loadClass("RacyMisbehavingHelper");
        skipVerification(helper1);  // Avoid class loading during verification.

        for (int i = 0; i != threads.length; ++i) {
            final int my_index = i;
            Thread t = new Thread() {
                public void run() {
                    try {
                        Method get = helper1.getDeclaredMethod("get");
                        results[my_index] = get.invoke(null);
                    } catch (InvocationTargetException ite) {
                        results[my_index] = ite.getCause();
                    } catch (Throwable t) {
                        results[my_index] = t;
                    }
                }
            };
            t.start();
            threads[i] = t;
        }
        for (Thread t : threads) {
            t.join();
        }
        dumpResultStats(results, 1);
        System.out.println("testRacyMisbehavingLoader2 done");
    }

    private static void dumpResultStats(Object[] results, int expected_unique) throws Exception {
        int throwables = 0;
        int classes = 0;
        int unique_classes = 0;
        for (int i = 0; i != results.length; ++i) {
            Object r = results[i];
            if (r instanceof Throwable) {
                ++throwables;
                System.out.println(((Throwable) r).getMessage());
            } else if (isClassPair(r)) {
                printPair(r);
                Object ref = getSecond(r);
                ++classes;
                ++unique_classes;
                for (int j = 0; j != i; ++j) {
                    Object rj = results[j];
                    if (isClassPair(results[j]) && getSecond(results[j]) == ref) {
                        --unique_classes;
                        break;
                    }
                }
            }
        }
        System.out.println("total: " + results.length);
        System.out.println("  throwables: " + throwables);
        System.out.println("  classes: " + classes
            + " (" + unique_classes + " unique)");
        if (expected_unique != unique_classes) {
            System.out.println("MISMATCH with expected_unique: " + expected_unique);
            ArrayList<Class<?>> list = new ArrayList<Class<?>>();
            for (int i = 0; i != results.length; ++i) {
                Object r = results[i];
                if (isClassPair(r)) {
                    list.add(getSecond(r));
                }
            }
            nativeDumpClasses(list.toArray());
        }
    }

    private static DelegatingLoader createDelegatingLoader() {
        ClassLoader system_loader = ClassLoader.getSystemClassLoader();
        DefiningLoader defining_loader = new DefiningLoader(system_loader);
        return new DelegatingLoader(system_loader, defining_loader);
    }

    private static void changeInner(DelegatingLoader delegating_loader) {
        ClassLoader system_loader = ClassLoader.getSystemClassLoader();
        DefiningLoader defining_loader = new DefiningLoader(system_loader);
        delegating_loader.resetDefiningLoader(defining_loader);
    }

    private static WeakReference<Class<?>> wrapHelperGet(Class<?> helper) throws Exception {
        Method get = helper.getDeclaredMethod("get");
        Object pair = get.invoke(null);
        printPair(pair);
        return new WeakReference<Class<?>>(getSecond(pair));
    }

    private static void printPair(Object pair) throws Exception {
        Method print = pair.getClass().getDeclaredMethod("print");
        print.invoke(pair);
    }

    private static Class<?> getSecond(Object pair) throws Exception {
        Field second = pair.getClass().getDeclaredField("second");
        return (Class<?>) second.get(pair);
    }

    private static boolean isClassPair(Object r) {
        return r != null && r.getClass().getName().equals("ClassPair");
    }

    public static void clearResolvedTypes(Class<?> c) {
        if (!usingRI) {
            nativeClearResolvedTypes(c);
        }
    }

    // Skip verification of a class on ART. Verification can cause classes to be loaded
    // while holding a lock on the class being verified and holding that lock can interfere
    // with the intent of the "racy" tests. In these tests we're waiting in the loadClass()
    // for all the tested threads to synchronize and they cannot reach that point if they
    // are waiting for the class lock on ClassLinker::InitializeClass(Helper1/Helper3).
    public static void skipVerification(Class<?> c) {
        if (!usingRI) {
            nativeSkipVerification(c);
        }
    }

    public static native void nativeClearResolvedTypes(Class<?> c);
    public static native void nativeSkipVerification(Class<?> c);
    public static native void nativeDumpClasses(Object[] array);

    static boolean usingRI = false;
}
