Source: components/LevelComponent.js

import { CancelablePromise } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { BaseComponent } from './BaseComponent';
import { ActionQueueType } from '../const';

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

// #region Private Functions

// Wait for async complete.
function _waitAsyncCallback(resolve, asyncCallback) {
	if (_DEBUG) {
		const _checkAsyncCallbackInterval = 10 * 1000;

		let handler = setInterval(() => {
			Utils.error(`Did you forget to invoke resolve() in\n`, asyncCallback);

			clearInterval(handler);
		}, _checkAsyncCallbackInterval);

		let callback = () => {
			clearInterval(handler);

			resolve();
		}

		return callback;
	}
	else {
		return resolve;
	}
}

// #endregion

/**
 * The level event info.
 * @typedef {Object} LevelEventInfo
 * @property {Array<THING.BaseObject>} path The level path.
 * @property {THING.BaseObject} origin The original level object.
 * @property {THING.BaseObject} prev The previous level object.
 * @property {THING.BaseObject} current The current level object.
 * @property {THING.BaseObject} next The next level object (only for leave operation).
 * @property {Object} options The level options.
 */

/**
 * When leave self level in async mode.
 * @callback OnLeaveLevelAsyncCallback
 * @param {LevelEventInfo} ev The event info.
 * @param {Function} resolve The promise resolve callback function.
 * @param {Function} reject The promise reject callback function.
 */

/**
 * When leave self level.
 * @callback OnLeaveLevelCallback
 * @param {LevelEventInfo} ev The event info.
 */

/**
 * When enter self level in async mode.
 * @callback OnEnterLevelAsyncCallback
 * @param {LevelEventInfo} ev The event info.
 * @param {Function} resolve The promise resolve callback function.
 * @param {Function} reject The promise reject callback function.
 */

/**
 * When enter self level.
 * @callback OnEnterLevelCallback
 * @param {LevelEventInfo} ev The event info.
 */

/**
 * When enter self level finished.
 * @callback OnFinishedEnterLevelCallback
 * @param {LevelEventInfo} ev The event info.
 */

