Source: common/Utils.js

import { StringEncoder, Utils as BaseUtils } from '@uino/base-thing';
import { MathUtils } from '../math/MathUtils';
import { System } from '../core/System';
import { createTypeChecker } from '../adapter/AdapterInterfaces';
import { EventType, LerpType, LoopType, NodeUserDataType, InheritType, MouseButtonType } from '../const';

const _hasExpiredKey = StringEncoder.toText("<@secret hasExpired>");

const _getMethod = StringEncoder.toText("<@secret GET>");
const _postMethod = StringEncoder.toText("<@secret POST>");

const _showLogoKey = StringEncoder.toText("<@secret showLogo>");
const _watermarkKey = StringEncoder.toText("<@secret watermark>");
const _watermarkEvent = StringEncoder.toText("<@secret __!water#mark*event@__>");
const _hideWatermarkEvent = StringEncoder.toText("<@secret __!hide!water#mark*event@__>");
const _authDataEvent = StringEncoder.toText("<@secret __!!a$u^t@h*event@__>");

const _expriedStr1 = StringEncoder.toText('<@secret Authorization>');
const _expriedStr2 = StringEncoder.toText('<@secret has>');
const _expriedStr3 = StringEncoder.toText('<@secret expried>');

const _officialStaticPath = StringEncoder.toText('<@secret https://www.thingjs.com/static>');

function _getAuthData() {
	return Object.assign({}, _authData);
}

function _buildWASMInfo(platformInfo, wasmOptions) {
	return {
		VERSION: platformInfo.version,
		SDKVERSION: platformInfo.sdkVersion,
		COMPILETIME: platformInfo.compileTime,
		features: wasmOptions.features,
		exports: wasmOptions.exports
	};
}

let _app = null;

let _defaultParams = {};

let _tempExpiredKey = '';

let _scriptLoader;
let _fileManager;
let _devDebugger;
let _renderDebugger;

let _logger;
if (!_NEED_LOGGER) {
	_logger = {
		dispose: function () { },
		setOptions: function (options) { },
		getOptions: function () { },
		api: function (name, options) { },
		error: function (msg, options) { },
		sum: function (key, value) { },
		setApiCollector: function (apiCollector) { },
		getApiCollector: function () { }
	};
}

let _typeCheker;
if (_DEBUG) {
	_typeCheker = createTypeChecker();
}

function _dummyFunc() {

}

let _system = new System();
let _authData = null;

/**
 * @class Utils
 * Useful functions.
 * @memberof THING
 * @public
 */
class Utils extends BaseUtils {

	static xAxis = [1, 0, 0];
	static yAxis = [0, 1, 0];
	static zAxis = [0, 0, 1];

	static dependencyLoaders = [];

	static get _system() {
		return _system;
	}

	static get _scriptLoader() {
		_scriptLoader = _scriptLoader || Utils.createObject('ScriptLoader');
		return _scriptLoader;
	}

	static get _fileManager() {
		_fileManager = _fileManager || Utils.createObject('FileManager');
		return _fileManager;
	}

	static get _devDebugger() {
		_devDebugger = _devDebugger || Utils.createObject('DevDebugger');
		return _devDebugger;
	}

	static get _renderDebugger() {
		_renderDebugger = _renderDebugger || Utils.createObject('RenderDebugger');
		return _renderDebugger;
	}

	static get logger() {
		if (_NEED_LOGGER) {
			_logger = _logger || Utils.createObject('Logger');
		}

		return _logger;
	}
	static set logger(value) {
		if (_NEED_LOGGER) {
			if (_logger) {
				_logger.dispose();
			}

			_logger = value;
		}
	}

	static update(deltaTime) {
		super.update(deltaTime);

		_system.update(deltaTime);

		if (_fileManager) {
			_fileManager.update(deltaTime);
		}

		if (_tempExpiredKey) {
			Utils.error(_expriedStr1 + ' ' + _expriedStr2 + ' ' + _expriedStr3);

			_tempExpiredKey = '';
		}
	}

	static getCurrentApp() {
		return _app;
	}

	static setCurrentApp(app) {
		if (_app && !_app.isDisposed && !_app.isDisposing) {
			_app.trigger(EventType.AppLeave);
		}
		_app = app;

		// maybe this no app; then the current app is null
		if (!app) {
			return;
		}

		// set current app use setTimeout because that the app maybe has not been initialed
		Utils.setTimeout(() => {
			app.trigger(EventType.AppEnter);
		});
		if (_authData) {
			// Hide logo
			if (_authData[_showLogoKey] === 0 || _authData[_showLogoKey] === false) {
				// We delay to next frame to make sure app load completed
				Utils.setTimeout(() => {
					app.trigger(_hideWatermarkEvent);
				});
			}
			else {
				// Change watermark if login auth data has it
				let watermark = _authData[_watermarkKey];
				if (watermark) {
					// We delay to next frame to make sure app load completed
					Utils.setTimeout(() => {
						app.trigger(_watermarkEvent, {
							opacity: watermark['opacity'],
							text: watermark['text'],
							data: watermark['data']
						});
					});
				}
			}

			if (_authData[_hasExpiredKey]) {
				// Write result into temporary key and prepare to print expired log
				_tempExpiredKey = MathUtils.generateUUID();
			}
		}
	}

