package com.qcloud.cos.internal;

import java.io.File;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.util.Map;
import java.util.TreeMap;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.qcloud.cos.exception.FileLockException;
import com.qcloud.cos.utils.IOUtils;

/**
 * An internal utility used to provide both inter and intra JVM file locking.
 * This works well as long as this class is loaded with the same class loader in
 * a JVM. Otherwise, intra-JVM file locking is not guaranteed.
 * <p>
 * Per javadoc of {@link FileLock}, "File locks are held on behalf of the entire
 * Java virtual machine. They are not suitable for controlling access to a file
 * by multiple threads within the same virtual machine."
 * <p>
 * Hence the need for this utility class.
 */
public enum FileLocks {
    ;
    // External file lock doesn't seem to work correctly on Windows, 
    // so disabling for now (Ref: TT0047889941)
    private static final boolean EXTERNAL_LOCK = false;
    private static final Logger log = LoggerFactory.getLogger(FileLocks.class);
    private static final Map<File, RandomAccessFile> lockedFiles = new TreeMap<File, RandomAccessFile>();

    /**
     * Acquires an exclusive lock on the specified file, creating the file as
     * necessary. Caller of this method is responsible to call the
     * {@link #unlock(File)} method to prevent release leakage.
     * 
     * @return true if the locking is successful; false otherwise.
     * 
     * @throws FileLockException if we failed to lock the file
     */
    public static boolean lock(File file) {
        synchronized(lockedFiles) {
            if (lockedFiles.containsKey(file))
                return false;   // already locked
        }
        FileLock lock = null;
        RandomAccessFile raf = null;
        try {
            // Note if the file does not already exist then an attempt will be
            // made to create it because of the use of "rw".
            raf = new RandomAccessFile(file, "rw");
            FileChannel channel = raf.getChannel();
            if (EXTERNAL_LOCK)
                lock = channel.lock();
        } catch (Exception e) {
            IOUtils.closeQuietly(raf, log);
            throw new FileLockException(e);
        }
        final boolean locked;
        synchronized(lockedFiles) {
            RandomAccessFile prev = lockedFiles.put(file, raf);
            if (prev == null) {
                locked = true;
            } else {
                // race condition: some other thread got locked it before this
                locked = false;
                lockedFiles.put(file, prev);    // put it back
            }
        }
        if (locked) {
            if (log.isDebugEnabled())
                log.debug("Locked file " + file + " with " + lock);
        } else {
            IOUtils.closeQuietly(raf, log);
        }
        return locked;
    }

    /**
     * Returns true if the specified file is currently locked; false otherwise.
     */
    public static boolean isFileLocked(File file) {
        synchronized(lockedFiles) {
            return lockedFiles.containsKey(file);
        }
    }

    /**
     * Unlocks a file previously locked via {@link #lock(File)}.
     * 
     * @return true if the unlock is successful; false otherwise. Successful
     *         unlock means we have found and attempted to close the locking
     *         file channel, but ignoring the fact that the close operation may
     *         have actually failed.
     */
    public static boolean unlock(File file) {
        synchronized(lockedFiles) {
            final RandomAccessFile raf = lockedFiles.get(file);
            if (raf == null)
                return false;
            else {
                // Must close out the channel before removing it from the map;
                // or else risk giving a false negative (of no lock but in fact
                // the file is still locked by the file system.)
                IOUtils.closeQuietly(raf, log);
                lockedFiles.remove(file);
            }
        }
        if (log.isDebugEnabled())
            log.debug("Unlocked file " + file);
        return true;
    }
}
