/*
 * Copyright (c) 2014, 2018, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package gc.g1.unloading.loading;

import gc.g1.unloading.ExecutionTask;
import gc.g1.unloading.bytecode.*;
//import gc.g1.unloading.check.*;
import gc.g1.unloading.check.Assertion;
import gc.g1.unloading.check.ClassAssertion;
import gc.g1.unloading.check.PhantomizedAssertion;

import gc.g1.unloading.check.FinalizedAssertion;
import gc.g1.unloading.check.PhantomizationServiceThread;
import gc.g1.unloading.check.cleanup.UnusedThreadKiller;
import gc.g1.unloading.classloaders.DoItYourselfClassLoader;
import gc.g1.unloading.classloaders.FinalizableClassloader;
import gc.g1.unloading.classloaders.JNIClassloader;
import gc.g1.unloading.classloaders.ReflectionClassloader;
import gc.g1.unloading.configuration.ClassloadingMethod;
import gc.g1.unloading.configuration.KeepRefMode;
import gc.g1.unloading.configuration.TestConfiguration;
import gc.g1.unloading.keepref.*;
import nsk.share.test.ExecutionController;
import sun.hotspot.WhiteBox;
import jdk.internal.misc.Unsafe;

import java.lang.ref.*;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.LinkedList;
import java.util.Random;

/**
 * This helper performs dirty job: loads classes, instantiate objects, performs redefinition etc...
 */
public class ClassLoadingHelper {

    private static final int NATIVE_VERBOSITY = 2;

    private static final Object[] NO_CP_PATCHES = new Object[0];

    private static BytecodeFactory bf;

    private ExecutionController executionController;

    private PhantomizationServiceThread phantomizationServiceThread;

    private Random random;

    private TestConfiguration configuration;

    /**
     * Constructor that creates instance of helper. All arguments are self-explaining.
     * @param executionController
     * @param randomSeed
     * @param testConfiguration
     */
    public ClassLoadingHelper(ExecutionController executionController,
                              long randomSeed, TestConfiguration testConfiguration) {
        random = new Random(randomSeed);
        this.executionController = executionController;
        this.configuration = testConfiguration;

        phantomizationServiceThread = new PhantomizationServiceThread(executionController);
        Thread thread = new Thread(phantomizationServiceThread);
        thread.setDaemon(true);
        thread.start();

        if (configuration.isInMemoryCompilation() && !configuration.isHumongousClass() && !(configuration.getKeepRefMode() == KeepRefMode.THREAD_ITSELF)) {
            bf = new BytecodeGeneratorFactory(random.nextLong());
        } else {
            if (configuration.isHumongousClass()) {
                bf = new BytecodeMutatorFactory(HumongousTemplateClass.class.getName());
            } else if (configuration.getKeepRefMode() == KeepRefMode.THREAD_ITSELF) {
                bf = new BytecodeMutatorFactory(ThreadTemplateClass.class.getName());
            } else {
                bf = new BytecodeMutatorFactory();
            }
        }
    }

