Source: parsers/SceneObjectParser.js

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

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

const cStyleTextureKeys = [
	'map',
	'envMap'
];

const _defaultOptions = {};

// #region Private Functions

// Get resource objects number.
function _getResourceObjectsNumber(objects) {
	let number = objects.length;

	for (let i = 0, l = objects.length; i < l; i++) {
		let object = objects[i];

		if (object.externalComponents) {
			number++;
		}

		if (object.children) {
			number += _getResourceObjectsNumber(object.children);
		}
	}

	return number;
}

function _fixRootPath(rootPath) {
	if (!rootPath) {
		return;
	}

	for (let key in rootPath) {
		if (!rootPath[key]._endsWith('/')) {
			rootPath[key] += '/';
		}
	}
}

// #endregion

/**
 * @class SceneObjectParser
 * The scene object parser.
 * @memberof THING
 */
class SceneObjectParser {

	/**
	 * The parser of objects scene in tree mode.
	 */
	constructor() {
		this[__.private] = {};
		let _private = this[__.private];

		_private.models = [];
		_private.tags = [];
		_private.images = [];
		_private.cubeTextures = [];
		_private.resolvers = {};

		_private.info = {
			number: 0,
			totalNumber: 0,
		};

		_private.version = new Version('1.0.0');

		_private.resources = {};
		_private.options = {};

		_private.modelRootPath = '';
		_private.textureRootPath = '';

		_private.objects = [];

		_private.toolkit = {
			getRootPath: function () {
				return _private.options['rootPath'] || _private.resources['rootPath'];
			},
			getTextureRootPath: function () {
				if (!_private.textureRootPath) {
					let rootPath = this.getRootPath();

					_private.textureRootPath = rootPath['texture'];
				}

				return _private.textureRootPath;
			},
			getModelRootPath: function () {
				if (!_private.modelRootPath) {
					let rootPath = this.getRootPath();

					_private.modelRootPath = rootPath['model'];
				}

				return _private.modelRootPath;
			},
			resolveURL: function (url) {
				if (url._startsWith('http://') || url._startsWith('https://')) {
					return url;
				}
				else {
					let basePath = _private.options['basePath'];
					if (basePath) {
						return basePath._appendURL(url);
					}
					else {
						return url;
					}
				}
			},
			resolveTextureURL: function (url) {
				if (url._startsWith('http://') || url._startsWith('https://')) {
					return url;
				}
				else if (url._startsWith('.')) {
					return this.resolveURL(url);
				}
				else {
					let rootPath = this.resolveURL(this.getTextureRootPath());

					return rootPath._appendPath(url);
				}
			},
			resolveModelURL: function (url, version) {
				if (url._startsWith('http://') || url._startsWith('https://') || url._startsWith('file://') || url._startsWith('blob:')) {
					return url;
				}
				else if (url._startsWith('.')) {
					return this.resolveURL(url);
				}
				else {
					let rootPath = this.getModelRootPath();

					let basePath = rootPath + url;

					let modelPath = '';
					let modelVersion = _private.options['useLatestModel'] ? 0 : version;
					if (modelVersion !== undefined) {
						modelPath = basePath + `/${modelVersion}/gltf`;
					}
					else {
						modelPath = basePath;
					}

					return this.resolveURL(modelPath);
				}
			},
			getResolver: function (type) {
				return _private.resolvers[type];
			},
			getModelDataByIndex: function (index) {
				let modelData = _private.models[index];
				return modelData;
			},
			getCubeTextureByIndex: function (index) {
				let cubeTexture = _private.cubeTextures[index];
				return cubeTexture;
			}

		}
	}

	// #region Private

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

		// Get the class type
		let classType = Utils.getRegisteredClasses()[res.type];

		// Try to get class type from outside
		let onGetClassType = _private.options['onGetClassType'];
		if (onGetClassType) {
			classType = onGetClassType(res) || classType;
		}

