import { Empty, ImageSlotType, ImageFilterType, InheritType, ImageMappingType, SideType, ImageWrapType, EventType } from '../const';
import { MathUtils } from '../math/MathUtils';
import { Utils } from '../common/Utils';
import { Object3D } from '../objects/Object3D';
import { ParticleSystem } from '../objects/ParticleSystem';
import { EffectGroundObject } from '../objects/EffectGroundObject.js';
import { ImageTexture } from '../resources/ImageTexture';
import { Box } from '../objects/Box';
import { PixelLine } from '../objects/PixelLine';
import { BaseComponent } from '../components/BaseComponent';

// One thing that can be optimized is that these textures are not registered and saved when they are used

const _pixel = 128;                     // gradient texture size

const _emptyTex = Empty.Texture;

/**
 * @class ThemeManager
 * 效果模板管理器 - 用于管理、注册、应用和切换场景效果模板
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 * @example
 * const manager = app.themeManager;
 */
class ThemeManager extends BaseComponent {

	/**
	 * 效果模板管理器 - 用于管理、注册、应用和切换场景效果模板
	 */
	constructor() {
		super();

		this._themeMap = new Map();			// save theme json
		this._gradTexMap = new Map();          // save grad tex
		this._scrollMap = new Map();           // save scroll offset
		this._scrollTexMap = new Map();        // save scrollTex
		this._envTexMap = new Map();           // save env map tex
		this._particleItemsMap = new Map();	// particle items
		this._colorTexMap = new Map();			// color image tex
		this._effectGroundObjects = new WeakMap();
		this._particleObjects = new WeakMap();
	}

	onRemove() {
		super.onRemove();
	}

	register(name, json, baseUrl) {
		this._themeMap.set(name, { baseUrl: baseUrl, json });     // save theme json      // baseUrl, json, strategy {each class}     can improve performance

		const { styles } = json;
		const app = this.object;

		if (styles) {
			this._gradTexMap.set(name, generateGradientTextures(styles.class));

			this._scrollMap.set(name, generateScroll(styles.class));

			this._scrollTexMap.set(name, generateScrollTex(styles.class, baseUrl));

			this._envTexMap.set(name, generateEnvTex(app, styles.environment, baseUrl));

			this._colorTexMap.set(name, generateColorTex(styles.class));
		}

		updateScroll(app, this._scrollMap, name);
	}

	_clearMaps(name) {
		const gradMap = this._gradTexMap.get(name);
		if (gradMap) {
			gradMap.forEach((value, key) => {
				if (value) {
					value.dispose();
				}
			})
			gradMap.clear();
		}
		this._gradTexMap.delete(name);

		const _scrollTex = this._scrollTexMap.get(name);
		if (_scrollTex) {
			_scrollTex.forEach((value, key) => {
				if (value) {
					value.dispose();
				}
			})
			this._scrollTexMap.clear();
		}
		this._scrollTexMap.delete(name);

		const envTex = this._envTexMap.get(name);
		if (envTex) {
			envTex.dispose();
		}
		this._envTexMap.delete(name);

		// this._scrollMap
		const scrollMap = this._scrollMap.get(name);
		if (scrollMap) {
			scrollMap.clear();
		}
		this._scrollMap.delete(name);
	}

	unregister(name) {
		this._themeMap.delete(name);
		this._particleItemsMap.delete(name);
		stopUpdateScroll(this.object, name, this._scrollMap);
		this._clearMaps(name);
	}

	getTheme(name) {
		const theme = this._themeMap.get(name);
		return theme ? theme.json : null;
	}

	loadParticles(name) {
		if (!this._themeMap.has(name)) return;

		const { baseUrl, json } = this._themeMap.get(name);

		const particles = json.particles;

		if (!particles) {
			console.warn(name + " theme doesn't have particles!");
			return;
		}

		if (particles.enable) {
			const promises = [];
			const particleItems = {};

			particles.item.forEach((item) => {
				let itemUrl;
				if (item.url.endsWith("index.json")) {
					itemUrl = baseUrl._appendPath(item.url);
				}
				else {		// @compatible
					itemUrl = baseUrl._appendPath(item.url)._appendPath('./index.json');
				}

				item.themePkg = baseUrl;

				const promise = new Promise((resolve) => {
					Utils.loadJSONFileAsync(itemUrl).then(data => {
						if (!data.version || !data.version[0] == 2) {
							data = convertParticle(data);
						}
						particleItems[item.themePkg + item.url] = data;
						resolve();
					})
				})

				promises.push(promise);
			});

			return Promise.all(promises).then(() => {
				this._particleItemsMap.set(name, particleItems);
			});
		}
		else {
			return Promise.resolve();
		}
	}

	/**
	 * 应用效果模版的全局设置
	 * @param {string} name 主题名称,必须是已注册的主题
	 * @returns {void}
	 * @example
	 * 	// 应用名为 'night' 的主题渲染设置
	 * 	app.themeManager.applyRenderSettings('night');
	 *
	 * 	// 保存当前渲染设置以便后续还原
	 * 	const config = app.renderSettings;
	 *
	 * 	// 应用主题渲染设置
	 * 	app.themeManager.applyRenderSettings('night');
	 *
	 * 	// 还原之前保存的渲染设置
	 * 	app.renderSettings = config;
	 * @public
	 */
	applyRenderSettings(name) {
		if (!this._themeMap.has(name)) return;

		const { baseUrl, json } = this._themeMap.get(name);

		const renderSettings = json.renderSettings;

		if (!renderSettings) {
			console.warn(name + " theme doesn't have renderSettings!");
			return;
		}

		const appRenderSettings = {};

		const app = this.object;


		app.query('[userData/themeLight]').forEach(light => {
			app.scene[light.userData.themeLight] = null;		// may cause bug, for mainLight?
			light.destroy();
		});

		const background = renderSettings.background;
		const environmentMap = renderSettings.environmentMap || renderSettings.environment;		// @compatible

		if (background) {																		// @compatible app.renderSettigs
			if (background.type !== undefined) {
				const backgroundType = background.type;
				const backgroundValue = background.value;

				let backgroundTex;

				switch (backgroundType) {
					case 'cube':
						if (isLocalResource(backgroundValue[0])) {
							let resourceURL = getEnvTexUrl(backgroundValue, baseUrl);
							backgroundTex = app.loadCubeTexture(resourceURL);		// 这个需不需在组件remove的时候释放资源?TODO
						}
						else {
							backgroundTex = app.loadCubeTexture(backgroundValue);
						}
						appRenderSettings.background = backgroundTex;
						break;
					case 'image':
						if (isLocalResource(backgroundValue)) {
							let resourceURL = baseUrl._appendPath(backgroundValue);
							backgroundTex = app.loadImageTexture(resourceURL);
						}
						else {
							backgroundTex = app.loadImageTexture(backgroundValue);
						}
						appRenderSettings.background = backgroundTex;
						break;
					case 'color':
						appRenderSettings.background = backgroundValue;
						break;
					case 'none':
						appRenderSettings.background = null;
					default:
						break;
				}
			}
			else {
				appRenderSettings.background = background;
			}
		}

		appRenderSettings.environment = {};

		if (environmentMap) {
			if (environmentMap.type !== undefined) {						// @compatible app.renderSettings
				const environmentMapType = environmentMap && environmentMap.type;
				const environmentMapValue = environmentMap && environmentMap.value;
				const environmentMapLightIntensity = environmentMap && environmentMap.diffuseIntensity;

				// envDiffuse and envSpecular
				const envSpecularIntensity = environmentMap.specularIntensity;

				let envMapTex;

				switch (environmentMapType) {
					case 'cube':
						if (isLocalResource(environmentMapValue[0])) {
							let resourceURL = getEnvTexUrl(environmentMapValue, baseUrl);
							envMapTex = app.loadCubeTexture(resourceURL);		// 这个需不需在组件remove的时候释放资源?TODO
						}
						else {
							envMapTex = app.loadCubeTexture(environmentMapValue);
						}
						appRenderSettings.environment.envMap = envMapTex;
						break;
					case 'image':
						if (isLocalResource(environmentMapValue)) {
							let resourceURL = baseUrl._appendPath(environmentMapValue);
							envMapTex = app.loadImageTexture(resourceURL, { mappingType: ImageMappingType.EquirectangularReflection });
						}
						else {
							envMapTex = app.loadImageTexture(environmentMapValue, { mappingType: ImageMappingType.EquirectangularReflection });
						}
						appRenderSettings.environment.envMap = envMapTex;
						break;
					case 'none':
						appRenderSettings.environment.envMap = null;
						break;
					default:
						break;
				}

				if (environmentMapLightIntensity !== undefined) {
					appRenderSettings.environment.diffuseIntensity = environmentMapLightIntensity;
				}

				if (Utils.isValid(envSpecularIntensity)) {
					appRenderSettings.environment.specularIntensity = envSpecularIntensity;
				}
			}
			else {
				appRenderSettings.environment.envMap = environmentMap.envMap;		// maybe null
				appRenderSettings.environment.diffuseIntensity = environmentMap.diffuseIntensity;
			}
		}

		// set fog config

		const fogConfig = renderSettings.fog;
		if (fogConfig) {
			appRenderSettings.fog = fogConfig;
		}

		// set lights
		const lightConfig = renderSettings.lights || renderSettings.light;	// @compatible

		appRenderSettings.lights = lightConfig;

		// compatible some renderSettings has this lights
		const pointLightConfigs = lightConfig.pointLights;

		if (pointLightConfigs) {
			pointLightConfigs.forEach((pointLightConfig, index) => {
				let pointLight;
				const result = app.query('.PointLight')[index];

				if (!!result) {
					pointLight = result;
				}
				else {
					pointLight = new THING.PointLight();

					// editor's light
					pointLight.userData = pointLight.userData || {};
					pointLight.userData['themeLight'] = 'pointLight';
				}

				for (let key in pointLightConfig) {
					pointLight[key] = pointLightConfig[key];
				}
			})
		}


		const spotLightConfigs = lightConfig.spotLights;	// maybe not have
		if (spotLightConfigs) {
			spotLightConfigs.forEach((spotLightConfig, index) => {
				let spotLight;
				const result = app.query('.SpotLight')[index];

				if (!!result) {
					spotLight = result;
				}
				else {
					spotLight = new THING.SpotLight();

					// editor's light
					spotLight.userData = spotLight.userData || {};
					spotLight.userData['themeLight'] = 'spotLight'
				}

				for (let key in spotLightConfig) {
					spotLight[key] = spotLightConfig[key];
				}
			})
		}

		appRenderSettings.postEffects = renderSettings.postEffects;

		appRenderSettings.cameraEffects = renderSettings.effects || renderSettings.cameraEffect || renderSettings.cameraEffects;			// @compatible

		app.renderSettings = appRenderSettings;
	}

