package com.atlassian.bitbucket.mesh.app;

import com.google.common.io.CharStreams;

import java.io.*;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.concurrent.TimeUnit;

/**
 * Oh my, what a monstrosity!
 * <p>
 * The JVM throws exceptions if you try acquire the same file lock more than once in the same process. Since this is not
 * something we would actually do with a {@link DirectoryLock} in Mesh, we need a way to have a separate process to
 * acquire the lock. This class acts in a similar manner to {@link DirectoryLock} from a consumer perspective, but
 * instead of acquiring the lock in the current process it creates a sub-process with the same classpath and invokes
 * the main method in this class. That in turn uses {@link DirectoryLock} to acquire the lock and will wait until its
 * input stream is closed before releasing the lock and terminating the process.
 */
public class ExternalProcessDirectoryLock implements Closeable {

    private final Path directory;
    private final boolean sharedLock;

    private Process process;

    public ExternalProcessDirectoryLock(Path directory, boolean sharedLock) {
        this.directory = directory;
        this.sharedLock = sharedLock;
    }

    public boolean lock() throws IOException {
        if (process != null) {
            throw new OverlappingFileLockException();
        }

        String javaBinary = System.getProperty("java.home") + File.separator + "bin" + File.separator + "java";

        ProcessBuilder processBuilder = new ProcessBuilder()
                .command(javaBinary, "-Dlogback.configurationFile=", getClass().getCanonicalName(),
                        directory.toAbsolutePath().toString(), String.valueOf(sharedLock));
        processBuilder.environment().put("CLASSPATH", System.getProperty("java.class.path"));

        Process subProcess = processBuilder.start();

        int locked = subProcess.getInputStream().read();
        if (locked == -1) {
            throw new IOException(String.format("Failed to start java process. stdErr = %s, exitValue = %d",
                    CharStreams.toString(new InputStreamReader(subProcess.getErrorStream(), StandardCharsets.UTF_8)),
                    subProcess.exitValue()));
        }

        process = subProcess;

        return locked == 1;
    }

    @Override
    public void close() throws IOException {
        if (process != null) {
            process.getOutputStream().close();
            try {
                if (!process.waitFor(10, TimeUnit.SECONDS)) {
                    process.destroy(); // A safety guard to make sure we cleanup the process
                }
            } catch (InterruptedException e) {
                process.destroy();
                throw new InterruptedIOException(e.getMessage());
            } finally {
                process = null;
            }
        }
    }

    public static void main(String... args) throws Exception {
        Path directory = Paths.get(args[0]);
        boolean sharedLock = Boolean.parseBoolean(args[1]);

        System.err.printf("directory = %s, shared = %s\n", directory, sharedLock);
        try (DirectoryLock lock = new DirectoryLock(directory, sharedLock)) {
            System.err.println("Locking...");
            boolean locked = lock.lock();
            System.err.println("Locked");
            System.out.write(locked ? 1 : 0);
            System.out.flush();

            while (System.in.read() != -1) {
                Thread.sleep(1L);
            }
        }
    }
}
