// === Auto Jvm Hacker 1.5 ===
// project: https://github.com/InkerBot/AutoHackerJvm

// The MIT License (MIT)
//
// Copyright (c) 2023 InkerBot
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

// @formatter:off
package pers.neige.neigeitems;

import sun.misc.Unsafe;

import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.instrument.Instrumentation;
import java.lang.invoke.MethodHandles;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.StringJoiner;

@SuppressWarnings("all")
public final class JvmHacker {
    private static final String AUTHOR = "https://github.com/InkerBot/AutoHackerJvm";

    private static final String AGENT_MAIN_CLASS = new String(new char[]{'b', 'o', 't', '.', 'i', 'n', 'k', 'e', 'r', '.', 'a', 'c', 'j', '.', 'a', 'g', 'e', 'n', 't', '.', 'A', 'c', 'j', 'A', 'g', 'e', 'n', 't', 'M', 'a', 'i', 'n'});
    private static final String AGENT_REFERENCE_CLASS = new String(new char[]{'b', 'o', 't', '.', 'i', 'n', 'k', 'e', 'r', '.', 'a', 'c', 'j', '.', 'a', 'g', 'e', 'n', 't', '.', 'A', 'c', 'j', 'A', 'g', 'e', 'n', 't', 'R', 'e', 'f', 'e', 'r', 'e', 'n', 'c', 'e'});
    private static final byte[] AGENT_PAYLOAD_FILE = Base64.getDecoder().decode(" ");
    private static final DcLazy<Instrumentation> lazy$instrumentation = new DcLazy<>(() -> {
        Class<?> agentReferenceClass = null;
        try {
            agentReferenceClass = Class.forName(AGENT_REFERENCE_CLASS);
        } catch (ClassNotFoundException e) {
            //
        }
        if (agentReferenceClass == null) {
            attachAgent();
            agentReferenceClass = Class.forName(AGENT_REFERENCE_CLASS);
        }
        return (Instrumentation) agentReferenceClass.getDeclaredField("instrumentation").get(null);
    });
    private static final DcLazy<Unsafe> lazy$unsafe = new DcLazy<>(() -> {
        Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafeField.setAccessible(true);
        return (Unsafe) theUnsafeField.get(null);
    });
    private static final DcLazy<MethodHandles.Lookup> lazy$lookup = new DcLazy<>(() -> {
        Field implLookupField = MethodHandles.Lookup.class.getDeclaredField("IMPL_LOOKUP");
        return (MethodHandles.Lookup) unsafe().getObject(unsafe().staticFieldBase(implLookupField), unsafe().staticFieldOffset(implLookupField));
    });

    private JvmHacker() {
        throw new UnsupportedOperationException();
    }

    public static Unsafe unsafe() {
        return lazy$unsafe.get();
    }

    public static MethodHandles.Lookup lookup() {
        return lazy$lookup.get();
    }

    public static Instrumentation instrumentation() {
        return lazy$instrumentation.get();
    }

    private static <E extends Throwable, R> R throwImpl(Throwable e) throws E {
        throw (E) e;
    }

    private static void attachAgent() throws Throwable {
        File agentFile = createTempJar();
        String processId = getProcessId();
        try {
            attachSelf(processId, agentFile);
            return;
        } catch (Throwable ignored) {
        }
        try {
            runAttach(processId, agentFile);
            return;
        } catch (Throwable ignored) {
        }
        throw new IllegalStateException("Failed to attach agent hacker");
    }

    private static File createTempJar() throws Throwable {
        File jarFile = File.createTempFile("acj_agent_payload_", ".jar");
        jarFile.deleteOnExit();
        try (OutputStream out = new FileOutputStream(jarFile)) {
            out.write(AGENT_PAYLOAD_FILE);
        }
        return jarFile;
    }