	_getTag(styleClass, node) {
		if (!styleClass) {
			return '';
		}

		let tag;

		const userData = node.userData;
		const internalData = node.internalData;
		const type = node.type;

		if (internalData && internalData._styleTag_ && styleClass[internalData._styleTag_]) {
			tag = internalData._styleTag_;
		}
		else if (userData && userData._styleTag_ && styleClass[userData._styleTag_]) {
			tag = userData._styleTag_;
		}
		else if (styleClass[type]) {
			tag = type;
		}
		else {
			tag = Utils.getClassNamesFromProto(this.object).find(name => !!styleClass[name]);
		}

		return tag;
	}

	_applyStyle(node, config, name, tag) {
		const compatible = this.object.view._compatibleRender;
		beginDefaultValues(node);
		setStyleColorMapping(node, config, name, tag, this._gradTexMap);

		setStyleColorImage(node, config, name, tag, this._colorTexMap);			// @Compatible colorImage

		setStyleColor(node, config);
		setStyleRemoveDiffuseMap(node, config);
		setStyleRemoveNormalMap(node, config);
		setStyleRemoveAoMap(node, config);
		setStyleOpacity(node, config);
		setStyleWireFrame(node, config);
		setStyleEnvMap(node, config, name, this._envTexMap, compatible);
		setStyleEdge(node, config);
		setStyleEffect(node, config);
		setStyleReflection(node, config);
		setStyleScrollTex(node, config, name, tag, this._scrollMap, this._scrollTexMap);    // TODO No switch off
		setStyleSpecularFactor(node, config);
		setStyleFresnel(node, config);
		setStyleEmissive(node, config);
		setStyleLightingGroup(node, config);

		endDefaultValues(node);

		if (node.body.style.isInstancedDrawing || Utils.isValid(node.resource.instanceId)) {
			node.makeInstancedDrawing(true);
		}
	}

	/**
	 * 应用效果模版样式 - 将效果模版样式应用到指定的根节点及其子节点
	 * 
	 * 【功能说明】
	 * 这是应用效果模板的核心方法,将主题中定义的样式配置应用到场景对象上。
	 * 样式包括颜色、透明度、线框模式、环境光贴图、边缘效果、反射效果、
	 * 滚动纹理、高光因子、菲涅尔效果、自发光等多种视觉效果。
	 * 
	 * 【样式匹配规则】
	 * 1. 优先匹配 internalData._styleTag_ 指定的样式类
	 * 2. 其次匹配 userData._styleTag_ 指定的样式类
	 * 3. 再次匹配对象类型(node.type)
	 * 4. 最后匹配对象的原型类名
	 * 
	 * 【使用时机】
	 * 在应用渲染设置后调用,通常在场景加载完成或切换效果模版时调用。
	 * 
	 * 【参数说明】
	 * @param {string} name - 效果模板名称,必须是已注册的效果模板
	 * @param {Object3D} rootNode - 要应用效果模板的根节点对象
	 * @param {boolean} [traverse=true] - 是否遍历子节点并应用效果模板,默认为true
	 * @returns {void}
	 * @example
	 * 	// 应用名为 'dark' 的主题到场景中的所有对象
	 * 	app.themeManager.applyStyles('dark', app.root);
	 *
	 * 	// 仅应用主题到指定对象,不遍历子节点
	 * 	app.themeManager.applyStyles('dark', myEntity, false);
	 * @public
	 */
	applyStyles(name, rootNode, traverse = true) {
		if (rootNode.userData['_skipTheme_']) {
			return;
		}

		if (rootNode.isLight || rootNode.isCamera) {
			return;
		}

		if (!this._themeMap.has(name)) return;

		const { baseUrl, json } = this._themeMap.get(name);
		const styles = json.styles;

		if (!styles) {
			console.warn(name + " theme doesn't have styles!");
			return;
		}

		this._applyStylesInternal(name, rootNode, styles, traverse);
	}

	_applyStylesInternal(name, rootNode, styles, traverse) {
		if (rootNode.userData['_skipTheme_']) {
			return;
		}

		if (rootNode.isLight || rootNode.isCamera) {
			return;
		}

		rootNode.internalData._themeTag = name;

		if (traverse) {
			rootNode.children.forEach(child => {
				this._applyStylesInternal(name, child, styles, traverse);
			});
		}

		const tag = this._getTag(styles.class, rootNode);		// optimize: cache node => tag

		if (!tag) return;

		const config = styles.class[tag];

		// enable is false will clear current style(theme style/default style)
		if (config.enable == false) {
			_initStyle(rootNode);
			return;
		}

		// _themeTag will add on root and node which applied styles
		this._applyStyle(rootNode, config, name, tag);
	}

	/**
	 * 清除效果模版样式 - 清除指定节点及其子节点的效果模版样式
	 * 
	 * 【功能说明】
	 * 移除对象上应用的效果模版样式,恢复到默认状态。
	 * 通常在切换效果模版或不再需要效果样式时调用。
	 * 
	 * 清除整个场景的主题样式
	 * app.themeManager.clearStyles(app.root);
	 * 
	 * 【相关方法】
	 * - applyStyles: 应用效果模版样式
	 * 
	 * 【关键词】
	 * 清除样式、clearStyles、移除样式、清除效果模版样式、应用效果模板
	 * 
	 * 
	 * 【参数说明】
	 * @param {Object3D} rootNode - 要清除样式的根节点对象
	 * @returns {void}
	 * @example
	 * 	// 清除指定对象及其子对象的效果模版样式
	 * 	app.themeManager.clearStyles(myEntity);
	 * @public
	 */
	clearStyles(rootNode) {
		_initStyle(rootNode);
		rootNode.children.forEach(child => {
			this.clearStyles(child);
		})
	}

	/**
	 * 创建主题粒子特效 - 根据主题配置创建粒子系统
	 * 
	 * 【功能说明】
	 * 根据主题中定义的粒子配置创建粒子系统特效,如雨雪天气、烟雾等。
	 * 创建前必须先调用 loadParticles 加载粒子数据。
	 * 
	 * 【使用时机】
	 * 在应用渲染设置和样式后调用,通常在主题初始化的最后阶段。
	 * 
	 * 【参数说明】
	 * @param {string} name - 主题名称,必须是已注册的主题
	 * @param {Object3D} rootNode - 粒子系统的父节点
	 * @param {Function} [onCreateThemeObjects] - 创建主题对象时的回调函数,可用于修改粒子参数
	 * @returns {Array<ParticleSystem>|undefined} 返回创建的粒子系统数组,如果主题不存在或未启用粒子则返回undefined
	 * @example
	 * 	// 创建主题粒子特效
	 * 	const particles = app.themeManager.createEffectParticles('snow', campus);
	 *
	 * 	// 销毁创建的粒子特效
	 * 	if (particles) {
	 * 		particles.forEach(particle => {
	 * 			particle.destroy();
	 * 		});
	 * 	}
	 * @public
	 */
	createEffectParticles(name, rootNode, onCreateThemeObjects) {
		if (!this._themeMap.has(name)) return;

		const { baseUrl, json } = this._themeMap.get(name);

		const particles = json.particles;

		if (!particles) {
			console.warn(name + " theme doesn't have particles!");
			return;
		}

		if (particles.enable) {
			const particleItems = this._particleItemsMap.get(name);

			if (!particleItems) return;

			const particleParents = [];

			particles.item.forEach((item) => {
				const data = particleItems[item.themePkg + item.url];
				const particleParent = _createParticle(item, data, rootNode, onCreateThemeObjects);
				particleParent.userData['_themeParticle_'] = true;
				particleParents.push(particleParent);
			});

			return particleParents;
		}
	}

