/*******************************************************************************
 * Copyright (c) 2015 Eclipse RDF4J contributors, Aduna, and others.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Distribution License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *******************************************************************************/
package org.eclipse.rdf4j.sail.helpers;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.management.ManagementFactory;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.security.AccessControlException;

import org.eclipse.rdf4j.common.concurrent.locks.Lock;
import org.eclipse.rdf4j.sail.LockManager;
import org.eclipse.rdf4j.sail.SailLockedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Used to create a lock in a directory.
 *
 * @author James Leigh
 * @author Arjohn Kampman
 */
public class DirectoryLockManager implements LockManager {

	private static final String LOCK_DIR_NAME = "lock";

	private static final String LOCK_FILE_NAME = "locked";

	private static final String INFO_FILE_NAME = "process";

	private final Logger logger = LoggerFactory.getLogger(DirectoryLockManager.class);

	private final File dir;

	public DirectoryLockManager(File dir) {
		this.dir = dir;
	}

	@Override
	public String getLocation() {
		return dir.toString();
	}

	private File getLockDir() {
		return new File(dir, LOCK_DIR_NAME);
	}

	/**
	 * Determines if the directory is locked.
	 *
	 * @return <code>true</code> if the directory is already locked.
	 */
	@Override
	public boolean isLocked() {
		return getLockDir().exists();
	}

	/**
	 * Creates a lock in a directory if it does not yet exist.
	 *
	 * @return a newly acquired lock or null if the directory is already locked.
	 */
	@Override
	public Lock tryLock() {
		File lockDir = getLockDir();

		if (lockDir.exists()) {
			removeInvalidLock(lockDir);
		}

		if (!lockDir.mkdir()) {
			return null;
		}

		Lock lock = null;

		try {
			File infoFile = new File(lockDir, INFO_FILE_NAME);
			File lockedFile = new File(lockDir, LOCK_FILE_NAME);

			RandomAccessFile raf = new RandomAccessFile(lockedFile, "rw");
			try {
				FileLock fileLock = raf.getChannel().lock();
				lock = createLock(raf, fileLock);
				sign(infoFile);
			} catch (IOException e) {
				if (lock != null) {
					// Also closes raf
					lock.release();
				} else {
					raf.close();
				}
				throw e;
			}
		} catch (IOException e) {
			logger.error(e.toString(), e);
		}

		return lock;
	}

	/**
	 * Creates a lock in a directory if it does not yet exist.
	 *
	 * @return a newly acquired lock.
	 * @throws SailLockedException if the directory is already locked.
	 */
	@Override
	public Lock lockOrFail() throws SailLockedException {
		Lock lock = tryLock();

		if (lock != null) {
			return lock;
		}

		String requestedBy = getProcessName();
		String lockedBy = getLockedBy();

		if (lockedBy != null) {
			throw new SailLockedException(lockedBy, requestedBy, this);
		}

		lock = tryLock();
		if (lock != null) {
			return lock;
		}

		throw new SailLockedException(requestedBy);
	}

	/**
	 * Revokes a lock owned by another process.
	 *
	 * @return <code>true</code> if a lock was successfully revoked.
	 */
	@Override
	public boolean revokeLock() {
		File lockDir = getLockDir();
		File lockedFile = new File(lockDir, LOCK_FILE_NAME);
		File infoFile = new File(lockDir, INFO_FILE_NAME);
		lockedFile.delete();
		infoFile.delete();
		return lockDir.delete();
	}

	private void removeInvalidLock(File lockDir) {
		try {
			boolean revokeLock = false;

			File lockedFile = new File(lockDir, LOCK_FILE_NAME);
			try (RandomAccessFile raf = new RandomAccessFile(lockedFile, "rw")) {
				FileLock fileLock = raf.getChannel().tryLock();

				if (fileLock != null) {
					logger.warn("Removing invalid lock {}", getLockedBy());
					fileLock.release();
					revokeLock = true;
				}
			} catch (OverlappingFileLockException exc) {
				// lock is still valid
			}

			if (revokeLock) {
				revokeLock();
			}
		} catch (IOException e) {
			logger.warn(e.toString(), e);
		}
	}

	private String getLockedBy() {
		try {
			File lockDir = getLockDir();
			File infoFile = new File(lockDir, INFO_FILE_NAME);
			try (BufferedReader reader = new BufferedReader(new FileReader(infoFile))) {
				return reader.readLine();
			}
		} catch (IOException e) {
			logger.warn(e.toString(), e);
			return null;
		}
	}

	private Lock createLock(final RandomAccessFile raf, final FileLock fileLock) {
		return new Lock() {

			private Thread hook;

			{
				try {
					Thread hook = new Thread(this::delete);
					Runtime.getRuntime().addShutdownHook(hook);
					this.hook = hook;
				} catch (AccessControlException e) {
					// okay, just remember to close it yourself
				}
			}

			@Override
			public boolean isActive() {
				return fileLock.isValid() || hook != null;
			}

			@Override
			public void release() {
				try {
					if (hook != null) {
						Runtime.getRuntime().removeShutdownHook(hook);
						hook = null;
					}
				} catch (IllegalStateException e) {
					// already shutting down
				} catch (AccessControlException e) {
					logger.warn(e.toString(), e);
				}
				delete();
			}

			synchronized void delete() {
				try {
					if (raf.getChannel().isOpen()) {
						fileLock.release();
						raf.close();
					}
				} catch (IOException e) {
					logger.warn(e.toString(), e);
				}

				revokeLock();
			}
		};
	}

	private void sign(File infoFile) throws IOException {
		try (FileWriter out = new FileWriter(infoFile)) {
			out.write(getProcessName());
			out.flush();
		}
	}

	private String getProcessName() {
		return ManagementFactory.getRuntimeMXBean().getName();
	}
}
