package example.jvm.diagnostics;

import java.lang.ref.Reference;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.LockSupport;

public final class DiagnosticTarget {
    private static final int BLOCK_SIZE = 1024 * 1024;
    private static final int ATTEMPTED_BLOCKS = 160;
    private static volatile boolean running = true;
    private static volatile long checksum;

    private DiagnosticTarget() {
    }

    public static void main(String[] args) throws Exception {
        if (args.length == 0) {
            throw new IllegalArgumentException("usage: live <release-file> | bounded | unbounded");
        }
        switch (args[0]) {
            case "live" -> runLive(Path.of(requireArgument(args, 1)));
            case "bounded" -> allocate(true);
            case "unbounded" -> allocate(false);
            default -> throw new IllegalArgumentException("unknown mode: " + args[0]);
        }
    }

    private static void runLive(Path releaseFile) throws Exception {
        List<byte[]> retained = new ArrayList<>();
        for (int index = 0; index < 12; index++) {
            byte[] block = new byte[BLOCK_SIZE];
            block[0] = (byte) index;
            retained.add(block);
        }

        Object monitor = new Object();
        CountDownLatch ownerReady = new CountDownLatch(1);
        Thread owner = new Thread(() -> holdMonitor(monitor, ownerReady), "diagnostic-lock-owner");
        Thread waiter = new Thread(() -> waitForMonitor(monitor), "diagnostic-lock-waiter");
        Thread cpuWorker = new Thread(DiagnosticTarget::burnCpu, "diagnostic-cpu-worker");

        owner.start();
        ownerReady.await();
        waiter.start();
        cpuWorker.start();

        System.out.println("pid=" + ProcessHandle.current().pid());
        System.out.println("retained-mib=" + retained.size());
        System.out.println("release-file=" + releaseFile);
        System.out.flush();

        long deadline = System.nanoTime() + 300_000_000_000L;
        while (!Files.exists(releaseFile) && System.nanoTime() < deadline) {
            Thread.sleep(25L);
        }

        running = false;
        owner.interrupt();
        cpuWorker.interrupt();
        owner.join();
        waiter.join();
        cpuWorker.join();
        System.out.println("live-checksum=" + checksum);
        Reference.reachabilityFence(retained);
    }

    private static void holdMonitor(Object monitor, CountDownLatch ownerReady) {
        synchronized (monitor) {
            ownerReady.countDown();
            while (running) {
                LockSupport.parkNanos(10_000_000L);
            }
        }
    }

    private static void waitForMonitor(Object monitor) {
        synchronized (monitor) {
            checksum ^= 0x5a5a5a5aL;
        }
    }

    private static void burnCpu() {
        long value = 0x9e3779b97f4a7c15L;
        while (running && !Thread.currentThread().isInterrupted()) {
            value = Long.rotateLeft(value ^ 0xbf58476d1ce4e5b9L, 13) + 0x94d049bb133111ebL;
        }
        checksum ^= value;
    }

    private static void allocate(boolean bounded) {
        List<byte[]> retained = new ArrayList<>();
        long value = 0L;
        for (int index = 0; index < ATTEMPTED_BLOCKS; index++) {
            byte[] block = new byte[BLOCK_SIZE];
            block[0] = (byte) index;
            block[block.length - 1] = (byte) (index >>> 1);
            retained.add(block);
            value += Byte.toUnsignedInt(block[0]) + Byte.toUnsignedInt(block[block.length - 1]);
            if (bounded && retained.size() > 8) {
                retained.remove(0);
            }
        }
        System.out.println("attempted-blocks=" + ATTEMPTED_BLOCKS);
        System.out.println("retained-blocks=" + retained.size());
        System.out.println("allocation-checksum=" + value);
    }

    private static String requireArgument(String[] args, int index) {
        if (args.length <= index || args[index].isBlank()) {
            throw new IllegalArgumentException("missing argument at index " + index);
        }
        return args[index];
    }
}