	/**
	 * 创建主题地面特效 - 根据主题配置创建地面特效对象
	 * 
	 * 【功能说明】
	 * 根据主题中定义的地面特效配置创建地面特效,如水面反射、地面流动纹理等。
	 * 地面特效可以包含水面反射效果,支持动态更新和反射因子配置。
	 * 
	 * 【使用时机】
	 * 在应用渲染设置和样式后调用,通常在主题初始化阶段。
	 * 
	 * 【参数说明】
	 * @param {string} name - 主题名称,必须是已注册的主题
	 * @param {Object3D} rootNode - 地面特效的父节点
	 * @param {Function} [onCreateThemeObjects] - 创建主题对象时的回调函数,可用于修改地面特效参数
	 * @returns {Array<EffectGroundObject>|undefined} 返回创建的地面特效对象数组,如果主题不存在或未启用地面特效则返回undefined
	 * @example
	 * 	// 创建主题地面特效
	 * 	const grounds = app.themeManager.createEffectGrounds('water', campus);
	 *
	 * 	// 销毁创建的地面特效
	 * 	if (grounds) {
	 * 		grounds.forEach(ground => {
	 * 			ground.destroy();
	 * 		});
	 * 	}
	 * @public
	 */
	createEffectGrounds(name, rootNode, onCreateThemeObjects) {
		if (!this._themeMap.has(name)) return;

		const { baseUrl, json } = this._themeMap.get(name);

		const effectGrounds = json.effectGrounds;

		if (!effectGrounds) {
			console.warn(name + " theme doesn't have effectGrounds!");
			return;
		}

		if (effectGrounds.enable) {
			const grounds = [];

			effectGrounds.item.forEach(item => {
				const curParam = JSON.parse(JSON.stringify(item));      // Need to keep the original data

				curParam.url = baseUrl._appendPath(curParam.url);
				if (curParam.maskUrl) curParam.maskUrl = baseUrl._appendPath(curParam.maskUrl);
				if (curParam.alphaMap) curParam.alphaMap = baseUrl._appendPath(curParam.alphaMap);

				onCreateThemeObjects && onCreateThemeObjects(curParam);

				Object.assign(curParam, {
					parent: rootNode
				});

				const ground = new EffectGroundObject(curParam);
				ground.userData['_themeGround_'] = true;
				ground.renderOrder = -10;
				grounds.push(ground);
			});

			if (effectGrounds.groundReflect) {
				const reflectParams = {
					groundClearance: 0.3,
					groundReflect: true,
					parent: rootNode,
					reflectFactor: effectGrounds.reflectFactor
				}

				onCreateThemeObjects && onCreateThemeObjects(reflectParams);

				const ground = new EffectGroundObject(reflectParams);
				// ground.userData['_themeGround_'] = true;
				ground.userData['_themeReflectGround_'] = true;
				ground.renderOrder = 5;
				grounds.push(ground);
			}

			return grounds;
		}
	}

	createEffectFacades(name, rootNode, onCreateThemeObjects) {
		if (!this._themeMap.has(name)) return;

		const { json } = this._themeMap.get(name);

		const effectFacades = json.effectFacades;

		if (!effectFacades) {
			console.warn(name + " theme doesn't have effectFacades!");
			return;
		}

		if (effectFacades.enable) {
			const facades = [];
			const facade = rootNode.queryByTags('Facade')[0];
			if (facade) {
				rootNode = facade;
			}

			effectFacades.item.forEach(item => {
				const effectFacade = _createEffectFacade(item, rootNode, onCreateThemeObjects);
				facades.push(...effectFacade);
			});

			return facades;
		}
	}

	// 1.0 bundle name, this will add inner and outer
	applyLevelThemeChange(name, root) {
		// whether remove level events before

		if (!root || root.type !== 'Campus') {
			return;
		}

		const outerThemeName = name + '_' + 'outer';
		const innerThemeName = name + '_' + 'inner';

		// must have theme
		if (!this._themeMap.has(outerThemeName) || !this._themeMap.has(innerThemeName)) {
			return;
		}

		this.initEffectObjectsCache(root);

		const innerGrounds = this._effectGroundObjects.get(root).inner;
		const outerGrounds = this._effectGroundObjects.get(root).outer;
		const particlesObject = this._particleObjects.get(root);

		root.on(EventType.BeforeLeaveLevel, '.Building', (ev) => {
			if (!!ev.next && ev.next.type == 'Campus') {
				this.applyRenderSettings(outerThemeName);
				if (outerGrounds.length <= 0) {
					const outerEffectGrounds = this.createEffectGrounds(outerThemeName, ev.next);
					outerEffectGrounds && outerGrounds.push(...outerEffectGrounds);
				}
				else {
					outerGrounds.forEach(ground => {
						if (!ground.destroyed) {		// currently fixed
							ground.updateGround(ev.next);
						}
					});
				}

				if (particlesObject.length <= 0) {
					const outerEffectParticles = this.createEffectParticles(outerThemeName, ev.next);
					outerEffectParticles && particlesObject.push(...outerEffectParticles);
				}
				else {
					particlesObject.forEach(particle => {
						particle.destroy();
					});

					particlesObject.push(...this.createEffectParticles(outerThemeName, ev.next));
				}
			}
		}, root.uuid + 'Theme1.x' + 'BuildingToCampus');

		root.on(EventType.BeforeEnterLevel, '.Building', (ev) => {
			if (ev.prev.type == 'Campus') {
				this.applyRenderSettings(innerThemeName);

				if (innerGrounds.length <= 0) {
					const innerEffectGrounds = this.createEffectGrounds(innerThemeName, ev.object);
					innerEffectGrounds && innerGrounds.push(...innerEffectGrounds);
				}
				else {
					innerGrounds.forEach(ground => {
						if (!ground.destroyed) {		// currently fixed
							ground.updateGround(ev.object);
						}
					});
				}
			}
		}, root.uuid + 'Theme1.x' + 'CampusToBuilding');

		root.on(EventType.BeforeEnterLevel, '.Floor', (ev) => {
			if (ev.prev.type == 'Building') {
				if (innerGrounds.length <= 0) {
					const innerEffectGrounds = this.createEffectGrounds(innerThemeName, ev.object);
					innerEffectGrounds && innerGrounds.push(...innerEffectGrounds);
				}
				else {
					innerGrounds.forEach(ground => {
						if (!ground.destroyed) {		// currently fixed
							ground.updateGround(ev.object);
						}
					});
				}
			}
		}, root.uuid + 'Theme1.x' + 'BuildingToFloor');

		root.on(EventType.BeforeLeaveLevel, '.Floor', (ev) => {
			if (!!ev.next && ev.next.type == 'Building') {
				if (innerGrounds.length <= 0) {
					const innerEffectGrounds = this.createEffectGrounds(innerThemeName, ev.object);
					innerEffectGrounds && innerGrounds.push(...innerEffectGrounds);
				}
				else {
					innerGrounds.forEach(ground => {
						if (!ground.destroyed) {		// currently fixed
							ground.updateGround(ev.next);
						}
					});
				}
			}
		}, root.uuid + 'Theme1.x' + +'FloorToBuilding');
	}

	initEffectObjectsCache(root) {
		if (!this._effectGroundObjects.has(root)) {
			const effectGroundObjects = {
				inner: [],
				outer: []
			}

			this._effectGroundObjects.set(root, effectGroundObjects);

			root.on(EventType.AfterDestroy, () => {
				const effectGroundObjects = this._effectGroundObjects.get(root);

				effectGroundObjects.inner.forEach((ground) => {
					ground.destroy()
				});

				effectGroundObjects.inner.length = 0;

				effectGroundObjects.outer.forEach((ground) => {
					ground.destroy()
				});

				effectGroundObjects.outer.length = 0;

				this._effectGroundObjects.delete(root);

				// off events
				root.off(EventType.AfterDestroy, root.uuid + 'destroyThemeGrounds');
			}, root.uuid + 'destroyThemeGrounds');
		}

		if (!this._particleObjects.has(root)) {
			const particleObjects = [];
			this._particleObjects.set(root, particleObjects);

			root.on(EventType.AfterDestroy, () => {
				const particleObjects = this._particleObjects.get(root);

				particleObjects.forEach((particle) => {
					particle.destroy();
				});
				particleObjects.length = 0;

				this._particleObjects.delete(root);

				// off events
				root.off(EventType.AfterDestroy, root.uuid + 'destroyThemeParticles');
			}, root.uuid + 'destroyThemeParticles');
		}
	}