/**
 * @class LevelComponent
 * The object level component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class LevelComponent extends BaseComponent {

	static mustCopyWithInstance = true;

	/**
	 * The level actions of object, it could process object's action(s) when enter its level.
	 */
	constructor() {
		// #region Overrides

		/**
		 * When leave level callback function.
		 * @member {OnLeaveLevelCallback} onLeave
		 * @memberof THING.LevelComponent
		 * @instance
		 * @private
		 */

		/**
		 * When leave level callback function in async mode.
		 * @member {OnLeaveLevelAsyncCallback} onLeaveAsync
		 * @memberof THING.LevelComponent
		 * @instance
		 * @private
		 */

		/**
		 * When enter level callback function.
		 * @member {OnEnterLevelCallback} onEnter
		 * @memberof THING.LevelComponent
		 * @instance
		 * @private
		 */

		/**
		 * When enter level callback function in async mode.
		 * @member {OnEnterLevelAsyncCallback} onEnterAsync
		 * @memberof THING.LevelComponent
		 * @instance
		 * @private
		 */

		/**
		 * When finish level callback function.
		 * @member {OnFinishedEnterLevelCallback} onFinish
		 * @memberof THING.LevelComponent
		 * @instance
		 * @private
		 */

		// #endregion

		super();

		this[__.private] = {};
		let _private = this[__.private];

		_private.isLeave = false;

		// Settings config in the level
		_private.config = {
			ignoreVisible: false,
			ignoreEvent: false,
			ignoreStyle: false
		}

		_private.generateLevelControlParams = (eventParams) => {
			let target = null;
			if (eventParams.path) {
				target = eventParams.path[eventParams.path.length - 1];
			}

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

		_private.viewpoint = null;
	}

	// #region Private

	/**
	 * Leave self from object.
	 * @param {Object} param The parameters.
	 * @private
	 */
	leave(param) {
		return new CancelablePromise((resolve, reject, onCancel) => {
			onCancel(() => {
			});

			if (this.onLeaveAsync) {
				this.onLeaveAsync.call(this, param, _waitAsyncCallback(resolve, this.onLeaveAsync), reject);
			}
			else if (this.onLeave) {
				this.onLeave.call(this, param);

				resolve();
			}
			else {
				resolve();
			}
		});
	}

	/**
	 * Enter self from object.
	 * @param {Object} param The parameters.
	 * @private
	 */
	enter(param) {
		let _private = this[__.private];

		return new CancelablePromise((resolve, reject, onCancel) => {
			onCancel((cancelParam) => {
				let levelControls = cancelParam.levelControls;
				let controlParams = _private.generateLevelControlParams(cancelParam);
				levelControls.forEach(control => {
					control.onLeave(controlParams);
				});
				_private.isLeave = true;
			});

			if (this.onEnterAsync) {
				this.onEnterAsync.call(this, param, _waitAsyncCallback(resolve, this.onEnterAsync), reject);
			}
			else if (this.onEnter) {
				this.onEnter.call(this, param);

				resolve();
			}
			else {
				resolve();
			}
		});
	}

	/**
	 * Finish to enter self level.
	 * @param {Object} param The parameters.
	 * @private
	 */
	finish(param) {
		if (this.onFinish) {
			this.onFinish(param);
		}
	}

	/**
	 * Get level action by name.
	 * @param {String} name The action name.
	 * @returns {ActionProcessor}
	 * @private
	 */
	getActionByName(name) {
		let actionQueue = this.actionQueue;
		if (!actionQueue) {
			return null;
		}

		return actionQueue.getByName(name);
	}

	/**
	 * Get level action by type.
	 * @param {*} type The action type.
	 * @returns {ActionProcessor}
	 * @private
	 */
	getActionByType(type) {
		let actionQueue = this.actionQueue;
		if (!actionQueue) {
			return null;
		}

		return actionQueue.getByType(type);
	}

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

		actionQueue.enable(name, value);
	}

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

		return actionQueue.isEnabled(name);
	}

	// #endregion

	get isCurrentLevel() {
		let app = this.object.app;

		return this.object == app.levelManager.current;
	}

	/**
	 * Get level action queue.
	 * @type {THING.ActionQueue}
	 * @private
	 */
	get actionQueue() {
		return this.object.actionGroup.get(ActionQueueType.EnterLevel);
	}

	/**
	 * Get level actions.
	 * @type {Array<ActionProcessor>}
	 * @private
	 */
	get actions() {
		let actionQueue = this.actionQueue;
		if (!actionQueue) {
			return [];
		}

		return actionQueue.actions;
	}

	/**
	 * The level configuration object.
	 * @typedef {Object} LevelConfig
	 * @property {Boolean} ignoreVisible - Flag indicating whether to ignore the visibility of level setting objects.
	 * @property {Boolean} ignoreEvent - Flag indicating whether mouse events for level objects are ignored.
	 * @property {Boolean} ignoreStyle - Flag indicating whether to ignore the style of level setting objects.
	 */

	/**
	 * Get or set the level configuration
	 * @type {LevelConfig}
	 *
	 * @example
	 * // Get the current level configuration
	 * let levelComponent  = THING.App.current.root.level;
	 * levelComponent.config = { ignoreVisible: true, ignoreStyle: false };
	 * // @expect { levelComponent.config.ignoreVisible === true }
	 * // @expect { levelComponent.config.ignoreStyle === false }
	 * @public
	 */
	get config() {
		let _private = this[__.private];

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

		_private.config = value;
	}

	/**
	 * The viewpoint.
	 * @typedef {Object} Viewpoint
	 * @property {Array<Number>} position the camera position
	 * @property {Array<Number>} target the camera target position
	 */

	/**
	 * Gets or sets the local camera position and target position (read from the scene file).
	 * @type {Viewpoint}
	 *
	 * @example
	 * let levelComponent = THING.App.current.root.level;
	 * // Get the local viewpoint
	 * let localViewpoint = levelComponent.localViewpoint;
	 * let ret1 = typeof localViewpoint === 'object';
	 * let ret2 = localViewpoint === null;
	 * // @expect { ret1 == true || ret2 == true }
	 *
	 * // Set the local viewpoint
	 * levelComponent.localViewpoint = {
	 *   position: [0, 0, 0],
	 *   target: [1, 1, 1]
	 * };
	 * // @expect {levelComponent.localViewpoint.position[0] === 0 && levelComponent.localViewpoint.target[2] === 1}
	 * @public
	 */
	get localViewpoint() {
		let _private = this[__.private];
		return _private.viewpoint;
	}
	set localViewpoint(value) {
		let _private = this[__.private];
		_private.viewpoint = value;
	}

	/**
	 * Gets or sets the world camera viewpoint position and camera target position (read from the scene file).
	 * @type {Viewpoint}
	 *
	 * @example
	 * let levelComponent = THING.App.current.root.level;
	 * // Get the world viewpoint
	 * let viewpoint = levelComponent.viewpoint;
	 * let ret1 = typeof viewpoint === 'object';
	 * let ret2 = viewpoint === null;
	 * // @expect { ret1 == true || ret2 == true }
	 *
	 * // Set the world viewpoint
	 * levelComponent.viewpoint = {
	 *   position: [0, 0, 0],
	 *   target: [1, 1, 1]
	 * };
	 * // @expect {levelComponent.viewpoint.position[0] === 0 && levelComponent.viewpoint.target[2] === 1}
	 * @public
	 */
	get viewpoint() {
		let _private = this[__.private];
		if (!_private.viewpoint) {
			return null;
		}
		return {
			target: this.object.selfToWorld(_private.viewpoint.target),
			position: this.object.selfToWorld(_private.viewpoint.position)
		};
	}
	set viewpoint(value) {
		let _private = this[__.private];
		if (value) {
			_private.viewpoint = {
				target: this.object.worldToSelf(value.target),
				position: this.object.worldToSelf(value.position)
			};
		}
	}

	/**
	 * @type {Function}
	 * @private
	 */
	async onLeaveAsync(param, resolve, reject) {
		let _private = this[__.private];

		if (!_private.isLeave) {
			let controlParams = _private.generateLevelControlParams(param);

			let levelControls = param.levelControls;
			for (let i = 0; i < levelControls.length; i++) {
				const control = levelControls[i];
				await control.onLeave(controlParams);
			}
			_private.isLeave = true;
		}
		resolve();
	}

	/**
	 * @type {Function}
	 * @private
	 */
	async onEnterAsync(param, resolve, reject) {
		let _private = this[__.private];
		_private.isLeave = false;

		let controlParams = _private.generateLevelControlParams(param);

		let levelControls = param.levelControls;

		for (let i = 0; i < levelControls.length; i++) {
			const control = levelControls[i];
			if (control) {
				await control.onEnter(controlParams);
			}
		}
		resolve();
	}

	/**
	 * Imports data from an external source.
	 * @param {Object} external - The external data to import.
	 *
	 * @example
	 * let levelComponent = THING.App.current.root.level;
	 * // Import data from an external source
	 * // Example 1: Importing data
	 * levelComponent.onImport({ viewpoint: { position: [1, 2, 3], target: [4, 5, 6] }, config: { ignoreStyle:true, ignoreVisible:false } });
	 * // @expect { levelComponent.localViewpoint.position[0] === 1 }
	 * // @expect { levelComponent.localViewpoint.target[2] === 6 }
	 * // @expect { levelComponent.config.ignoreStyle === true }
	 * // @expect { levelComponent.config.ignoreVisible === false }
	 * @public
	 */
	onImport(external) {
		if (!external) {
			return;
		}
		let _private = this[__.private];

		// Import viewpoint
		const viewpoint = external.viewpoint;
		if (viewpoint) {
			_private.viewpoint = Utils.cloneObject(viewpoint);
		}

		// Import config
		const config = external.config;
		if (config) {
			if (Utils.isBoolean(config.ignoreEvent)) {
				_private.config.ignoreEvent = config.ignoreEvent;
			}

			if (Utils.isBoolean(config.ignoreStyle)) {
				_private.config.ignoreStyle = config.ignoreStyle;
			}

			if (Utils.isBoolean(config.ignoreVisible)) {
				_private.config.ignoreVisible = config.ignoreVisible;
			}
		}
	}

	/**
	 * Export data to an external source.
	 * @returns {Object|null} - The exported data.
	 *
	 * @example
	 * let levelComponent = THING.App.current.root.level;
	 * levelComponent.onImport({ viewpoint: { position: [1, 2, 3], target: [4, 5, 6] }, config: { ignoreStyle:true, ignoreVisible:false } });
	 * // Export data to an external source
	 * let exportedData = levelComponent.onExport();
	 * // @expect { exportedData.viewpoint.position[1] === 2 }
	 * // @expect { exportedData.viewpoint.target[0] === 4 }
	 * // @expect { exportedData.config.ignoreStyle === true }
	 * let ret = exportedData.config.ignoreVisible == undefined;
	 * // @expect { ret === true }
	 * @public
	 */
	onExport() {
		let _private = this[__.private];
		let result = {};

		// Export viewpoint
		if (_private.viewpoint) {
			result.viewpoint = Utils.cloneObject(_private.viewpoint);
		}

		// False is the default value and is not export
		let config = null;
		if (_private.config.ignoreEvent === true) {
			config = config ? config : {};
			config.ignoreEvent = true;
		}

		if (_private.config.ignoreStyle === true) {
			config = config ? config : {};
			config.ignoreStyle = true;
		}

		if (_private.config.ignoreVisible === true) {
			config = config ? config : {};
			config.ignoreVisible = true;
		}

		// Export config
		if (config) {
			result.config = config;
		}

		return Object.keys(result).length ? result : null;
	}

}

export { LevelComponent }