	static onCompleteApp(app) {
		// Initialize auth data when if we need
		if (_NEED_AUTH) {
			if (!_authData) {
				_authData = {};

				// Wait for auth to update expired date key
				_authData[_hasExpiredKey] = true;
			}
		}

		// Let auth pass when it's in development env
		if (_DEV) {
			_authData = _authData || {};
			_authData[_hasExpiredKey] = false; // Let it always passed
		}
		else if (!_PRO_) {
			_authData = _authData || {};
			_authData[_hasExpiredKey] = true; // The community version can not use any pro features
		}

		if (_authData) {
			app.trigger(_authDataEvent, _authData);
		}
	}

	static lerpTo(from, to, time = 1000, delayTime = 0) {
		return _app.tweenManager.lerpTo(from, to, time, delayTime);
	}

	static getMD5CodeFromFile(url, load, progress) {
		this.loadBinaryFile(url,
			(ev) => {
				let blob = this.arrayBufferToBlob(ev.data);

				this.getMD5CodeFromBuffer(blob).then(function (hash) {
					if (load) {
						load(hash);
					}
				});
			},
			progress,
		);
	}

	static normalizeURL(url) {
		return Utils._scriptLoader.normalizeURL(url);
	}

	static createObjectURL(data) {
		return Utils._scriptLoader.createObjectURL(data);
	}

	/**
	 * Load code.
	 * @param {String} code The code string.
	 * @param {Object} options The options.
	 * @param {Boolean} options.es6Mode True indicates load code in es6 mode.
	 * @param {String} options.sourceURL The source URL in sync mode.
	 * @param {Function} options.onLoad The load finished callback function.
	 * @param {Function} options.onError The error occurred callback function.
	 * @example
	 * THING.Utils.loadCode('console.log("Code load finished")');
	 * @public
	 */
	static loadCode(code, options = {}) {
		return new Promise((resolve, reject) => {
			let onLoad = () => {
				options.onLoad && options.onLoad();
				resolve();
			}
			let onError = (ev) => {
				options.onError && options.onError(ev);
				reject(ev);
			}
			Utils._scriptLoader.loadCode(code, {
				es6Mode: options['es6Mode'],
				sourceURL: options['sourceURL'],
				onLoad: onLoad,
				onError: onError
			});
		});
	}

	/**
	 * Load code in async mode.
	 * @param {String} code The code string.
	 * @param {Object} options The options.
	 * @param {Boolean} options.es6Mode True indicates load code in es6 mode.
	 * @param {String} options.sourceURL The source URL in sync mode.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * await THING.Utils.loadCodeAsync('console.log("Code load finished")');
	 * @public
	 */
	static loadCodeAsync(code, options = {}) {
		return Utils.loadCode(code, options);
	}

	/**
	 * Load file(s).
	 * @param {String|Array<String>} resource The file(s) resource url.
	 * @param {Object|Array<String>} options The options.
	 * @param {Function} options.onLoad The load finished callback function.
	 * @param {Function} options.onError The error occurred callback function.
	 * @param {Boolean} [options.cache=true] True indicates try to use cache.
	 * @param {Boolean} [options.inOrder=true] True indicates keep order to load files one by one.
	 * @example
	 * // Load 'my-lib.js' file by load and error callback functions
	 * THING.Utils.loadFile('my-lib.js', {
	 * 	onLoad: function() {
	 * 		console.log('Load finished');
	 * 	},
	 * 	onError: function() {
	 * 		console.error('Load failed');
	 * 	}
	 * });
	 * @public
	 */
	static loadFile(resource, options) {
		return Utils._scriptLoader.loadAsync(resource, options);
	}

	/**
	 * Load file(s) in async mode.
	 * @param {String|Array<String>} resource The file(s) resource url.
	 * @param {Object|Array<String>} options The options.
	 * @param {Boolean} [options.cache=true] True indicates try to use cache.
	 * @param {Boolean} [options.inOrder=true] True indicates keep order to load files one by one.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * // Load 'my-lib.js' file and wait to load completed
	 * await THING.Utils.loadFileAsync('my-lib.js');
	 * @public
	 */
	static loadFileAsync(resource, options) {
		return Utils.loadFile(resource, options);
	}

	/**
	 * Check whether it's compressed texture.
	 * @param {String} url The compressed texture url.
	 * @returns {Boolean}
	 * @example
	 * let compress = THING.Utils.isCompressedTexture('image.dds');
	 * let uncompress = THING.Utils.isCompressedTexture('image.png');
	 * // @expect(compress == true && uncompress == false);
	 * @public
	 */
	static isCompressedTexture(url) {
		let extension = url._getExtension();
		return (extension == 'dds' || extension == 'pvr');
	}

	/**
	 * Load image file.
	 * @param {String} url The file URL.
	 * @param {Function} onLoad When load finished callback function.
	 * @param {Function} onProgress When loading in progression callback function.
	 * @param {Function} onError When laod error occurred callback function.
	 * @example
	 * // Load image resource by load callback function.
	 * THING.Utils.loadImageFile('./assets/images/blue.png',
	 * 	function(image) {
	 * 		console.log(image);
	 * 	}
	 * );
	 * @public
	 */
	static loadImageFile(url, onLoad, onProgress, onError, options) {
		return new Promise((resolve, reject) => {
			let load = (image) => {
				onLoad && onLoad(image);
				resolve(image);
			};
			let error = (ev) => {
				onError && onError(ev);
				reject(ev);
			};
			if (Utils.isCompressedTexture(url)) {
				Utils.getCurrentApp().resourceManager.loadCompressedTextureData(url, load, onProgress, error);
			}
			else {
				Utils._fileManager.loadImageFile(url, load, onProgress, error, options);
			}
		});
	}