		return classType;
	}

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

		// Get the class type
		let classType = Utils.getRegisteredClasses()[res.type];

		// Try to get class type from outside
		let onGetComponentClassType = _private.options['onGetComponentClassType'];
		if (onGetComponentClassType) {
			classType = onGetComponentClassType(res) || classType;
		}

		return classType;
	}

	_loadModels(models, error) {
		let _private = this[__.private];

		for (let i = 0, l = models.length; i < l; i++) {
			let model = models[i];

			let version = Utils.parseValue(model['version'], 0);
			let url = model['url'];
			let size = model['size'];

			// Check URL resource path
			if (!url) {
				if (error) {
					error(`load model resource failed, due to url is empty`, model);
				}

				continue;
			}

			// Check model bounding size
			if (!size) {
				if (error) {
					error(`load model resource failed, due to size is invalid`, model);
				}

				continue;
			}

			// Build the URL resource path
			let resURL = _private.toolkit.resolveModelURL(url, version);

			// Fix size to prevent 0 value
			MathUtils.fixScaleFactor(size);

			// Update models
			_private.models.push({
				url: resURL,
				size,
				initialLocalBoundingBox: {
					// object center are at the foot,however the boundingbox center in the center of box
					// so we up the bounding half of y,make the two center coinside
					center: [0, size[1] / 2, 0],
					halfSize: MathUtils.divideVector(size, 2)
				}
			});
		}
	}

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

		_private.tags = Array.from(tags);
	}

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

		let app = Utils.getCurrentApp();

		let textureManager = app.resourceManager.getTextureManager();

		for (let i = 0, l = images.length; i < l; i++) {
			let image = images[i];

			let url = _private.toolkit.resolveTextureURL(image.url);

			let imageTexture = textureManager.load(url, undefined, { flipY: Utils.isValid(image.flipY) ? image.flipY : false });

			_private.images.push(imageTexture);
		}
	}

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

		for (let i = 0; i < cubeTextures.length; i++) {
			let cubeTexture = new CubeTexture({ url: cubeTextures[i] });

			_private.cubeTextures.push(cubeTexture);
		}
	}

	_loadResolvers(resolvers) {
		let _private = this[__.private];
		if (resolvers) {
			resolvers.forEach(resolver => {
				const type = resolver.type;
				const cls = Utils.getRegisteredClass(type);
				if (cls) {
					_private.resolvers[type] = new cls({ data: resolver.data });
				}
			});
		}
	}

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

		let resource = {};

		let id = res.id;
		if (Utils.isValid(id)) {
			let model = _private.models[id];
			if (model) {
				resource['url'] = model.url;
			}
		}

		let position = res.position;
		if (position) {
			resource['localPosition'] = position;
		}

		let quaternion = res.quaternion;
		if (quaternion) {
			resource['localAngles'] = MathUtils.getAnglesFromQuat(quaternion);
		}

		let scale = res.scale;
		if (scale) {
			resource['localScale'] = MathUtils.fixScaleFactor(scale);
		}

		let children = res.children;
		if (children) {
			resource['children'] = [];

			for (let i = 0, l = children.length; i < l; i++) {
				let child = children[i];

				resource['children'].push(this._buildBodyResource(child));
			}
		}

		return resource;
	}

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

		let ret = {};
		for (let key in style) {
			let value = style[key];
			ret[key] = value;

			if (cStyleTextureKeys.includes(key)) {
				switch (key) {
					case 'map':
						let image = _private.images[value];
						if (!image) {
							continue;
						}
						ret[key] = image;
						break;
					case 'envMap':
						let envMap = _private.cubeTextures[value];
						if (!envMap) {
							continue;
						}
						ret[key] = envMap;
						break;
				}
			}
		}
		return ret;
	}

	_loadComponents(object, components, error, args) {
		let _private = this[__.private];

		object.onImportComponents(components, {
			args,
			onGetObjectClassType: (component) => {
				return this._getComponentClassType(component);
			},
			onGetComponentExternalData: _private.options['onGetComponentExternalData'],
			onError: error
		});
	}

	_loadTemporaryComponents(object, components, isEditor, load, progress, error, resolve) {
		this._loadComponents(object, components, error, {
			isTemporary: true, // We just use these options as temporary usage, do not store in cache
			isEditor
		});

		// We finish to load object with components
		this._updateProgress(load, progress, resolve);
	}

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

		return tags.map(index => {
			return _private.tags[index];
		});
	}

	_onUpdateCreateOptions(classType, createOptions, options) {
		if (classType.onParseCreateOptions) {
			classType.onParseCreateOptions(createOptions, options);
		}

		let onUpdateCreateOptions = options['onUpdateCreateOptions'];
		if (onUpdateCreateOptions) {
			let _private = this[__.private];

			onUpdateCreateOptions(_private.options['owner'], classType, createOptions);
		}
	}

	_onCreateObject(object, createOptions, options) {
		let onCreateObject = options['onCreateObject'];
		if (onCreateObject) {
			let _private = this[__.private];

			onCreateObject(_private.options['owner'], object, createOptions);
		}
	}

	_updateProgress(load, progress, resolve) {
		let _private = this[__.private];

		let info = _private.info;

		// Update progress
		info.number++;

		// Notify loading progress
		if (progress) {
			progress({ progress: info.number / info.totalNumber });
		}

		// Check whether load completed
		if (info.number == info.totalNumber) {
			if (load) {
				load({ objects: Utils.cloneObject(_private.objects) });
			}

			resolve();

			_private.objects.length = 0;
		}
	}

	_loadObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject) {
		let _private = this[__.private];

		// Get resources
		let resources = _private.resources;
		let uuids = resources['uuids'];

		// Get options
		let dynamic = Utils.parseValue(_private.options['dynamic'], false);
		let hidden = Utils.parseValue(_private.options['hidden'], false);
		let isEditor = Utils.parseValue(_private.options['isEditor'], false);

		// Start to create objects
		for (let i = 0, l = objects.length; i < l; i++) {
			let objRes = objects[i];

			// Get class type
			let classType = this._getObjectClassType(objRes);
			if (!classType) {
				if (error) {
					error({ err: `Create object failed in scene builder, due to 'THING[${objRes.type}]' is not existing` });
				}

				continue;
			}

			// Get UUID
			let uuid = uuids ? uuids[objRes.uuid] : null;

			// Get object info
			let id = objRes.id;
			let name = objRes.name;
			let visible = objRes.visible;
			let localPosition = objRes.position;
			let localQuaternion = objRes.quaternion;
			let localScale = objRes.scale;
			let userData = objRes.userData;
			let tags = objRes.tags;
			let body = objRes.body;
			let style = objRes.style;
			let components = objRes.components;
			let externalComponents = objRes.externalComponents;
			let layerMask = objRes.layerMask;
			let queryable = objRes.queryable;
			let renderOrder = objRes.renderOrder;
			let castShadow = objRes.castShadow;

			// Compatible external
			objRes.extras = objRes.extras || objRes.external;

			// Parse scale
			if (localScale) {
				MathUtils.fixScaleFactor(localScale);
			}

			// Parse tags
			if (tags) {
				tags = this._mapTags(tags);
			}

			// Update extras data by class
			let extras;
			let parseExtrasData = classType.parseExtrasData || classType.parseExternalData;
			if (parseExtrasData) {
				extras = parseExtrasData({
					parentData: parentExtrasData,
					data: objRes,
					children: objRes.children || null,
					resources: _private.resources,
					toolkit: _private.toolkit,
					extras: _private.options['extras'] || _private.options['external'] || _defaultOptions
				});
			}
			else {
				extras = objRes.extras || {};
			}

			// Build create options
			let createOptions = {
				id,
				name,
				uuid,
				visible,
				layerMask,
				queryable,
				renderOrder,
				localPosition,
				localQuaternion,
				localScale,
				castShadow,
				userData,
				tags,
				parent,
				dynamic,
				extras,
				components,
				toolkit: _private.toolkit,
				error: () => {
					// Need to update progress even it's error ...
					this._updateProgress(load, progress, resolve);
				},
				syncComplete: () => {
					this._updateProgress(load, progress, resolve);
				},
				syncBeforeDestroy: (ev) => {
					if (!ev.object.loaded) {
						this._updateProgress(load, progress, resolve);
					}
				}
			};
			if (objRes.bundleInfo) {
				createOptions.bundleInfo = objRes.bundleInfo
			}

			// Try to get body URL from models
			let model;
			if (body) {
				model = this._parseBodyResource(body, error);

				createOptions['url'] = model.url;
				createOptions['nodeName'] = body.node;
				createOptions['inverseRotationMode'] = body.inverseRotationMode;
				createOptions['excludeNodeNames'] = body.excludeNodeNames;
			}

			// Build style
			if (style) {
				createOptions.style = this._buildStyle(style);
			}

			// instance data
			if (extras) {
				if (extras.instanceId) {
					createOptions['instanceId'] = extras.instanceId;
				}

				if (extras.instanceCount) {
					createOptions['instanceCount'] = extras.instanceCount;
				}
				if (extras.instanceStyle) {
					if (Utils.isValid(extras.instanceStyle.map)) {
						const imageTexture = _private.images[extras.instanceStyle.map];
						extras.instanceStyle.map = {
							url: imageTexture.url,
							flipY: imageTexture.flipY,
						};
					}
					if (Utils.isValid(extras.instanceStyle.envMap)) {
						extras.instanceStyle.envMap = _private.cubeTextures[extras.instanceStyle.envMap];
					}
					createOptions['instanceStyle'] = extras.instanceStyle;
				}
			}

			// Update create options
			this._onUpdateCreateOptions(classType, createOptions, _private.options);

			// Create object
			let object = new classType(createOptions);

			// set visible
			if (hidden) {
				object.visible = false;
			}

			// Update created objects
			_private.objects.push(object);

			// Set the initial bound size to calculate bounding box
			// If object had been loaded then skip to set initial local bounding box
			if (model && !object.loaded) {
				object.initialLocalBoundingBox = model.initialLocalBoundingBox;
			}
			// Load body children resources
			else if (body) {
				let children = body.children;
				if (children && children.length) {
					object.setResource(this._buildBodyResource(body));

					if (!dynamic) {
						object.loadResource(false);
					}
				}
			}

			// Load external components
			if (externalComponents) {
				object.onImportExternalComponents(externalComponents).then(() => {
					// Continue to load children
					if (objRes.children) {
						this._loadObjects(objRes.children, object, createOptions.extras, load, progress, error, resolve, reject);
					}

					// Notify outside we have created object
					this._onCreateObject(object, createOptions, _private.options);

					// We finish to load object with external component
					this._updateProgress(load, progress, resolve);
				});
			}
			else {
				// Continue to load children
				if (objRes.children) {
					this._loadObjects(objRes.children, object, createOptions.extras, load, progress, error, resolve, reject);
				}

				// Notify outside we have created object
				this._onCreateObject(object, createOptions, _private.options);
			}
		}
	}

	_parseResources(resources, error) {
		let _private = this[__.private];

		const onGetRootPath = _private.options['onGetRootPath'];
		if (onGetRootPath) {
			let rootPath = resources.rootPath || { model: '', texture: '' };
			rootPath = onGetRootPath(rootPath);
			if (rootPath) {
				resources.rootPath = rootPath;
			}
		}

		_private.resources = resources;
		_fixRootPath(_private.resources['rootPath']);

		// Make sure uuids is exist
		_private.resources['uuid'] = _private.resources['uuid'] || [];

		// Load models
		if (resources['models']) {
			this._loadModels(resources['models'], error);
		}

		// Load tags
		if (resources['tags']) {
			this._loadTags(resources['tags']);
		}

		// Load images
		if (resources['images']) {
			this._loadImages(resources['images']);
		}

		// Load cubeTextures
		if (resources['cubeTextures']) {
			this._loadCubeTextures(resources['cubeTextures']);
		}

		// Load resolvers
		if (resources['resolvers']) {
			this._loadResolvers(resources['resolvers']);
		}
	}

	_parseObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject) {
		let _private = this[__.private];

		let info = _private.info;
		info.totalNumber += _getResourceObjectsNumber(objects);

		_private.objects.length = 0;

		if (info.totalNumber) {
			this._loadObjects(objects, parent, parentExtrasData, load, progress, error, resolve, reject);
		}
		// If there are no any objects need to load then notify complete now
		else {
			// Notify loading progress
			if (progress) {
				progress({ progress: 1 });
			}

			// Notify load completed
			if (load) {
				load({ objects: _private.objects });
			}

			resolve();
		}
	}

	_copyOptions(options, onError) {
		let _private = this[__.private];

		_private.options = Object.assign({}, options);

		_fixRootPath(_private.options['rootPath']);

		_private.options['useLatestModel'] = Utils.parseValue(_private.options['useLatestModel'], true);

		// toolkit
		let isEditor = Utils.parseValue(_private.options['isEditor'], false);

		_private.toolkit.args = {
			isTemporary: true, // We just use these options as temporary usage, do not store in cache
			isEditor
		};

		_private.toolkit.onGetObjectClassType = (component) => {
			return this._getComponentClassType(component);
		};

		_private.toolkit.onGetComponentExternalData = options.onGetComponentExternalData;
		_private.toolkit.onError = onError;
	}

	_parseBodyResource(body, error) {
		let _private = this[__.private];

		let model = _private.models[body.id];
		if (!model) {
			if (error) {
				error(`load model resource failed, due to body id('${body.id}') is invalid`);
			}

			return null;
		}

		return model;
	}

	_parseOwner(owner, data, load, progress, error, resolve, reject) {
		let _private = this[__.private];

		if (owner.isRootObject) {
			return;
		}

		// Parse tags
		let tags = data['tags'];
		if (tags) {
			owner.tags = Utils.mergeSet(owner.tags, this._mapTags(tags));
		}

		// Start to load components of owner
		let components = data['components'];
		if (components) {
			let isEditor = Utils.parseValue(_private.options['isEditor'], false);

			this._loadComponents(owner, components, error, {
				isTemporary: true, // We just use these options as temporary usage, do not store in cache
				isEditor
			});
		}

		// Build style
		let style = data['style'];
		if (style) {
			const bodyStyle = owner.body.style;
			let styleData = this._buildStyle(style);

			// Merge create options
			let options = owner.options;
			if (options && options.style) {
				Utils.mergeObject(styleData, options.style, true);
			}

			// Set style
			for (const key in styleData) {
				if (Object.hasOwnProperty.call(styleData, key)) {
					const element = styleData[key];
					bodyStyle[key] = element;
				}
			}
		}

		// Load body resource
		let body = data['body'];
		if (body) {
			let model = this._parseBodyResource(body, error);
			if (model) {
				let info = _private.info;
				info.totalNumber++;

				let resourceManager = Utils.getCurrentApp().resourceManager;

				resourceManager.loadModelAsync(model.url).then((ev) => {
					let node = ev.node;
					owner.setBodyNode(node);

					let position = body.position;
					if (position) {
						owner.body.localPosition = position;
					}

					let quaternion = body.quaternion;
					if (quaternion) {
						owner.body.localQuaternion = quaternion;
					}

					let scale = body.scale;
					if (scale) {
						owner.body.localScale = scale;
					}

					// We finish to load object with components
					this._updateProgress(load, progress, resolve);
				}).catch((ev) => {
					// Error ...
					this.clear();

					if (error) {
						error(ev);
					}

					reject(ev);
				});
			}
		}
	}

	_parseViewpoint(owner, data, options) {
		const useDefaultViewpoint = Utils.parseValue(options["useDefaultViewpoint"], true);
		let viewpoint = data['viewpoint'];
		if (useDefaultViewpoint && viewpoint) {
			let app = Utils.getCurrentApp();
			if (owner) {
				app.camera.position = owner.selfToWorld(viewpoint.position);
				app.camera.target = owner.selfToWorld(viewpoint.target);
			}
			else {
				app.camera.position = viewpoint.position;
				app.camera.target = viewpoint.target;
			}

			app.camera.processAdjustNear();
		}
	}

	// #endregion

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

		_private.models.length = 0;
		_private.tags.length = 0;

		_private.images.forEach(image => {
			image.release();
		});

		_private.images.length = 0;

		let info = _private.info;
		info.number = 0;
		info.totalNumber = 0;

		_private.modelRootPath = '';
		_private.textureRootPath = '';

		_private.resources = {};
		_private.options = {};
		_private.resolvers = {};
	}

	parse(data, load, progress, error, options) {
		let _private = this[__.private];

		return new Promise((resolve, reject) => {
			// Prepare for loading
			this.clear();

			// Copy options
			this._copyOptions(options, error);

			// Get the data info
			let resources = data['resources'];
			let objects = data['children'] || data['objects'] || [];

			// Start to load resources
			this._parseResources(resources, error);

			// Start to load objects
			let owner = _private.options['owner'];

			// Parse viewpoint
			this._parseViewpoint(owner, data, options);

			// Make owner
			if (owner) {
				if (Utils.isNull(owner.isOwner)) {
					Object.defineProperty(owner, 'isOwner', {
						get: function () {
							return true;
						}
					});
				}

				// Parse owner
				this._parseOwner(owner, data, load, progress, error, resolve, reject);
			}

			// Parse all children of owner
			this._parseObjects(objects, owner, null, load, progress, error, resolve, reject);
		}).then(() => {
			// Finished
			this.clear();
		}).catch((ev) => {
			// Error ...
			this.clear();

			if (error) {
				error(ev);
			}
		});
	}

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

		return _private.version;
	}

}

export { SceneObjectParser }