	exportRenderSettings() {
		const app = this.object;

		const renderSettings = {
			'background': {
				'type': 'export',
				'value': app.background
			},
			'environment': {
				'type': 'export',
				'value': app.envMap,
				'diffuseIntensity': app.scene.envMapLightIntensity
			},
		};

		const lights = {

		}
		renderSettings.lights = lights;

		if (app.scene.mainLight) {
			const mainLight = app.scene.mainLight;
			lights.mainLight = {
				"enableShadow": mainLight.castShadow,
				"intensity": mainLight.intensity,
				"color": mainLight.color,
				"shadowBias": mainLight.shadowBias,
				"helperVisible": mainLight.helper.visible,
				"shadowQuality": mainLight.shadowQuality,
				"adapter": {
					"enable": mainLight.adapter.enable,
					"horzAngle": mainLight.adapter.horzAngle,
					"vertAngle": mainLight.adapter.vertAngle
				}
			}
		}
		if (app.scene.ambientLight) {
			const ambientLight = app.scene.ambientLight;
			lights.ambientLight = {
				'intensity': ambientLight.intensity,
				'color': ambientLight.color
			}
		}
		if (app.scene.hemisphereLight) {
			const hemisphereLight = app.scene.hemisphereLight;
			lights.hemisphereLight = {
				'intensity': hemisphereLight.intensity,
				'color': hemisphereLight.color,
				'groundColor': hemisphereLight.groundColor
			}
		}
		if (app.scene.secondaryLight) {
			const secondaryLight = app.scene.secondaryLight;
			lights.secondaryLight = {
				"enableShadow": secondaryLight.castShadow,
				"intensity": secondaryLight.intensity,
				"color": secondaryLight.color,
				"shadowBias": secondaryLight.shadowBias,
				"helperVisible": secondaryLight.helper.visible,
				"shadowQuality": secondaryLight.shadowQuality,
				"adapter": {
					"enable": secondaryLight.adapter.enable,
					"horzAngle": secondaryLight.adapter.horzAngle,
					"vertAngle": secondaryLight.adapter.vertAngle
				}
			}
		}

		renderSettings.postEffects = Utils.cloneObject(app.camera.postEffect.config, false);
		renderSettings.effects = Utils.cloneObject(app.camera.effect.config, false);

		return renderSettings;
	}

	_setStylesConfig(name, strategy, styleConfig, tag) {
		if (strategy.useColorMap !== undefined) {
			if (strategy.useColorMap) {        // colorMapping
				const colorMapTex = this._gradTexMap.get(name).get(tag);
				styleConfig.colorMapping = colorMapTex;
			}
		}
		else {
			if (strategy.colorMapping && strategy.colorMapping.enable) {        // colorMapping
				const colorMapTex = this._gradTexMap.get(name).get(tag);
				styleConfig.colorMapping = colorMapTex;
				styleConfig.attributes = styleConfig.attributes || {};
				styleConfig.attributes['ColorMappingIntensity'] = strategy.colorMapping.intensity;
			}
		}

		if (strategy.color.enable == true) {                                // color
			styleConfig.color = strategy.color.value;
		}

		if (strategy.removeDiffuseMap) {
			styleConfig.map = _emptyTex;
		}

		if (strategy.removeNormalMap) {
			styleConfig.normalMap = _emptyTex;
		}

		if (strategy.removeAoMap) {
			styleConfig.aoMap = _emptyTex;
		}

		if (strategy.transparent) {
			styleConfig.transparent = strategy.transparent;
		}

		if (strategy.opacity !== undefined) {
			styleConfig.opacity = strategy.opacity;
			if (styleConfig.opacity < 1) {            // if need transparent
				styleConfig.transparent = true;
			}
		}

		if (strategy.wireframe) {
			styleConfig.wireframe = strategy.wireframe;
		}

		if (strategy.useEnvMapType) {
			switch (strategy.useEnvMapType) {
				case 'global':
					styleConfig.envMap = null;
					styleConfig.envMapping = true;
					break;
				case 'styleFiction':
					styleConfig.envMapping = true;
					styleConfig.envMap = this._envTexMap.get(name);
					break;
				case 'none':
					styleConfig.envMap = null;
					styleConfig.envMapping = false;
					break;
				default:
					break;
			}
			styleConfig.attributes = styleConfig.attributes || {};
			styleConfig.attributes['EnvMapIntensity'] = Utils.parseNumber(strategy.envMapIntensity, 1);
		}

		if (strategy.edge.enable) {
			styleConfig.edge = styleConfig.edge || {};
			styleConfig.edge.enable = strategy.edge.enable;
			styleConfig.edge.color = strategy.edge.color;
			styleConfig.edge.glow = strategy.edge.glow;
			styleConfig.edge.opacity = strategy.edge.opacity;
		}

		if (strategy.effect) {
			styleConfig.effect = styleConfig.effect || {};

			styleConfig.effect.glow = strategy.effect.glow || null;
			styleConfig.effect.innerGlow = strategy.effect.innerGlow || null;
			styleConfig.effect.lineBloom = strategy.effect.lineBloom || null;
			styleConfig.effect.tailing = strategy.effect.tailing || null;
			styleConfig.effect.radial = strategy.effect.radial || null;
			styleConfig.effect.ghosting = strategy.effect.ghosting || null;
		}

		if (strategy.reflection.enable) {
			styleConfig.metalness = strategy.reflection.metalness;
			styleConfig.roughness = strategy.reflection.roughness;
		}

		if (strategy.useScrollTex !== undefined) {				// @ compatible
			if (strategy.useScrollTex) {						// @ compatible
				// const { texture, color } = strategy.scroll;

				const texture = strategy.scrollTex;
				const color = strategy.scrollColor;

				const scrollTex = this._scrollTexMap.get(name).get(texture);

				styleConfig.imageSlotType = ImageSlotType.EmissiveMap;
				styleConfig.attributes = styleConfig.attributes || {};
				styleConfig.attributes['VerticalEmissive'] = true;
				styleConfig.emissiveMap = scrollTex;
				styleConfig.emissive = color;
			}
		}
		else {
			if (strategy.scroll && strategy.scroll.enable) {
				const { texture, color } = strategy.scroll;
				const scrollTex = this._scrollTexMap.get(name).get(texture);

				styleConfig.imageSlotType = ImageSlotType.EmissiveMap;
				styleConfig.attributes = styleConfig.attributes || {};
				styleConfig.attributes['VerticalEmissive'] = true;
				styleConfig.emissiveMap = scrollTex;
				styleConfig.emissive = color;
			}
		}

		if (strategy.specularFactor !== undefined) {
			const specularFactor = strategy.specularFactor;

			styleConfig.attributes = styleConfig.attributes || {};
			styleConfig.attributes['SpecularFactor'] = specularFactor;
		}

		if (strategy.fresnel) {
			const fresnelPower = strategy.fresnel.power;
			const fresnelInverse = strategy.fresnel.inverse;

			styleConfig.attributes = styleConfig.attributes || {};
			styleConfig.attributes['FresnelPower'] = fresnelPower;
			styleConfig.attributes['FresnelInverse'] = fresnelInverse;
		}

		if (strategy.emissive && strategy.emissive.enable) {
			styleConfig.emissive = strategy.emissive.color;
		}

		if (strategy.lightGroupMask) {
			styleConfig.attributes = styleConfig.attributes || {};
			styleConfig.attributes['LightingGroup'] = strategy.lightGroupMask;
		}
	}

	_updateScrollMapUv(name, styleConfig, object, tag) {			// @ compatible
		if (styleConfig.useScrollTex !== undefined) {							// @ compatible
			if (!styleConfig.useScrollTex) return false;

			object.on('update', () => {
				if (this._scrollMap.has(name)) {
					const scrollMap = this._scrollMap.get(name);
					if (scrollMap.has(tag)) {
						object.style.uv.offset = scrollMap.get(tag).arr;
					}
				}
			}, 'updateScrollTexUv')

			return true;
		}
		else {
			if (!styleConfig.scroll.enable) return false;

			object.on('update', () => {
				if (this._scrollMap.has(name)) {
					const scrollMap = this._scrollMap.get(name);
					if (scrollMap.has(tag)) {
						object.style.uv.offset = scrollMap.get(tag).arr;
					}
				}
			}, 'updateScrollTexUv')

			return true;
		}
	}

}

export { ThemeManager };

