Source: core/Scene.js

import { StringEncoder, Callbacks, ObjectProxy } from '@uino/base-thing';
import { Utils } from '../common/Utils';
import { Object3D } from '../objects/Object3D';
import { RootObject } from '../objects/RootObject';
import { AmbientLight } from '../objects/AmbientLight';
import { DirectionalLight } from '../objects/DirectionalLight';
import { ImageTexture } from '../resources/ImageTexture';
import { ImageMappingType } from '../const';
import { CubeTexture } from '../resources/CubeTexture';

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


const _renderOverlayFuncName = StringEncoder.toText("<@secret renderOverlay>");
const _ClearColorBuffer = StringEncoder.toText("<@secret ClearColorBuffer>");

/**
 * @class Scene
 * The scene object.
 * @memberof THING
 */
class Scene {

	/**
	 * The scene of object(s).
	 * @param {Object} param The initial parameters.
	 */
	constructor(param = {}) {
		this[__.private] = {};
		let _private = this[__.private];

		_private.app = Utils.getCurrentApp();
		_private.visible = true;

		// Root objects
		_private.rootObjects = {};
		_private.scene = null;

		// Lights
		_private.ambientLight = null;
		_private.mainLight = null;

		// Environment
		_private.envMap = null;

		// Render options
		_private.instancedOffset = null;
		_private.renderOptions = null;

		// Camera
		_private.curCameraIndex = -1; // The current logic camera index
		_private.cameras = []; // [0]: main render camera

		// Callbacks
		_private.beforeRenderCallbacks = new Callbacks();
		_private.afterRenderCallbacks = new Callbacks();

		this._initScene(param);
		this._initLight(param);
		this._initRenderOptions();
		this._initEnvMap(param);

		// Enable static mode as default
		this.staticMode = true;
	}

	// #region Private

	_onBeforeRender(deltaTime, camera) {
		let _private = this[__.private];

		_private.beforeRenderCallbacks.invoke(deltaTime, camera);

		_private.app.delegate.onBeforeRender(camera);

		_private.scene.beforeRender(deltaTime);
	}

	_onAfterRender(deltaTime, camera) {
		let _private = this[__.private];

		_private.scene.afterRender(deltaTime);

		_private.app.delegate.onAfterRender(camera);

		_private.afterRenderCallbacks.invoke(deltaTime, camera);
	}

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

		// Create scene
		_private.scene = param['scene'];

		// The scene root object
		_private.rootObjects['scene'] = new RootObject({
			name: 'scene-root',
			loaded: true,
			queryable: false
		});

		// Create logical root nodes
		let rootNodes = {
			'attachedPoint': Utils.createObject('Node'),
			'debug': Utils.createObject('Node'),
			'bodyBridge': Utils.createObject('Node')
		};

		// Create other logical root objects
		for (let key in rootNodes) {
			let rootName = key + '-root';

			let rootObject = new Object3D({
				rootNode: rootNodes[key],
				name: rootName,
				queryable: false,
				loaded: true,
				castShadow: false,
				receiveShadow: false
			});

			_private.rootObjects[key] = rootObject;
		}

		// Bind root objects
		let root = _private.scene.getRoot();
		for (let key in _private.rootObjects) {
			root.add(_private.rootObjects[key].node);
		}

		// Get overlay root node
		let getOverlayRootFunc = StringEncoder.toText("<@secret getOverlayRoot>");
		let overlayRoot = _private.scene[getOverlayRootFunc]();
		overlayRoot.setVisible(true);

		// Create overlay root node
		_private.overlayRootNode = Utils.createObject('Node');
		_private.overlayRootNode.setName(StringEncoder.toText("<@secret OverlayRoot>"));
		overlayRoot.add(_private.overlayRootNode);

		// Create overlay logo root node
		_private.overlayLogoRootNode = Utils.createObject('Node');
		_private.overlayLogoRootNode.setName(StringEncoder.toText("<@secret OverlayLogoRoot>"));
		overlayRoot.add(_private.overlayLogoRootNode);

