Source: components/BaseComponentGroup.js

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

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

let _defaultParams = {};

/**
 * @class BaseComponentGroup
 * The base component group.
 * @memberof THING
 * @public
 */
class BaseComponentGroup {

	/**
	 * The base component group to manage components, you should inherit from it when you want to manage multiple components.
	 */
	constructor() {
		this[__.private] = {};
		let _private = this[__.private];

		// Components
		_private.components = new Map();
		// Quick components accessor info
		_private.componentsAccessorInfo = {};

		// Quick components for some specified actions
		_private.tickableComponents = null;
		_private.renderableComponents = null;
		_private.resizableComponents = null;
		_private.refreshableComponents = null;
		_private.loadableComponents = null;
		_private.unloadableComponents = null;
		_private.visibleComponents = null;
		_private.parentChangeComponents = null;

		_private.componentRegisterOrder = 0;
	}

	// #region Private Functions

	_linkProperties(classType, componentName) {
		let exportProperties = classType.exportProperties;
		if (exportProperties) {
			exportProperties.forEach(propertyName => {
				Object.defineProperty(this, propertyName, {
					enumerable: false,
					configurable: true,
					set(value) {
						let component = this[componentName];
						component[propertyName] = value;
					},
					get() {
						let component = this[componentName];
						return component[propertyName];
					}
				});
			});
		}
	}

	_linkFunctions(classType, componentName) {
		let exportFunctions = classType.exportFunctions;
		if (exportFunctions) {
			exportFunctions.forEach(funcName => {
				this[funcName] = function () {
					let component = this[componentName];
					return component[funcName].apply(component, arguments);
				}
			});
		}
	}

	_linkComponent(classType, componentName, args = _defaultParams) {
		let autoRegister = Utils.parseValue(args['autoRegister'], true);
		if (autoRegister) {
			this._linkProperties(classType, componentName);
			this._linkFunctions(classType, componentName);
		}
	}

	_unlinkComponent(classType) {
		// Get properties from component to register into self object
		let exportProperties = classType.exportProperties;
		if (exportProperties) {
			exportProperties.forEach(propertyName => {
				delete this[propertyName];
			});
		}

		// Get functions from component to register into self object
		let exportFunctions = classType.exportFunctions;
		if (exportFunctions) {
			exportFunctions.forEach(funcName => {
				delete this[funcName];
			});
		}
	}

	_setComponent(component, name, args) {
		if (!component) {
			return null;
		}

		let _private = this[__.private];

		// Add component (we need to add it to set first, prevent infinity-loop in onAdd() interface of BaseCompnent)
		_private.components.set(name, component);

		// Notify added component event
		this.onAddComponent(component, args);

		// Update tickable components list
		if (component.onUpdate) {
			_private.tickableComponents = _private.tickableComponents || [];
			_private.tickableComponents.push(component);
		}

		// Update renderable components list
		if (component.onRender) {
			_private.renderableComponents = _private.renderableComponents || [];
			_private.renderableComponents.push(component);
		}

		// Update resizable components list
		if (component.onResize) {
			_private.resizableComponents = _private.resizableComponents || [];
			_private.resizableComponents.push(component);

			// Resize event should call it now
			let object = this.object;
			if (object) {
				if (object.isApp) {
					component.onResize(object.size[0], object.size[1]);
				}
				else {
					component.onResize(object.app.size[0], object.app.size[1]);
				}
			}
		}

		// Update refresh components list
		if (component.onRefresh) {
			_private.refreshableComponents = _private.refreshableComponents || [];
			_private.refreshableComponents.push(component);
		}

		// Update loadable components list
		if (component.onLoadResource) {
			_private.loadableComponents = _private.loadableComponents || [];
			_private.loadableComponents.push(component);
		}

		// Update unloadable components list
		if (component.onUnloadResource) {
			_private.unloadableComponents = _private.unloadableComponents || [];
			_private.unloadableComponents.push(component);
		}

		// Update visible components list
		if (component.onVisibleChange) {
			_private.visibleComponents = _private.visibleComponents || [];
			_private.visibleComponents.push(component);
		}

		// Update parent change components list
		if (component.onParentChange) {
			_private.parentChangeComponents = _private.parentChangeComponents || [];
			_private.parentChangeComponents.push(component);
		}

		return component;
	}

	_getComponentFromAccessor(classType, name, args) {
		let _private = this[__.private];

		let component = _private.components.get(name);
		if (!component) {
			component = args ? new classType(args) : new classType();
			this._setComponent(component, name, args);
		}

		// Replace component to speed up accessor
		delete this[name];
		this[name] = component;

		// Mark component as initialized
		let accessorinfo = _private.componentsAccessorInfo[name];
		accessorinfo.hasInstance = true;
		accessorinfo.classType = classType;

		return component;
	}

