package com.fourspaces.featherdb.utils;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Lock class... Should allow the programmer to obtain a named lock (only by string or filename).  They can then operate on this named entity
 * for as long as they want.  After which, they can release the lock, and allow another section of code to obtain the lock.  This is good for
 * intra JVM locks on "operations", not objects... for example, if you want to lock on a file, new File(filename) will give you different objects,
 * so you can't operate synchronized.
 * <p>
 * You can also specify a timeout for the lock, after which time it will expire and release itself.  This is helpful for avoiding dead-locks.
 * <p>
 * For locks on file, it will create a absolute/file/name.lock file that can be used to lock resources outside of the JVM. 
 * (it is removed upon release).
 * <p>
 * Note: this class doesn't enforce locks, it just ensures that concurrent threads don't try to access the same named (string id) resource
 * at the same time.
 * 
 * @author mbreese
 *
 */

// TODO: Make this able to have shared (read-only) locks
final public class Lock {
	private Logger log = Logger.get(Lock.class);
	private static Map<String,Queue<Lock>> locks = new HashMap<String,Queue<Lock>> ();
	private static Map<String,Lock> currentLock = new HashMap<String,Lock> ();
	private static Boolean processing = false;
	
	final private String key;
	final private long timeout;
	
	private boolean released = false;
	private boolean expired = false;
	
	private File lockFile = null;
	
	private Lock(String s) {
		this.key = s;
		addLock(this);
		this.timeout = -1;
	}
	
	private Lock(File f) {
		this.key = f.getAbsolutePath();
		addLock(this);
		this.timeout = -1;
		createLockFile();
	}

	private Lock(String s, int timeoutInterval) {
		this.key = s;
		addLock(this);
		this.timeout = new Date().getTime() + timeoutInterval;
		startWatchdog();
	}
	
	private Lock(File f, int timeoutInterval) {
		this.key = f.getAbsolutePath();
		addLock(this);
		this.timeout = new Date().getTime() + timeoutInterval;
		
		createLockFile();
		startWatchdog();
	}

	private void createLockFile() {
		lockFile = new File(key+".lock");
		int timeout = 500; // wait upto 5 seconds for lock file.
		while (this.lockFile.exists() && timeout > 0) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			timeout--;
		}
		try {
			this.lockFile.createNewFile();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	private void startWatchdog() {
		Thread t = new Thread(new Runnable() {
			public void run() {
				while (new Date().getTime() > timeout && !Thread.interrupted() && !released && !expired) {
					try {
						Thread.sleep(10);
					} catch (InterruptedException e) {
						
					}
				}
				if (!released) {
					expire();
				}
			}
		});
		t.setDaemon(true);
		t.run();
	}
	
	/**
	 * Releases the lock...
	 *
	 */
	public void release() {
		if (!released) {
			log.debug("Lock on '{}' released ({})" , key,this);
			released=true;
			currentLock.remove(key);
			if (lockFile!=null && lockFile.exists()) {
				lockFile.delete();
			}
		}
	}
	
	private void expire() {
		if (!expired && !released && timeout > 0) {
			log.warn("Lock on '{}' expired at {} ({})" , key,timeout,this);
			expired=true;
			release();
		}
	}
	
	/**
	 * Has this lock expired?
	 * @return
	 */
	public boolean isExpired() {
		return expired;
	}

	/** 
	 * Has this lock been released?
	 * @return
	 */
	public boolean isReleased() {
		return released;
	}

	private void addLock(Lock lock) {
		if (!locks.containsKey(lock.key)) {
			locks.put(lock.key, new ConcurrentLinkedQueue<Lock>());
		}
		Queue<Lock> q = locks.get(lock.key);
		q.add(lock);
		waitForLock(lock);
		log.debug("Lock on '{}' obtained ({})" , key,lock);
	}
	
	private void waitForLock(Lock lock) {
		processQueue();
		log.debug("Waiting for lock on '{}' ({})" , key,lock);
		while (currentLock.containsKey(lock.key) && currentLock.get(lock.key)!=lock && (lock.timeout==-1 || lock.timeout > new Date().getTime())) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				return;
			}
		}
	}

	private void processQueue() {
		if (!processing) {
			new Thread(new Runnable() {
				public void run() {
					synchronized(processing) {
						if (!processing) {
							processing = true;
							
							/*
							 * Keep processing so long as there is a queue that still has locks pending.
							 * For each named lock, if there isn't a currentLock, then pull the head from 
							 * the queue and make it the current lock.
							 * 
							 * Remove keys from the lockmap that have empty queues.
							 */
							boolean found = true;
							while (found) {
								found = false;
								List<String> removeList = new ArrayList<String>();
								for (String key: locks.keySet()) {
									if (!currentLock.containsKey(key)) {
										Lock lock = locks.get(key).poll();
										if (lock!=null && (lock.timeout==-1 || lock.timeout > new Date().getTime())) {
											currentLock.put(key, lock);
										}
									}
									if (locks.get(key).isEmpty()) {
										removeList.add(key);
									} else {
										found = true;
									}
								}
								for (String key:removeList) {
									locks.remove(key);
								}
								if (found) {
									try {
										Thread.sleep(10);
									} catch (InterruptedException e) {
										found=false;
									}
								}
							}
							processing = false;
						}
					}
				}
			}).run();
		}
	}
	
	/**
	 * retrieve a named lock
	 * @param s
	 * @return
	 */
	public static Lock lock(String s) {
		return new Lock(s);
	}
	
	/**
	 * retrieve a file lock
	 * @param s
	 * @return
	 */
	public static Lock lock(File f) {
		return new Lock(f);
	}

	/**
	 * retrieve a named lock with a timeout
	 * @param s
	 * @param timeout - the timeout length in milliseconds
	 * @return
	 */
	public static Lock lock(String s, int timeout) {
		return new Lock(s, timeout);
	}
	
	/**
	 * retrieve a file lock with a timeout
	 * @param s
	 * @param timeout - the timeout length in milliseconds
	 * @return
	 */
	public static Lock lock(File f, int timeout) {
		return new Lock(f, timeout);
	}

}