	/**
	 * Load image file in async mode.
	 * @param {String} url The file URL.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * // Load image resource and wait to load finished.
	 * await THING.Utils.loadImageFileAsync('image.png');
	 * @public
	 */
	static loadImageFileAsync(url, options) {
		return Utils.loadImageFile(url, null, null, null, options);
	}

	static loadImageFileFromBase64(src) {
		return Utils._fileManager.loadImageFileFromBase64(src);
	}

	static loadBitmapImageFromBase64Async(src) {
		return Utils._fileManager.loadBitmapImageFromBase64Async(src);
	}

	static loadBitmapImageFromUrlAsync(url, options) {
		return Utils._fileManager.loadBitmapImageFromUrlAsync(url, options);
	}

	static loadImageFileFromData(type, data, load, progress, error, options) {
		Utils._fileManager.loadImageFileFromData(type, data, load, progress, error, options);
	}

	static loadVideoFile(url, load, progress, error, options) {
		return Utils._fileManager.loadVideoFile(url, load, progress, error, options);
	}

	/**
	 * Load binary file.
	 * @param {Object} resource The resource object.
	 * @example
	 * let resource = document.createElement('video');;
	 * let check = THING.Utils.isVideoResource(resource);
	 * // @expect(check == true);
	 * @public
	 */
	static isVideoResource(resource) {
		return Utils._fileManager.isVideoResource(resource);
	}

	/**
	 * Load binary file.
	 * @param {String} url The file URL.
	 * @param {Function} onLoad When load finished callback function.
	 * @param {Function} onProgress When loading in progression callback function.
	 * @param {Function} onError When laod error occurred callback function.
	 * @param {LoadFileOptions} options The options.
	 * @example
	 * // Load 'my-file.bin' file in binary format.
	 * THING.Utils.loadBinaryFile('my-file.bin',
	 * 	function(data) {
	 * 		console.log('Load finished', data);
	 * 	},
	 * 	function() {
	 * 		console.error('Load failed');
	 * 	}
	 * );
	 * @public
	 */
	static loadBinaryFile(url, onLoad, onProgress, onError, options) {
		return new Promise((resolve, reject) => {
			let load = (ev) => {
				let data = ev.data;
				onLoad && onLoad(ev);
				resolve(data);
			};
			let error = (ev) => {
				onError && onError(ev);
				reject(ev);
			};
			Utils._fileManager.loadBinaryFile(url, load, onProgress, error, options);
		});
	}

	/**
	 * Load binary file in async mode.
	 * @param {String} url The file URL.
	 * @param {LoadFileOptions} options The options.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * // Load 'my-file.bin' file and wait to load finished.
	 * await THING.Utils.loadBinaryFileAsync('my-file.bin');
	 * @public
	 */
	static loadBinaryFileAsync(url, options) {
		return Utils.loadBinaryFile(url, null, null, null, options);
	}

	/**
	 * Save data as file.
	 * @param {String} fileName The file name.
	 * @param {String|Blob|Image} data The file data.
	 * @returns {Boolean}
	 * @example
	 * let data = JSON.stringify('{ name: "Nice to meet you"}');
	 * THING.Utils.saveAsFile('test.json', data);
	 * @public
	 */
	static saveAsFile(fileName, data) {
		return Utils._fileManager.saveAsFile(fileName, data);
	}

	/**
	 * Save data as image.
	 * @param {Number} width The image width in pixel.
	 * @param {Number} height The image height in pixel.
	 * @param {Uint8Array} pixelBuffer The image pixel buffer.
	 * @returns {Object}
	 * @example
	 * // Save 32x32 image with random pixel color
	 * const width = 32, height = 32;
	 * let pixelBuffer = [];
	 * for(let y = 0; y < height; y++) {
	 * 	for(let x = 0; x < width * 4; x++) {
	 * 		pixelBuffer[y * width * 4 + x] = THING.Math.randomInt(0, 255);
	 * 	}
	 * }
	 * THING.Utils.saveAsImage(width, height, new Uint8Array(pixelBuffer));
	 * @public
	 */
	static saveAsImage(width, height, pixelBuffer) {
		return Utils._fileManager.saveAsImage(width, height, pixelBuffer);
	}

	/**
	 * Load text file.
	 * @param {String} url The file URL.
	 * @param {Function} onLoad When load finished callback function.
	 * @param {Function} onProgress When loading in progression callback function.
	 * @param {Function} onError When laod error occurred callback function.
	 * @param {LoadFileOptions} options The options.
	 * @example
	 * // Load 'my-file.txt' file in text format.
	 * THING.Utils.loadTextFile('my-file.txt',
	 * 	function(data) {
	 * 		console.log('Load finished', data);
	 * 	},
	 * 	function() {
	 * 		console.error('Load failed');
	 * 	}
	 * );
	 * @public
	 */
	static loadTextFile(url, onLoad, onProgress, onError, options) {
		return new Promise((resolve, reject) => {
			let load = (ev) => {
				let data = ev.data;
				onLoad && onLoad(ev);
				resolve(data);
			};
			let error = (ev) => {
				onError && onError(ev);
				reject(ev);
			};
			Utils._fileManager.loadTextFile(url, load, onProgress, error, options);
		});
	}

