package de.invesdwin.instrument;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.jar.JarEntry;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;

import javax.annotation.concurrent.ThreadSafe;

import org.apache.commons.io.IOUtils;
import org.springframework.context.support.GenericXmlApplicationContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver;
import org.zeroturnaround.exec.ProcessExecutor;
import org.zeroturnaround.exec.stream.slf4j.Slf4jStream;

import de.invesdwin.instrument.internal.AgentClassLoaderReference;
import de.invesdwin.instrument.internal.DynamicInstrumentationAgent;
import de.invesdwin.instrument.internal.DynamicInstrumentationLoadAgentMain;
import de.invesdwin.instrument.internal.JdkFilesFinder;

@ThreadSafe
public final class DynamicInstrumentationLoader {

    private static volatile Throwable threadFailed;
    private static volatile String toolsJarPath;
    private static volatile String attachLibPath;
    /**
     * keeping a reference here so it is not garbage collected
     */
    private static GenericXmlApplicationContext ltwCtx;

    private DynamicInstrumentationLoader() {}

    public static boolean isInitialized() {
        return InstrumentationLoadTimeWeaver.isInstrumentationAvailable();
    }

    public static void waitForInitialized() {
        try {
            while (!isInitialized() && threadFailed == null) {
                TimeUnit.MILLISECONDS.sleep(1);
            }
            if (threadFailed != null) {
                final String javaVersion = getJavaVersion();
                final String javaHome = getJavaHome();
                throw new RuntimeException("Additional information: javaVersion=" + javaVersion + "; javaHome="
                        + javaHome + "; toolsJarPath=" + toolsJarPath + "; attachLibPath=" + attachLibPath,
                        threadFailed);
            }
        } catch (final InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }

    public static synchronized GenericXmlApplicationContext initLoadTimeWeavingContext() {
        org.assertj.core.api.Assertions.assertThat(isInitialized()).isTrue();
        if (ltwCtx == null) {
            final GenericXmlApplicationContext ctx = new GenericXmlApplicationContext();
            ctx.load(new ClassPathResource("/META-INF/ctx.spring.weaving.xml"));
            ctx.refresh();
            ltwCtx = ctx;
        }
        return ltwCtx;
    }

    static {
        if (!isInitialized()) {
            try {
                final File tempAgentJar = createTempAgentJar();
                setAgentClassLoaderReference();
                final String pid = DynamicInstrumentationProperties.getProcessId();
                final Thread loadAgentThread = new Thread() {

                    @Override
                    public void run() {
                        try {
                            loadAgent(tempAgentJar, pid);
                        } catch (final Throwable e) {
                            threadFailed = e;
                            throw new RuntimeException(e);
                        }
                    }
                };

                DynamicInstrumentationReflections.addPathToSystemClassLoader(tempAgentJar);

                final JdkFilesFinder jdkFilesFinder = new JdkFilesFinder();

                if (DynamicInstrumentationReflections.isBeforeJava9()) {
                    final File toolsJar = jdkFilesFinder.findToolsJar();
                    DynamicInstrumentationReflections.addPathToSystemClassLoader(toolsJar);
                    DynamicInstrumentationLoader.toolsJarPath = toolsJar.getAbsolutePath();

                    final File attachLib = jdkFilesFinder.findAttachLib();
                    DynamicInstrumentationReflections.addPathToJavaLibraryPath(attachLib.getParentFile());
                    DynamicInstrumentationLoader.attachLibPath = attachLib.getAbsolutePath();
                }

                loadAgentThread.start();
            } catch (final Exception e) {
                throw new RuntimeException(e);
            }

        }
    }

    private static void loadAgent(final File tempAgentJar, final String pid) throws Exception {
        if (DynamicInstrumentationReflections.isBeforeJava9()) {
            DynamicInstrumentationLoadAgentMain.loadAgent(pid, tempAgentJar.getAbsolutePath());
        } else {
            //-Djdk.attach.allowAttachSelf https://www.bountysource.com/issues/45231289-self-attach-fails-on-jdk9
            //workaround this limitation by attaching from a new process
            final File loadAgentJar = createTempJar(DynamicInstrumentationLoadAgentMain.class, false,
                    de.invesdwin.instrument.internal.DummyAttachProvider.class);
            final String javaExecutable = getJavaHome() + File.separator + "bin" + File.separator + "java";
            final List<String> command = new ArrayList<String>();
            command.add(javaExecutable);
            command.add("-classpath");
            command.add(loadAgentJar.getAbsolutePath()); //tools.jar not needed since java9
            command.add(DynamicInstrumentationLoadAgentMain.class.getName());
            command.add(pid);
            command.add(tempAgentJar.getAbsolutePath());
            new ProcessExecutor().command(command)
                    .destroyOnExit()
                    .exitValueNormal()
                    .redirectOutput(Slf4jStream.of(DynamicInstrumentationLoader.class).asInfo())
                    .redirectError(Slf4jStream.of(DynamicInstrumentationLoader.class).asWarn())
                    .execute();
        }
    }

    private static String getJavaHome() {
        //CHECKSTYLE:OFF
        return System.getProperty("java.home");
        //CHECKSTYLE:ON
    }

    private static String getJavaVersion() {
        //CHECKSTYLE:OFF
        return System.getProperty("java.version");
        //CHECKSTYLE:ON
    }

    private static void setAgentClassLoaderReference() throws Exception {
        final Class<AgentClassLoaderReference> agentClassLoaderReferenceClass = AgentClassLoaderReference.class;
        final File tempAgentClassLoaderJar = createTempJar(agentClassLoaderReferenceClass, false);
        DynamicInstrumentationReflections.addPathToSystemClassLoader(tempAgentClassLoaderJar);
        final ClassLoader systemClassLoader = DynamicInstrumentationReflections.getSystemClassLoader();
        final Class<?> systemAgentClassLoaderReferenceClass = systemClassLoader
                .loadClass(agentClassLoaderReferenceClass.getName());
        final Method setAgentClassLoaderMethod = systemAgentClassLoaderReferenceClass
                .getDeclaredMethod("setAgentClassLoader", ClassLoader.class);
        setAgentClassLoaderMethod.invoke(null, DynamicInstrumentationReflections.getContextClassLoader());
    }

    private static File createTempAgentJar() throws ClassNotFoundException {
        try {
            return createTempJar(DynamicInstrumentationAgent.class, true);
        } catch (final Throwable e) {
            final String message = "Unable to find class [de.invesdwin.instrument.internal.DynamicInstrumentationAgent] in classpath."
                    + "\nPlease make sure you have added invesdwin-instrument.jar to your classpath properly,"
                    + "\nor make sure you have embedded it correctly into your fat-jar."
                    + "\nThey can be created e.g. with \"maven-shade-plugin\"."
                    + "\nPlease be aware that some fat-jar solutions might not work well due to classloader issues.";
            throw new ClassNotFoundException(message, e);
        }
    }

    /**
     * Creates a new jar that only contains the DynamicInstrumentationAgent class.
     */
    private static File createTempJar(final Class<?> clazz, final boolean agent, final Class<?>... additionalClasses)
            throws Exception {
        final String className = clazz.getName();
        final File tempAgentJar = new File(DynamicInstrumentationProperties.TEMP_DIRECTORY, className + ".jar");
        final Manifest manifest = new Manifest(clazz.getResourceAsStream("/META-INF/MANIFEST.MF"));
        if (agent) {
            manifest.getMainAttributes().putValue("Premain-Class", className);
            manifest.getMainAttributes().putValue("Agent-Class", className);
            manifest.getMainAttributes().putValue("Can-Redefine-Classes", String.valueOf(true));
            manifest.getMainAttributes().putValue("Can-Retransform-Classes", String.valueOf(true));
        }
        final JarOutputStream tempJarOut = new JarOutputStream(new FileOutputStream(tempAgentJar), manifest);
        final JarEntry entry = new JarEntry(className.replace(".", "/") + ".class");
        tempJarOut.putNextEntry(entry);
        final InputStream classIn = DynamicInstrumentationReflections.getClassInputStream(clazz);
        IOUtils.copy(classIn, tempJarOut);
        tempJarOut.closeEntry();
        if (additionalClasses != null) {
            for (final Class<?> additionalClazz : additionalClasses) {
                final String additionalClassName = additionalClazz.getName();
                final JarEntry additionalEntry = new JarEntry(additionalClassName.replace(".", "/") + ".class");
                tempJarOut.putNextEntry(additionalEntry);
                final InputStream additionalClassIn = DynamicInstrumentationReflections
                        .getClassInputStream(additionalClazz);
                IOUtils.copy(additionalClassIn, tempJarOut);
                tempJarOut.closeEntry();
            }
        }
        tempJarOut.close();
        return tempAgentJar;
    }

}