Source: common/ActionQueue.js

import { Flags } from '@uino/base-thing';
import { Utils } from './Utils';

const __ = {
	private: Symbol('private'),
}

const Flag = {
	Enable: 1 << 0,
	Running: 1 << 1,
}

// #region Private Functions

function _onSort(a, b) {
	if (a.priority !== b.priority) {
		return b.priority - a.priority;
	}

	return a.index - b.index;
};

// Get action index by name.
function _getActionIndexByName(actions, name) {
	for (let i = 0; i < actions.length; i++) {
		let action = actions[i];

		if (action.name != name) {
			continue;
		}

		return i;
	}

	return -1;
}

// Get action index by type.
function _getActionIndexByType(actions, type) {
	for (let i = 0; i < actions.length; i++) {
		let action = actions[i];

		if (action.processor instanceof type === false) {
			continue;
		}

		return i;
	}

	return -1;
}

// #endregion

/**
 * @class ActionQueue
 * The action queue.
 * @memberof THING
 * @public
 */
class ActionQueue {

	/**
	 * The action queue what can process multiple actions.
	 * @param {Object} param The initial parameters.
	 * @example
	 * let actionQueue = new THING.ActionQueue({ name: 'MyActionQueue' });
	 * @public
	 */
	constructor(param = {}) {
		this[__.private] = {};
		let _private = this[__.private];

		_private.name = Utils.parseValue(param['name'], '');
		_private.actions = [];
		_private.actionRuntimeData = {};
	}

	// #region Private

	// Get action by name.
	_getActionByName(name) {
		let _private = this[__.private];

		let actions = _private.actions;

		let index = _getActionIndexByName(actions, name);
		if (index === -1) {
			return null;
		}

		return actions[index];
	}