    /**
     * Load class that's supposed to live. Method returns collection of assertions to check it will live.
     * @param className_
     * @return
     */
    public Collection<Assertion> loadClassThatGonnaLive(String className_) {
        Bytecode kit = bf.createBytecode(className_);
        String className = kit.getClassName();
        byte[] bytecode = kit.getBytecode();
        Class<?> clazz = loadClass(className, bytecode);
        Object object = instantiateObject(clazz);
        Object referenceToKeep = configuration.getWhatToKeep().decideUponRefToKeep(clazz, clazz.getClassLoader(), object);

        redefineIfNeeded(bytecode, clazz);

        warmUpClassIfNeeded(object);
        Assertion assertion;
        // The JVM prepends the host class's package to the anonymous class name.
        if (configuration.getClassloadingMethod() != ClassloadingMethod.ANONYMOUS_CLASSLOADER) {
            assertion = new ClassAssertion(className, true);
        } else {
            assertion = new ClassAssertion("gc/g1/unloading/loading/" + className, true);
        }
        switch (configuration.getKeepRefMode()) {
            case STRONG_REFERENCE:
                assertion.keepLink(referenceToKeep);
                break;
            case SOFT_REFERENCE:
                assertion.keepLink(new SoftReference<Object>(referenceToKeep));
                break;
            case STATIC_FIELD:
                RefHolder holder1 = new InStaticFieldHolder();
                assertion.keepLink(holder1.hold(referenceToKeep));
                break;
            case STACK_LOCAL:
                RefHolder holder2 = new InStackLocalHolder(); // UnusedThreadKiller
                assertion.keepLink(holder2.hold(referenceToKeep));
                break;
            case THREAD_FIELD:
                RefHolder holder3 = new InThreadFieldHolder(); // UnusedThreadKiller
                assertion.keepLink(holder3.hold(referenceToKeep));
                break;
            case THREAD_ITSELF:
                Thread objectThread = (Thread) object;
                objectThread.setDaemon(true);
                objectThread.start();
                assertion.keepLink(new UnusedThreadKiller(objectThread.getId())); // UnusedThreadKiller
                break;
            case STATIC_FIELD_OF_ROOT_CLASS:
                RefHolder holder4 = new NullClassloaderHolder();
                Object keep = holder4.hold(referenceToKeep);
                if (keep != null) {
                    assertion.keepLink(keep);
                }
                break;
            case JNI_GLOBAL_REF:
                JNIGlobalRefHolder holder5 = new JNIGlobalRefHolder();
                assertion.keepLink(holder5.hold(referenceToKeep));
                break;
            case JNI_LOCAL_REF:
                JNILocalRefHolder holder6 = new JNILocalRefHolder();
                assertion.keepLink(holder6.hold(referenceToKeep));
                break;
        }

        Collection<Assertion> returnValue = new LinkedList<>();
        returnValue.add(assertion);
        return returnValue;
    }

    /**
     * Load class that's supposed to be unloaded. Method returns collection of assertions to check it will be unloaded.
     * @param className_
     * @return
     */
    public Collection<Assertion> loadClassThatGonnaDie(String className_) {
        Collection<Assertion> returnValue = new LinkedList<>();
        Bytecode kit = bf.createBytecode(className_);
        String className = kit.getClassName();
        byte[] bytecode = kit.getBytecode();
        Class<?> clazz = loadClass(className, bytecode);
        FinalizableClassloader cl = null;
        if (clazz.getClassLoader() instanceof FinalizableClassloader) {
            cl = (FinalizableClassloader) clazz.getClassLoader();
        }
        Object object = instantiateObject(clazz);
        Object referenceToKeep = configuration.getWhatToKeep().decideUponRefToKeep(clazz, clazz.getClassLoader(), object);

        redefineIfNeeded(bytecode, clazz);

        warmUpClassIfNeeded(object);
        Assertion assertion;
        // The JVM prepends the host class's package to the anonymous class name.
        if (configuration.getClassloadingMethod() != ClassloadingMethod.ANONYMOUS_CLASSLOADER) {
            assertion = new ClassAssertion(className, false);
        } else {
            assertion = new ClassAssertion("gc/g1/unloading/loading/" + className, false);
        }
        switch (configuration.getReleaseRefMode()) {
            case WEAK:
                assertion.keepLink(new WeakReference<Object>(referenceToKeep));
                break;
            case PHANTOM:
                final ReferenceQueue queue = new ReferenceQueue<Object>();
                assertion.keepLink(new PhantomReference<Object>(referenceToKeep, queue));
                new Thread(new ReferenceCleaningThread(executionController, queue)).start();
                break;
        }
        returnValue.add(assertion);

        if (cl != null) {
            // Check that classloader will be finalized
            FinalizedAssertion finalizedAssertion = new FinalizedAssertion();
            cl.setFinalizedAssertion(finalizedAssertion);
            returnValue.add(finalizedAssertion);

            // Check that classloader will be phantomized
            PhantomizedAssertion phantomizedAssertion = new PhantomizedAssertion();
            PhantomReference phantomReference = new PhantomReference<Object>(cl, phantomizationServiceThread.getQueue());
            phantomizationServiceThread.add(phantomReference, phantomizedAssertion);
            returnValue.add(phantomizedAssertion);
        }
        return returnValue;
    }