function _createParticle(config, data, rootNode, onCreateThemeObjects) {
	const bbx = rootNode.getOBB(true, false);
	const radius = bbx.halfSize[1] + 0.2;
	const rDis = MathUtils.scaleVector(rootNode.up, radius);
	const pos = MathUtils.subVector(bbx.center, rDis);

	const getParticleData = (source, style) => {
		for (let key in style) {
			if (key !== 'maxParticleCount' && key !== 'position' && key !== 'particleCount') {
				if (source[key] instanceof Array) {
					if (source[key][0] instanceof Object || source[key][0] instanceof Array) {
						getParticleData(source[key], style[key]);
					}
					else if (source[key].length > 0) {
						source[key] = style[key];
					}
				}
				else if (source[key] instanceof Object) {
					getParticleData(source[key], style[key]);
				}
				else {
					if (style[key] !== '') {
						source[key] = style[key];
					}
				}
			}
		}
		return source;
	}

	data.groups = getParticleData(data.groups, config.content.groups);

	for (let i = 0; i < data.groups.length; i++) {
		if (config.content.groups[i].texture.url === '') {
			if (!(data.groups[i].texture.url.startsWith('http://') || data.groups[i].texture.url.startsWith('https://'))) {
				if (config.url.endsWith("index.json")) {
					config.url = config.url.replace('/index.json', '');
				}
				data.groups[i].texture.url = Utils.resolveURL(config.themePkg._appendPath(config.url._appendPath(data.groups[i].texture.url)));
			}
		}
		else {
			data.groups[i].texture.url = Utils.resolveURL(config.themePkg._appendPath(config.content.groups[i].texture.url));
		}

		let maxCount = [];
		let count = [];
		for (let j = 0; j < data.groups[i].emitters.length; j++) {
			const dividend = data.groups[i].emitters[j].position.spread;
			maxCount[j] = MathUtils.ceil(MathUtils.min(
				10000,
				((
					config.content.density *
					data.groups[i].maxParticleCount *
					bbx.size[0] *
					bbx.size[2]) /
					dividend[0] /
					dividend[2]) *
				4
			));
			count[j] = MathUtils.ceil(
				(data.groups[i].emitters[j].particleCount /
					data.groups[i].maxParticleCount) *
				maxCount[j]
			);

			data.groups[i].emitters[j].position.spread = [
				bbx.size[0] * 2,
				config.content.height,
				bbx.size[2] * 2,
			];
		}

		data.groups[i].maxParticleCount = MathUtils.max(...maxCount);
		const maxParticleCount = data.groups[i].maxParticleCount;
		let start = 0;
		for (let k = 0; k < count.length; k++) {
			let end = MathUtils.min(start + count[k], maxParticleCount);
			data.groups[i].emitters[k].particleCount = end - start;
			start = end;
		}
	}

	const pBoxParams = {
		id: `ParticleDecorationModelParent_${config.code}`,
		parent: rootNode,
		name: `ParticleDecorationModelParent_${config.code}`,
		position: pos,
		visible: true,
	}

	onCreateThemeObjects && onCreateThemeObjects(pBoxParams);

	const pBox = new Object3D(pBoxParams);

	const particleParams = {
		id: `ParticleDecorationModel_${config.code}`,
		type: 'ParticleSystem',
		name: `ParticleDecorationModel_${config.code}`,
		data: data,
		parent: pBox,
		localPosition: [0, config.content.offsetHeight + config.content.height / 2, 0],
		angle: 0,
		visible: true,
	}

	onCreateThemeObjects && onCreateThemeObjects(particleParams);

	const particle = new ParticleSystem(particleParams);

	particle.bounding.inheritType = InheritType.Jump;
	particle.userData.cfg = particle.userData.cfg || {};
	particle.userData.cfg.offsetHeight = config.content.offsetHeight + config.content.height / 2;

	return pBox;
}

function beginDefaultValues(node) {
	node.body.style.begin();
	node.body.style.beginDefaultValues();
}

function endDefaultValues(node) {
	node.body.style.endDefaultValues();
	node.body.style.end();
}

function setStyleColorMapping(node, styleConfig, name, tag, _gradTexMap) {
	if (styleConfig.useColorMap !== undefined) {
		if (styleConfig.useColorMap) {        // colorMapping
			const colorMapTex = _gradTexMap.get(name).get(tag);
			node.body.style.colorMapping = colorMapTex;
		}
	}
	else {
		if (styleConfig.colorMapping && styleConfig.colorMapping.enable) {
			const colorMapTex = _gradTexMap.get(name).get(tag);

			node.body.style.colorMapping = colorMapTex;

			node.body.style.setAttribute('ColorMappingIntensity', styleConfig.colorMapping.intensity);
		}
		else {
			node.body.style.colorMapping = null;
		}
	}
}

function setStyleColorImage(node, styleConfig, name, tag, _colorTexMap) {
	if (styleConfig.colorImage && styleConfig.colorImage.enable) {
		const colorImageTex = _colorTexMap.get(name).get(tag);
		node.body.style.map = colorImageTex;
	}
	else {
		node.body.style.map = null;
	}
}

function setStyleColor(node, styleConfig) {
	if (styleConfig.color.enable !== true) {
		node.body.style.color = null;
		return;
	}
	node.body.style.color = styleConfig.color.value;
}

function setStyleRemoveDiffuseMap(node, styleConfig) {
	if (!styleConfig.removeDiffuseMap) {
		node.body.style.map = null;
		return;
	}
	node.body.style.map = _emptyTex;
}

function setStyleRemoveAoMap(node, styleConfig) {
	if (!styleConfig.removeAoMap) {
		node.body.style.aoMap = null;
		return;
	}
	node.body.style.aoMap = _emptyTex;
}

function setStyleRemoveNormalMap(node, styleConfig) {
	if (!styleConfig.removeNormalMap) {
		node.body.style.normalMap = null;
		return;
	}
	node.body.style.normalMap = _emptyTex;
}

function setStyleOpacity(node, styleConfig) {
	node.body.style.transparent = Utils.parseBoolean(styleConfig.transparent, null);

	// currently fix use number
	// body.style.opacity = function(baseValue) {
	// 	return baseValue * styleConfig.opacity;
	// }
	if (styleConfig.opacity < 1) {            // if need transparent
		node.body.style.transparent = true;
	}

	node.body.style.opacity = Utils.parseNumber(styleConfig.opacity, null);
}

function setStyleWireFrame(node, styleConfig) {
	node.body.style.wireframe = Utils.parseBoolean(styleConfig.wireframe, null);
}

function setStyleEnvMap(node, styleConfig, name, _envTexMap, compatible) {
	node.body.style.setAttribute('EnvMapIntensity', Utils.parseNumber(styleConfig.envMapIntensity, 1));
	switch (styleConfig.useEnvMapType) {
		case 'global':
			node.body.style.envMap = null;
			node.body.style.envMapping = true;
			break;
		case 'styleFiction':
			const envMap = _envTexMap.get(name);

			node.body.style.envMapping = true;
			node.body.style.envMap = envMap;
			if (compatible) {
				node.body.style.setAttribute('EnvMapIntensity', 1.5);
			}
			break;
		case 'none':
			node.body.style.envMap = null;
			node.body.style.envMapping = false;
			break;
		default:
			node.body.style.envMap = null;
			node.body.style.envMapping = null;
			break;
	}
}

function setStyleEdge(node, styleConfig) {
	if (!styleConfig.edge.enable) {
		node.body.style.edge.enable = false;
		node.body.style.edge.color = [1, 1, 1];
		node.body.style.edge.glow = false;
		node.body.style.edge.opacity = 1;
		return;
	}

	node.body.style.edge.enable = styleConfig.edge.enable;
	node.body.style.edge.color = styleConfig.edge.color;
	node.body.style.edge.glow = styleConfig.edge.glow;
	node.body.style.edge.opacity = styleConfig.edge.opacity;
}

function setStyleEffect(node, styleConfig) {
	if (!styleConfig.effect) {
		node.body.style.effect.glow = null;
		node.body.style.effect.innerGlow = null;
		node.body.style.effect.lineBloom = null;
		node.body.style.effect.tailing = null;
		node.body.style.effect.radial = null;
		node.body.style.effect.ghosting = null;
		return;
	}

	node.body.style.effect.glow = styleConfig.effect.glow !== undefined ? styleConfig.effect.glow : null;
	node.body.style.effect.innerGlow = styleConfig.effect.innerGlow !== undefined ? styleConfig.effect.innerGlow : null;
	node.body.style.effect.lineBloom = styleConfig.effect.lineBloom !== undefined ? styleConfig.effect.lineBloom : null;
	node.body.style.effect.tailing = styleConfig.effect.tailing !== undefined ? styleConfig.effect.tailing : null;
	node.body.style.effect.radial = styleConfig.effect.radial !== undefined ? styleConfig.effect.radial : null;
	node.body.style.effect.ghosting = styleConfig.effect.ghosting !== undefined ? styleConfig.effect.ghosting : null;
}

function setStyleReflection(node, styleConfig) {
	if (!styleConfig.reflection.enable) {
		node.body.style.metalness = null;
		node.body.style.roughness = null;
		return;
	}
	// const metalness = styleConfig.reflection.metalness;
	// const roughness = styleConfig.reflection.roughness;
	// const useType = styleConfig.reflection.useType;		//  use for call back, now can't

	node.body.style.metalness = styleConfig.reflection.metalness;
	node.body.style.roughness = styleConfig.reflection.roughness;
}

