package com.atlassian.stash.internal.home;

import com.google.common.io.CharStreams;
import org.apache.commons.lang3.SystemUtils;

import java.io.*;
import java.nio.channels.OverlappingFileLockException;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.*;

/**
 * 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 HomeLock} in Stash, we need a way to have a separate process to acquire
 * the lock. This class acts in a similar manner to {@link HomeLock} 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 HomeLock} to acquire the lock and will wait until its input stream is closed
 * before releasing the lock and terminating the process.
 */
public class ExternalProcessHomeLock implements Closeable {

    private final File homeDir;
    private final boolean sharedLock;

    private Process process;

    public ExternalProcessHomeLock(File homeDir, boolean sharedLock) {
        this.homeDir = homeDir;
        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";
        String javaClasspath = System.getProperty("java.class.path");

        if (SystemUtils.IS_OS_WINDOWS) {
            javaBinary += ".exe";
        }

        Process subProcess = new ProcessBuilder(javaBinary, "-cp", javaClasspath,
                ExternalProcessHomeLock.class.getCanonicalName(), homeDir.getAbsolutePath(), Boolean.toString(sharedLock))
                .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 (!waitFor(process, 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 {
        File homeDir = new File(args[0]);
        boolean sharedLock = Boolean.parseBoolean(args[1]);

        System.err.printf("homeDir = %s, shared = %s\n", homeDir, sharedLock);
        try (HomeLock lock = new HomeLock(homeDir, 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);
            }
        }
    }

    /**
     * Unfortunately, {@code boolean waitFor(long timeout, TimeUnit unit)} on {@code Process} was not added until
     * Java 1.8 so this is a workaround until we can use that
     */
    private static boolean waitFor(final Process process, long timeout, TimeUnit unit) throws InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        try {
            Future<Boolean> future = executorService.submit(new Callable<Boolean>() {
                @Override
                public Boolean call() {
                    try {
                        process.waitFor();
                        return true;
                    } catch (InterruptedException e) {
                        return false;
                    }
                }
            });

            try {
                return future.get(timeout, unit);
            } catch (ExecutionException | TimeoutException e) {
                return false;
            }
        } finally {
            executorService.shutdown();
        }
    }
}