	_getActionByType(type) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.processor instanceof type) {
				return action;
			}
		}

		return null;
	}

	_addAction(processor, name, priority) {
		let _private = this[__.private];

		let actions = _private.actions;

		// Create flags
		let flags = new Flags();
		flags.enable(Flag.Enable, true);

		// Create action
		let action = {
			flags,
			processor,
			name: Utils.parseValue(name, ''),
			priority: Utils.parseValue(priority, 0),
			index: actions.length
		};

		// Add the actions and sort it by priority
		actions.push(action);
		actions.sort(_onSort);
	}

	// #endregion

	/**
	 * Clear all groups and result.
	 * @example
	 * let actionQueue = new THING.ActionQueue({ name: 'MyActionQueue' });
	 * class ActionProcessor {
	 * 		onStop() {
	 * 		}
	 * 		onRun(options) {
	 * 		}
	 * };
	 * actionQueue.add(new ActionProcessor(), 'FirstActionProcessor', 1000);
	 * actionQueue.clear();
	 * // @expect(actionQueue.actions.length == 1);
	 * @public
	 */
	clear() {
		this.stop();

		let _private = this[__.private];

		_private.actions.forEach(action => {
			// Stop to run
			action.flags.enable(Flag.Running, true);

			let onStop = action.processor.onStop;
			if (onStop) {
				onStop.call(action.processor);
			}
		});

		_private.actions.length = 0;
		_private.actionRuntimeData = {};
	}

	/**
	 * The function to call when stop action.
	 * @callback OnActionProcessorStop
	 */

	/**
	 * The function to call when run action.
	 * @callback OnActionProcessorRun
	 * @param {Object} options The options.
	 * @returns {Promise<any>}
	 */

	/**
	 * The function to call when run action.
	 * @callback OnActionProcessorEnable
	 * @param {Boolean} value The value.
	 */

	/**
	 * @typedef {Object} ActionProcessor
	 * @property {OnActionProcessorStop} onStop
	 * @property {OnActionProcessorRun} onRun
	 * @property {OnActionProcessorEnable} onEnable
	 */

	/**
	 * Add action.
	 * @param {Array<ActionProcessor>|ActionProcessor} processor The action processor(s).
	 * @param {String} name The action name(only works for single processor mode).
	 * @param {Number} priority The action priority value(default is 0, higher value indicates higher priority).
	 * @example
	 * // Create action processor
	 * class ActionProcessor {
	 * 	onStop() {
	 * 	}
	 * 
	 * 	onRun(options) {
	 * 	}
	 * };
	 * 
	 * actionQueue.add(new ActionProcessor(), 'FirstActionProcessor', 1000);
	 * @public
	 */
	add(processor, name = '', priority = 0) {
		if (!processor) {
			return;
		}

		if (!Utils.isString(name)) {
			Utils.error(`Add action failed, due to the name('${name}') is not string type`);
			return;
		}

		if (!Utils.isNumber(priority)) {
			Utils.error(`Add action failed, due to the priority('${priority}') is not number type`);
			return;
		}

		if (Utils.isArray(processor)) {
			processor.forEach(action => {
				this._addAction(action);
			});
		}
		else {
			this._addAction(processor, name, priority);
		}
	}

	/**
	 * Set action.
	 * @param {String} name The action name.
	 * @param {Object} options The options.
	 * @param {Number} options.priority The action priority value(default is 0, higher value indicates higher priority).
	 * @param {ActionProcessor} options.processor The action processor.
	 * @private
	 */
	set(name, options) {
		let _private = this[__.private];

		let priority = options['priority'];
		let processor = options['processor'];

		let action = this._getActionByName(name);
		if (action) {
			// Backup previous options
			let prePriority = action.priority;

			// Update action options
			action.priority = Utils.parseValue(priority, action.priority);
			action.processor = Utils.parseValue(processor, action.processor);

			// If priority changed the we need to resort it
			if (prePriority != action.priority) {
				_private.actions.sort(_onSort);
			}
		}
		else {
			this._addAction(processor, name, priority);
		}
	}

	/**
	 * Get action by name.
	 * @param {String} name The action name.
	 * @returns {ActionProcessor}
	 * @example
	 * let actionProcessor = actionQueue.getByName('FirstActionProcessor');
	 * @public
	 */
	getByName(name) {
		let action = this._getActionByName(name);
		if (!action) {
			return null;
		}

		return action.processor;
	}

	/**
	 * Get action by type.
	 * @param {*} type The action type.
	 * @returns {ActionProcessor}
	 * @example
	 * // ActionProcess is class type(class ActionProcessor)
	 * let actionProcessor = actionQueue.getByType(ActionProcessor);
	 * @public
	 */
	getByType(type) {
		let action = this._getActionByType(type);
		if (!action) {
			return null;
		}

		return action.processor;
	}

	/**
	 * Remove action by name.
	 * @param {String} name The action name.
	 * @example
	 * actionQueue.removeByName('FirstActionProcessor');
	 * @public
	 */
	removeByName(name) {
		let _private = this[__.private];

		let actions = _private.actions;

		let index = _getActionIndexByName(actions, name);
		if (index === -1) {
			return;
		}

		actions._removeAt(index);
	}

	/**
	 * Remove action by type.
	 * @param {*} type The action type.
	 * @example
	 * // ActionProcess is class type(class ActionProcessor)
	 * actionQueue.removeByType(ActionProcessor);
	 * @public
	 */
	removeByType(type) {
		let _private = this[__.private];

		let actions = _private.actions;

		let index = _getActionIndexByType(actions, type);
		if (index === -1) {
			return;
		}

		actions._removeAt(index);
	}

	/**
	 * The function to call when traverse action processor.
	 * @callback OnTraverseActionProcessorCallback
	 */

	/**
	 * Traverse by name.
	 * @param {String} name The action name.
	 * @param {OnTraverseActionProcessorCallback} callback The callback function.
	 * @example
	 * actionQueue.traverseByName('PlayAction', (actionProcessor) => {
	 * 	console.log(actionProcessor);
	 * });
	 * @public
	 */
	traverseByName(name, callback) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.name == name) {
				callback(action.processor);
			}
		}
	}

	/**
	 * Traverse by type.
	 * @param {*} type The action type.
	 * @param {OnTraverseActionProcessorCallback} callback The callback function.
	 * @example
	 * // ActionProcess is class type(class ActionProcessor)
	 * actionQueue.traverseByType(ActionProcess, (actionProcessor) => {
	 * 	console.log(actionProcessor);
	 * });
	 * @public
	 */
	traverseByType(type, callback) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.processor instanceof type) {
				callback(action.processor);
			}
		}
	}

	/**
	 * Replace action.
	 * @param {ActionProcessor} desProcessor The target processor to be replaced.
	 * @param {ActionProcessor} srcProcessor The source processor to replace.
	 * @returns {Boolean}
	 * @private
	 */
	replace(desProcessor, srcProcessor) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.processor != desProcessor) {
				continue;
			}

			let onStop = action.processor.onStop;
			if (onStop) {
				onStop.call(action.processor);
			}

			action.processor = srcProcessor;

			return true;
		}

		return false;
	}

	/**
	 * Get action priority.
	 * @param {ActionProcessor} processor The action processor.
	 * @returns {Number}
	 * @private
	 */
	getPriority(processor) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.processor != processor) {
				continue;
			}

			return action.priority;
		}

		return 0;
	}

	/**
	 * Set action priority.
	 * @param {ActionProcessor} processor The action processor.
	 * @param {Number} priority The action priority.
	 * @returns {Boolean}
	 * @private
	 */
	setPriority(processor, priority) {
		let _private = this[__.private];

		let actions = _private.actions;

		for (let i = 0; i < actions.length; i++) {
			let action = actions[i];

			if (action.processor != processor) {
				continue;
			}

			action.priority = priority;

			actions.sort(_onSort);

			return true;
		}

		return false;
	}

	/**
	 * Enable/Disable action.
	 * @param {String} name The action name.
	 * @param {Boolean} value The action enable state.
	 * @private
	 */
	enable(name, value) {
		let action = this._getActionByName(name);
		if (!action) {
			return;
		}

		if (action.flags.enable(Flag.Enable, value)) {
			let processor = action.processor;
			if (processor.onEnable) {
				processor.onEnable(value);
			}
		}
	}

	/**
	 * Check whether enable action or not.
	 * @param {String} name The action name.
	 * @returns {Boolean}
	 * @private
	 */
	isEnabled(name) {
		let action = this._getActionByName(name);
		if (!action) {
			return false;
		}

		return action.flags.has(Flag.Enable);
	}

	/**
	 * Stop.
	 * @private
	 */
	stop() {
		let _private = this[__.private];

		let actions = _private.actions;

		actions.forEach(action => {
			let flags = action.flags;

			if (!flags.has(Flag.Running)) {
				return;
			}

			let processor = action.processor;
			if (!processor) {
				return;
			}

			// Record the runtime data of the actions
			if (processor.getRuntimeData) {
				const data = processor.getRuntimeData();
				_private.actionRuntimeData[action.name] = data;
			}

			// Reset the action runtime data
			processor.resetRuntimeData();


			let onStop = processor.onStop;
			if (onStop) {
				onStop.call(processor);
			}

			flags.enable(Flag.Running, false);
		});
	}

	/**
	 * Run.
	 * @param {Object} options The options.
	 * @returns {Promise<any>}
	 * @private
	 */
	run(options = {}) {
		let _private = this[__.private];

		this.stop();

		return new Promise((resolve, reject) => {
			const actions = _private.actions;
			if (actions.length) {
				let abort = false;

				actions.forEachAsync((action, index) => {
					if (abort) {
						return;
					}

					// Start to run
					action.flags.enable(Flag.Running, true);

					// Get action processor
					if (Utils.isFunction(action.processor)) {
						action.processor = action.processor();
					}

					// Get action processor
					let processor = action.processor;
					if (!processor) {
						return;
					}

					// Recover the runtime data of the action
					if (processor.setRuntimeData) {
						const data = _private.actionRuntimeData[action.name];
						if (data) {
							processor.setRuntimeData(data)
						}
					}

					// Check whether enable processor or not
					if (!action.flags.has(Flag.Enable)) {
						return;
					}

					// Run action by processor
					let promise = processor.onRun(options);
					if (!promise) {
						// Check whether it's the last or abort action
						if (processor.isAbort || index == actions.length - 1) {
							abort = true;
							resolve();
						}

						return;
					}

					// Wait to finish
					return promise.then(() => {
						// Check whether it's the last or abort action
						if (processor.isAbort || index == actions.length - 1) {
							abort = true;
							resolve();
						}
					}).catch(ex => {
						reject(ex);
					});
				});
			}
			else {
				resolve();
			}
		});
	}

	/**
	 * Get/Set name.
	 * @type {String}
	 * @example
	 * // Print action queue's name
	 * console.log(actionQueue.name);
	 * @public
	 */
	get name() {
		let _private = this[__.private];

		return _private.name;
	}
	set name(value) {
		let _private = this[__.private];

		_private.name = value;
	}

	/**
	 * Get actions.
	 * @type {Array<ActionProcessor>}
	 * @example
	 * // Print action queue's (actions/processors)
	 * console.log(actionQueue.actions);
	 * @public
	 */
	get actions() {
		let _private = this[__.private];

		return _private.actions;
	}

}

export { ActionQueue }