text: 'use strict';
  
  var AsyncLock = function (opts) {
  	opts = opts || {};
  
  	this.Promise = opts.Promise || Promise;
  
  	// format: {key : [fn, fn]}
  	// queues[key] = null indicates no job running for key
  	this.queues = Object.create(null);
  
  	// lock is reentrant for same domain
  	this.domainReentrant = opts.domainReentrant || false;
  	if (this.domainReentrant) {
  		if (typeof process === 'undefined' || typeof process.domain === 'undefined') {
  			throw new Error(
  				'Domain-reentrant locks require `process.domain` to exist. Please flip `opts.domainReentrant = false`, ' +
  				'use a NodeJS version that still implements Domain, or install a browser polyfill.');
  		}
  		// domain of current running func {key : fn}
  		this.domains = Object.create(null);
  	}
  
  	this.timeout = opts.timeout || AsyncLock.DEFAULT_TIMEOUT;
  	this.maxWaitTime = opts.maxWaitTime || AsyncLock.DEFAULT_MAX_WAIT_TIME;
  	if (opts.maxPending === Infinity || (Number.isInteger(opts.maxPending) && opts.maxPending >= 0)) {
  		this.maxPending = opts.maxPending;
  	} else {
  		this.maxPending = AsyncLock.DEFAULT_MAX_PENDING;
  	}
  };
  
  AsyncLock.DEFAULT_TIMEOUT = 0; //Never
  AsyncLock.DEFAULT_MAX_WAIT_TIME = 0; //Never
  AsyncLock.DEFAULT_MAX_PENDING = 1000;
  
  /**
   * Acquire Locks
   *
   * @param {String|Array} key 	resource key or keys to lock
   * @param {function} fn 	async function
   * @param {function} cb 	callback function, otherwise will return a promise
   * @param {Object} opts 	options
   */
  AsyncLock.prototype.acquire = function (key, fn, cb, opts) {
  	if (Array.isArray(key)) {
  		return this._acquireBatch(key, fn, cb, opts);
  	}
  
  	if (typeof (fn) !== 'function') {
  		throw new Error('You must pass a function to execute');
  	}
  
  	// faux-deferred promise using new Promise() (as Promise.defer is deprecated)
  	var deferredResolve = null;
  	var deferredReject = null;
  	var deferred = null;
  
  	if (typeof (cb) !== 'function') {
  		opts = cb;
  		cb = null;
  
  		// will return a promise
  		deferred = new this.Promise(function(resolve, reject) {
  			deferredResolve = resolve;
  			deferredReject = reject;
  		});
  	}
  
  	opts = opts || {};
  
  	var resolved = false;
  	var timer = null;
  	var self = this;
  
  	var done = function (locked, err, ret) {
  		if (locked) {
  			if (self.queues[key] && self.queues[key].length === 0) {
  				delete self.queues[key];
  			}
  			if (self.domainReentrant) {
  				delete self.domains[key];
  			}
  		}
  
  		if (!resolved) {
  			if (!deferred) {
  				if (typeof (cb) === 'function') {
  					cb(err, ret);
  				}
  			}
  			else {
  				//promise mode
  				if (err) {
  					deferredReject(err);
  				}
  				else {
  					deferredResolve(ret);
  				}
  			}
  			resolved = true;
  		}
  
  		if (locked) {
  			//run next func
  			if (!!self.queues[key] && self.queues[key].length > 0) {
  				var maxWaitTime = opts.maxWaitTime || self.maxWaitTime;
  				if (maxWaitTime) {
  					setTimeout(function () {
  						done(true);
  					}, maxWaitTime);
  				}
  				self.queues[key].shift()();
  			}
  		}
  	};
  
  	var exec = function (locked) {
  		if (resolved) { // may due to timed out
  			return done(locked);
  		}
  
  		if (timer) {
  			clearTimeout(timer);
  			timer = null;
  		}
  
  		if (self.domainReentrant && locked) {
  			self.domains[key] = process.domain;
  		}
  
  		// Callback mode
  		if (fn.length === 1) {
  			var called = false;
  			fn(function (err, ret) {
  				if (!called) {
  					called = true;
  					done(locked, err, ret);
  				}
  			});
  		}
  		else {
  			// Promise mode
  			self._promiseTry(function () {
  				return fn();
  			})
  				.then(function(ret){
  					done(locked, undefined, ret);
  				}, function(error){
  					done(locked, error);
  				});
  		}
  	};
  	if (self.domainReentrant && !!process.domain) {
  		exec = process.domain.bind(exec);
  	}
  
  	if (!self.queues[key]) {
  		self.queues[key] = [];
  		exec(true);
  	}
  	else if (self.domainReentrant && !!process.domain && process.domain === self.domains[key]) {
  		// If code is in the same domain of current running task, run it directly
  		// Since lock is re-enterable
  		exec(false);
  	}
  	else if (self.queues[key].length >= self.maxPending) {
  		done(false, new Error('Too much pending tasks'));
  	}
  	else {
  		var taskFn = function () {
  			exec(true);
  		};
  		if (opts.skipQueue) {
  			self.queues[key].unshift(taskFn);
  		} else {
  			self.queues[key].push(taskFn);
  		}
  
  		var timeout = opts.timeout || self.timeout;
  		if (timeout) {
  			timer = setTimeout(function () {
  				timer = null;
  				done(false, new Error('async-lock timed out'));
  			}, timeout);
  		}
  
  
  	}
  
  	if (deferred) {
  		return deferred;
  	}
  };
  
  /*
   * Below is how this function works:
   *
   * Equivalent code:
   * self.acquire(key1, function(cb){
   *     self.acquire(key2, function(cb){
   *         self.acquire(key3, fn, cb);
   *     }, cb);
   * }, cb);
   *
   * Equivalent code:
   * var fn3 = getFn(key3, fn);
   * var fn2 = getFn(key2, fn3);
   * var fn1 = getFn(key1, fn2);
   * fn1(cb);
   */
  AsyncLock.prototype._acquireBatch = function (keys, fn, cb, opts) {
  	if (typeof (cb) !== 'function') {
  		opts = cb;
  		cb = null;
  	}
  
  	var self = this;
  	var getFn = function (key, fn) {
  		return function (cb) {
  			self.acquire(key, fn, cb, opts);
  		};
  	};
  
  	var fnx = fn;
  	keys.reverse().forEach(function (key) {
  		fnx = getFn(key, fnx);
  	});
  
  	if (typeof (cb) === 'function') {
  		fnx(cb);
  	}
  	else {
  		return new this.Promise(function (resolve, reject) {
  			// check for promise mode in case keys is empty array
  			if (fnx.length === 1) {
  				fnx(function (err, ret) {
  					if (err) {
  						reject(err);
  					}
  					else {
  						resolve(ret);
  					}
  				});
  			} else {
  				resolve(fnx());
  			}
  		});
  	}
  };
  
  /*
   *	Whether there is any running or pending asyncFunc
   *
   *	@param {String} key
   */
  AsyncLock.prototype.isBusy = function (key) {
  	if (!key) {
  		return Object.keys(this.queues).length > 0;
  	}
  	else {
  		return !!this.queues[key];
  	}
  };
  
  /**
   * Promise.try() implementation to become independent of Q-specific methods
   */
  AsyncLock.prototype._promiseTry = function(fn) {
  	try {
  		return this.Promise.resolve(fn());
  	} catch (e) {
  		return this.Promise.reject(e);
  	}
  };
  
  module.exports = AsyncLock;
  