	/**
	 * Load text file in async mode.
	 * @param {String} url The file URL.
	 * @param {LoadFileOptions} options The options.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * // Load 'my-file.txt' file and wait to load finished.
	 * await THING.Utils.loadTextFileAsync('my-file.txt');
	 * @public
	 */
	static loadTextFileAsync(url, options) {
		return this.loadTextFile(url, null, null, null, options);
	}

	/**
	 * Load JSON file.
	 * @param {String} url The file URL.
	 * @param {Function} onLoad When load finished callback function.
	 * @param {Function} onProgress When loading in progression callback function.
	 * @param {Function} onError When laod error occurred callback function.
	 * @param {LoadFileOptions} options The options.
	 * @example
	 * // Load 'my-file.json' file in json format.
	 * THING.Utils.loadJSONFile('my-file.json',
	 * 	function(data) {
	 * 		console.log('Load finished', data);
	 * 	},
	 * 	function() {
	 * 		console.error('Load failed');
	 * 	}
	 * );
	 * @public
	 */
	static loadJSONFile(url, onLoad, onProgress, onError, options) {
		return new Promise((resolve, reject) => {
			let load = (ev) => {
				let data = ev.data;
				onLoad && onLoad(ev);
				resolve(data);
			};
			let error = (ev) => {
				onError && onError(ev);
				reject(ev);
			};
			Utils._fileManager.loadJSONFile(url, load, onProgress, error, options);
		});
	}

	/**
	 * Load JSON file(s) in async mode.
	 * @param {String|Array<String>} url The file URL(s).
	 * @param {LoadFileOptions} options The options.
	 * @returns {Promise<any>}
	 * @private
	 * @deprecated 2.7
	 * @private
	 * @example
	 * // Load 'my-file.json' file and wait to load finished.
	 * await THING.Utils.loadJSONFileAsync('my-file.json');
	 * @public
	 */
	static loadJSONFileAsync(url, options) {
		if (Utils.isArray(url)) {
			let promises = url.map(_url => {
				return Utils.loadJSONFileAsync(_url, options).then((data) => {
					return { url: _url, data };
				});
			});

			return Promise.all(promises).then((items) => {
				let result = {};

				items.forEach(item => {
					result[item.url] = item.data;
				});

				return result;
			});
		}
		else {
			return this.loadJSONFile(url, null, null, null, options);
		}
	}

	static encodeImageFromURL(type, url, load, error, options) {
		Utils._fileManager.encodeImageFromURL(type, url, load, error, options);
	}

	static encodeImageFromData(type, data, load, error, options) {
		Utils._fileManager.encodeImageFromData(type, data, load, error, options);
	}

	static encodeImageFromRGBA(type, width, height, pixelBuffer, load, error, options) {
		Utils._fileManager.encodeImageFromRGBA(type, width, height, pixelBuffer, load, error, options);
	}

	/**
	 * When run action.
	 * @param {number} value The button type.
	 * @example
	 * let type = THING.Utils.parseMouseButtonType(1);
	 * // @expect(type == 'Middle');
	 * @public
	 */
	static parseMouseButtonType(value) {
		switch (value) {
			case 0: return MouseButtonType.Left;
			case 1: return MouseButtonType.Middle;
			case 2: return MouseButtonType.Right;
			default:
				return MouseButtonType.None;
		}
	}

	// Parse event with options.
	static parseEventWithOptions(info, args) {
		// .(tag)
		if (Utils.isString(args[0])) {
			info.tag = args[0];

			// .(tag, {options})
			if (Utils.isObject(args[1])) {
				info.useCapture = Utils.parseValue(args[1].useCapture, info.useCapture);
				info.enumerable = Utils.parseValue(args[1].enumerable, info.enumerable);
				info.pausable = Utils.parseValue(args[1].pausable, info.pausable);
				info.includeSelf = Utils.parseValue(args[1].includeSelf, info.includeSelf);
			}
			// .(tag, priority)
			else if (Utils.isNumber(args[1])) {
				info.priority = args[1];

				// .(tag, priority, {options})
				if (Utils.isObject(args[2])) {
					info.useCapture = Utils.parseValue(args[2].useCapture, info.useCapture);
					info.enumerable = Utils.parseValue(args[2].enumerable, info.enumerable);
					info.pausable = Utils.parseValue(args[2].pausable, info.pausable);
					info.includeSelf = Utils.parseValue(args[2].includeSelf, info.includeSelf);
				}
			}
		}
		// .(priority)
		else if (Utils.isNumber(args[0])) {
			info.priority = args[0];

			// .(priority, {options})
			if (Utils.isObject(args[1])) {
				info.useCapture = Utils.parseValue(args[1].useCapture, info.useCapture);
				info.enumerable = Utils.parseValue(args[1].enumerable, info.enumerable);
				info.pausable = Utils.parseValue(args[1].pausable, info.pausable);
				info.includeSelf = Utils.parseValue(args[1].includeSelf, info.includeSelf);
			}
		}
		// .({options})
		else if (Utils.isObject(args[0])) {
			info.useCapture = Utils.parseValue(args[0].useCapture, info.useCapture);
			info.enumerable = Utils.parseValue(args[0].enumerable, info.enumerable);
			info.pausable = Utils.parseValue(args[0].pausable, info.pausable);
			info.includeSelf = Utils.parseValue(args[0].includeSelf, info.includeSelf);

			// .({options}, tag, priority)
			if (Utils.isString(args[1])) {
				info.tag = Utils.parseValue(args[1], '');
				info.priority = Utils.parseValue(args[2], 0);
			}
			// .({options}, priority)
			else {
				info.priority = Utils.parseValue(args[1], 0);
			}
		}
		// .(tag, priority, options)
		else if (args.length == 3) {
			info.tag = Utils.parseValue(args[0], '');
			info.priority = Utils.parseValue(args[1], 0);

			let options = args[2];
			if (options) {
				info.useCapture = Utils.parseValue(options.useCapture, info.useCapture);
				info.enumerable = Utils.parseValue(options.enumerable, info.enumerable);
				info.pausable = Utils.parseValue(options.pausable, info.pausable);
				info.includeSelf = Utils.parseValue(options.includeSelf, info.includeSelf);
			}
		}
	}

