Source: components/RenderComponent.js

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

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

const Flag = {
	CastShadow: 1 << 0,
	ReceiveShadow: 1 << 1
}

// Instanced drawing mode
const InstancedDrawingMode = {
	None: 1,
	Doing: 2,
	Done: 3,
};

const _callbackKeys = [
	'opacity',
	'color',
	'metalness',
	'roughness',
	'emissive'
];

const _defaultOptions = {};

const _alwaysOnTopLayerNumber = 2;

const _defaultTextureParam = { wrapS: ImageWrapType.Repeat, wrapT: ImageWrapType.Repeat };

// #region Private Functions

// Check whether need to create instanced drawing style from style group.
function _canCreateInstancedDrawingStyle(object, styleGroupPool) {
	let style = object.bodyNode.getStyle();

	// If we have already used instanced drawing mode then skip to recreate it again
	if (style.isEnabled('InstancedDrawing')) {
		return false;
	}

	// Check whether it's already using style from group
	if (styleGroupPool.hasStyle(style)) {
		return false;
	}

	return true;
}

// Convert 2d array matrix to 1d array matrix.
function _convertMatrices(matrices) {
	let _matrices = [];

	for (let i = 0, il = matrices.length; i < il; i++) {
		let m = matrices[i];

		for (let j = 0, jl = m.length; j < jl; j++) {
			_matrices.push(m[j]);
		}
	}

	return _matrices;
}

