"use strict";

function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }

module.exports = {

	// Settings (Considered private and constant)
	defaultWait: 1000,
	retryDelay: 50,
	retryJitter: 100,
	locksHashKey: 'RugbyLocks',

	// For queueing multiple local locks for the same key
	localLocksWaiting: {},

	// Local vars
	clientLocks: [],

	initLocking() {
		this.localLocksWaiting = {};
		this.clientLocks = [];
		delete this.debugLogging;
	},
	logLocks() {
		this.debugLogging = true;
	},
	logLocking() {
		this.logLocks();
	},

	// ------------------------------------------- Public functions


	/**
  * This function should be used to contruct lock keya lock key
  * for any time you need to lock an item to make changes to it
  */
	makeItemLockKey(table, item, pk = 'Id') {

		if (typeof item === 'object') {
			if (pk === 'Id' && !(pk in item) && 'id' in item) {
				item[pk] = item.id;
			}

			if (item[pk]) {
				// Construct and return
				return `item:${table}:${item[pk]}`;
			} else {
				this.errorInternal(new Error('Object passed to makeItemLockKey did not have a primary key'));
			}
		} else {
			this.errorInternal(new Error('Non-object was passed to makeItemLockKey'));
		}
	},

	/**
  * Gets a lock from redlock
  * ttl = ms
  * ttl can be omitted
  */
	lock(key, ttl, callback) {
		var _this = this;

		return _asyncToGenerator(function* () {

			// Allow ttl to be omitted
			if (typeof ttl === 'function') {
				callback = ttl;
				ttl = _this.defaultWait;
			}

			ttl = ttl || _this.defaultWait;
			let lockKey = _this.makeLockKey(key);

			try {

				// Redlock supports promises out of the box
				let lock = yield _this.redlock.lock(lockKey, ttl);
				yield _this.saveLockAsync(lockKey, lock);
				if (_this.debugLogging) console.log('LOCKED ->> ' + key);
				lock.key = key;
				callback(null, lock);
			} catch (err) {
				// we failed to lock the resource
				callback(null, false);
			}
		})();
	},

	/**
  * Unlocks a lock which was returned by lock()
  */
	unlock(lock, callback) {
		var _this2 = this;

		return _asyncToGenerator(function* () {

			try {

				// Allow calling unlock even when lock was unsuccessful
				if (typeof lock !== 'object') {
					callback();return;
				}

				yield _this2.removeSavedLockAsync(lock.resource);
				yield lock.unlock();

				if (lock.disengageLock) {
					lock.disengageLock();
				}

				if (_this2.debugLogging) console.log('UNLOCKED <<- ', lock.key);

				if (typeof callback === 'function') {
					callback();
				}
			} catch (err) {
				// we weren't able to reach redis; your lock will eventually
				// expire, but you probably want to log this error
				console.log('Could not unlock', err);
			}
		})();
	},

	/**
  * This can be used to unlock a lock via the original lock key
  * This can be performed from any client (not necessarily the
  * original - allowing remote lock management)
  */
	unlockKey(key, callback) {
		var _this3 = this;

		return _asyncToGenerator(function* () {

			let lock = yield _this3.getLockAsync(key);
			if (lock) {
				_this3.unlock(lock, callback);
			} else {
				let lockKey = _this3.makeLockKey(key);
				_this3.removeSavedLock(lockKey, callback);
			}
		})();
	},

	/**
  * Gets a lock from redlock
  * 
  * ttl 
  * Will wait as long as ttl for current locks to expire
  */

	/**
  * Wait for a lock
  * @param  {string}   key                  The lock key
  * @param  {int}      ttl                  The time to live (also used for maxWait). Can be omitted.
  * @param  {bool}     releaseAllAfterFirst Allows us to release all local locks with the same key at once, after the first has unlocked. This is handy when there may be many promises trying to get the same lock, i.e. generating a bunch of players, and they all try to access the same setting. If we know that after the first returns, there will be locally cached setting available, we can release all the rest at once. Can be omitted.
  * @param  {Function} callback             The callback function
  */
	waitForLock(key, ttl, releaseAllAfterFirst, callback) {
		var _this4 = this;

		// Allow releaseAllAfterFirst to be omitted
		if (typeof releaseAllAfterFirst === 'function') {
			callback = releaseAllAfterFirst;
			releaseAllAfterFirst = null;
		}
		// Allow ttl & releaseAllAfterFirst to be omitted
		if (typeof ttl === 'function') {
			callback = ttl;
			ttl = this.defaultWait;
			releaseAllAfterFirst = null;
		}

		// For local lock management (performance optimisation)
		let token = this.randtoken.generate(11);

		// Vars
		let totalWait = 0;
		let startTime = Date.now();
		let maxWait = ttl + (blackoutLocalMode ? 5000 : 0);
		let currentWait = 0;
		let locksWaiting = this.localLocksWaiting;

		/**
   * This is called once an engaged lock is done
   */
		let disengageLock = () => {

			// Let others know we're done
			locksWaiting[key].engaged = null;

			// Call next item in the queue
			if (locksWaiting[key].queue.length) {

				// If we can release all now
				if (releaseAllAfterFirst) {

					do {
						let nextCallback = locksWaiting[key].queue.shift();
						nextCallback();
					} while (locksWaiting[key].queue.length);
				} else {

					// Just release next in the queue
					let nextCallback = locksWaiting[key].queue.shift();
					nextCallback();
				}
			} else {

				// No more items
				delete locksWaiting[key];
			}
		};

		/**
   * This is called in a polling mannger until we get out lock, or we timeout
   */
		let attemptLock = (() => {
			var _ref = _asyncToGenerator(function* (key, ttl) {
				totalWait = Date.now() - startTime;

				if (totalWait < maxWait) {

					if (_this4.debugLogging) console.log('LOCKING Waiting for lock (' + token + ') ->> ' + key);

					// Make sure there isn't already a local lock waiting for this key
					if (key in locksWaiting && locksWaiting[key].engaged && locksWaiting[key].engaged !== token) {

						if (_this4.debugLogging) console.log('LOCKING Queued local lock (' + token + ') ->> ' + key);

						// Don't even attempt right now, just queue a callback
						let timeoutId;
						let queueCallback = function () {

							if (_this4.debugLogging) console.log('LOCKING Retrying queued local lock (' + token + ') ->> ' + key);

							// If we have reached the top of the queue before the timeout, cancel the timeout
							if (timeoutId) {
								clearTimeout(timeoutId);
							}

							// If we can release all now
							if (releaseAllAfterFirst) {
								callback(null, true);
							} else {
								// Reattempt our lock
								attemptLock(key, ttl);
							}
						};

						// Queue our callback
						locksWaiting[key].queue.push(queueCallback);

						// Set a timeout so we can cancel if we still haven't been called
						timeoutId = setTimeout(function () {

							timeoutId = null;

							// Make sure our callback exists in the queue
							if (key in locksWaiting) {
								let callbackIndex = locksWaiting[key].queue.indexOf(queueCallback);
								if (callbackIndex >= 0) {
									// Remove from the queue
									locksWaiting[key].queue.splice(callbackIndex, 1);
								}
							}

							// Reattempt our lock (which should show that we've waited too long, and then return)
							attemptLock(key, ttl);
						}, maxWait - totalWait + 50);
					} else {

						// Create a local key so that we can prevent other locks from polling redis just yet
						if (!(key in locksWaiting)) {
							locksWaiting[key] = {
								queue: []
							};
						}

						// Let others know we're working
						locksWaiting[key].engaged = token;

						// Attempt lock
						let lock = yield _this4.lockAsync(key, ttl);

						if (!lock) {

							// Increase wait (We can overshoot the maxWait by a bit)
							currentWait += _this4.retryDelay + Math.round(Math.random() * _this4.retryJitter);

							// Wait and try again
							yield _this4.sleep(currentWait);
							attemptLock(key, ttl);
						} else {

							lock.disengageLock = disengageLock;

							// We've got a lock!
							callback(null, lock);
						}
					}
				} else {
					if (_this4.debugLogging) console.log('LOCKING Failed waiting for lock (' + token + ') ->> ' + key);
					// Nope, we've waited long enough
					callback(null, false);

					// Are we the lock currently engaged?
					if (key in locksWaiting && locksWaiting[key].engaged === token) {
						disengageLock();
					}
				}
			});

			return function attemptLock(_x, _x2) {
				return _ref.apply(this, arguments);
			};
		})();

		attemptLock(key, ttl);
	},

	// ------------------------------------------- Private functions


	/**
  * This saves an obtained lock to a managed list so that we can
  * perform remote lock management
  */
	saveLock(lockKey, lock, callback) {
		var _this5 = this;

		return _asyncToGenerator(function* () {

			// Make basic lock hash
			let lockInfo = {
				key: lock.resource,
				value: lock.value,
				expiration: lock.expiration
			};

			// Track client locks
			_this5.clientLocks.push(lock);

			// Add lock to locks hash
			if (!_this5.redis) {
				console.log('Redis is gone (during saveLock()). Lock key: ' + lockKey);
			}
			yield _this5.redis.hsetAsync(_this5.locksHashKey, lockKey, JSON.stringify(lockInfo));
			callback();
		})();
	},

	/**
  * This gets an obtained lock from a managed list so that we can
  * perform remote lock management
  */
	getLock(key, callback) {
		var _this6 = this;

		return _asyncToGenerator(function* () {

			let lockKey = _this6.makeLockKey(key);

			try {

				// Load lock from locks hash, rebuild it, return it
				let lockInfo = yield _this6.redis.hgetAsync(_this6.locksHashKey, lockKey);
				lockInfo = JSON.parse(lockInfo);
				let lock = _this6.rebuildLock(lockKey, lockInfo.value, lockInfo.expiration);
				callback(lock);
			} catch (err) {
				console.log('Failed to get lockKey ' + lockKey);
				callback(false);
			}
		})();
	},

	/**
  * This rebuilds an obtained lock based on lock key from our managed list
  * which allows us to perform remote lock management
  */
	rebuildLock(lockKey, value, expiration) {

		// Lock Class
		// ----
		// Taken from redlock source
		// https://github.com/mike-marcacci/node-redlock/blob/master/redlock.js
		function Lock(redlock, resource, value, expiration) {
			this.redlock = redlock;
			this.resource = resource;
			this.value = value;
			this.expiration = expiration;
		}

		Lock.prototype.unlock = function unlock() {
			return this.redlock.unlock(this);
		};

		Lock.prototype.extend = function extend(ttl) {
			return this.redlock.extend(this, ttl);
		};
		// ----
		// End Lock Class

		return new Lock(this.redlock, lockKey, value, expiration);
	},

	cleanupLocks(callback) {
		var _this7 = this;

		return _asyncToGenerator(function* () {
			let awaits = [];

			for (let lock of _this7.clientLocks) {
				awaits.push(_this7.unlockAsync(lock));
			}

			yield Promise.all(awaits);
			callback();
		})();
	},

	/**
  * This removes an saved lock from our managed list once we
  * no longer need it (once expired or unlocked)
  */
	removeSavedLock(lockKey, callback) {
		var _this8 = this;

		return _asyncToGenerator(function* () {

			yield _this8.redis.hdelAsync(_this8.locksHashKey, lockKey);

			// Track client locks
			for (let [i, lock] of _this8.clientLocks.entries()) {
				if (lock.resource === lockKey) {
					_this8.clientLocks.splice(i, 1);
					break;
				}
			}

			callback();
		})();
	},

	listLocks(callback) {
		var _this9 = this;

		return _asyncToGenerator(function* () {

			let locks = yield _this9.redis.hgetallAsync(_this9.locksHashKey);

			let now = Date.now();

			console.log('-------------- LOCKS --------------');

			if (locks) {
				for (let lockKey in locks) {

					let lockInfoJSON = locks[lockKey];
					let lockInfo = JSON.parse(lockInfoJSON);

					// If expired, remove lock and don't show
					if (lockInfo.expiration <= now) {
						yield _this9.redis.hdelAsync(_this9.locksHashKey, lockKey);
					} else {
						console.log('LOCK => ' + _this9.revertLockKey(lockKey) + ' => ' + lockInfoJSON);
					}
				}
			}

			console.log('-----------------------------------');

			callback();
		})();
	},

	makeLockKey(key) {
		return 'lock:' + key;
	},

	revertLockKey(lockKey) {
		if (lockKey.indexOf('lock:') === 0) {
			return lockKey.substr(5);
		}
	}

};
//# sourceMappingURL=locking.js.map