Source: managers/LevelManager.js

import { Flags, CancelablePromise } from '@uino/base-thing';
import { Utils } from '../common/Utils'
import { EventType } from '../const';

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

const Flag = {
	Changing: 1 << 0,
}

const cDestroyObjectEventTag = '__LevelManager_object_destroy_event';

/**
 * @class LevelManager
 * The level manager.
 * @memberof THING
 * @public
 */
class LevelManager {

	/**
	 * The level manager to manage object(s) throught between parent and child object.
	 */
	constructor() {
		this[__.private] = {};
		let _private = this[__.private];

		_private.enable = true;

		_private.pendingPromises = [];
		_private.changeOptions = null;

		_private.prev = null;
		_private.current = null;
		_private.leavingObject = null;
		_private.enteringObject = null;

		_private.flags = new Flags();

		_private.onFilterPath = null;

		_private.app = Utils.getCurrentApp();

		_private.levelControls = [];
		_private.registerOrder = 0;

		// Leave current level when it had been destroyed
		_private.app.on(EventType.BeforeDestroy, '*', (ev) => {
			if (_private.current == ev.object) {
				this.quit();
			}
		}, cDestroyObjectEventTag);
	}

	// #region Private

	// Notify event.
	_notifyEvent(type, params) {
		let _private = this[__.private];

		let app = _private.app;

		let object = params['current'];

		// Skip for root enter level event
		if (object == app.root) {
			return;
		}

		// Trigger global events
		app.trigger(type, params);

		// Prevent root trigger duplicated event
		if (object != app.root) {
			object.trigger(type, params);
		}
	}

	_notifyComplete(options, path) {
		let _private = this[__.private];

		// Clear change options
		_private.changeOptions = null;

		// Clear pendingPromises
		_private.pendingPromises.length = 0;

		// Finished change
		_private.flags.enable(Flag.Changing, false);

		// Build event to notify outside
		let ev = {
			prev: _private.prev,
			current: _private.current,
			path
		};

		let onComplete = Utils.parseValue(options['onComplete'], options['complete']);
		if (onComplete) {
			onComplete(ev);
		}

		_private.app.trigger(EventType.CompleteEnterLevel, ev);

		_private.current.trigger(EventType.CompleteEnterLevel, ev);
	}

	_enterObjectLevel(object, options, path, origin) {
		let _private = this[__.private];

		_private.enteringObject = object;

		let enterParam = {
			path,
			origin,
			prev: _private.prev,
			current: _private.current,
			next: null,
			options
		};
		enterParam.levelControls = this.getControlsByObject(object);

		// Enter level
		return object.level.enter(enterParam).then(() => {
			// Finished level changed
			object.level.finish(enterParam);

			// Check whether reach the target object
			if (object == path[path.length - 1]) {
				this._notifyComplete(options, path);
			}

			_private.enteringObject = null;
		});
	}

	_changeObject(object, options, path, origin) {
		let _private = this[__.private];

		return new CancelablePromise((resolve, reject, onCancel) => {
			let leavePromise = null;
			let enterPromise = null;

			onCancel((cancelParam) => {
				if (leavePromise) {
					leavePromise.cancel();
				}
				if (enterPromise) {
					enterPromise.cancel(cancelParam);
				}
			});

			// Start to change
			_private.flags.enable(Flag.Changing, true);

			// Exit current level
			if (_private.current && !_private.current.destroyed && _private.current != _private.leavingObject) {
				_private.leavingObject = _private.current;

				let leaveEventParam = this._generateEventParam(path, origin, object, options);

				// Notify before leave level event
				this._notifyEvent(EventType.BeforeLeaveLevel, leaveEventParam);

				let leaveParam = Object.assign({}, leaveEventParam);
				leaveParam.levelControls = this.getControlsByObject(_private.current);

				// Leave level
				leavePromise = _private.current.level.leave(leaveParam).then(() => {
					// Notify leave level event
					this._notifyEvent(EventType.LeaveLevel, leaveEventParam);

					// Notify after leave level event
					this._notifyEvent(EventType.AfterLeaveLevel, leaveEventParam);

					_private.leavingObject = null;
				});
			}
			else {
				leavePromise = CancelablePromise.resolve();
			}

			leavePromise.then(() => {
				// Update level
				_private.prev = _private.current;
				_private.current = object;

				if (object.destroyed) {
					resolve();
					return;
				}

				let enterEventParam = this._generateEventParam(path, origin, null, options);

				// Notify before enter level event
				this._notifyEvent(EventType.BeforeEnterLevel, enterEventParam);

				// Enter object level
				enterPromise = this._enterObjectLevel(object, options, path, origin).then(() => {
					resolve();
					// Notify enter level event
					this._notifyEvent(EventType.EnterLevel, enterEventParam);

					// Notify after enter level event
					this._notifyEvent(EventType.AfterEnterLevel, enterEventParam);

					enterPromise = null;
				});

				leavePromise = null;
			});
		});
	}