function setStyleScrollTex(node, styleConfig, name, tag, _scrollMap, _scrollTexMap) {
	if (styleConfig.useScrollTex !== undefined) {
		if (!styleConfig.useScrollTex) {
			if (styleConfig.colorImage && styleConfig.colorImage.enable) {
				node.body.style.emissive = [0, 0, 0];
			}

			node.off('update', 'updateScrollTexUv');

			// scroll
			node.body.style.emissiveMap = null;
			node.body.style.emissive = null;
			node.body.style.setAttribute('VerticalEmissive', false);
			node.body.style.imageSlotType = THING.ImageSlotType.EmissiveMap; // fix will change diffuseMap uv

			node.body.style.uv.offset = [0, 0];

			return
		}

		const texture = styleConfig.scrollTex;
		const color = styleConfig.scrollColor;

		const scrollTex = _scrollTexMap.get(name).get(texture);

		node.body.style.imageSlotType = ImageSlotType.EmissiveMap;
		node.body.style.setAttribute('VerticalEmissive', true);
		node.body.style.emissiveMap = scrollTex;
		node.body.style.emissive = changeColorArray(color);

		node.on('update', () => {
			if (_scrollMap.has(name)) {
				const scrollMap = _scrollMap.get(name);
				if (scrollMap.has(tag)) {
					node.body.style.uv.offset = scrollMap.get(tag).arr;
				}
			}
		}, 'updateScrollTexUv');
	}
	else {
		if (!styleConfig.scroll.enable) {
			if (styleConfig.colorImage && styleConfig.colorImage.enable) {
				node.body.style.emissive = [0, 0, 0];
			}

			node.off('update', 'updateScrollTexUv');

			// scroll
			node.body.style.emissiveMap = null;
			node.body.style.emissive = null;
			node.body.style.setAttribute('VerticalEmissive', false);
			node.body.style.imageSlotType = THING.ImageSlotType.EmissiveMap; // fix will change diffuseMap uv

			node.body.style.uv.offset = [0, 0];

			return
		}

		const { texture, color } = styleConfig.scroll;

		const scrollTex = _scrollTexMap.get(name).get(texture);

		node.body.style.imageSlotType = ImageSlotType.EmissiveMap;
		node.body.style.setAttribute('VerticalEmissive', true);
		node.body.style.emissiveMap = scrollTex;
		node.body.style.emissive = color;

		node.on('update', () => {
			if (_scrollMap.has(name)) {
				const scrollMap = _scrollMap.get(name);
				if (scrollMap.has(tag)) {
					node.body.style.uv.offset = scrollMap.get(tag).arr;
				}
			}
		}, 'updateScrollTexUv')
	}
}

function setStyleSpecularFactor(node, styleConfig) {
	if (styleConfig.specularFactor == undefined) {
		node.body.style.setAttribute('SpecularFactor', 1);
		return;
	}

	const specularFactor = styleConfig.specularFactor;

	node.body.style.setAttribute('SpecularFactor', specularFactor)
}

function setStyleFresnel(node, styleConfig) {
	if (!styleConfig.fresnel || !styleConfig.fresnel.power) {
		node.body.style.setAttribute('FresnelPower', 0);
		node.body.style.setAttribute('FresnelInverse', false);
		return;
	}

	const fresnelPower = styleConfig.fresnel.power;
	const fresnelInverse = styleConfig.fresnel.inverse;

	node.body.style.setAttribute('FresnelPower', fresnelPower);
	node.body.style.setAttribute('FresnelInverse', fresnelInverse);
}

function setStyleEmissive(node, styleConfig) {
	if (!styleConfig.emissive || !styleConfig.emissive.enable) return;		// if scrollTex is disenabled whether need clear emissive?

	const emissive = styleConfig.emissive.color;

	node.body.style.emissive = emissive;
}

function setStyleLightingGroup(node, styleConfig) {
	const lightGroupMask = styleConfig.lightGroupMask;
	if (!Utils.isValid(lightGroupMask)) {
		node.body.style.setAttribute('LightingGroup', 0);
		return;
	}

	node.body.style.setAttribute('LightingGroup', lightGroupMask);
}

function _initStyle(node) {
	node.internalData._themeTag = null; // clear style tag

	const style = node.body.style;
	if (!style) {
		return;
	}

	node.off('update', 'updateScrollTexUv');

	style.begin();
	style.beginDefaultValues();

	style.color = null;
	style.colorMapping = null;

	// envMap
	style.envMap = null;
	style.envMapping = null;

	// scroll
	style.emissiveMap = null;
	style.emissive = null;
	style.setAttribute('VerticalEmissive', false);
	style.imageSlotType = THING.ImageSlotType.EmissiveMap; // fix will change diffuseMap uv

	style.uv.offset = [0, 0];

	// edge
	style.edge.enable = false;
	style.edge.color = [1, 1, 1];
	style.edge.glow = false;
	style.edge.opacity = 1;

	style.map = null;
	style.normalMap = null;
	style.metalness = null;
	style.roughness = null;

	style.aoMap = null;

	style.wireframe = null;
	style.transparent = null;
	style.opacity = null;

	style.setAttribute('SpecularFactor', 1);
	style.setAttribute('FresnelPower', 0);
	style.setAttribute('FresnelInverse', false);
	style.setAttribute('EnvMapIntensity', 1);
	style.setAttribute('LightingGroup', 0);

	style.effect.glow = null;
	style.effect.innerGlow = null;
	style.effect.lineBloom = null;
	style.effect.tailing = null;
	style.effect.radial = null;
	style.effect.ghosting = null;

	style.endDefaultValues();
	style.end();
}

function updateScroll(app, scrollMap, name) {
	const map = scrollMap.get(name);
	if (map) {
		map.forEach((value, key) => {
			if (value.speed == 0) {
				value.arr[1] = value.offset;
			}
			else {
				app.on('update', () => {        // will caught loading the same theme bundle invalidates the previous one
					value.arr[1] -= value.speed;
				}, name + key + '_scroll');
				// app.on('update', () => {			// may cause update method not closed
				// 	scroll.arr[1] -= scroll.speed;
				// });
			}
		})
	}
}

function generateColorTex(strategy) {								// @compatible
	let map = new Map();
	for (let key in strategy) {
		if (strategy[key].colorImage !== undefined) {
			const color = strategy[key].colorImage.color;

			if (!map.has(key)) {
				map.set(key, createColorImage(color));
			}
		}
	}
	return map;
}

function createColorImage(color) {
	const colorArray = Utils.parseColor(color);
	const ui8 = new Uint8Array([
		255 * colorArray[0], 255 * colorArray[1], 255 * colorArray[2], 255, 255 * colorArray[0], 255 * colorArray[1], 255 * colorArray[2], 255,
		255 * colorArray[0], 255 * colorArray[1], 255 * colorArray[2], 255, 255 * colorArray[0], 255 * colorArray[1], 255 * colorArray[2], 255,
		255, 255, 255, 255,
	]);
	const image = new ImageTexture({ data: ui8, width: 2, height: 2 });

	return image;
}

function generateGradientTextures(strategy) {							// @compatible
	let map = new Map();
	for (let key in strategy) {
		if (strategy[key].useColorMap !== undefined) {					// @compatible colorMapping
			let grad = strategy[key].colorMapping;
			if (grad) {
				if (!map.has(key)) {
					map.set(key, createGradientTexture(grad));
				}
			}
		}
		else {
			let grad = strategy[key].colorMapping.gradient;
			if (grad) {
				if (!map.has(key)) {
					map.set(key, createGradientTexture(grad));
				}
			}
		}
	}
	return map;
}

function createGradientTexture(gradient) {
	const imageData = MathUtils.getGradientPixelData(gradient, 128);

	const texture = new ImageTexture({
		data: imageData,
		width: 128,
		height: 1,
		generateMipmaps: false,
		flipY: false,
		anisotropy: 1,
		wrapTypeS: ImageWrapType.ClampToEdge,		// 1.0's config
		wrapTypeT: ImageWrapType.ClampToEdge
	});
	texture.magFilterType = texture.minFilterType = ImageFilterType.LinearFilter;

	return texture;
}

function generateScroll(strategy) {								// @compatible
	let map = new Map();
	for (let key in strategy) {
		if (strategy[key].useScrollTex !== undefined) {			// @compatible scroll
			const enable = strategy[key].useScrollTex;
			if (enable) {
				let speed = strategy[key].scrollSpeed;
				let offset = strategy[key].scrollOffset;
				map.set(key, { arr: [0, 0], speed, offset });
			}
		}
		else {
			const enable = strategy[key].scroll.enable;
			if (enable) {
				let speed = strategy[key].scroll.speed;
				let offset = strategy[key].scroll.offset;
				map.set(key, { arr: [0, 0], speed, offset });
			}
		}
	}
	return map;
}

function generateScrollTex(strategy, baseUrl) {					// @ compatible
	let map = new Map();

	for (let key in strategy) {
		if (strategy[key].useScrollTex !== undefined) {			// @ compatible
			const enable = strategy[key].useScrollTex;
			const url = strategy[key].scrollTex;
			if (enable) {
				if (!map.has(url)) {
					map.set(url, createScrollTex(url, baseUrl));
				}
			}
		}
		else {
			const enable = strategy[key].scroll.enable;
			const url = strategy[key].scroll.texture;
			if (enable) {
				if (!map.has(url)) {
					map.set(url, createScrollTex(url, baseUrl));
				}
			}
		}
	}

	return map;
}

function createScrollTex(url, baseUrl) {
	if (!url) return null;

	let clonePath = url;
	if (isLocalResource(url)) {
		clonePath = baseUrl._appendPath(url);
	}

	const texture = new ImageTexture({ url: clonePath });
	texture.wrapType = 'Repeat';
	texture.premultiplyAlpha = true;

	return texture;
}