    private static Class<?> getVirtualMachine() throws Throwable {
        try {
            return Class.forName("com.sun.tools.attach.VirtualMachine");
        } catch (ClassNotFoundException ignored) {
        }
        try {
            return Class.forName("com.ibm.tools.attach.VirtualMachine");
        } catch (ClassNotFoundException ignored) {
        }
        List<File> possibleToolsJars = scanPossibleToolsJars();
        URL[] urls = new URL[possibleToolsJars.size()];
        for (int i = 0; i < possibleToolsJars.size(); i++) {
            urls[i] = possibleToolsJars.get(i).toURI().toURL();
        }
        URLClassLoader cl = new URLClassLoader(urls, null);
        try {
            return Class.forName("com.sun.tools.attach.VirtualMachine", true, cl);
        } catch (ClassNotFoundException ignored) {
        }
        try {
            return Class.forName("com.ibm.tools.attach.VirtualMachine", true, cl);
        } catch (ClassNotFoundException ignored) {
        }
        throw new ClassNotFoundException("com.sun.tools.attach.VirtualMachine and com.ibm.tools.attach.VirtualMachine");
    }

    private static String getProcessId() {
        try {
            return getProcessIdFromProcessHandle();
        } catch (Throwable ignored) {
        }
        return getProcessIdFromMx();
    }

    private static String getProcessIdFromProcessHandle() throws Throwable {
        Class<?> processHandleClass = Class.forName("java.lang.ProcessHandle");
        Method currentMethod = processHandleClass.getMethod("current");
        Method pidMethod = processHandleClass.getMethod("pid");
        return pidMethod.invoke(currentMethod.invoke(null)).toString();
    }

    private static String getProcessIdFromMx() {
        String runtimeName = ManagementFactory.getRuntimeMXBean().getName();
        int processIdIndex = runtimeName.indexOf('@');
        if (processIdIndex == -1) {
            throw new IllegalStateException("Cannot extract process id from runtime management bean");
        } else {
            return runtimeName.substring(0, processIdIndex);
        }
    }

    private static List<File> scanPossibleToolsJars() {
        String javaHome = System.getProperty("java.home");
        String[] possibleToolsJars = new String[]{
                "../lib/tools.jar",
                "lib/tools.jar",
                "../Classes/classes.jar"
        };
        List<File> result = new ArrayList<>(1);
        for (String possibleToolsJar : possibleToolsJars) {
            File possibleToolsJarFile = new File(javaHome, possibleToolsJar);
            if (possibleToolsJarFile.isFile()) {
                result.add(possibleToolsJarFile);
            }
        }
        return result;
    }

    private static void attachSelf(String processId, File agentFile) throws Throwable {
        Class<?> virtualMachineClass = getVirtualMachine();
        Method attachMethod = virtualMachineClass.getMethod("attach", String.class);
        Object virtualMachine = attachMethod.invoke(null, processId);

        try {
            Method loadAgentMethod = virtualMachineClass.getMethod("loadAgent", String.class, String.class);
            loadAgentMethod.invoke(virtualMachine, agentFile.getAbsolutePath(), "");
        } finally {
            Method detachMethod = virtualMachineClass.getMethod("detach");
            detachMethod.invoke(virtualMachine);
        }
    }

    private static void runAttach(String processId, File agentFile) throws Throwable {
        String javaHome = System.getProperty("java.home");
        StringJoiner classPathJoiner = new StringJoiner(":");
        for (File possibleToolsJar : scanPossibleToolsJars()) {
            classPathJoiner.add(possibleToolsJar.getAbsolutePath());
        }
        classPathJoiner.add(agentFile.getAbsolutePath());
        if (new ProcessBuilder(javaHome + "/bin/" + (File.separatorChar == '\\' ? "java.exe" : "java"), "-cp", classPathJoiner.toString(), AGENT_MAIN_CLASS, processId, agentFile.getAbsolutePath()).inheritIO().start().waitFor() != 0) {
            throw new IllegalStateException("Could not self-attach to current VM using external process");
        }
    }

    private static class DcLazy<T> {
        private static final Object NO_INIT = new Object();
        private final Provider<T> provider;
        private volatile T object = (T) NO_INIT;

        public DcLazy(Provider<T> provider) {
            this.provider = provider;
        }

        public T get() {
            T result = object;
            if (result == NO_INIT) {
                synchronized (this) {
                    result = object;
                    if (result == NO_INIT) {
                        try {
                            result = provider.provide();
                        } catch (Throwable e) {
                            return throwImpl(e);
                        }
                        object = result;
                    }
                }
            }
            return result;
        }

        @FunctionalInterface
        private interface Provider<T> {
            T provide() throws Throwable;
        }
    }
}