	_createAccessor(classType, name, args) {
		return {
			enumerable: false,
			configurable: true,
			get: () => {
				return this._getComponentFromAccessor(classType, name, args);
			}
		}
	}

	_linkAccessor(classType, name, args) {
		let _private = this[__.private];

		// Create accessor
		let accessor = this._createAccessor(classType, name, args);

		// Build accessor info
		let accessorInfo = {
			classType,
			accessor,
			order: _private.componentRegisterOrder++
		};

		// Save constructor arguments
		if (args && !args['isTemporary']) {
			accessorInfo.args = Object.assign({}, args);

			// Check whether it's resident component
			accessorInfo.isResident = Utils.parseValue(args['isResident'], false);
		}

		// Update accessor info
		_private.componentsAccessorInfo[name] = accessorInfo;

		// Update object's component accessor
		Object.defineProperty(this, name, accessor);
	}

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

		delete this[name];

		// Remove component accessor with name
		delete _private.componentsAccessorInfo[name];
	}

	_removeComponent(name, unlink = true) {
		let _private = this[__.private];

		// Get existing component by name
		let component = _private.components.get(name);
		if (!component) {
			return false;
		}

		// Notify component will be removed
		this.onRemoveComponent(component);

		// Remove component
		_private.components.delete(name);

		// Remove from list
		Utils.removeFromArray(_private.tickableComponents, component);
		Utils.removeFromArray(_private.renderableComponents, component);
		Utils.removeFromArray(_private.resizableComponents, component);
		Utils.removeFromArray(_private.refreshableComponents, component);
		Utils.removeFromArray(_private.loadableComponents, component);
		Utils.removeFromArray(_private.unloadableComponents, component);
		Utils.removeFromArray(_private.visibleComponents, component);
		Utils.removeFromArray(_private.parentChangeComponents, component);

		// Unlink accessor
		if (unlink) {
			this._unlinkAccessor(name);
		}

		return true;
	}

	_registerComponent(classType, name, args) {
		// Remove existing component
		if (this.hasComponent(name)) {
			this.removeComponent(name);
		}

		// Link component
		this._linkAccessor(classType, name, args);
		this._linkComponent(classType, name, args);

		// Check whether auto create component
		if (classType.isInstancedComponent) {
			if (!this._getComponentFromAccessor(classType, name, args)) {
				return false;
			}
		}

		return true;
	}

	_unregisterComponent(accessor, name) {
		if (this._removeComponent(name)) {
			if (accessor.hasInstance) {
				this._unlinkComponent(accessor.classType);
			}
		}
		else {
			this._unlinkAccessor(name);
		}
	}

	_importComponentExternalData(name, external, options) {
		if (external) {
			let onGetComponentExternalData = options['onGetComponentExternalData'];
			if (onGetComponentExternalData) {
				external = onGetComponentExternalData(this, name, external);
				if (!external) {
					return;
				}
			}

			let component = this[name];
			if (component.onImport) {
				component.onImport(external, options);
			}
		}
	}

	// #endregion

	// #region Overrides

	onAddComponent(component, args) {
		component.onAdd(this, args);
	}

	onRemoveComponent(component) {
		component.onRemove();
	}

	// #endregion

	/**
	 * Check whether has registered component.
	 * @param {String} name The name.
	 * @returns {Boolean}
	 * @private
	 */
	hasRegisteredComponent(name) {
		let _private = this[__.private];

		let accessor = _private.componentsAccessorInfo[name];
		if (!accessor) {
			return false;
		}

		return true;
	}

	/**
	 * Get the accessor info of components.
	 * @returns {Object}
	 * @private
	 */
	getAccessorInfo() {
		let _private = this[__.private];

		return _private.componentsAccessorInfo;
	}

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

		_private.componentsAccessorInfo = {};
	}

	/**
	 * Add component.
	 * @param {THING.BaseComponent|Object} component The component class or component object.
	 * @param {String} name The name.
	 * @param {Object} args? The initial arguments to create component.
	 * @returns {Boolean}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'myComponent');
	 * // @expect(obj.myComponent != null)
	 */
	addComponent(component, name, args) {
		if (!component) {
			return false;
		}

		if (!Utils.isString(name)) {
			// Replace component args
			if (Utils.isObject(name)) {
				args = name;
			}

			// Generate unique name if not provide
			name = MathUtils.generateUUID();
		}

		if (Utils.isObject(component)) {
			// Remove the previous component if we provide name
			if (name) {
				this._removeComponent(name, false);
			}

			// Bind component
			this._setComponent(component, name, args);

			// Link component
			this._linkAccessor(component.constructor, name, args);
			this._linkComponent(component.constructor, name, args);
		}
		else {
			// Bind component
			this._registerComponent(component, name, args);
		}

		return true;
	}

	/**
	 * Remove component.
	 * @param {String} name The name.
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'myComponent');
	 * obj.removeComponent('myComponent');
	 * // @expect(obj.components.size == 0)
	 */
	removeComponent(name) {
		let _private = this[__.private];

		let accessorInfo = _private.componentsAccessorInfo[name];

		this._removeComponent(name);

		if (accessorInfo) {
			this._linkAccessor(accessorInfo.classType, name, accessorInfo.args);
		}

		this._unregisterComponent(accessorInfo, name);
	}

	/**
	 * Remove all components.
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'component1');
	 * obj.addComponent(new THING.BaseComponent(), 'component2');
	 * obj.removeAllComponents();
	 * // @expect(obj.components.size == 0)
	 */
	removeAllComponents(force = false) {
		let _private = this[__.private];

		// Get all component names.
		let componentNames = [..._private.components.keys()];

		// Remove none resident components first
		componentNames.sort((a, b) => {
			let a1 = _private.componentsAccessorInfo[a];
			let b1 = _private.componentsAccessorInfo[b];

			if (a1.isResident && !b1.isResident) {
				return 1;
			}

			if (b1.isResident && !a1.isResident) {
				return -1;
			}

			return b1.order - a1.order;
		});

		// Remove all components
		componentNames.forEach(name => {
			// Skip for resident component
			let accessorInfo = _private.componentsAccessorInfo[name];
			if (!force && accessorInfo.isResident) {
				return;
			}

			this.removeComponent(name);
		});
	}

	/**
	 * The function to call when traverse component by type.
	 * @callback TraverseComponentByTypeCallback
	 * @param {THING.BaseComponent} component The component.
	 * @param {String} name The component name.
	 */

	/**
	 * Traverse component by type.
	 * @param {*} type The component type.
	 * @param {TraverseComponentByTypeCallback} callback The callback function.
	 * @public
	 * @example
	 * 	object.addComponent(new MyComponent(), 'myComponent');
	 * 	object.traverseComponentByType(MyComponent, (component, name) => {
	 * 		console.log(component, name);
	 * 	});
	 */
	traverseComponentByType(type, callback) {
		let _private = this[__.private];

		let accessors = _private.componentsAccessorInfo;
		for (let key in accessors) {
			let component = this[key];
			if (!component) {
				continue;
			}

			if (component instanceof type) {
				callback(this[key], key);
			}
		}
	}

	/**
	 * Get component by name.
	 * @param {String} name The name.
	 * @returns {THING.BaseComponent}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'myComponent');
	 * let component = obj.getComponentByName('myComponent');
	 * // @expect(component != null)
	 */
	getComponentByName(name) {
		let _private = this[__.private];

		let component = _private.components.get(name);
		if (component) {
			return component;
		}

		let accessorInfo = _private.componentsAccessorInfo[name];
		if (accessorInfo) {
			return accessorInfo.accessor.get();
		}

		return null;
	}

	/**
	 * Get component by type.
	 * @param {*} type The component type.
	 * @returns {THING.BaseComponent}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'myComponent');
	 * let component = obj.getComponentByType(THING.BaseComponent);
	 * // @expect(component != null)
	 */
	getComponentByType(type) {
		let _private = this[__.private];

		let accessors = _private.componentsAccessorInfo;
		for (let key in accessors) {
			let component = this[key];
			if (!component) {
				continue;
			}

			if (component instanceof type) {
				return component;
			}
		}

		return null;
	}

	/**
	 * Get components by type.
	 * @param {*} type The component type.
	 * @returns {Array<THING.BaseComponent>}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'component1');
	 * obj.addComponent(new THING.BaseComponent(), 'component2');
	 * let components = obj.getComponentsByType(THING.BaseComponent);
	 * // @expect(components.length == 2)
	 */
	getComponentsByType(type) {
		let components = [];
		this.traverseComponentByType(type, (component) => {
			components.push(component);
		});

		return components;
	}

	/**
	 * Get all components(it would create all registered components).
	 * @returns {Array<THING.BaseComponent>}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'component1');
	 * obj.addComponent(new THING.BaseComponent(), 'component2');
	 * let components = obj.getAllComponents();
	 * // @expect(components.length == 2)
	 */
	getAllComponents() {
		let _private = this[__.private];

		let components = [];

		let accessors = _private.componentsAccessorInfo;
		for (let key in accessors) {
			let component = this.getComponentByName(key);
			if (!component) {
				continue;
			}

			components.push(component);
		}

		return components;
	}

	/**
	 * Check whether has component.
	 * @param {String} name The name.
	 * @returns {Boolean}
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'myComponent');
	 * let ret = obj.hasComponent('myComponent')
	 * // @expect(ret == true)
	 */
	hasComponent(name) {
		let _private = this[__.private];

		return _private.components.has(name);
	}

	/**
	 * Check whether has component instance.
	 * @param {String} name The name.
	 * @returns {Boolean}
	 * @private
	 */
	hasComponentInstance(name) {
		let _private = this[__.private];

		let accessorInfo = _private.componentsAccessorInfo[name];
		if (!accessorInfo) {
			return false;
		}

		return accessorInfo.hasInstance;
	}

	/**
	 * Check whether it's resident component.
	 * @param {String} name The name.
	 * @returns {Boolean}
	 * @private
	 */
	isResidentComponent(name) {
		let _private = this[__.private];

		let accessorInfo = _private.componentsAccessorInfo[name];
		if (!accessorInfo) {
			return false;
		}

		return !!accessorInfo.isResident;
	}

	onImportComponents(components, options = {}) {
		let onGetObjectClassType = options['onGetObjectClassType'];
		let onError = options['onError'];

		// Import all components
		components.forEach(componentData => {
			let name = componentData.name;
			let external = componentData.external || componentData.params || {};

			let component = this[name];
			if (component) {
				// Import external data
				this._importComponentExternalData(name, external, options);
			}
			else {
				// Get component class type
				let componentClassType = onGetObjectClassType(componentData);
				if (!componentClassType) {
					if (onError) {
						onError(`Load components failed, due to '${componentData.type}' is unkonwn`);
					}
					return;
				}

				// Register component by class type
				let params = Object.assign({}, external);
				Object.assign(params, options);
				delete params.args;
				params = Object.assign(params, options['args']);
				this.addComponent(componentClassType, name, params);

				// Import external data
				this._importComponentExternalData(name, external, options);
			}
		});
	}

	onExportComponents(options = {}) {
		let onGetComponentType = options['onGetComponentType'];

		let components = [];

		// Export all components
		this.components.forEach((component, name) => {
			if (!component.onExport) {
				return;
			}

			// Export data from component
			let data = component.onExport(options);
			if (!data) {
				return;
			}

			// Get the component type
			let type;
			if (onGetComponentType) {
				type = onGetComponentType(component.constructor.name);
			}
			else {
				type = component.constructor.name;
			}

			// Update components
			components.push({
				name,
				type,
				params: data
			});
		});

		return components;
	}

	onImportExternalComponents(externalComponents) {
		return Promise.resolve();
	}

	onExportExternalComponents() {
	}

	/**
	 * Get the tickable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get tickableComponents() {
		let _private = this[__.private];

		return _private.tickableComponents;
	}

	/**
	 * Get the renderable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get renderableComponents() {
		let _private = this[__.private];

		return _private.renderableComponents;
	}

	/**
	 * Get the resizable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get resizableComponents() {
		let _private = this[__.private];

		return _private.resizableComponents;
	}

	/**
	 * Get the refreshable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get refreshableComponents() {
		let _private = this[__.private];

		return _private.refreshableComponents;
	}

	/**
	 * Get the loadable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get loadableComponents() {
		let _private = this[__.private];

		return _private.loadableComponents;
	}

	/**
	 * Get the unloadable components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get unloadableComponents() {
		let _private = this[__.private];

		return _private.unloadableComponents;
	}

	/**
	 * Get the visible components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get visibleComponents() {
		let _private = this[__.private];

		return _private.visibleComponents;
	}

	/**
	 * Get the parent change components.
	 * @type {Array<THING.BaseComponent>}
	 * @private
	 */
	get parentChangeComponents() {
		let _private = this[__.private];

		return _private.parentChangeComponents;
	}

	/**
	 * Get all components.
	 * @type {Map<String, THING.BaseComponent>}
	 * @readonly
	 * @public
	 * @example
	 * let obj = new THING.BaseObject();;
	 * obj.addComponent(new THING.BaseComponent(), 'component1');
	 * obj.addComponent(new THING.BaseComponent(), 'component2');
	 * // @expect(obj.components.size == 2)
	 */
	get components() {
		return this[__.private].components;
	}

	get isComponentGroup() {
		return true;
	}

}

export { BaseComponentGroup }