Source: objects/BodyObject.js

import { Utils } from '../common/Utils';
import { MathUtils } from '../math/MathUtils';
import { NodeObject } from './NodeObject';
import { Style } from '../resources/Style';
import { StyleModifier } from '../resources/StyleModifier';

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

let _vec3 = MathUtils.createVec3();
let _scale = MathUtils.createVec3();
let _quat = MathUtils.createQuat();
let _mat4 = MathUtils.createMat4();
let _identity = MathUtils.createMat4();
let _axis = [0, 0, 0];

const _defaultOptions = {};

/**
 * @class BodyObject
 * The body object.
 * @memberof THING
 */
class BodyObject {

	/**
	 * The body object to access object's self renderable node(s).
	 */
	constructor() {
		this[__.private] = {};
		let _private = this[__.private];

		_private.node = null;
		_private.object = null;

		_private.pivotNode = null;
		_private.rootNode = null;
		_private.renderableNode = null;

		_private.style = null;
	}

	// #region Private

	_createParentNode(name, node) {
		// Create parent node
		let parentNode = Utils.createObject('Node');
		parentNode.setName(name);

		// Link to root node
		if (node.getParent()) {
			node.getParent().add(parentNode);
		}

		// Keep transform
		parentNode.setMatrixWorld(node.getMatrixWorld(_mat4));
		node.setMatrix(_identity);
		parentNode.add(node);

		// Copy user data
		let toUserData = parentNode.getUserData();
		let fromUserData = node.getUserData();
		for (let key in fromUserData) {
			toUserData[key] = fromUserData[key];
		}

		return parentNode;
	}

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