	// Parse event.
	static parseEvent() {
		let info = {
			type: arguments[0].toLowerCase(),
			condition: null,
			callback: _dummyFunc,
			tag: null,
			priority: 0,
			once: false,
			useCapture: false,
			includeSelf: false,
			enumerable: true,
			pausable: true
		};

		let onInitEventArgs = arguments[6];
		if (onInitEventArgs) {
			onInitEventArgs(info);
		}

		// .(function())
		if (Utils.isFunction(arguments[1])) {
			info.callback = arguments[1];

			Utils.parseEventWithOptions(info, Array.prototype.slice.call(arguments, 2));
		}
		// .('condition', function())
		else if (Utils.isString(arguments[1])) {
			if (!Utils.isFunction(arguments[2])) {
				Utils.error(`Parse event failed, due to callback is not function with condition`);
			}

			info.condition = arguments[1];
			info.callback = arguments[2];

			Utils.parseEventWithOptions(info, Array.prototype.slice.call(arguments, 3));
		}
		// .(null/undefined, function())
		else if (Utils.isNull(arguments[1])) {
			if (!Utils.isFunction(arguments[2])) {
				Utils.error(`Parse event failed, due to callback is not function with condition`);
			}

			info.callback = arguments[2];

			Utils.parseEventWithOptions(info, Array.prototype.slice.call(arguments, 3));
		}

		// We must provide callback function
		if (!info.callback) {
			Utils.error(`Parse event failed, deu to callback function is not existing`);
		}

		return info;
	}

	/**
	 * parse fly param.
	 * @param {Object} param The fly param.
	 * @returns {Object}
	 * @example
	 * let ret1 = THING.Utils.parseFlyParam(100);
	 * let obj = new THING.BaseObject();
	 * let ret2 = THING.Utils.parseFlyParam(obj);
	 * // @expect(ret1 == 100 && ret2.target == obj);
	 * @public
	 */
	static parseFlyParam(param) {
		if (param.isBaseObject || Utils.isArray(param)) {
			return { target: param };
		}

		return param;
	}

	/**
	 * parse loop type.
	 * @param {String} name The loop type.
	 * @returns {String}
	 * @example
	 * let ret1 = THING.Utils.parseLoopType('repeat');
	 * let ret2 = THING.Utils.parseLoopType('pingpong');
	 * let ret3 = THING.Utils.parseLoopType('temp');
	 * // @expect(ret1 == 'Repeat' && ret2 == 'PingPong' && ret3 == '');
	 * @public
	 */
	static parseLoopType(name) {
		if (name) {
			switch (name.toLowerCase()) {
				case 'repeat': return LoopType.Repeat;
				case 'pingpong': return LoopType.PingPong;
				default:
					break;
			}
		}

		return '';
	}