function generateEnvTex(app, environment, baseUrl) {					// @compatible
	if (!environment) return;

	if (environment.path !== undefined) {							// @compatible envTex
		const path = environment.path;

		if (environment.name == 'none' || !path) {
			return;
		}

		let texture;

		if (Array.isArray(path)) {
			const clonepath = path.slice(0);

			if (isLocalResource(clonepath[0])) {
				for (let i = 0; i < clonepath.length; i++) {
					clonepath[i] = baseUrl._appendPath(clonepath[i]);
				}
			}

			texture = app.loadCubeTexture(clonepath); 		// Array url
		}
		else {
			let clonePath = path;
			if (isLocalResource(path)) {
				clonePath = baseUrl._appendPath(path);
			}

			texture = app.loadImageTexture(clonePath, { mappingType: ImageMappingType.EquirectangularReflection });
		}
		return texture;
	}
	else {
		const { type, value } = environment;

		let envTex;

		switch (type) {
			case "cube":
				const path = value.slice(0);
				if (isLocalResource(path[0])) {
					for (let i = 0; i < path.length; i++) {
						path[i] = baseUrl._appendPath(path[i]);
					}
				}
				envTex = app.loadCubeTexture(path); 		// Array url
				break;
			case "image":
				let url = value;
				if (isLocalResource(url)) {
					url = baseUrl._appendPath(url);
				}
				envTex = app.loadImageTexture(url, { mappingType: ImageMappingType.EquirectangularReflection });
				break;
			case "none":
			default:
				break;
		}
		return envTex;
	}
}

function stopUpdateScroll(app, name, _scrollMap) {
	const map = _scrollMap.get(name);
	if (map) {
		map.forEach((value, key) => {
			if (value.speed !== 0) {
				app.off('update', name + key + '_scroll');
			}
		})
	}
}

// Get gradient color
function getBetwColors(startNumber, startColor, endNumber, endColor) {
	const colors = [];

	const leftColor = Utils.parseColor(startColor);
	const rightColor = Utils.parseColor(endColor);
	let lRed = leftColor[0] * 255, lGreen = leftColor[1] * 255, lBlue = leftColor[2] * 255;
	let rRed = rightColor[0] * 255, rGreen = rightColor[1] * 255, rBlue = rightColor[2] * 255;
	let alpha = 255;

	const maxRed = Math.abs(lRed - rRed),
		maxGreen = Math.abs(lGreen - rGreen),
		maxBlue = Math.abs(lBlue - rBlue);

	const colorLevel = parseInt(Math.abs(startNumber - endNumber) * _pixel);
	let level = colorLevel;

	while (level--) {
		lRed <= rRed ? lRed += maxRed / colorLevel : lRed -= maxRed / colorLevel;
		lGreen <= rGreen ? lGreen += maxGreen / colorLevel : lGreen -= maxGreen / colorLevel;
		lBlue <= rBlue ? lBlue += maxBlue / colorLevel : lBlue -= maxBlue / colorLevel;

		colors.push(lRed); colors.push(lGreen); colors.push(lBlue); colors.push(alpha);
	}

	return colors;
}

function isLocalResource(str) {
	return str.indexOf('http') == -1 ? true : false;
}

function getEnvTexUrl(envURL, baseUrl) {
	let url = [];
	for (let i = 0; i < envURL.length; i++) {
		url[i] = baseUrl._appendPath(envURL[i]);
	}
	return url;
}

function changeColorArray(arr) {
	const intArr = [];
	arr.forEach(arg => {
		intArr.push(arg / 255);
	})
	return intArr;
}

function convertParticle(data) {
	const systemOptions = {};
	systemOptions["version"] = "2.0.1";
	systemOptions["groups"] = [];
	for (let i = 0; i < data.listGroups.length; i++) {
		const group = data.listGroups[i];
		const groupOptions = _getGroupOptions(group);
		systemOptions["groups"].push(groupOptions);
	}
	return systemOptions;
}

function _getGroupOptions(group) {
	const groupOptions = {
		texture: {
			animation: {
				frames: [
					group.texture.vec2Frames.x,
					group.texture.vec2Frames.y
				],
				frameCount: group.texture.iFrameCount,
				loop: group.texture.iLoop
			},
			url: group.texture.url,
		},

		fixedTimeStep: group.fFixedTimeStep,
		meshParams: group.arrayMeshParams,
		hasPerspective: group.strHasPerspective,
		isColorize: group.strIsColorize,
		blendingMode: group.iBlendingMode,
		transparent: group.strIsTransparent,
		alphaTest: group.fAlphaTest,
		depthWrite: group.strIsDepthWrite,
		depthTest: group.strIsDepthTest,
		fog: group.strIsFog,
		scale: group.fScale,
		maxParticleCount: group.iMaxParticleCount,
		useMesh: group.strUseMesh,
		meshType: group.strMeshUrl,
		emitters: [],
	};
	for (let i = 0; i < group.listEmitters.length; i++) {
		const emitter = group.listEmitters[i];
		const emitterOptions = _getEmitterOptions(emitter);
		groupOptions["emitters"].push(emitterOptions);
	}
	return groupOptions;
}

function _getEmitterOptions(emitter) {
	const clrArray = [];
	for (let i = 0, len = emitter.listColor.length; i < len; i++) {
		const colorOption = {
			randomise: emitter.listColor[i].strIsRandomise,
			value: [
				emitter.listColor[i].vec3Value.x,
				emitter.listColor[i].vec3Value.y,
				emitter.listColor[i].vec3Value.z,
			],
			spread: [
				emitter.listColor[i].vec3Spread.x,
				emitter.listColor[i].vec3Spread.y,
				emitter.listColor[i].vec3Spread.z,
			],
		};
		clrArray.push(colorOption);
	}

	const opacityArray = [];
	for (let i = 0, len = emitter.listOpacity.length; i < len; i++) {
		const opacityOption = {
			randomise: emitter.listOpacity[i].strIsRandomise,
			value: emitter.listOpacity[i].fValue,
			spread: emitter.listOpacity[i].fSpread,
		};
		opacityArray.push(opacityOption);
	}

	const sizeArray = [];
	for (let i = 0, len = emitter.listSize.length; i < len; i++) {
		const sizeOption = {
			randomise: emitter.listSize[i].strIsRandomise,
			value: emitter.listSize[i].fValue,
			spread: emitter.listSize[i].fSpread,
		};
		sizeArray.push(sizeOption);
	}

	const angleArray = [];
	for (let i = 0, len = emitter.listAngle.length; i < len; i++) {
		const angleOption = {
			randomise: emitter.listAngle[i].strIsRandomise,
			value: emitter.listAngle[i].fValue,
			spread: emitter.listAngle[i].fSpread,
		};
		angleArray.push(angleOption);
	}

	const emitterOptions = {
		distribution: emitter.iDistribution,
		particleCount: emitter.iParticleCount,
		duration: emitter.fDuration,
		isStatic: emitter.strIsStatic,
		isLookAtCamera: emitter.strIsLookAtCamera,
		isLookAtCameraOnlyY: emitter.strIsLookAtCameraOnlyY,
		activeMultiplier: emitter.fActiveMultiplier,
		direction: emitter.iDirection,
		maxAge: {
			value: emitter.maxAge.fValue,
			spread: emitter.maxAge.fSpread,
		},
		position: {
			value: [
				emitter.position.vec3Value.x,
				emitter.position.vec3Value.y,
				emitter.position.vec3Value.z,
			],
			spread: [
				emitter.position.vec3Spread.x,
				emitter.position.vec3Spread.y,
				emitter.position.vec3Spread.z,
			],
			spreadClamp: [
				emitter.position.vec3SpreadClamp.x,
				emitter.position.vec3SpreadClamp.y,
				emitter.position.vec3SpreadClamp.z,
			],
			radius: emitter.position.fRadius,
			radiusScale: [
				emitter.position.vec3RadiusScale.x,
				emitter.position.vec3RadiusScale.y,
				emitter.position.vec3RadiusScale.z,
			],
			distribution: emitter.position.iDistribution,
			randomise: emitter.position.strIsRandomise,
		},
		velocity: {
			value: [
				emitter.velocity.vec3Value.x,
				emitter.velocity.vec3Value.y,
				emitter.velocity.vec3Value.z,
			],
			spread: [
				emitter.velocity.vec3Spread.x,
				emitter.velocity.vec3Spread.y,
				emitter.velocity.vec3Spread.z,
			],
			distribution: emitter.velocity.iDistribution,
			randomise: emitter.velocity.iDistribution,
		},
		acceleration: {
			value: [
				emitter.acceleration.vec3Value.x,
				emitter.acceleration.vec3Value.y,
				emitter.acceleration.vec3Value.z,
			],
			spread: [
				emitter.acceleration.vec3Spread.x,
				emitter.acceleration.vec3Spread.y,
				emitter.acceleration.vec3Spread.z,
			],
			distribution: emitter.acceleration.iDistribution,
			randomise: emitter.acceleration.strIsRandomise,
		},
		drag: {
			randomise: emitter.drag.strIsRandomise,
			value: emitter.drag.fValue,
			spread: emitter.drag.fSpread,
		},
		wiggle: {
			value: emitter.wiggle.fValue,
			spread: emitter.wiggle.fSpread,
		},
		rotation: {
			axis: [
				emitter.rotation.vec3Axis.x,
				emitter.rotation.vec3Axis.y,
				emitter.rotation.vec3Axis.z,
			],
			axisSpread: [
				emitter.rotation.vec3AxisSpread.x,
				emitter.rotation.vec3AxisSpread.y,
				emitter.rotation.vec3AxisSpread.z,
			],
			angle: emitter.rotation.fAngle,
			angleSpread: emitter.rotation.fAngleSpread,
			static: emitter.rotation.strIsStatic,
			center: [
				emitter.rotation.vec3Center.x,
				emitter.rotation.vec3Center.y,
				emitter.rotation.vec3Center.z,
			],
			randomise: emitter.rotation.strIsRandomise,
		},
		colors: clrArray,
		opacities: opacityArray,
		sizes: sizeArray,
		angles: angleArray,
	};
	return emitterOptions;
}

