Source: relationship/Relationship.js

import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { RelationshipDirection } from '../const';
import { Selector } from '../selector/Selector';

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

/**
 * @class Relationship
 * The Relationship between objects.
 * @memberof THING
 * @public
 */
class Relationship {

	/**
	 * The relationship between objects.
	 * @param {Object} param
	 * @param {String} param.type The relationship type.
	 * @param {String} param.name The relationship name.
	 * @param {THING.BaseObject|Array<THING.BaseObject>|THING.Selector} param.source The source object(s) of relation.
	 * @param {THING.BaseObject|Array<THING.BaseObject>|THING.Selector} param.target The target object(s) of relation.
	 * @param {String} [param.queryDirection] The default query relationship direction.
	 */
	constructor(param = {}) {
		this[__.private] = {};
		let _private = this[__.private];

		_private.app = Utils.getCurrentApp();

		let source = this._checkObject(param['source']);
		let target = this._checkObject(param['target']);

		this._type = param['type'];
		this._name = Utils.parseValue(param['name'], '');
		this._source = source;
		this._target = target;
		this._uuid = param['uuid'] || MathUtils.generateUUID();
		this._queryDirection = Utils.parseValue(param['queryDirection'], RelationshipDirection.Out);

		_private.app.relationshipManager.addRelationship(this);
	}

	// #region Set

	/**
	 * check object
	 * @private
	 */
	_checkObject(object) {
		if (Utils.isNull(object)) {
			return null;
		}
		else if (object.isSelector) {
			return [...new Set(object.objects)];
		}
		else if (Utils.isArray(object)) {
			return [...new Set(object)];
		}
		else if (object.isBaseObject) {
			return object;
		}
		else {
			Utils.error('The object is not an allowed value.');
			return null;
		}
	}

	// #endregion

	// #region test

	/**
	 * Test whether the relationship meets the condition.
	 * @param {Object} options test condition
	 * @returns {Boolean}
	 * @private
	 * @example
	 * app.queryRelationships().forEach((relstionship) => {
	 *      relationship.test({ type: 'BelongTo' });
	 * });
	 */
	test(options = {}) {
		let type = options['type'];
		let name = options['name'];

		if (type) {
			if (name) {
				return (this.type == type && this.name == name);
			}
			else {
				return this.type == type;
			}
		}
		else if (name) {
			return this.name == name;
		}

		return false;
	}

	/**
	 * Query other objects in the relationship.
	 * @param {RelationshipDirection} queryDirection Query direction.
	 * @returns {THING.Selector}
	 * @private
	 * @example
	 * let lightSwitch = new Box();
	 * let light = new Box();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 *      source: light,
	 *      target: lightSwitch
	 * });
	 * app.queryRelationships().forEach((relstionship) => {
	 *      relationship.queryObjects()
	 * });
	 */
	queryObjects(object, queryDirection) {
		let target = this.target;
		let source = this.source;

		let objects = new Selector();

		queryDirection = Utils.parseValue(queryDirection, this.queryDirection);

		switch (queryDirection) {
			case RelationshipDirection.Out:
				if (this._includeObject(source, object)) {
					objects.push(target);
				}
				break;
			case RelationshipDirection.In:
				if (this._includeObject(target, object)) {
					objects.push(source);
				}
				break;
			case RelationshipDirection.InOut:
				if (this._includeObject(source, object)) {
					objects.push(target);
				}
				if (this._includeObject(target, object)) {
					objects.push(source);
				}
				break;
			case RelationshipDirection.None:
				if (this._includeObject(source, object)) {
					objects.push(source);
				}
				if (this._includeObject(target, object)) {
					objects.push(target);
				}
				break;
		}

		return objects;
	}

	_includeObject(object, target) {
		if (Utils.isNull(object)) {
			return false;
		}
		else if (Utils.isArray(object)) {
			return object.includes(target);
		}
		else if (object.isBaseObject) {
			return object == target;
		}
	}

	// #endregion

	// #region destroy

	/**
	 * When destroy.
	 * @private
	 */
	onDestroy() {
		let _private = this[__.private];

		_private.app.relationshipManager.removeRelationship(this);
	}