		_private.node = _private.rootNode || _private.pivotNode || _private.renderableNode;
	}

	_updateNodeOptions(options = _defaultOptions) {
		let _private = this[__.private];

		let renderableNode = _private.renderableNode;

		// Set the local scale
		let localScale = options['localScale'];
		if (localScale) {
			if (localScale[0] != 1 || localScale[1] != 1 || localScale[2] != 1) {
				this.createRootNode();

				MathUtils.vec3.multiply(_vec3, renderableNode.getScale(_vec3), localScale);
				renderableNode.setScale(_vec3);
			}
		}

		// Set the local position(offset)
		let localPosition = options['localPosition'];
		if (localPosition) {
			if (localPosition[0] != 0 || localPosition[1] != 0 || localPosition[2] != 0) {
				this.createRootNode();

				MathUtils.vec3.add(_vec3, renderableNode.getPosition(_vec3), localPosition)
				renderableNode.setPosition(_vec3);
			}
		}

		// Set the local angles
		let localAngles = options['localAngles'];
		if (localAngles) {
			if (localAngles[0] != 0 || localAngles[1] != 0 || localAngles[2] != 0) {
				this.createRootNode();

				MathUtils.quat.multiply(_quat, renderableNode.getQuaternion(_quat), MathUtils.getQuatFromAngles(localAngles));
				renderableNode.setQuaternion(_quat);
			}
		}
	}

	_getNodeByName(name, rootNode, filter, complete) {
		let _private = this[__.private];

		let nodes = this._getSubNodes(_private.renderableNode, rootNode);

		for (let i = 0; i < nodes.length; i++) {
			let node = nodes[i];

			if (filter) {
				if (!filter(node)) {
					continue;
				}
			}

			if (node.name == name) {
				if (complete) {
					return complete(node);
				}
				else {
					return node;
				}
			}
		}

		return null;
	}

	// #endregion

	// #region BaseComponent

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

		// Unbind sub node for renderable node
		let renderableNode = this.getRenderableNode();
		if (renderableNode) {
			renderableNode.unbindSubNode();
		}

		// Unapply current style
		if (_private.style) {
			_private.style.dispose();
			_private.style = null;
		}

		if (_private.rootNode) {
			_private.rootNode.dispose();
		}
		else if (_private.pivotNode) {
			_private.pivotNode.dispose();
		}
		else if (_private.renderableNode) {
			_private.renderableNode.dispose();
		}

		_private.rootNode = null;
		_private.pivotNode = null;
		_private.renderableNode = null;

		_private.node = null;
		_private.object = null;
	}

	// #endregion

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

		_private.object = object;

		let node = options['node'];
		let rootNode = options['rootNode'];
		let renderableNode = options['renderableNode'];

		if (rootNode || renderableNode) {
			_private.rootNode = rootNode;
			_private.renderableNode = renderableNode || rootNode;
		}
		else {
			let app = object.app;

			let nodePool = app.resourceManager.getNodePool();
			_private.renderableNode = nodePool ? nodePool.alloc() : Utils.createObject('Node');

			if (node) {
				_private.renderableNode.add(node);
			}
		}

		// Update current node
		this._updateNode();

		// Update node options
		let bodyOptions = options['body'];
		if (bodyOptions) {
			this._updateNodeOptions(bodyOptions);
		}

		// Bind renderable node with object
		let userData = _private.node.getUserData();
		userData['baseObject_orderID'] = _private.object.orderId;
	}

	translateOnAxis(axis, distance) {
		MathUtils.vec3.normalize(_axis, axis);
		MathUtils.vec3.transformQuat(_vec3, _axis, this.localQuaternion);
		MathUtils.vec3.scale(_vec3, _vec3, distance);

		let position = this.localPosition;
		this.localPosition = MathUtils.addVector(position, _vec3);
	}

	translateX(distance) {
		this.translateOnAxis(Utils.xAxis, distance);
	}

	translateY(distance) {
		this.translateOnAxis(Utils.yAxis, distance);
	}

	translateZ(distance) {
		this.translateOnAxis(Utils.zAxis, distance);
	}

	// #region State

	/**
	 * Get/Set visible state.
	 * @type {Boolean}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.visible == true;
	 * // @expect(ret == true)
	 * object.body.visible = false;
	 * ret = object.body.visible == false;
	 * // @expect(ret == true)
	 */
	get visible() {
		let _private = this[__.private];

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

		_private.renderableNode.setVisible(value);
	}

	// #endregion

	// #region Transform

	/**
	 * Get/Set local(offset) position of the parent space.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.localPosition[1] == 0;
	 * // @expect(ret == true)
	 * 	object.body.localPosition = [0, 10, 0];
	 * ret = object.body.localPosition[1] == 10;
	 * // @expect(ret == true)
	 */
	get localPosition() {
		let _private = this[__.private];

		let target = [0, 0, 0];
		return _private.renderableNode.getPosition(target);
	}
	set localPosition(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setPosition(value);
	}

	/**
	 * Get/Set quaternion of the inertial space.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.localQuaternion[1] == 0;
	 * // @expect(ret == true)
	 * 	object.body.localQuaternion = [0, 10, 0, 1];
	 * ret = object.body.localQuaternion[1] == 10;
	 * // @expect(ret == true)
	 * 	object.body.localQuaternion = [0, 10, 0, 1];
	 */
	get localQuaternion() {
		let _private = this[__.private];

		let target = [0, 0, 0, 1]
		return _private.renderableNode.getQuaternion(target);
	}
	set localQuaternion(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setQuaternion(value);
	}

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

		let target = [0, 0, 0, 1]
		return _private.renderableNode.getWorldQuaternion(target);
	}
	set quaternion(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setWorldQuaternion(value);
	}

	/**
	 * Get/Set rotation of the inertial space.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.localRotation[1] == 0;
	 * // @expect(ret == true)
	 * 	object.body.localRotation = [0, 10, 0];
	 * ret = object.body.localRotation[1] == 10;
	 * // @expect(ret == true)
	 */
	get localRotation() {
		return this.localAngles;
	}
	set localRotation(value) {
		this.localAngles = value;
	}

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

		let target = [0, 0, 0, 1]
		return MathUtils.getAnglesFromQuat(_private.renderableNode.getQuaternion(target));
	}
	set localAngles(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setQuaternion(MathUtils.getQuatFromAngles(value));
	}

	/**
	 * Get/Set scale of the self coordinate system.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.localScale[1] == 1;
	 * // @expect(ret == true)
	 * 	object.body.localScale = [10, 10, 10];
	 * ret = object.body.localScale[1] == 10;
	 * // @expect(ret == true)
	 */
	get localScale() {
		let _private = this[__.private];

		let target = [1, 1, 1];
		return _private.renderableNode.getScale(target);
	}
	set localScale(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setScale(value);
	}

	/**
	 * Get/Set world position of the world space.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.position[1] == 0;
	 * // @expect(ret == true)
	 * 	object.body.position = [10, 10, 10];
	 * ret = object.body.position[1] == 10;
	 * // @expect(ret == true)
	 */
	get position() {
		let _private = this[__.private];

		let target = [0, 0, 0];
		return _private.renderableNode.getWorldPosition(target);
	}
	set position(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setWorldPosition(value);
	}

	/**
	 * Get/Set rotation of the world space.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.rotation[1] == 0;
	 * // @expect(ret == true)
	 * 	object.body.rotation = [10, 10, 10];
	 * ret = object.body.rotation[1] == 10;
	 * // @expect(ret == true)
	 */
	get rotation() {
		return this.angles;
	}
	set rotation(value) {
		this.angles = value;
	}

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

		let target = [0, 0, 0, 1]
		return MathUtils.getAnglesFromQuat(_private.renderableNode.getWorldQuaternion(target));
	}
	set angles(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setWorldQuaternion(MathUtils.getQuatFromAngles(value));
	}

	/**
	 * Get/Set scale of the world coordinate system.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.scale[1] == 1;
	 * // @expect(ret == true)
	 * 	object.body.scale = [10, 10, 10];
	 * ret = object.body.scale[1] == 10;
	 * // @expect(ret == true)
	 */
	get scale() {
		let _private = this[__.private];

		let target = [1, 1, 1];
		return _private.renderableNode.getWorldScale(target);
	}
	set scale(value) {
		let _private = this[__.private];

		this.createRootNode();

		_private.renderableNode.setWorldScale(value);
	}

	/**
	 * Get/Set the transform.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.matrix[12] == 0;
	 * // @expect(ret == true)
	 * 	object.body.matrix =THING.Math.mat4.fromTranslation([], [10, 10, 10]);
	 * ret = object.body.matrix[12] == 10;
	 * // @expect(ret == true)
	 */
	get matrix() {
		let _private = this[__.private];

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

		this.createRootNode();

		_private.renderableNode.setMatrix(value);
	}

	/**
	 * Get/Set the world transform.
	 * @type {Array<Number>}
	 * @example
	 * let object = new THING.Object3D();
	 * let ret = object.body.matrixWorld[12] == 0;
	 * // @expect(ret == true)
	 * 	object.body.matrixWorld =THING.Math.mat4.fromTranslation([], [10, 10, 10]);
	 * ret = object.body.matrixWorld[12] == 10;
	 * // @expect(ret == true)
	 */
	get matrixWorld() {
		let _private = this[__.private];

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

		this.createRootNode();

		_private.renderableNode.setMatrixWorld(value);
	}

	// #endregion

	// #region BoundingBox

	/**
	 * @typedef {Object} BoundingBoxResult
	 * @property {Array<Number>} center The center of box.
	 * @property {Array<Number>} halfSize The half size of box.
	 */

	/**
	 * Get the local bounding box of body.
	 * @returns {BoundingBoxResult}
	 * @private
	 */
	getLocalBoundingBox() {
		let target = {
			center: [0, 0, 0],
			halfSize: [0, 0, 0],
		};

		let node = this.getRenderableNode();
		if (node.isRenderableNode) {
			node.getLocalBoundingBox(target);
		}
		else {
			node.getWorldPosition(target.center);
		}

		return target;
	}

	// #endregion

	// #region Resource

	/**
	 * Create the root node.
	 * @private
	 */
	createRootNode() {
		let _private = this[__.private];

		if (!_private.rootNode) {
			let node = _private.renderableNode;
			if (node.isRenderableNode) {
				_private.rootNode = this._createParentNode('root', node);
			}
			else {
				_private.rootNode = node;
			}

			this._updateNode();
		}
	}

	/**
	 * Clear pivot node.
	 * @private
	 */
	clearPivotNode() {
		let _private = this[__.private];

		let pivotNode = _private.pivotNode;
		if (!pivotNode) {
			return;
		}

		let parentNode = pivotNode.getParent();
		if (parentNode) {
			let children = pivotNode.getChildren().slice(0);
			children.forEach(child => {
				child.setPosition([0, 0, 0]);

				parentNode.attach(child);
			});

			parentNode.remove(pivotNode);
		}

		pivotNode.dispose();

		_private.pivotNode = null;

		this._updateNode();
	}

	/**
	 * Create pivot node.
	 * @private
	 */
	createPivotNode() {
		let _private = this[__.private];

		if (!_private.pivotNode) {
			let node = _private.renderableNode;

			_private.pivotNode = this._createParentNode('pivot', node);

			this._updateNode();
		}

		return _private.pivotNode;
	}

	/**
	 * Get the pivot node.
	 * @type {Object}
	 * @private
	 */
	getPivotNode() {
		return this[__.private].pivotNode;
	}

	/**
	 * Get the root node.
	 * @type {Object}
	 * @private
	 */
	getRootNode() {
		return this[__.private].rootNode;
	}

	/**
	 * Get the renderable node.
	 * @type {Object}
	 * @private
	 */
	getRenderableNode() {
		return this[__.private].renderableNode;
	}

	/**
	 * Get the node.
	 * @type {Object}
	 * @private
	 */
	getNode() {
		return this[__.private].node;
	}

	/**
	 * Set node.
	 * @param {Object} node The node.
	 * @private
	 */
	setNode(node) {
		let _private = this[__.private];

		// Get the renderable node
		let renderableNode = _private.renderableNode;

		// Get the previous node's matrix to sync
		// Becareful: do not use matrix here, due to local angles would be changed by negative scale or some angels value
		renderableNode.getPosition(_vec3);
		renderableNode.getScale(_scale);
		renderableNode.getQuaternion(_quat);

		// Get the current states
		let curStates = {
			parentNode: renderableNode.getParent(),
			name: renderableNode.getName(),
			userData: renderableNode.getUserData()
		};

		// Get the current render node stats
		if (renderableNode.isRenderableNode) {
			if (renderableNode.hasStyle()) {
				curStates.style = renderableNode.getStyle();
			}

			curStates.visible = renderableNode.getVisible();
			curStates.renderOrder = renderableNode.getRenderOrder();
			curStates.castShadow = renderableNode.getCastShadow();
			curStates.receiveShadow = renderableNode.getReceiveShadow();
			curStates.layerMask = renderableNode.getLayerMask();
		}

		let app = _private.object.app;

		// Dispose current node
		let nodePool = app.resourceManager.getNodePool();
		if (nodePool) {
			nodePool.free(renderableNode);
		}
		else {
			renderableNode.dispose();
		}

		// Update node
		renderableNode = node;
		_private.renderableNode = node;

		// Resume matrix
		renderableNode.setPosition(_vec3);
		renderableNode.setScale(_scale);
		renderableNode.setQuaternion(_quat);

		// Relink to parent node
		if (curStates.parentNode) {
			curStates.parentNode.add(renderableNode);
		}

		// Resume the states
		renderableNode.setName(curStates.name);
		renderableNode.setUserData(curStates.userData);
		if (renderableNode.isRenderableNode) {
			if (curStates.style) {
				if (this.style.isChanged) {
					let cloneStyle = curStates.style.clone();
					let oriStyle = renderableNode.getStyle();
					this.style.initResource(oriStyle);
					this.style.updateResource(cloneStyle);
				}
			}

			renderableNode.setVisible(curStates.visible);

			if (renderableNode.getRenderOrder() != curStates.renderOrder) {
				renderableNode.setRenderOrder(curStates.renderOrder);
			}

			if (renderableNode.getCastShadow() != curStates.castShadow) {
				renderableNode.setCastShadow(curStates.castShadow);
			}

			if (renderableNode.getReceiveShadow() != curStates.receiveShadow) {
				renderableNode.setReceiveShadow(curStates.receiveShadow);
			}

			if (renderableNode.getLayerMask() != curStates.layerMask) {
				renderableNode.setLayerMask(curStates.layerMask);
			}
		}

		this._updateNode();
	}

	/**
	 * Just update node.
	 * @param {Object} node The node.
	 * @private
	 */
	updateNode(node) {
		let _private = this[__.private];

		_private.renderableNode = node;

		this._updateNode();
	}

	/**
	 * Unload resource.
	 * @param {Function} onCreateBodyNode When create body node callback function.
	 * @private
	 */
	unloadResource(onCreateBodyNode) {
		let _private = this[__.private];

		let renderableNode = _private.renderableNode;
		if (!renderableNode) {
			return;
		}

		// Get the user data and prepare to resume
		let userData = renderableNode.getUserData();

		// Get some useful info and wait to resume
		let parentNode = renderableNode.getParent();
		let visible = renderableNode.getVisible();
		let type = renderableNode.getType();
		let name = renderableNode.getName();
		let matrix = renderableNode.getMatrix(_mat4);

		// Get style
		let style;
		if (renderableNode.isRenderableNode) {
			style = renderableNode.getStyle();
		}

		// Destroy renderable node
		renderableNode.dispose();

		// Recreate renderable node
		renderableNode = onCreateBodyNode(type);
		renderableNode.setName(name);

		// Resume info
		parentNode.add(renderableNode);
		renderableNode.setMatrix(matrix);
		renderableNode.setUserData(userData);
		renderableNode.setVisible(visible);

		if (style) {
			renderableNode.setStyle(style);
		}

		// Update renderable node
		_private.renderableNode = renderableNode;

		// Update node
		this._updateNode();
	}

	getGeometryInfo() {
		return this.getRenderableNode().getGeometryInfo();
	}

	// #endregion

	// #region SubNodes

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

		let target = this._getSubNodes(_private.renderableNode, _private.node);

		target.forEach(node => {
			let nodeObject = new NodeObject({
				renderableNode: _private.renderableNode,
				node
			});
			callback(nodeObject);
		});
	}

	/**
	 * Get nodes sorted by node depth.
	 * @type {Array<NodeObject>}
	 * @private
	 */
	get nodes() {
		let _private = this[__.private];

		let target = this._getSubNodes(_private.renderableNode, _private.node);

		let subNodes = target.sort(function (a, b) {
			return b.depth - a.depth;
		}).map(node => {
			return new NodeObject({
				renderableNode: _private.renderableNode,
				node
			});
		});

		return subNodes;
	}

	/**
	 * Get the renderable nodes sorted by node depth.
	 * @type {Array<NodeObject>}
	 * @private
	 */
	get renderableNodes() {
		let _private = this[__.private];

		let target = this._getSubNodes(_private.renderableNode, _private.node);

		let subNodes = target.filter(node => {
			return node.isRenderable;
		}).sort(function (a, b) {
			return b.depth - a.depth;
		}).map(node => {
			return new NodeObject({
				renderableNode: _private.renderableNode,
				node
			});
		});

		return subNodes;
	}

	_getSubNodes(renderableNode, rootNode) {
		let subNodes = [];
		let name = renderableNode.getName();

		renderableNode.getSubNodes(subNodes, rootNode);

		return subNodes.filter(subNode => {
			return subNode.name !== name;
		})
	}

	/**
	 * Get node names sorted by node depth.
	 * @type {Array<String>}
	 * @private
	 */
	get nodeNames() {
		let _private = this[__.private];

		// Use 'null' to skip matrix calculation
		let subNodes = this._getSubNodes(_private.renderableNode, null);

		let names = subNodes.sort(function (a, b) {
			return b.depth - a.depth;
		}).map(node => {
			return node.name;
		});

		return names;
	}

	/**
	 * Get sub node result by name.
	 * @param {String} name The node name.
	 * @returns {NodeObject}
	 * @private
	 */
	getNodeByName(name) {
		let _private = this[__.private];

		return this._getNodeByName(name, _private.node, null,
			(node) => {
				return new NodeObject({
					renderableNode: _private.renderableNode,
					node
				});
			}
		);
	}

	/**
	 * Promote node to object.
	 * @param {String} name The node name.
	 * @param {Object} parentNode The parent node.
	 * @returns {Object}
	 * @private
	 */
	promoteNode(name, parentNode) {
		// Find sub node by name
		let node = this._getNodeByName(name, null);
		if (!node) {
			return null;
		}

		// Attach sub node to parent
		return parentNode.attachSubNode(node, {
			keepLocalTransform: true
		});
	}

	// #endregion

	// #region Style

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

		return !!_private.style;
	}

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

		if (!_private.style) {
			let bodyNode = this.getRenderableNode();
			if (!bodyNode.isRenderableNode) {
				return null;
			}

			_private.style = new Style(new StyleModifier(_private.object));
		}

		return _private.style;
	}

	setStyle(value) {
		if (!value) {
			return;
		}

		let style = this.getStyle();
		if (style == value) {
			return;
		}

		style.copy(value);
	}

	/**
	 * Get/Set style of body.
	 * @type {THING.Style}
	 * @example
	 *  let object = new THING.Object3D();
	 * 	let style = object.body.style;
	 * 	style.color = 'red';
	 * 	style.opacity = 0.1;
	 *  let ret = object.body.style.color[0] == 1;
	 *  // @expect(ret == true)
	 *  ret = object.body.style.opacity == 0.1;
	 *  // @expect(ret == true)
	 */
	get style() {
		return this.getStyle();
	}
	set style(value) {
		this.setStyle(value);
	}

	// #endregion

}

export {
	BodyObject
}