    private void redefineIfNeeded(byte[] bytecode, Class<?> clazz) {
        if (configuration.isRedefineClasses()) {
            BytecodePatcher.patch(bytecode);
            makeRedefinition(NATIVE_VERBOSITY, clazz, bytecode);

            // This will call class's method
            instantiateObject(clazz);
        }
    }

    private Class<?> loadClass(String className, byte[] bytecode) {
        try {
            switch (configuration.getClassloadingMethod()) {
                case PLAIN:
                    DoItYourselfClassLoader loader1 = new DoItYourselfClassLoader();
                    return loader1.defineClass(className, bytecode);
                case REFLECTION:
                    return Class.forName(className, true, new ReflectionClassloader(bytecode, className));
                case JNI:
                    return JNIClassloader.loadThroughJNI(className, bytecode);
                case ANONYMOUS_CLASSLOADER:
                    return getUnsafe().defineAnonymousClass(ClassLoadingHelper.class, bytecode, NO_CP_PATCHES);
            }
            return null;
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Test bug!", e);
        }
    }

    private Object instantiateObject(Class<?> clazz) {
        try {
            Object object = clazz.newInstance();

            // Call method just for fun
            for (Method m : clazz.getMethods()) {
                if (m.getName().equals("main")) {
                    m.invoke(object);
                }
            }
            return object;
        } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
            throw new RuntimeException("Test bug!", e);
        }
    }

    private void warmUpClassIfNeeded(Object object) {
        if (configuration.getCompilationLevel() < 1 || configuration.getCompilationNumber() == 0) {
            return;
        }
        Method m = null;
        for (Method method : object.getClass().getMethods()) {
            if (method.getName().equalsIgnoreCase("methodForCompilation")) {
                m = method;
            }
        }
        WhiteBox wb = WhiteBox.getWhiteBox();
        if (!wb.isMethodCompilable(m)) {
            throw new RuntimeException("Test bug! Method occured to be not compilable. Requires investigation.");
        }

        for (int i = configuration.getCompilationNumber(); i >= 0 && executionController.continueExecution(); i--) {
            if (!wb.isMethodCompilable(m, configuration.getCompilationLevel())) {
              continue;
            }
            wb.enqueueMethodForCompilation(m, configuration.getCompilationLevel());
            while (!wb.isMethodCompiled(m) && executionController.continueExecution()) {
                sleep(50);
                try {
                    m.invoke(object, new Object());
                } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
                    throw new RuntimeException("Something went wrong while compilation", e);
                }
            }
            if (i > 0) {
                wb.deoptimizeMethod(m);
            }
        }
    }

    native static int makeRedefinition0(int verbose, Class<?> redefClass, byte[] classBytes);

    private static void makeRedefinition(int verbose, Class<?> redefClass, byte[] classBytes) {
        new LibLoader().hashCode();
        if (makeRedefinition0(verbose, redefClass, classBytes) != 0) {
            throw new RuntimeException("Test bug: native method \"makeRedefinition\" return nonzero");
        }
    }

    private static void sleep(long millis) {
        try {
            Thread.sleep(millis);
        } catch (InterruptedException e) {
            throw new RuntimeException("Got InterruptedException while sleeping.", e);
        }
    }

    private static Unsafe getUnsafe() {
        return Unsafe.getUnsafe();
    }

}

class ReferenceCleaningThread extends ExecutionTask {

    private ReferenceQueue<?> queue;

    public ReferenceCleaningThread(ExecutionController executionController, ReferenceQueue<?> queue) {
        super(executionController);
        this.queue = queue;
    }

    @Override
    protected void task() throws Exception {
        Reference<?> ref = queue.remove(100);
        if (ref != null) {
            ref.clear();
            return;
        }
    }

}