	/**
	 * destroy relationship
	 * @public
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 *      source: source,
	 *      target: target
	 * });
	 * rel.destroy()
	 * // @expect(source.relationships.length == 0)
	 */
	destroy() {
		this.onDestroy();

		this._type = null;
		this._name = null;
		this._uuid = null;
		this._source = null;
		this._target = null;
		this._queryDirection = null;
	}

	/**
	 * object destroy release relationship
	 * @param {THING.BaseObject} object Remove the object from the relationship.
	 * @private
	 */
	releaseObject(object) {
		let source = this.source;
		let target = this.target;

		if (object == target) {
			this.destroy();
		}
		else if (Utils.isArray(target) && target.indexOf(object) !== -1) {
			Utils.removeFromArray(target, object);
			if (target.length == 0 || (target.length <= 1 && !source)) {
				this.destroy();
			}
		}
		else if (object == source) {
			this.destroy();
		}
		else if (Utils.isArray(source) && source.indexOf(object) !== -1) {
			Utils.removeFromArray(source, object);
			if (source.length == 0 || (source.length <= 1 && !target)) {
				this.destroy();
			}
		}
	}

	// #endregion

	// #region Accessor

	/**
	 * Get/Set the relationship type.
	 * @type {String}
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 *      source: source,
	 *      target: target
	 * });
	 * // @expect(rel.type == 'control')
	 * @public
	 */
	get type() {
		return this._type;
	}
	set type(value) {
		this._type = value;
	}

	/**
	 * Get/Set the relationship name.
	 * @type {String}
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 * 		name: 'myRelationship',
	 *      source: source,
	 *      target: target
	 * });
	 * // @expect(rel.name == 'myRelationship')
	 * @public
	 */
	get name() {
		return this._name;
	}
	set name(value) {
		this._name = value;
	}

	/**
	 * Get/Set uuid.
	 * @type {String}
	 * @example
	 * relationship.uuid = THING.Math.generateUUID();
	 * console.log(object.uuid);
	 */
	get uuid() {
		return this._uuid;
	}
	set uuid(value) {
		this._uuid = value;
	}

	/**
	 * Get/Set the relationship source object.
	 * @type {THING.BaseObject|Array<THING.BaseObject>|THING.Selector}
	 * @public
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 * 		name: 'myRelationship',
	 *      source: source,
	 *      target: target
	 * });
	 * // @expect(rel.source == source)
	 */
	get source() {
		return this._source;
	}
	set source(value) {
		let _private = this[__.private];

		value = this._checkObject(value);

		_private.app.relationshipManager.notifyObjectRemoveRelationship(this._source, this);
		_private.app.relationshipManager.notifyObjectAddRelationship(value, this);

		this._source = value;
	}

	/**
	 * Get/Set the relationship target.
	 * @type {THING.BaseObject|Array<THING.BaseObject>|THING.Selector}
	 * @public
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 * 		name: 'myRelationship',
	 *      source: source,
	 *      target: target
	 * });
	 * // @expect(rel.target == target)
	 */
	get target() {
		return this._target;
	}
	set target(value) {
		let _private = this[__.private];

		value = this._checkObject(value);

		_private.app.relationshipManager.notifyObjectRemoveRelationship(this._target, this);
		_private.app.relationshipManager.notifyObjectAddRelationship(value, this);

		this._target = value;
	}

	/**
	 * Get/Set the relationship default query direction.
	 * @type {RelationshipDirection}
	 * @public
	 * @example
	 * let source = new THING.Object3D()();
	 * let target = new THING.Object3D()();
	 * let rel = new THING.Relationship({
	 *      type: 'control',
	 * 		name: 'myRelationship',
	 *      source: source,
	 *      target: target
	 * });
	 * // @expect(rel.queryDirection == THING.RelationshipDirection.OUT)
	 */
	get queryDirection() {
		return this._queryDirection;
	}
	set queryDirection(value) {
		this._queryDirection = value;
	}

	// #endregion

}

export { Relationship }