function _getOffset(rootNode, item) {
	const orientedBox = rootNode.orientedBox;
	let orientSize;
	if (rootNode.facadeSize) {
		orientSize = rootNode.facadeSize;
	}
	else {
		orientSize = orientedBox.size;
		rootNode.facadeSize = JSON.parse(JSON.stringify(orientSize));
	}
	const min = MathUtils.min(...orientSize);
	const diff = min * (item.scaleFactor - 1);
	orientSize = MathUtils.divideVector(orientSize, rootNode.scale);
	return { orientSize, diff };
}

function _createBox(rootNode, item, position, size, onCreateThemeObjects) {
	const effectFacades = [];

	const effectFacadeParams = {
		name: 'effectFacade' + item.name,
		localPosition: position,
		width: size[0],
		height: size[1],
		depth: size[2],
		style: {
			color: item.color,
			opacity: item.opacity,
			sideType: SideType.Double
		},
		parent: rootNode,
		// queryable: false
	}

	onCreateThemeObjects && onCreateThemeObjects(effectFacadeParams);

	const box = new Box(effectFacadeParams);
	box.renderOrder = item.renderOrder === undefined ? 100 : item.renderOrder;
	box.pickable = false;
	box.userData['_skipTheme_'] = true;
	box.userData['_themeFacade_'] = true;
	effectFacades.push(box);
	if (item.wireframe.enable) {
		const lines = _createBoxLine(rootNode, item, box);
		effectFacades.push(...lines);
	}
	box.inherit.style = InheritType.Jump;
	box.bounding.inheritType = InheritType.Jump;
	box.level.config.ignoreStyle = true;

	return effectFacades;
}

function _createBoxLine(rootNode, item, box) {
	const lines = [];
	const orientedBox = box.orientedBox;
	const orientedPoints = orientedBox.getPoints();
	const pointsArr = [[0, 1], [1, 2], [2, 3], [3, 0], [4, 5], [5, 6], [6, 7], [7, 4], [0, 4], [1, 5], [2, 6], [3, 7]];
	pointsArr.forEach(arr => {
		const points = [];
		arr.forEach(index => {
			points.push(orientedPoints[index]);
		});
		const line = new PixelLine({
			name: 'facadeLine' + item.name,
			points: points,
			style: {
				color: item.wireframe.color,
				opacity: item.wireframe.opacity,
				sideType: SideType.Double
			},
			parent: rootNode,
			// queryable: false,
			tags: ['helper']  // use this can make this not add in Scene
		});
		line.renderOrder = item.wireframe.renderOrder === undefined ? 99 : item.wireframe.renderOrder;
		line.pickable = false;
		line.userData['_skipTheme_'] = true;
		line.userData['_themeFacade_'] = true;
		line.inherit.style = InheritType.Jump;
		line.bounding.inheritType = InheritType.Jump;
		line.level.config.ignoreStyle = true;
		lines.push(line);
	});
	return lines;
}

function _boxParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const center = rootNode.orientedBox.center;
	const size = [orientSize[0] + diff, orientSize[1] + diff / 2, orientSize[2] + diff];
	const localPos = rootNode.worldToSelf(center);
	const facadeEffects = _createBox(rootNode, item, localPos, size, onCreateThemeObjects);
	return facadeEffects;
}

function _downParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const center = rootNode.orientedBox.center;
	const size = [orientSize[0] + diff, item.thickness / rootNode.scale[1], orientSize[2] + diff];
	const localPos = rootNode.worldToSelf(center);
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const y = item.interval / rootNode.scale[1] * i;
			const position = [localPos[0], localPos[1] + y - orientSize[1] * 0.5, localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
		}
	}
	else {
		let y = 0;
		while (y <= orientSize[1]) {
			const position = [localPos[0], localPos[1] + y - orientSize[1] * 0.5, localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			y += item.interval / rootNode.scale[1];
		}
	}
	return facadeEffects;
}

function _topParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const center = rootNode.orientedBox.center;
	const size = [orientSize[0] + diff, item.thickness / rootNode.scale[1], orientSize[2] + diff];
	const localPos = rootNode.worldToSelf(center);
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const y = orientSize[1] - item.interval / rootNode.scale[1] * i;
			const position = [localPos[0], localPos[1] + y - orientSize[1] * 0.5, localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
		}
	}
	else {
		let y = orientSize[1];
		while (y >= 0) {
			const position = [localPos[0], localPos[1] + y - orientSize[1] * 0.5, localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			y -= item.interval / rootNode.scale[1];
		}
	}
	return facadeEffects;
}

function _leftParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const size = [item.thickness / rootNode.scale[0], orientSize[1] + diff / 2, orientSize[2] + diff];
	const center = rootNode.orientedBox.center;
	const localPos = rootNode.worldToSelf(center);
	let x = -orientSize[0] / 2;
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const position = [localPos[0] + x, localPos[1], localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			x += item.interval / rootNode.scale[0];
		}
	}
	else {
		while (x <= orientSize[0] / 2) {
			const position = [localPos[0] + x, localPos[1], localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			x += item.interval / rootNode.scale[0];
		}
	}
	return facadeEffects;
}

function _rightParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const size = [item.thickness / rootNode.scale[0], orientSize[1] + diff / 2, orientSize[2] + diff];
	const center = rootNode.orientedBox.center;
	const localPos = rootNode.worldToSelf(center);
	let x = orientSize[0] / 2;
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const position = [localPos[0] + x, localPos[1], localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			x -= item.interval / rootNode.scale[0];
		}
	}
	else {
		while (x >= -orientSize[0] / 2) {
			const position = [localPos[0] + x, localPos[1], localPos[2]];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			x -= item.interval / rootNode.scale[0];
		}
	}
	return facadeEffects;
}

function _frontParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const size = [orientSize[0] + diff, orientSize[1] + diff / 2, item.thickness / rootNode.scale[2]];
	const center = rootNode.orientedBox.center;
	const localPos = rootNode.worldToSelf(center);
	let z = orientSize[2] / 2;
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const position = [localPos[0], localPos[1], localPos[2] + z];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			z -= item.interval / rootNode.scale[2];
		}
	}
	else {
		while (z >= -orientSize[2] / 2) {
			const position = [localPos[0], localPos[1], localPos[2] + z];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			z -= item.interval / rootNode.scale[2];
		}
	}
	return facadeEffects;
}

function _backParam(rootNode, item, onCreateThemeObjects) {
	const { orientSize, diff } = _getOffset(rootNode, item);
	const size = [orientSize[0] + diff, orientSize[1] + diff / 2, item.thickness / rootNode.scale[2]];
	const center = rootNode.orientedBox.center;
	const localPos = rootNode.worldToSelf(center);
	let z = -orientSize[2] / 2;
	const facadeEffects = [];
	if (item.repeat === 'norepeat') {
		for (let i = 0; i < item.count; i++) {
			const position = [localPos[0], localPos[1], localPos[2] + z];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			z += item.interval / rootNode.scale[2];
		}
	}
	else {
		while (z <= orientSize[2] / 2) {
			const position = [localPos[0], localPos[1], localPos[2] + z];
			facadeEffects.push(..._createBox(rootNode, item, position, size, onCreateThemeObjects));
			z += item.interval / rootNode.scale[2];
		}
	}
	return facadeEffects;
}

function _createEffectFacade(item, rootNode, onCreateThemeObjects) {
	let effectFacade;
	switch (item.type) {
		case 'box':
			effectFacade = _boxParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'top':
			effectFacade = _topParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'down':
			effectFacade = _downParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'left':
			effectFacade = _leftParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'right':
			effectFacade = _rightParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'front':
			effectFacade = _frontParam(rootNode, item, onCreateThemeObjects);
			break;
		case 'back':
			effectFacade = _backParam(rootNode, item, onCreateThemeObjects);
			break;
	}

	return effectFacade;
}