'use strict';

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

/**
 * Licensed Materials - Property of IBM
 * IBM Business Analytics (C) Copyright IBM Corp. 2019, 2020
 * US Government Users Restricted Rights - Use, duplication or disclosure restricted by GSA ADP Schedule Contract with IBM Corp.
 */

/**
 * @class TransactionAPI
 * @hideconstructor
 * @classdesc API class that is used to control transactional operations across API calls
 */
define(['underscore', '../../../../../lib/@waca/dashboard-common/dist/core/APIFactory', '../TransactionAPI'], function (_, APIFactory, TransactionAPI) {
	var Transaction = function () {
		function Transaction() {
			_classCallCheck(this, Transaction);

			this._transactions = new Map();
			this._handlers = new Map();
		}

		Transaction.prototype.getAPI = function getAPI() {
			return APIFactory.createAPI(this, [TransactionAPI]);
		};

		Transaction.prototype.startTransaction = function startTransaction(parentToken) {
			if (!(parentToken && parentToken.transactionId)) {
				// create a new transaction entry
				var entry = this._createTransactionEntry(_.uniqueId('__transaction__'));
				if (parentToken && parentToken.source) {
					entry.token.source = parentToken.source;
					entry.token.eventGenerated = parentToken.eventGenerated;
				}
				return entry.token;
			} else {
				var _entry = this._get(parentToken.transactionId);
				if (_entry) {
					// add a sub-transaction to an existing transaction entry
					var subTransactionIdx = this._addSubTransaction(_entry);
					var res = _.extend({ subTransaction: subTransactionIdx }, _entry.token);
					if (parentToken && parentToken.source) {
						res.source = parentToken.source;
						res.eventGenerated = parentToken.eventGenerated;
					}
					return res;
				}
				console.warn('Invalid transactionId: ' + parentToken.transactionId);
				return parentToken;
			}
		};

		Transaction.prototype.startTransactionById = function startTransactionById(transactionId) {
			var entry = this._get(transactionId);
			if (entry) {
				return this.startTransaction(entry.token);
			}
			entry = this._createTransactionEntry(transactionId);
			return entry.token;
		};

		Transaction.prototype.endTransaction = function endTransaction(token) {
			var transactionId = token && token.transactionId;
			var entry = this._get(transactionId);
			if (entry && Number.isInteger(token.subTransaction)) {
				entry = entry.children[token.subTransaction];
			}
			if (entry) {
				entry.timestamp.end = Date.now();
				entry.end();
			} else {
				console.warn('Invalid transactionId: ' + transactionId);
			}
			return token;
		};

		Transaction.prototype.getTransactionById = function getTransactionById(id) {
			var entry = this._get(id);
			return entry ? entry.token : {};
		};

		Transaction.prototype.registerTransactionHandler = function registerTransactionHandler(token, handlerId, handler, args) {
			var isValid = this.isValidTransaction(token);

			if (isValid) {
				// register and invoke the handler
				this._register(handlerId, handler, token.transactionId, args);
				return this._invoke(handlerId);
			} else {
				// directly invoke the handler and move on
				var rv = handler([args]);
				return rv instanceof Promise ? rv : Promise.resolve();
			}
		};

		Transaction.prototype.whenTransactionComplete = function whenTransactionComplete(token) {
			// we only need wait for the root promise to complete
			var entry = this._get(token.transactionId);
			// return the transaction promise which disposes the transaction at the end
			return entry._promise.then(this._dispose.bind(this, token.transactionId));
		};

		Transaction.prototype.isValidTransaction = function isValidTransaction(token) {
			return !!this._get(token.transactionId);
		};

		Transaction.prototype.getActiveTransactionList = function getActiveTransactionList() {
			return _.map(this._getAll(), function (entry) {
				return entry.token;
			});
		};

		/**
   * @private
   * Create a new transaction entry
   * @param {String} uniqueId unique transaction identifier
   * {
   *      token: {
   *          transactionId: 'xyz'
   *      },
   *      end: function() { ... },
   *      timestamp: {
   *          start: 1551142727899,
   *          end: 1551142741757
   *      }
   * }
   */


		Transaction.prototype._createTransactionEntry = function _createTransactionEntry(uniqueId) {
			var entry = {
				'token': {
					'transactionId': uniqueId
				},
				'timestamp': {
					'start': Date.now()
				}
			};

			entry._promise = new Promise(function (resolve) {
				this.end = function () {
					if (this.children && this.children.length > 0) {
						this.children[0]._promise.then(resolve);
					} else {
						resolve();
					}
				}.bind(this);
			}.bind(entry));

			this._set(uniqueId, entry);

			return entry;
		};

		/**
   * @private
   * Add a sub-transcation to an existing transaction
   * @param {Object} entry parent transaction entry object
   * {
   *      token: {
   *          transactionId: 'xyz'
   *      },
   *      children: [{
   *          end: function() { ... },
   *          next: { ... }
   *          timestamp: {
   *              start: 1551142727899,
   *              end: 1551142741757
   *          }
   *      }, {
   *          end: function() { ... },
   *          timestamp: {
   *              start: 1551142727899,
   *              end: 1551142741757
   *          }
   *      }]
   *      end: function() { ... },
   *      timestamp: {
   *          start: 1551142727899
   *      }
   * }
   */


		Transaction.prototype._addSubTransaction = function _addSubTransaction(entry) {
			// prepare the subtransactions list
			if (!entry.children) {
				entry.children = [];
			}

			var subTransactionEntry = {
				timestamp: {
					start: Date.now()
				}
			};
			var transactionIndex = entry.children.length;
			var lastItem = entry.children && entry.children[entry.children.length - 1];
			// link the current transaction as the next transaction for the previous transaction
			if (lastItem) {
				lastItem.next = subTransactionEntry;
			}

			subTransactionEntry._promise = new Promise(function (resolve) {
				this.end = function () {
					if (this.next) {
						// resolve the my promise once the next promise is resolved
						this.next._promise.then(resolve);
					} else {
						resolve();
					}
				}.bind(this);
			}.bind(subTransactionEntry));

			// add the subtransaction to the existing entry
			entry.children.push(subTransactionEntry);

			return transactionIndex;
		};

		Transaction.prototype._getAll = function _getAll() {
			var all = [];
			this._transactions.forEach(function (tranaction) {
				all.push(tranaction);
			});
			return all;
		};

		Transaction.prototype._get = function _get(transactionId) {
			return this._transactions.get(transactionId);
		};

		Transaction.prototype._set = function _set(transactionId, entry) {
			this._transactions.set(transactionId, entry);
		};

		Transaction.prototype._dispose = function _dispose(transactionId) {
			this._transactions.delete(transactionId);
		};

		Transaction.prototype._register = function _register(handlerId, handler, transactionId, args) {
			var entry = this._handlers.get(handlerId);
			if (entry) {
				// update the transactions related to current handler
				if (entry.transactionIds.indexOf(transactionId) < 0) {
					entry.transactionIds.push(transactionId);
				}
				entry.args.push(args);
			} else {
				// register the handler
				this._handlers.set(handlerId, {
					'transactionIds': [transactionId],
					'handler': handler,
					'args': [args]
				});
			}
		};

		Transaction.prototype._isRegistered = function _isRegistered(handlerId) {
			return !!this._handlers.get(handlerId);
		};

		/**
   * This is an enhancement to replace Promise.all to handle cases
   * which the array of transactions can grow asynchronously.
   * Once the initial handler waits for all transactions complete,
   * transactions that are added afterwards may result the handler
   * to be invoked prematurely.
   */


		Transaction.prototype._waitForAllTransactions = function _waitForAllTransactions(transactionIds) {
			var _this = this;

			var index = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 0;

			if (transactionIds.length > index) {
				// We can not rely on Promise.all since a new transaction can be added
				// after the handler was initially register and invoked
				var transaction = this._get(transactionIds[index]);
				if (transaction) {
					// wait for the current transaction to complete
					return transaction._promise.then(function () {
						// seek for the next transaction during the runtime
						return _this._waitForAllTransactions(transactionIds, index + 1);
					});
				} else {
					// continue with the next transaction
					return this._waitForAllTransactions(transactionIds, index + 1);
				}
			}
			return Promise.resolve();
		};

		Transaction.prototype._invoke = function _invoke(handlerId) {
			var _this2 = this;

			var entry = this._handlers.get(handlerId);
			if (!entry._promise) {
				// @todo Use Transaction._waitForAllTransactions to support optimized rendering
				var transactions = _.map(entry.transactionIds, function (id) {
					return _this2._get(id)._promise;
				});
				// invoke the handler once all related transactions are complete
				entry._promise = Promise.all(transactions).then(function () {
					// entry._promise = this._waitForAllTransactions(entry.transactionIds).then(() => {
					// dispose all transactions that are registered for this handler
					_.each(entry.transactionIds, _this2._dispose.bind(_this2));

					try {
						return entry.handler(entry.args);
					} finally {
						// clean up the handler
						_this2._handlers.delete(handlerId);
					}
				});
			}
			return entry._promise;
		};

		return Transaction;
	}();

	return Transaction;
});
//# sourceMappingURL=Transaction.js.map