	_generateEventParam(path, origin, next, options) {
		let _private = this[__.private];

		return {
			path,
			origin,
			prev: _private.prev,
			current: _private.current,
			next,
			options
		};
	}

	_startToChange(path, options) {
		let _private = this[__.private];

		// Try to filter path
		let onFilterPath = _private.onFilterPath;
		if (onFilterPath) {
			path = onFilterPath(path) || path;
		}

		let origin = _private.current;

		path.forEachAsync(obj => {
			let promise = this._changeObject(obj, options, path, origin);

			_private.pendingPromises.push(promise);

			return promise;
		});
	}

	_change(object, options) {
		let _private = this[__.private];

		// Get the current level
		let current = _private.current;

		// Skip to change for the same object level
		if (current == object) {
			return;
		}

		if (current && !current.destroyed) {
			// Get path from current/root level to object
			let path = current.getPathTo(object) || app.root.getPathTo(object);

			// Start to change level
			this._startToChange(path, options);
		}
		else {
			// Get the level path from parents
			let path = object.parents.reverse();
			path.push(object);

			// Start to change level
			this._startToChange(path, options);
		}
	}

	_cancelPendingPromises(object, options) {
		let _private = this[__.private];

		if (!_private.pendingPromises.length) {
			return;
		}

		if (!_private.current) {
			return;
		}

		let path = _private.current.getPathTo(object);
		let levelControls = this.getControlsByObject(_private.current);
		let cancelParam = {
			origin: _private.current,
			prev: _private.prev,
			current: _private.current,
			next: object,
			path: path,
			options,
			levelControls
		}

		_private.pendingPromises.forEach(promise => {
			promise.cancel(cancelParam);
		});

		const changeOptions = _private.changeOptions;
		if (changeOptions) {
			let onStop = Utils.parseValue(changeOptions['onStop'], changeOptions['stop']);
			if (onStop) {
				onStop();
			}
			_private.changeOptions = null;
		}

		// Record options
		_private.changeOptions = options;

		_private.pendingPromises.length = 0;
	}

	_createCancelablePromise(object, options) {
		return new CancelablePromise((resolve, reject, onCancel) => {
			onCancel(() => { });


			let copyOptions = Object.assign({}, options);

			let onComplete = options['onComplete'] || options['complete'];
			let onStop = options['onStop'] || options['stop'];

			copyOptions.onComplete = copyOptions.complete = (ev) => {
				if (onComplete) {
					onComplete(ev);
				}
				resolve('onComplete');
			};

			copyOptions.onStop = copyOptions.stop = (ev) => {
				if (onStop) {
					onStop(ev);
				}
				resolve('onStop');
			}

			this._change(object, copyOptions);
		});
	}

	_getControlByTag(condition, tag) {
		let _private = this[__.private];
		let levelControls = _private.levelControls;

		for (let i = 0; i < levelControls.length; i++) {
			const controlData = levelControls[i];
			if (controlData.condition === condition) {
				if (!tag) {
					continue;
				}

				if (controlData.tag === tag) {
					return controlData.control;
				}
			}
		}

		return null;
	}

	// #endregion

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

		let app = _private.app;

		app.off(EventType.AfterDestroy, '*', cDestroyObjectEventTag);

		this._cancelPendingPromises(null);

		_private.flags.clear();

		_private.prev = null;
		_private.current = null;

