package com.atlassian.stash.internal.home;

import com.atlassian.stash.internal.ApplicationConstants;
import com.google.common.io.Closeables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import java.io.Closeable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.nio.file.StandardOpenOption;
import java.util.EnumSet;
import java.util.Set;

import static com.google.common.base.Preconditions.checkNotNull;

/**
 * A lock which is taken out on a home directory
 *
 * @since 3.2
 */
public class HomeLock implements Closeable {

    private static final Logger log = LoggerFactory.getLogger(HomeLock.class);

    private final boolean shared;
    private final File lockFile;

    private volatile FileChannel lockChannel;

    public HomeLock(@Nonnull File homeDir, boolean shared) {
        checkNotNull(homeDir, "homeDir");
        this.shared = shared;
        lockFile = new File(homeDir, ".lock");
    }

    /**
     * Unlocks the home directory, and delete the lock file if it is locked exclusively
     * <p>
     * Note: If the lock file cannot be deleted for some reason, an error is logged and mark the file for deletion on
     * exit. Because the server may refuse to come up on the subsequent start if the lock file cannot be deleted, we
     * want to do what we can to call attention to that state.
     */
    public void close() {
        if (lockChannel == null) {
            return;
        }

        try {
            lockChannel.close();
        } catch (IOException e) {
            log.warn("Failed to close channel for file {}", lockFile, e);
        }

        // When we acquire a shared lock, we don't want to delete the lock file when we exit. Otherwise
        // other processes who are also holding shared locks will lose their lock
        if (!shared && !lockFile.delete()) {
            //Try a more roundabout approach, in the hopes that, when we terminate, the lock file will be deleted then.
            lockFile.deleteOnExit();

            log.error("Failed to delete lock file {}; it may have to be deleted manually before {} can be started again",
                    lockFile, ApplicationConstants.PRODUCT_NAME);
        }

        lockChannel = null;
    }

    public boolean isShared() {
        return shared;
    }

    /**
     * Acquire the lock on the home directory
     *
     * @return {@code true} if the lock was successfully acquired, otherwise {@code false}
     * @throws OverlappingFileLockException when the lock is already acquired by the current JVM
     * @throws IOException                  if the lock file failed to be created
     */
    public boolean lock() throws IOException {
        if (lockChannel != null) {
            throw new OverlappingFileLockException();
        }

        // we check isFile() a second time in the slight chance that createNewFile returned false, which may have
        // happened because another Stash instances created it first
        if (lockFile.isFile() || lockFile.createNewFile() && lockFile.isFile()) {
            return acquireLock();
        }

        throw new FileNotFoundException("Failed to create lock file " + lockFile.getAbsolutePath());
    }

    @Override
    public String toString() {
        return lockFile.getAbsolutePath();
    }

    private boolean acquireLock() throws IOException {
        FileChannel fileChannel = FileChannel.open(lockFile.toPath(), fileOptionsFor(shared));
        try {
            FileLock lock = fileChannel.tryLock(0L, Long.MAX_VALUE, shared);
            if (lock == null) {
                return false;
            }
            lockChannel = fileChannel;
        } catch (IOException | RuntimeException e) {
            Closeables.close(lockChannel, true);
            throw e;
        }

        // Unfortunately when we acquire a shared lock, we cannot write any information to the file.
        // Luckily we only use this information for debugging purposes
        if (!shared) {
            String whoAmI = ManagementFactory.getRuntimeMXBean().getName();
            try {
                fileChannel.write(ByteBuffer.wrap((whoAmI + "\n").getBytes()));
            } catch (IOException e) {
                //We couldn't write an identifier into the lock file. Oh well!
                log.warn("Failed to write process information into the lock file", e);
            }
        }

        return true;
    }

    private static Set<StandardOpenOption> fileOptionsFor(boolean shared) {
        Set<StandardOpenOption> fileOptions = EnumSet.of(StandardOpenOption.READ);
        if (!shared) {
            fileOptions.add(StandardOpenOption.WRITE);
        }
        return fileOptions;
    }

}
