Source: components/Component.js

import { Flags, ResolvablePromise } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { BaseComponent } from './BaseComponent';

const Flag = {
	Started: 1 << 0,
	Enable: 1 << 1,
	Removed: 1 << 2,
	AsyncCompleted: 1 << 3,
};

/**
 * When start to active component(just only once) in async mode.
 * @callback OnStartAsyncComponentCallback
 * @param {Object} param The initial parameters.
 * @returns {Promise<any>}
 */

/**
 * When update before render.
 * @callback OnLateUpdateComponentCallback
 * @param {Number} deltaTime The delta time in seconds.
 */

/**
 * @class Component
 * The component.
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class Component extends BaseComponent {

	static isInstancedComponent = true;

	_flags = new Flags();

	/**
	 * The component that extends some useful interfaces.
	 * The interface work flow:
	 * 1. onAwake -> onStart/onStartAsync
	 * 2. onUpdate -> onLateUpdate
	 * @example
	 * class MyRotator extends THING.Component {
	 *  onAwake(param) {
	 *   this.speed = param['speed'];
	 *  }
	 *
	 *  onStart() {
	 *   this.object.style.color = "0xFF0000";
	 *  }
	 *
	 *  onUpdate(deltaTime) {
	 *   this.object.rotateY(this.speed * deltaTime);
	 *  }
	 * }
	 *
	 * let box = new THING.Box();
	 * box.addComponent(MyRotator, 'rotator');
	 * box.rotator.speed = 100;
	 */
	constructor(param = {}) {
		/**
		 * When start to active component(just only once) in async mode and wait for finish, it's after onAwake() interface.
		 * @member {OnStartAsyncComponentCallback} onStartAsync
		 * @memberof THING.Component
		 * @instance
		 */

		/**
		 * When update before render.
		 * @member {OnLateUpdateComponentCallback} OnLateUpdate
		 * @memberof THING.Component
		 * @instance
		 */

		super();

		this._flags.enable(Flag.Enable, Utils.parseValue(param['enable'], true));

		this._isWaitingForUpdate = false;

		this._startingPromise = null;
	}

	// #region Private


	_isEditorByObject(object) {
		if (object && object.options['isEditor']) {
			return true;
		}
		return false;
	}

	_isEditor(args) {
		if (args && args['isEditor']) {
			return true;
		}

		// Try to get options from self
		if (this._isEditorByObject(this.object)) {
			return true;
		}

		// Try to get options from parents object
		if (this._belongsToEditor()) {
			return true;
		}

		let isEditor = Utils.parseValue(this.app.options['isEditor'], false);
		return isEditor;
	}

	_initLateUpdate() {
		// Prevent object destroy
		if (!this.object) {
			return;
		}

		if (this._onUpdate) {
			this.onUpdate = this._onUpdate;
			this._onUpdate = null;
		}

		if (!this.onLateUpdate) {
			return;
		}

		this.app.objectManager.addLateUpdateObject(this);
	}

	_processStart(args) {
		if (this._flags.has(Flag.Removed)) {
			return;
		}

		if (this.onStartAsync) {
			let startPromise = this.onStartAsync(args);
			if (startPromise) {
				startPromise.then(() => {
					this._isWaitingForUpdate = false;

					this._initLateUpdate();

					if (this._startingPromise) {
						this._startingPromise.resolve();
					}

					this._flags.enable(Flag.AsyncCompleted, true);
				});
			}
			else {
				this._isWaitingForUpdate = false;

				this._initLateUpdate();
			}
		}
		else {
			this.onStart(args);

			this._initLateUpdate();
		}
	}

	_belongsToEditor() {
		let isEditor = false;

		const parents = this.object.parents;

		for (let i = 0; i < parents.length; i++) {
			const parent = parents[i];
			if (!parent.options) {
				continue;
			}

			if (parent.options.isEditor === true) {
				isEditor = true;
				break;
			}
		}

		return isEditor;
	}

	// #endregion

	// #region Component - Overrides

	onProcessStartEvent(args) {
		if (this._isEditor(args)) {
			return;
		}

		if (this._flags.has(Flag.Started)) {
			return;
		}

		this._flags.enable(Flag.Started, true);

		if (this.onStartAsync) {
			this._isWaitingForUpdate = true;
		}
		else {
			if (this._startingPromise) {
				this._startingPromise.resolve();
			}
		}

		// in child component, may not have onUpdate.
		this._onUpdate = this.onUpdate;
		if (this._onUpdate) {
			this.onUpdate = () => {};
		}

		this.object.waitForComplete().then(() => {
			Utils.setTimeout(() => {
				this._processStart(args);
			});
		});
	}

	onProcessAddEvent(args) {
		if (this._isEditor(args)) {
			// Remove 'onUpdate' interface when it's running in editor env
			this.onUpdate = null;
		}
		else {
			if (this._flags.has(Flag.Enable)) {
				args = args || {};

				this.onAwake(args);

				this.onProcessStartEvent(args);
			}
			else {
				this.active = false;
			}
		}
	}

	onProcessActiveChangeEvent(value) {
		if (this._isEditor()) {
			return;
		}

		if (value) {
			this.onEnable();

			this.onProcessStartEvent();
		}
		else {
			this.onDisable();
		}
	}

	onAdd(object, args) {
		super.onAdd(object);

		// If we add it with prefab object then need to merge options with args
		let ownerObject = this.getOwnerObject();
		if (ownerObject) {
			args = Utils.mergeObject(Object.assign({}, ownerObject.options), args);
		}

		this.onProcessAddEvent(args);
	}

	triggerBeforeAddCallback(object) {
		if (!this._isEditorByObject(object)) {
			super.triggerBeforeAddCallback();
		}
	}

	triggerAfterAddCallback() {
		if (!this._isEditor()) {
			super.triggerAfterAddCallback();
		}
	}

	onRemove() {
		this._flags.enable(Flag.Removed, true);

		if (this.onLateUpdate) {
			this.app.objectManager.removeLateUpdateObject(this);
		}

		this.onDestroy();

		super.onRemove();
	}

	triggerBeforeRemoveCallback() {
		if (!this._isEditor()) {
			super.triggerBeforeRemoveCallback();
		}
	}

	triggerAfterRemoveCallback(object) {
		if (!this._isEditorByObject(object)) {
			super.triggerAfterRemoveCallback();
		}
	}

	/**
	 * When add component(just only once).
	 * @param {Object} args The constructor arguments.
	 */
	onAwake(args) {
	}

	/**
	 * When start to active component(just only once), it's after onAwake() interface.
	 * @param {Object} args The constructor arguments.
	 */
	onStart(args) {

	}

	/**
	 * When remove component.
	 */
	onDestroy() {

	}

	/**
	 * When active it.
	 */
	onEnable() {

	}

	/**
	 * When deactivate it.
	 */
	onDisable() {

	}

	/**
	 * Import data
	 * @param {Object} param The import data.
	 */
	onImport(param) {

	}

	/**
	 * Export data
	 * @returns {Object}
	 */
	onExport() {
		return {
		}
	}

	onActiveChange(value) {
		this.onProcessActiveChangeEvent(value);
	}

	// #endregion

	getPrefabRootObject() {
		Utils.warn('The interface is outdated, please use it getOwnerObject().');
		return this.getOwnerObject();
	}

	getOwnerObject() {
		if (this.object.isOwner) {
			return this.object;
		}

		let owner = null;
		const parents = this.object.parents;
		for (let i = 0; i < parents.length; i++) {
			const parent = parents[i];
			if (parent.isOwner === true) {
				owner = parent;
				break;
			}
		}

		return owner;
	}

	resolveUrlFromRoot(url) {
		let ownerObject = this.getOwnerObject();
		if (ownerObject) {
			let path = ownerObject.resource.url;
			let ext = path._getExtension();
			if (ext) {
				path = path._getPath();
			}

			return path._appendPath(url);
		}
		return url;
	}

	// #region Accessors

	get isWaitingForUpdate() {
		return this._isWaitingForUpdate;
	}

	get camera() {
		return this.app.camera;
	}

	get root() {
		return this.app.root;
	}

	set enable(value) {
		this.active = value;
	}
	get enable() {
		return this.active;
	}

	/**
	 * Get the starting promise.
	 * @type {Promise<any>}
	 */
	get startingPromise() {
		if (!this._startingPromise) {
			if (this._flags.has(Flag.AsyncCompleted)) {
				this._startingPromise = Promise.resolve();
			}
			else {
				if (this.onStartAsync) {
					this._startingPromise = new ResolvablePromise();
				}
				else {
					if (this._flags.has(Flag.Started)) {
						return Promise.resolve();
					}
					else {
						this._startingPromise = new ResolvablePromise();
					}
				}
			}
		}

		return this._startingPromise;
	}

	// #endregion

	get isComponent() {
		return true;
	}

}

export { Component }