		_private.levelControls = null;
		_private.registerOrder = null;
	}

	/**
	 * The function to call when level changed.
	 * @callback LevelChangedCallback
	 * @param {Object} ev The event info.
	 * @param {THING.BaseObject} ev.current The current level.
	 * @param {THING.BaseObject} ev.prev The previous level.
	 * @param {Array<THING.BaseObject>} ev.path The path from start to target object.
	 */

	/**
	 * @typedef {Object} LevelChangeOptions
	 * @param {LevelChangedCallback} onStop The callback function would be trigged when stop level change.
	 * @param {LevelChangedCallback} onComplete The callback function would be trigged when complete level change.
	 */

	/**
	 * Change current level.
	 * @param {THING.BaseObject} object The object.
	 * @param {LevelChangeOptions} options The options.
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.change(target, {
	 *   onComplete: function(){
	 * 		let ret = level.current == target;
	 * 		// @expect(ret == true);
	 *   }
	 * });
	 */
	change(object, options = {}) {
		return this.changeAsync(object, options);
	}

	/**
	 * Change current level in async mode.
	 * @param {THING.BaseObject} object The object.
	 * @param {LevelChangeOptions} options The options.
	 * @returns {Promise<any>}
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.changeAsync(target, {
	 *   onComplete: function(){
	 * 		let ret = level.current == target;
	 * 		// @expect(ret == true);
	 *   }
	 * });
	 */
	changeAsync(object, options = {}) {
		let _private = this[__.private];

		// Skip for disable state
		if (!_private.enable) {
			return Promise.resolve();
		}

		// Skip to change for the same object level
		let current = _private.current;
		if (current == object) {
			return Promise.resolve();
		}

		this._cancelPendingPromises(object, options);

		// Start to change level
		const promise = this._createCancelablePromise(object, options);

		_private.pendingPromises.push(promise);

		return promise;
	}

	/**
	 * Change to the parent level.
	 * @param {LevelChangeOptions} options The options.
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.change(target, {
	 *   onComplete: function(){
	 * 		let ret = level.current == target;
	 * 		// @expect(ret == true);
	 * 		level.back({
	 *   		onComplete: function(){
	 * 				ret = level.current == target.parent;
	 * 				// @expect(ret == true)
	 *   		}
	 * 		});
	 *   }
	 * });
	 */
	back(options = {}) {
		let _private = this[__.private];

		let current = _private.current;
		if (!current) {
			return Promise.resolve();
		}

		let parent = current.parent;
		if (!parent || parent.isRootObject) {
			return Promise.resolve();
		}

		return this.changeAsync(parent, options);
	}

	/**
	 * Change to the parent level in async mode.
	 * @param {LevelChangeOptions} options The options.
	 * @returns {Promise<any>}
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.change(target, {
	 *   onComplete: function(){
	 * 		let ret = level.current == target;
	 * 		// @expect(ret == true);
	 * 		level.backAsync({
	 *   		onComplete: function(){
	 * 				ret = level.current == target.parent;
	 * 				// @expect(ret == true)
	 *   		}
	 * 		});
	 *   }
	 * });
	 */
	backAsync(options = {}) {
		let _private = this[__.private];

		// Skip for disable state
		if (!_private.enable) {
			return;
		}

		let current = _private.current;
		if (!current) {
			return Promise.resolve();
		}

		let parent = current.parent;
		if (!parent || parent.isRootObject) {
			return Promise.resolve();
		}

		this._cancelPendingPromises(parent, options);

		const promise = this._createCancelablePromise(parent, options);

		_private.pendingPromises.push(promise);

		return promise;
	}

	/**
	 * Quit.
	 * @returns {Promise<any>}
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * level.quit({
	 *   onComplete: function(){
	 * 		let ret = level.current == null;
	 * 		// @expect(ret == true);
	 *   }
	 * });
	 */
	quit() {
		let _private = this[__.private];


		_private.flags.clear();

		// Exit current level
		if (_private.current) {
			this._cancelPendingPromises(_private.current, null);

			let params = {
				origin: null,
				prev: _private.prev,
				current: _private.current,
				next: null,
			};

			this._notifyEvent(EventType.BeforeLeaveLevel, params);

			let levelControls = this.getControlsByObject(_private.current);

			return _private.current.level.leave({
				path: [_private.current],
				origin: null,
				prev: null,
				current: _private.current,
				next: null,
				levelControls
			}).then(() => {
				this._notifyEvent(EventType.AfterLeaveLevel, params);

				_private.prev = null;
				_private.current = null;
			});
		}
		else {
			return Promise.resolve();
		}
	}


	/**
	 * @typedef {Object} RegisterControlOptions
	 * @param {Number} priority The level control priority. The default value is 0. A smaller value is executed first.
	 * @param {String} tag  The level control tag.
	 */

	/**
	 * Register level control
	 * @param {String} condition Query conditions (The level control will apply to query results)
	 * @param {THING.BaseLevelControl} control The level control
	 * @param {RegisterControlOptions} options The level control options
	 * @public
	 */
	register(condition, control, options = {}) {
		let _private = this[__.private];

		if (!condition || !control) {
			return;
		}

		// Check Repeate
		for (let i = 0; i < _private.levelControls.length; i++) {
			const controlData = _private.levelControls[i];
			if (controlData.control == control) {
				Utils.warn('Cannot be repeat registered!');
				return;
			}
		}

		// Parse options
		const priority = options.priority || 0;
		const tag = options.tag;

		// Unregister the exist control
		if (tag) {
			let control = this._getControlByTag(condition, tag);
			if (control) {
				this.unregister(condition, tag);
			}
		}

		// Produce control data
		let order = ++_private.registerOrder;
		let controlData = { control, condition, tag, priority, order };

		// Push
		_private.levelControls.push(controlData);

		// Setup app
		Object.defineProperty(control, "app", {
			get: () => {
				return _private.app;
			},
			configurable: true
		});

		// Sort array by priority and order
		_private.levelControls.sort((a, b) => {
			if (a.priority != b.priority) {
				return b.priority - a.priority;
			}
			return a.order - b.order;
		});
	}

	/**
	 * Unregister level control
	 * @param {String} condition Query conditions
	 * @param {String} tag The level control tag.
	 * @public
	 */
	unregister(condition, tag) {
		if (!condition) {
			return;
		}

		let _private = this[__.private];
		let levelControls = _private.levelControls;

		for (let i = levelControls.length - 1; i >= 0; i--) {
			const controlData = levelControls[i];
			if (controlData.condition === condition) {
				if (!tag || controlData.tag === tag) {
					let control = controlData.control;
					if (control.isRunning) {
						Utils.warn('Cannot be unregister while running!');
						continue;
					}
					delete control.app;
					levelControls.splice(i, 1);
				}
			}
		}
	}

	unregisterByControl(control) {
		if (!control) {
			return;
		}

		let _private = this[__.private];
		let levelControls = _private.levelControls;

		for (let i = 0; i < levelControls.length; i++) {
			const controlData = levelControls[i];
			if (controlData.control == control) {
				levelControls.splice(i, 1);
			}
		}
	}

	getControlsByObject(object) {
		let _private = this[__.private];
		const levelControls = _private.levelControls;

		let result = [];
		let testResultCache = {};

		for (let i = 0; i < levelControls.length; i++) {
			const controlData = levelControls[i];
			const condition = controlData.condition;

			// Get the test result by cache
			let testResult = testResultCache[condition];

			// If it is null, test it
			if (Utils.isNull(testResult)) {
				testResult = testResultCache[condition] = object.test(condition);
			}

			// If true, add to the result array
			if (testResult === true) {
				result.push(controlData.control);
			}
		}

		return result;
	}

	/**
	 * Enable/Disable level manager.
	 * @type {Boolean}
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * level.enable = false;
	 * let target = app.query('.Entity')[0];
	 * level.change(target);
	 * let ret = level.isChanging;
	 * // @expect(ret == false);
	 */
	get enable() {
		let _private = this[__.private];

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

		_private.enable = value;
	}

	/**
	 * The function to call when start to get object level path.
	 * @callback FilterLevelPathCallback
	 * @param {Array<THING.BaseObject>} path The level path.
	 * @returns {Array<THING.BaseObject>} The new level path, null or undefined indicates use the current level path.
	 */

	/**
	 * Get/Set filter path callback function.
	 * @type {FilterLevelPathCallback}
	 * @private
	 */
	get onFilterPath() {
		let _private = this[__.private];

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

		_private.onFilterPath = value;
	}

	/**
	 * Get the previous object.
	 * @type {THING.BaseObject}
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let prev = level.prev;
	 * let target = app.query('.Entity')[0];
	 * level.change(target, {
	 *   onComplete: function(){
	 * 		let ret = level.prev == prev;
	 * 		// @expect(ret == true);
	 * 	 }
	 * });
	 */
	get prev() {
		let _private = this[__.private];

		return _private.prev;
	}

	/**
	 * Get the current object.
	 * @type {THING.BaseObject}
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.change(target, {
	 *   onComplete: function(){
	 * 		let ret = level.current == target;
	 * 		// @expect(ret == true);
	 * 	 }
	 * });
	 */
	get current() {
		let _private = this[__.private];

		return _private.current;
	}

	/**
	 * Check whether is changing level.
	 * @type {Boolean}
	 * @public
	 * @example
	 * let app = THING.App.current;
	 * let level = app.level;
	 * let target = app.query('.Entity')[0];
	 * level.change(target);
	 * let ret = level.isChanging;
	 * // @expect(ret == true);
	 */
	get isChanging() {
		let _private = this[__.private];

		return _private.flags.has(Flag.Changing);
	}

}

export { LevelManager }