// #endregion

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

	static mustCopyWithInstance = true;

	/**
	 * The render control of object, like using instanced drawing mode.
	 */
	constructor() {
		super();

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

		// Render attributes
		_private.renderOrder = 0;
		_private.initRenderLayer = null;
		_private.renderLayer = null;
		_private.flags = new Flags();
		_private.flags.set(Flag.CastShadow | Flag.ReceiveShadow);

		// Instanced drawing - Style
		_private.instancedDrawingMode = InstancedDrawingMode.None;
		_private.instanceGroupName = '';

		// Instanced drawing - Node
		_private.instancingNodeInfo = null;
		_private.instancingNode = null;

		// The callbacks
		_private.callbacks = null;

		_private.imageTexture = null;
		_private.modelInfo = null;

		_private.unloadImageTexture = () => {
			if (_private.imageTexture) {
				_private.imageTexture.release();
				_private.imageTexture = null;
			}
		}
	}

	// #region Private

	_setRenderAttribute(key, funcName, value, recursive) {
		let _private = this[__.private];

		_private[key] = value;

		// Get the object
		let object = this.object;

		// Update body node's render order
		let bodyNode = object.bodyNode;
		if (bodyNode[funcName]) {
			bodyNode[funcName](value);
		}

		// Change all children if needed
		if (recursive) {
			if (object.hasChildren()) {
				let children = object.children;
				for (let i = 0, l = children.length; i < l; i++) {
					let child = children[i];

					child[funcName](value, true);
				}
			}
		}
	}

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

		// Get the object
		let object = this.object;

		// Let other module to determine whether to process instanced drawing
		if (_private.callbacks) {
			let beforeMakeInstancedDrawingFunc = _private.callbacks.beforeMakeInstancedDrawing;
			if (beforeMakeInstancedDrawingFunc && beforeMakeInstancedDrawingFunc(object) === false) {
				return false;
			}
		}

		// Make sure instancing(node) is disable
		if (_private.instancingNodeInfo) {
			return false;
		}

		// Make sure it has instance group name or resource url
		if (!object.instanceGroupName && !object.resource.url) {
			return false;
		}

		// Make sure style is existing
		let style = object.body.style;
		if (!style) {
			return false;
		}

		// If some attribute use callback function then we can not enable instanced drawing
		for (let i = 0; i < _callbackKeys.length; i++) {
			let func = style[_callbackKeys[i]];

			// If it's function without returning constant value then skip to make instanced drawing
			if (Utils.isFunction(func) && !func.isConstantValue) {
				return false;
			}
		}

		// Make sure all animations are stopped
		if (object.hasComponent('animation')) {
			if (object.animation.hasInited()) {
				if (!object.animation.isAllAnimationsReady()) {
					return false;
				}
			}
		}

		return true;
	}

	_makeInstancedDrawing(style, styleGroupPool, renderMode) {
		let object = this.object;
		let instancedDrawing = renderMode == 'InstancedRendering' ? true : false;

		let instancedDrawingStyle = styleGroupPool.createStyleFromObject(object, { instancedDrawing });
		if (!instancedDrawingStyle) {
			return;
		}

		let _private = this[__.private];

		style.initResource(instancedDrawingStyle);
		style.updateBaseValues(object.body.style.values);

		_private.instancedDrawingMode = InstancedDrawingMode.Done;
	}

	_tryMakeInstancedDrawing(style, renderMode) {
		let _private = this[__.private];

		// If it can not make instanced drawing
		if (!this._canMakeInstancedDrawing()) {
			// Enable instanced drawing failed, resume the state
			_private.instancedDrawingMode = InstancedDrawingMode.None;

			return false;
		}

		let object = this.object;
		let styleGroupPool = object.app.resourceManager.getStyleGroupPool();

		// Check whether need to re-create style from group
		if (_canCreateInstancedDrawingStyle(object, styleGroupPool)) {
			// Wait copy operation finished first
			if (_private.copyPromise) {
				_private.copyPromise.then(() => {
					this._makeInstancedDrawing(style, styleGroupPool, renderMode);
				});
			}
			else {
				this._makeInstancedDrawing(style, styleGroupPool, renderMode);
			}
		}
		_private.instancedDrawingMode = InstancedDrawingMode.Done;

		return true;
	}

	// #endregion

	// #region Common

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

		return _private.renderOrder;
	}

	setRenderOrder(value, recursive) {
		this._setRenderAttribute('renderOrder', 'setRenderOrder', value, recursive);
	}

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

		return _private.flags.has(Flag.CastShadow);
	}

	setCastShadow(value, recursive) {
		let _private = this[__.private];

		this._setRenderAttribute('castShadow', 'setCastShadow', value, recursive);

		_private.flags.enable(Flag.CastShadow, value);
	}

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

		return _private.flags.has(Flag.ReceiveShadow);
	}

	setReceiveShadow(value, recursive) {
		let _private = this[__.private];

		this._setRenderAttribute('receiveShadow', 'setReceiveShadow', value, recursive);

		_private.flags.enable(Flag.ReceiveShadow, value);
	}

	syncAttributes() {
		let bodyNode = this.object.bodyNode;
		if (bodyNode.isRenderableNode) {
			let _private = this[__.private];

			if (bodyNode.getCastShadow() != this.castShadow) {
				bodyNode.setCastShadow(this.castShadow);
			}

			if (bodyNode.getReceiveShadow() != this.receiveShadow) {
				bodyNode.setReceiveShadow(this.receiveShadow);
			}

			if (bodyNode.getRenderOrder() != _private.renderOrder) {
				bodyNode.setRenderOrder(_private.renderOrder);
			}

			let renderLayer = bodyNode.getRenderLayer();
			_private.initRenderLayer = renderLayer;

			if (renderLayer != _private.renderLayer) {
				bodyNode.setRenderLayer(_private.renderLayer);
			}
		}
	}

	getGeometryInfo() {
		return this.object.node.getGeometryInfo();
	}

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

		_private.callbacks = _private.callbacks || {
			beforeMakeInstancedDrawing: null
		};

		return _private.callbacks;
	}

	get renderOrder() {
		return this.getRenderOrder();
	}
	set renderOrder(value) {
		this.setRenderOrder(value, true);
	}

	get castShadow() {
		return this.getCastShadow();
	}
	set castShadow(value) {
		this.setCastShadow(value, true);
	}

	get receiveShadow() {
		return this.getReceiveShadow();
	}
	set receiveShadow(value) {
		this.setReceiveShadow(value, true);
	}

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

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

		let bodyNode = this.object.bodyNode;
		if (!bodyNode.isRenderableNode) {
			return;
		}

		if (value) {
			_private.renderLayer = _alwaysOnTopLayerNumber;
			bodyNode.setRenderLayer(_alwaysOnTopLayerNumber);
		}
		else {
			_private.renderLayer = null;
			bodyNode.setRenderLayer(_private.initRenderLayer);
		}
	}

	// #endregion

	// #region Instanced Drawing - Style

	makeInstancedDrawing(value = true, options = _defaultOptions) {
		let _private = this[__.private];

		if (_private.instancedDrawingMode == InstancedDrawingMode.Doing) {
			return false;
		}

		// Get the object
		let object = this.object;

		// Get the current values of style, in order to update base values later
		let style = object.body.style;
		if (!style) {
			return false;
		}

		// If style instance equal value, return
		if (style.isInstancedDrawing == value) {
			_private.instancedDrawingMode = value ? InstancedDrawingMode.Done : InstancedDrawingMode.None;

			return true;
		}

		if (value) {
			// Get the render mode
			let renderMode = Utils.parseValue(options['renderMode'], 'InstancedRendering');

			if (object.loaded) {
				if (!this._tryMakeInstancedDrawing(style, renderMode)) {
					return false;
				}
			}
			else {
				// Wait for complete
				object.waitForComplete().then(
					// OK
					() => {
						this._tryMakeInstancedDrawing(style, renderMode);
					},
					// Error
					() => {
						// Enable instanced drawing mode failed due to object had been destroyed
						_private.instancedDrawingMode = InstancedDrawingMode.None;
					}
				);

				_private.instancedDrawingMode = InstancedDrawingMode.Doing;
			}
		}
		else {
			// Get the body node
			let bodyNode = object.bodyNode;

			// Disable instanced drawing
			_private.instancedDrawingMode = InstancedDrawingMode.None;

			// Create style without enable instanced drawing
			let resource = Utils.createObject('Style');
			resource.copy(bodyNode.getStyle());
			resource.enable('InstancedDrawing', false);

			// Update style base values
			let values = Object.assign({}, style.values);

			style.initResource(resource);
			style.updateBaseValues(values);
		}

		return true;
	}

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

		return _private.instancedDrawingMode != InstancedDrawingMode.None;
	}

	get instanceGroupName() {
		return this[__.private].instanceGroupName;
	}
	set instanceGroupName(value) {
		this[__.private].instanceGroupName = value;
	}

	// #endregion

	// #region Instanced Drawing - Node

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

		if (!_private.instancingNodeInfo) {
			// Make sure object had been loaded
			this.object.waitForComplete().then(() => {
				// We can not use style instanced drawing at the same time
				if (this.isInstancedDrawing) {
					return false;
				}

				let instancingNode = this.object.bodyNode.getAttribute('Instancing');
				if (!instancingNode) {
					return false;
				}

				_private.instancingNode = instancingNode;

				_private.instancingNodeInfo = {
					maxNumber: 0,
					number: 0,
					matrices: [],
					pickedIds: []
				};

				if (options) {
					if (!this.setInstancing(options)) {
						return false;
					}
				}
			});
		}

		return true;
	}

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

		if (!_private.instancingNodeInfo) {
			return;
		}

		this.object.bodyNode.setAttribute('Instancing', null);

		_private.instancingNodeInfo = null;
		_private.instancingNode = null;
	}

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

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

		let maxNumber = Utils.parseValue(options['maxNumber'], instancingNodeInfo.maxNumber);
		let number = Utils.parseValue(options['number'], instancingNodeInfo.number);
		let matrices = Utils.parseArray(options['matrices'], instancingNodeInfo.matrices);
		let pickedIds = Utils.parseArray(options['pickedIds'], instancingNodeInfo.pickedIds);

		if (number > maxNumber) {
			return false;
		}

		if (matrices.length != maxNumber) {
			return false;
		}

		if (pickedIds.length != maxNumber) {
			return false;
		}

		instancingNodeInfo.maxNumber = maxNumber;
		instancingNodeInfo.number = number;
		instancingNodeInfo.matrices = matrices;
		instancingNodeInfo.pickedIds = pickedIds;

		let instancingNode = _private.instancingNode;

		instancingNode.begin();
		instancingNode.setMaxCount(instancingNodeInfo.maxNumber);
		instancingNode.setCount(instancingNodeInfo.number);
		instancingNode.setMatrices(_convertMatrices(instancingNodeInfo.matrices));
		instancingNode.setPickedIds(instancingNodeInfo.pickedIds);
		instancingNode.end();

		return true;
	}

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

		return _private.instancingNodeInfo;
	}

	// #endregion

	load(options, resolve, reject) {
		let object = this.object;
		if (object.destroyed) {
			reject(`The object had been destroyed, skip to load resources`);
			return;
		}

		let _private = this[__.private];

		let modelOptions = {
			lights: true
		};
		let objResource = object.resource;

		if (objResource.nodeName) {
			modelOptions.nodeName = objResource.nodeName;
		}

		if (objResource.excludeNodeNames) {
			modelOptions.excludeNodeNames = objResource.excludeNodeNames;
		}

		if (objResource.inverseRotationMode) {
			modelOptions.inverseRotationMode = objResource.inverseRotationMode;
		}

		if (objResource.instanceStyle) {
			for (const key in objResource.instanceStyle) {
				modelOptions[key] = objResource.instanceStyle[key];
			}
		}

		const mapData = modelOptions.map;
		if (mapData) {
			_private.imageTexture = this.app.resourceManager.getTextureManager().load(mapData.url, _defaultTextureParam, {
				flipY: mapData.flipY
			});
			modelOptions['image'] = _private.imageTexture.getTextureResource();
			delete modelOptions['texture'];
		}

		const envapData = modelOptions.envMap;
		if (envapData) {
			modelOptions['envMap'] = envapData.getTextureResource();
		}

		if (objResource.instanceId) {
			modelOptions['id'] = objResource.instanceId;
			modelOptions['useInstance'] = true;
		}

		if (objResource.instanceCount) {
			modelOptions['instanceCount'] = objResource.instanceCount;
		}

		if (options.indexData) {
			modelOptions.indexData = options.indexData;
		}

		if (options.blobMap) {
			modelOptions.blobMap = options.blobMap;
		}

		const onBeforeSetup = modelOptions.onBeforeSetup;

		object.app.resourceManager.loadObjectResource(object, modelOptions, {
			onBeforeSetup: (ev) => {
				onBeforeSetup && onBeforeSetup(ev);
			},
			onSetup: (ev) => {
				let info = ev.info;

				_private.modelInfo = info;
			},
			onLoad: () => {
				if (_private.imageTexture) {
					_private.imageTexture.waitForComplete().then(resolve, reject);
				}
				else {
					resolve();
				}
			},
			onError: (ev) => {
				reject(ev);
			}
		});
	}

	onBeforeRemove() {
		let _private = this[__.private];
		_private.unloadImageTexture();
	}

	onUnloadResource() {
		let _private = this[__.private];
		_private.unloadImageTexture();
	}

	get modelInfo() {
		let _private = this[__.private];
		return _private.modelInfo;
	}

}

export { RenderComponent }