	/**
	 * parse lerp type
	 * @param {String | Object} name the lerp type or function.
	 * @returns {Function}
	 * @example
	 * let func1 = THING.Utils.parseLerpType('quadratic.in');
	 * let ret1 = func1(10);
	 * let customFunc = (amount) => {return amount + amount;};
	 * let func2 = THING.Utils.parseLerpType(customFunc);
	 * let ret2 = func2(10);
	 * let func3 = THING.Utils.parseLerpType('temp');
	 * let ret3 = func3(10);
	 * // @expect(ret1 == 100 && ret2 == 20 && ret3 == 10)
	 * @public
	 */
	static parseLerpType(name) {
		if (Utils.isString(name)) {
			let value = name.toLowerCase();

			switch (value) {
				case 'quadratic.in': return LerpType.Quadratic.In;
				case 'quadratic.out': return LerpType.Quadratic.Out;
				case 'quadratic.inout': return LerpType.Quadratic.InOut;
				case 'cubic.in': return LerpType.Cubic.In;
				case 'cubic.out': return LerpType.Cubic.Out;
				case 'cubic.inout': return LerpType.Cubic.InOut;
				case 'quartic.in': return LerpType.Quartic.In;
				case 'quartic.out': return LerpType.Quartic.Out;
				case 'quartic.inout': return LerpType.Quartic.InOut;
				case 'quintic.in': return LerpType.Quintic.In;
				case 'quintic.out': return LerpType.Quintic.Out;
				case 'quintic.inout': return LerpType.Quintic.InOut;
				case 'sinusoidal.in': return LerpType.Sinusoidal.In;
				case 'sinusoidal.out': return LerpType.Sinusoidal.Out;
				case 'sinusoidal.inout': return LerpType.Sinusoidal.InOut;
				case 'exponential.in': return LerpType.Exponential.In;
				case 'exponential.out': return LerpType.Exponential.Out;
				case 'exponential.inout': return LerpType.Exponential.InOut;
				case 'circular.in': return LerpType.Circular.In;
				case 'circular.out': return LerpType.Circular.Out;
				case 'circular.inout': return LerpType.Circular.InOut;
				case 'elastic.in': return LerpType.Elastic.In;
				case 'elastic.out': return LerpType.Elastic.Out;
				case 'elastic.inout': return LerpType.Elastic.InOut;
				case 'back.in': return LerpType.Back.In;
				case 'back.out': return LerpType.Back.Out;
				case 'back.inout': return LerpType.Back.InOut;
				case 'bounce.in': return LerpType.Bounce.In;
				case 'bounce.out': return LerpType.Bounce.Out;
				case 'bounce.inout': return LerpType.Bounce.InOut;
				default:
					break;
			}
		}
		else if (Utils.isFunction(name)) {
			return name;
		}

		return LerpType.Linear.None;
	}

	/**
	 * parse the cubetexture url
	 * @param {String} path the cubetexture path.
	 * @returns {String}
	 * @example
	 * let ret1 = THING.Utils.parseCubeTextureUrlsByPath();
	 * let ret2 = THING.Utils.parseCubeTextureUrlsByPath('./tempPath');
	 * let ret3 = THING.Utils.parseCubeTextureUrlsByPath('./tempPath/');
	 * // @expect(ret1 == null && ret2[0] == './tempPath/posx.jpg' && ret3[0] == './tempPath/posx.jpg')
	 * @public
	 */
	static parseCubeTextureUrlsByPath(path) {
		if (!path) {
			return null;
		}

		if (!path.startsWith('.') && !path.startsWith('/')) {
			path = _officialStaticPath._appendURL('skyboxes')._appendURL(path);
		}

		const urls = [
			path._appendURL('posx.jpg'),
			path._appendURL('posy.jpg'),
			path._appendURL('posz.jpg'),
			path._appendURL('negx.jpg'),
			path._appendURL('negy.jpg'),
			path._appendURL('negz.jpg')
		];

		return urls;
	}

	static markAsDebugNode(node) {
		node.setAttribute('Pickable', false);
		node.getUserData()[NodeUserDataType.BoundingBoxInheritType] = InheritType.Stop;
	}

	static addFactory(factory, priority = 0) {
		if (_DEBUG) {
			_typeCheker.checkObject(factory);
		}

		super.addFactory(factory, priority);
	}

	/**
	 * create object by type
	 * @param {string} type the object type.
	 * @param {Object} options the create options.
	 * @returns {Object}
	 * @example
	 * let line = THING.Utils.createObject('PixelLine');
	 * let ret = line instanceof THING.PixelLine;
	 * // @expect(ret == true)
	 * @public
	 */
	static createObject(type, options) {
		if (_DEBUG) {
			let object = super.createObject(type, options);
			if (object) {
				_typeCheker.checkObject(type, object);
			}

			return object;
		}
		else {
			return super.createObject(type, options);
		}
	}

	static createThreadWorkerFromClass(classType) {
		let funcString = classType.toString();

		return Utils.createThreadWorkerFromCode(funcString);
	}

	static createThreadWorkerFromCode(code) {
		let funcString = Utils.isFunction(code) ? code.toString() : code;
		let funcScript = funcString.substring(funcString.indexOf('{') + 1, funcString.lastIndexOf('}'));
		let url = Utils.createObjectURL(funcScript);

		return Utils.createObject('ThreadWorker', { url });
	}

	static waitForAttach(timeout) {
		return Utils._devDebugger.waitForAttach(timeout);
	}

	static beginRenderAnalyze(object) {
		return Utils._renderDebugger.beginAnalyze(object || Utils.getCurrentApp().root.node);
	}

	static endRenderAnalyze() {
		return Utils._renderDebugger.endAnalyze();
	}

	static getRenderAnalyzeResult(type, options = {}) {
		let app = Utils.getCurrentApp();
		let objectManager = app.objectManager;

		return Utils._renderDebugger.getAnalyzeResult(type, {
			onGetObjectByNode: function (node) {
				return objectManager.getBaseObjectFromNode(node);
			},
			onCloneObjet: function (node) {
				return Utils.cloneObject(node);
			},
			onCheckIsArray: function (node) {
				return Utils.isArray(node);
			},
			...options
		});
	}