		// Update accessors for internal usage
		_private.accessors = {};
		_private.accessors[StringEncoder.toText("<@secret overlayRootNode>")] = _private.overlayRootNode;
		_private.accessors[StringEncoder.toText("<@secret overlayLogoRootNode>")] = _private.overlayLogoRootNode;
	}

	_initLight(param) {
		let initLights = Utils.parseValue(param['initLights'], true);
		if (!initLights) {
			return;
		}

		let _private = this[__.private];

		let root = _private.rootObjects['scene'];

		// Create ambient light
		let ambientLight = new AmbientLight({
			parent: root,
			name: 'mainAmbientLight',
		});

		// Create main light
		let mainLight = new DirectionalLight({
			parent: root,
			name: 'mainDirectionalLight',
			target: [0, 0, 0]
		});

		// Initialize adapater for main light as default
		let adapter = mainLight.adapter;
		if (_private.app.view._compatibleRender) {
			adapter.horzAngle = 30;
		}
		else {
			adapter.horzAngle = 40;
		}
		adapter.vertAngle = 30;
		adapter.bind(root);

		// Create lights finished
		this.ambientLight = ambientLight;
		this.mainLight = mainLight;
	}

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

		_private.renderOptions = new ObjectProxy({
			data: {
				environment: {
					quaternion: null
				},
			},
			onChange: function (ev) {
				if (_private.scene) {
					_private.scene.setRenderOptions(ev.data);
				}
			}
		});
	}

	_initEnvMap(param) {
		const envMap = Utils.parseValue(param['envMap'], null);

		let _private = this[__.private];
		const app = _private.app;

		if (Utils.isArray(envMap)) {
			const imageTexture = new CubeTexture(envMap);
			this.envMap = imageTexture;
		}
		else if (Utils.isString(envMap)) {
			if ((envMap.indexOf('.png') > -1 || envMap.indexOf('.jpg') > -1)) {
				const imageTexture = new ImageTexture({
					url: envMap,
					mappingType: ImageMappingType.EquirectangularReflection
				});
				this.envMap = imageTexture;
			}
			else {
				const urls = Utils.parseCubeTextureUrlsByPath(envMap);
				const imageTexture = new ImageTexture(urls);
				this.envMap = imageTexture;
			}
		}
		else if (param.defaultSettings.envMap) {
			app.view.getDefaultEnvImage().then((envMap) => {
				if (envMap) {
					this.envMap = new CubeTexture({ data: envMap });
				}
			});
		}
	}

	// #endregion

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

		if (_private.overlayRootObject) {
			_private.overlayRootObject.destroy();
			_private.overlayRootObject = null;
		}

		_private.overlayRootNode.dispose();
		_private.overlayLogoRootNode.dispose();
		_private.overlayRootNode = null;
		_private.overlayLogoRootNode = null;
		_private.accessors = null;

		_private.app.objectManager.clearKeepAliveObjects();

		for (let key in _private.rootObjects) {
			_private.rootObjects[key].destroy(true);
		}

		_private.rootObjects = {};

		_private.renderOptions.dispose();
		_private.renderOptions = null;

		_private.scene.dispose();
		_private.scene = null;

		_private.ambientLight = null;
		_private.mainLight = null;

		_private.envMap = null;

		_private.curCameraIndex = -1;
		_private.cameras = [];

		_private.beforeRenderCallbacks.clear();
		_private.afterRenderCallbacks.clear();
	}

	resize(width, height) {
		let _private = this[__.private];

		_private.cameras.forEach(camera => {
			camera.resize(width, height);
		});

		if (_private.overlayRootObject) {
			_private.overlayRootObject.width = width;
			_private.overlayRootObject.height = height;
		}
	}

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

		let cameras = _private.cameras;

		let mainCamera = cameras[0];
		if (!mainCamera) {
			return;
		}

		let mainCameraNode = mainCamera.node;

		// Prepare for rendering
		this._onBeforeRender(deltaTime, mainCameraNode);

		if (_private.visible) {
			// Render main camera
			mainCameraNode.render();

			// Render scene with other cameras
			for (let i = 1, l = cameras.length; i < l; i++) {
				let camera = cameras[i];

				if (!camera.visible) {
					continue;
				}

				// Render to texture
				if (camera.renderTexture) {
					camera.node.render(camera.renderTexture.getTextureResource());
				}
				// Render to viewport
				else if (camera.enableViewport && camera.viewport) {
					camera.node.render();
				}
			}

			// Render overlay
			if (_private.overlayRootNode && _private.overlayRootNode.getVisible()) {
				// We can not clear color buffer due to context use the same color buffer ...
				let clearColorBuffer = mainCameraNode.getAttribute(_ClearColorBuffer);
				mainCameraNode.setAttribute(_ClearColorBuffer, false);
				mainCameraNode[_renderOverlayFuncName]();
				mainCameraNode.setAttribute(_ClearColorBuffer, clearColorBuffer);
			}
		}

		// Finished for rendering
		this._onAfterRender(deltaTime, mainCameraNode);
	}

	addRenderCamera(camera) {
		let _private = this[__.private];
		let cameras = _private.cameras;

		// For other render camera, we are not going to clear color buffer as default
		let cameraNode = camera.node;
		cameraNode.setAttribute(_ClearColorBuffer, false);

		let mainCamera = cameras[_private.curCameraIndex];

		Utils.pushToArray(cameras, camera);

		_private.curCameraIndex = cameras.indexOf(mainCamera);
	}

	removeRenderCamera(camera) {
		let _private = this[__.private];
		let cameras = _private.cameras;

		let mainCamera = cameras[_private.curCameraIndex];

		Utils.removeFromArray(cameras, camera);

		_private.curCameraIndex = cameras.indexOf(mainCamera);
	}

	/**
	 * The function to call when render.
	 * @callback OnRenderCallback
	 * @param {Number} deltaTime The delta time in seconds.
	 * @param {Object} camera The camera node.
	 * @private
	 */

	/**
	 * Add callback before render.
	 * @param {OnRenderCallback} callback The function.
	 * @private
	 */
	addBeforeRenderCallback(callback) {
		this[__.private].beforeRenderCallbacks.add(callback);
	}

	/**
	 * Remove callback before render.
	 * @param {OnRenderCallback} callback The function.
	 * @private
	 */
	removeBeforeRenderCallback(callback) {
		this[__.private].beforeRenderCallbacks.remove(callback);
	}

	/**
	 * Add callback after render.
	 * @param {OnRenderCallback} callback The function.
	 * @private
	 */
	addAfterRenderCallback(callback) {
		this[__.private].afterRenderCallbacks.add(callback);
	}

	/**
	 * Remove callback after render.
	 * @param {OnRenderCallback} callback The function.
	 * @private
	 */
	removeAfterRenderCallback(callback) {
		this[__.private].afterRenderCallbacks.remove(callback);
	}

	/**
	 * Get attribute by type.
	 * @param {String} type The type string.
	 * @returns {*}
	 * @private
	 */
	getAttribute(type) {
		const overlayRootObject = StringEncoder.toText("<@secret OverlayRootObject>");
		const accessors = StringEncoder.toText("<@secret Accessors>");

		let _private = this[__.private];

		switch (type) {
			case overlayRootObject:
				if (!_private.overlayRootObject) {
					let app = _private.app;

					_private.overlayRootObject = new Object3D({
						name: StringEncoder.toText("<@secret overlayRoot>"),
						rootNode: _private.overlayRootNode,
						parent: null,
						queryable: false,
					});

					_private.overlayRootObject.x = 0;
					_private.overlayRootObject.y = 0;
					_private.overlayRootObject.width = app.size[0];
					_private.overlayRootObject.height = app.size[1];
				}

				return _private.overlayRootObject;

			case accessors:
				return _private.accessors;

			default:
				return null;
		}
	}

	// #region Accessor

	/**
	 * Get root.
	 * @type {Object}
	 * @private
	 */
	get root() {
		return this[__.private].scene.getRoot();
	}

	/**
	 * Get root objects.
	 * @type {Object}
	 * @private
	 */
	get rootObjects() {
		return this[__.private].rootObjects;
	}

	/**
	 * Get cameras.
	 * @type {Array<THING.Camera>}
	 * @private
	 */
	get cameras() {
		return this[__.private].cameras;
	}

	/**
	 * Get/Set the logic camera.
	 * @type {THING.Camera}
	 * @private
	 */
	get camera() {
		let _private = this[__.private];

		return _private.cameras[_private.curCameraIndex];
	}
	set camera(value) {
		let _private = this[__.private];

		let index = _private.cameras.indexOf(value);
		if (index === -1) {
			_private.cameras.push(value);

			index = _private.cameras.length - 1;
		}

		_private.curCameraIndex = index;

		// Use camera as render camera
		this.renderCamera = value;

		// Keep it alive
		_private.app.objectManager.addKeepAliveObject(value);
	}

	/**
	 * Get/Set the main render camera.
	 * @type {THING.Camera}
	 * @private
	 */
	get renderCamera() {
		let _private = this[__.private];

		return _private.cameras[0];
	}
	set renderCamera(value) {
		let _private = this[__.private];
		let cameras = _private.cameras;

		let mainCamera = cameras[_private.curCameraIndex];

		// Disable previous main camera render to viewport
		let mainRenderCamera = cameras[0];
		if (mainRenderCamera && mainRenderCamera != value) {
			mainRenderCamera.enableViewport = false;
		}

		let index = cameras.indexOf(value);
		if (index === -1) {
			cameras._insert(0, value);
		}
		else if (index !== 0) {
			cameras._swap(0, index);
		}

		_private.curCameraIndex = cameras.indexOf(mainCamera);

		// Enable render to viewport
		value.enableViewport = true;
	}

	/**
	 * Get/Set the instanced offset(It would effect relative position of all instanced drawing objects).
	 * @type {Array<Number>}
	 * @example
	 * 	app.scene.instancedOffset = [10000, 10000, 10000];
	 * @private
	 */
	get instancedOffset() {
		let _private = this[__.private];

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

		if (value) {
			_private.instancedOffset = value.slice(0);

			this.root.setInstancedOffset(_private.instancedOffset);
		}
		else {
			_private.instancedOffset = null;

			this.root.setInstancedOffset([0, 0, 0]);
		}
	}

	/**
	 * Enable/Disable static mode to improve matrix calculation.
	 * We should disable it when loading resources, then enable it when rendering in preview mode.
	 * @type {Boolean}
	 * @private
	 */
	get staticMode() {
		let _private = this[__.private];

		return _private.scene.isEnabled('TransformCache');
	}
	set staticMode(value) {
		let _private = this[__.private];

		_private.scene.enable('TransformCache', value);
	}

	/**
	 * Get/Set environment map resource.
	 * @type {THING.CubeTexture|THING.ImageTexture}
	 * @private
	 */
	get envMap() {
		let _private = this[__.private];

		return _private.envMap;
	}

	/**
	 * Set environment map resource.
	 * @type {THING.CubeTexture|String}
	 * @private
	 */
	set envMap(value) {
		let _private = this[__.private];
		let scene = _private.scene;

		if (_private.envMap) {
			_private.envMap.release();
		}

		if (value) {
			if (value.isImageTexture || value.isCubeTexture) {
				_private.envMap = value;
				_private.envMap.addRef();

				_private.envMap.waitForComplete().then((image) => {
					scene.setEnvironment(image.getTextureResource());
				});
			}
			// Image url of String type
			else if (Utils.isString(value)) {
				_private.envMap = new ImageTexture(value);
				_private.envMap.mappingType = ImageMappingType.EquirectangularReflection;

				_private.envMap.waitForComplete().then((image) => {
					scene.setEnvironment(image.getTextureResource());
				});
			}
			else {
				_private.envMap = null;
				scene.setEnvironment(null);
			}
		}
		else {
			_private.envMap = null;
			scene.setEnvironment(null);
		}
	}

	/**
	 * Get/Set the environment map light intensity between 0 and 1.
	 * @type {Number}
	 * @public
	 * @example
	 * 	app.scene.envMapLightIntensity = 0.5;
	 * 	// @expect(app.scene.envMapLightIntensity == 0.5);
	 */
	get envMapLightIntensity() {
		let _private = this[__.private];
		return _private.scene.getEnvironmentLightIntensity();
	}
	set envMapLightIntensity(value) {
		let _private = this[__.private];
		_private.scene.setEnvironmentLightIntensity(value);
	}

	/**
	 * Get/Set the ambient light.
	 * @type {THING.AmbientLight}
	 * @example
	 * 	let app = THING.App.current;
	 * 	app.scene.ambientLight.color = 'blue';
	 * 	app.scene.ambientLight.intensity = 0.1;
	 *  let ret1 = app.scene.ambientLight.color == 'blue';
	 *  let ret2 = app.scene.ambientLight.intensity == 0.1;
	 * // @expect(ret1 == true && ret2 == true);
	 */
	get ambientLight() {
		return this[__.private].ambientLight;
	}
	set ambientLight(value) {
		let _private = this[__.private];

		_private.ambientLight = value;

		// Keep it alive to prevent destroy action
		_private.app.objectManager.addKeepAliveObject(value);
	}

	/**
	 * Get/Set the main light.
	 * @type {THING.DirectionalLight}
	 * @private
	 */
	get mainLight() {
		return this[__.private].mainLight;
	}
	set mainLight(value) {
		let _private = this[__.private];

		_private.mainLight = value;

		// Keep it alive to prevent destroy action
		_private.app.objectManager.addKeepAliveObject(value);
	}

	/**
	 * @typedef {Object} SceneRenderEnvironmentOptions
	 * @property {Array<Number>} quaternion The quaternion.
	 * @private
	 */

	/**
	 * @typedef {Object} SceneRenderOptions
	 * @property {SceneRenderEnvironmentOptions} environment The environment.
	 * @private
	 */

	/**
	 * Get/Set render options.
	 * @type {SceneRenderOptions}
	 * @private
	 */
	get renderOptions() {
		let _private = this[__.private];

		return _private.renderOptions.dataProxy;
	}
	set renderOptions(value) {
		let _private = this[__.private];
		let scene = _private.scene;
		let dataProxy = _private.renderOptions.dataProxy;

		if (value) {
			Utils.mergeObject(dataProxy, value, true);

			scene.setRenderOptions(dataProxy);
		}
		else {
			dataProxy.environment.quaternion = null;

			scene.setRenderOptions(dataProxy);
		}
	}

	/**
	 * Get the current render state.
	 * @type {RenderStateResult}
	 * @private
	 */
	get renderState() {
		let _private = this[__.private];

		return _private.scene.getRenderState();
	}

	/**
	 * Get/Set the output enconding type.
	 * @type {TexelEncodingType}
	 */
	get outputEncodingType() {
		let _private = this[__.private];

		return _private.scene.getOutputEncodingType();
	}
	set outputEncodingType(value) {
		let _private = this[__.private];

		_private.scene.setOutputEncodingType(value);
	}

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

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

		_private.visible = value;
	}

	// #endregion

	get isScene() {
		return true;
	}

}

export { Scene }