	/**
	 * check is valid texture
	 * @param {Object} value the texture object.
	 * @example
	 * let ret1 = THING.Utils.isValidTexture(new THING.ImageTexture());
	 * let ret2 = THING.Utils.isValidTexture(new THING.CubeTexture());
	 * let ret3 = THING.Utils.isValidTexture(new THING.RenderTexture());
	 * let ret4 = THING.Utils.isValidTexture(new THING.VideoTexture());
	 * let ret5 = THING.Utils.isValidTexture(new THING.EmptyTexture());
	 * let ret6 = THING.Utils.isValidTexture(new THING.BaseObject());
	 * // @expect(ret1 == true && ret2 == true && ret3 == true && ret4 == true && ret5 == true && ret6 == false)
	 * @public
	 */
	static isValidTexture(value) {
		if (value) {
			if (value.isImageTexture) {
				return true;
			}

			if (value.isCubeTexture) {
				return true;
			}

			if (value.isRenderTexture) {
				return true;
			}

			if (value.isVideoTexture) {
				return true;
			}

			if (value.isEmptyTexture) {
				return true
			}
		}

		return false;
	}

	static syncOptions(target, keys, param, external = _defaultParams) {
		keys.forEach(key => {
			let value = Utils.parseValue(param[key], external[key]);
			if (Utils.isNull(value)) {
				return;
			}

			target[key] = value;
		});
	}

	/**
	 * Register class type.
	 * @param {*} classType The class type.
	 * @param {String} type The type name, it would define property with 'is${type}' as getter.
	 * @private
	 * @example
	 *	THING.Utils.registerClassType(MyObject, 'MyObject');
	 * @public
	 */
	static registerClassType(classType, type) {
		Utils.warn(`Please call 'THING.Utils.registerClass()' instead`);

		if (!classType) {
			return;
		}

		classType.className = type;
	}

	/**
	 * The function to call when get the module name.
	 * @callback OnGetWasmModuleCallback
	 * @param {String} url The url.
	 * @returns {Object}
	 */

	/**
	 * @typedef {Object} LoadWasmFileOptions
	 * @property {OnGetWasmModuleCallback} onGetWasmModule
	 */

	/**
	 * Load WASM file.
	 * @param {String} url The url.
	 * @param {LoadWasmFileOptions} options The options.
	 * @returns {Promise<any>}
	 */
	static loadWasmFile(url, options = {}) {
		return new Promise(async (resolve, reject) => {
			// Initialize dependency loaders
			if (!Utils.dependencyLoaders.length) {
				let factories = Utils.getFactories();
				factories.forEach(factory => {
					let loader = factory.createObject("DependencyLoader");
					if (!loader) {
						return;
					}

					let fileNames = loader.getWasmFileNames();
					if (!fileNames.length) {
						return;
					}

					Utils.dependencyLoaders.push({
						loader,
						fileNames
					});
				});
			}

			// Load wasm module
			let module = await new Promise((resolve, reject) => {
				Utils._scriptLoader.loadAsync(url, { onError: () => { reject(null); } }).then(() => {
					let wasmModule = null;
					if (options['onGetWasmModule']) {
						wasmModule = options['onGetWasmModule']();
					}

					if (wasmModule) {
						wasmModule().then(function (module) {
							resolve(module);
						});
					}
					else if (typeof Module !== 'undefined') {
						Module().then(function (module) {
							resolve(module);
						});
					}
					else {
						Utils.error(`The Module is not existing when load '${url}' wasm file`);
						reject(null);
					}
				});
			});

			if (!module) {
				reject(null);
			}

			// Load wasm and send the module result to loader
			let fileName = url._getFileName();
			for (let i = 0; i < Utils.dependencyLoaders.length; i++) {
				let { loader, fileNames } = Utils.dependencyLoaders[i];

				if (fileNames.includes(fileName)) {
					loader.loadWasmFile(url, module);
					break;
				}
			}

			resolve(module);
		})
	}

	/**
	 * @typedef {Object} LoginOptions
	 * @property {String} url The auth resource url.
	 * @property {String} wasmRootPath The wasm root path.
	 * @property {String} method The request method.
	 * @property {String} requestHeaders The request headers.
	 * @property {String} postFields The post field when using 'POST' method.
	 * @property {Number} maxWorkerNumber The max worker number, -1 indicates use as default.
	 * @property {Function} onGetResourceType The resource type callback function, we can set it to tell whether it's encrypted resource.
	 */

	/**
	 * Login.
	 * @param {String|LoginOptions} options The url or options.
	 * @return {Promise<any>}
	 * @example
	 *	let promise1 = THING.Utils.login('http://127.0.0.1:3000/auth.json');
	 *	let promise2 = THING.Utils.login({
	 * 		method: 'GET',
	 * 		url: 'http://127.0.0.1:3000/auth.json',
	 * 		wasmRootPath: 'js/wasm',
	 * 		postFields: 'post data'
	 * 	});
	 * @public
	 */
	static login(options = {}) {
		if (Utils.isString(options)) {
			options = { url: options };
		}

		let url = options['url'];
		let wasmRootPath = Utils.parseValue(options['wasmRootPath'], Utils.currentScriptPath._appendPath('wasm'));
		let method = Utils.parseValue(options['method'], _postMethod);
		let requestHeaders = Utils.parseValue(options['requestHeaders'], '');
		let postFields = Utils.parseValue(options['postFields'], '');
		let maxWorkerNumber = Utils.parseValue(options['maxWorkerNumber'], -1);
		let disableBackgroundThreads = Utils.parseValue(options['disableBackgroundThreads'], false);

		return new Promise(async (resolve, reject) => {
			let onGetResourceType = (url) => {
				if (options['onGetResourceType']) {
					let result = options['onGetResourceType'](url);
					if (result && result.toLowerCase() == 'normal') {
						return module.ResourceType.Normal;
					}
				}
			};

			let { module, wasmOptions } = await _system.loadWasmModule(wasmRootPath, { maxNumber: maxWorkerNumber, disableBackgroundThreads, onGetResourceType }, { onGetResourceType });
			let helper = module.Helper;

			// Add wasm module version and compile time
			let platformInfo = helper.getPlatformInfo();
			if (platformInfo) {
				let macros = Utils.getMacros();
				macros['WASM'] = _buildWASMInfo(platformInfo, wasmOptions);

				// Add to THING namespace also
				if (typeof THING !== 'undefined') {
					THING.WASM = THING.WASM || {};
					Utils.mergeObject(THING.WASM, macros['WASM'], true);
				}
			}

			if (url) {
				// We fill some random data when it's POST method and user did not provide it
				if (method.toLowerCase() == _postMethod.toLowerCase() && !postFields) {
					postFields = MathUtils.generateUUID();
				}

				let authData = await _system.login(method, url, requestHeaders, postFields);

				if (_NEED_AUTH) {
					if (authData) {
						_authData = authData;
					}
				}
				else {
					_authData = _authData || {};
					_authData[_hasExpiredKey] = false;
				}
			}

			resolve();
		});
	}

	static error() {
		BaseUtils.error.apply(console, arguments);

		if (_NEED_LOGGER) {
			let msg = "";
			try {
				for (let index = 0; index < arguments.length; index++) {
					let element = arguments[index].toString();
					msg += element;
					if (index != arguments.length - 1) {
						msg += "\n";
					}
				}
			}
			catch (e) {
				console.error(e);
				msg = e;
			}

			Utils.logger.error(msg);
		}
	}

	/**
	 * unique the object array. process the repeat and inherit object
	 * @param {Array<Object>} objects object array.
	 * @example
	 * let object1 = new THING.Object3D();
	 * let object2 = new THING.Object3D();
	 * object1.add(object2);
	 * let objArr = [object1, object2, object2];
	 * let ret = THING.Utils.uniqueObjects(objArr);
	 * // @expect(ret.length == 1 && ret[0] == object1)
	 */
	static uniqueObjects(objects) {
		let cloneArr = objects.slice();

		for (let i = cloneArr.length - 1; i >= 0; i--) {
			const obj = cloneArr[i];
			for (let j = i - 1; j >= 0; j--) {
				const compObj = cloneArr[j];
				if (obj == compObj) {
					cloneArr._removeAt(i);
					break;
				}
				if (obj.isChildOf(compObj)) {
					cloneArr._removeAt(i);
					break;
				}
				if (compObj.isChildOf(obj)) {
					cloneArr._removeAt(j);
					i -= 1;
				}
			}
		}
		return cloneArr;
	}

	static importScript(url) {
		return Utils._scriptLoader.importScript(url);
	}
	static extractClassesFromScript(url) {
		let data = {
			componentClasses: new Map(),
			bpnodeClasses: new Map(),
			objectClasses: new Map(),
			unknownClasses: new Map()
		}
		return new Promise((resolve, reject) => {
			Utils._scriptLoader.importScript(url).then((module) => {
				for (const key in module) {
					const cls = module[key];
					if (cls.prototype.isComponent) {
						data.componentClasses.set(key, cls);
					}
					else if (cls.prototype.isBaseNode) {
						data.bpnodeClasses.set(key, cls);
					}
					else if (cls.prototype.isBaseObject) {
						data.objectClasses.set(key, cls);
					}
					else {
						data.unknownClasses.set(key, cls);
					}
				}
				resolve(data)
			});
		});
	}
	/**
	 * check is gltf or glb file
	 * @param {String} url the url of the scene.
	 * @example
	 * let ret1 = THING.Utils.isModelSceneUrl('a.gltf');
	 * let ret2 = THING.Utils.isModelSceneUrl('b.json');
	 * // @expect(ret1 == true && ret2 == false)
	 * @public
	 */
	static isModelSceneUrl(url) {
		const ext = url._getExtension();
		return Utils.isModelSceneExtension(ext);
	}

	/**
	 * check is gltf or glb extension
	 * @param {String} ext the file extension.
	 * @example
	 * let ret1 = THING.Utils.isModelSceneExtension('gltf');
	 * let ret2 = THING.Utils.isModelSceneExtension('json');
	 * // @expect(ret1 == true && ret2 == false)
	 * @public
	 */
	static isModelSceneExtension(ext) {
		return ext == 'gltf' || ext == 'glb';
	}

	static getFileSize(url, callback) {
		Utils._fileManager.getFileSize(url, callback);
	}

	static get currentScriptPath() {
		return Utils._scriptLoader.getMainScriptUrl()._getPath();
	}

}

export { Utils, _getAuthData };