import { Utils } from '../common/Utils';

import { MathUtils } from '../math/MathUtils';

import { BaseComponent } from './BaseComponent';

import { Flags } from '@uino/base-thing';

import { ImageWrapType, PickType, InheritType } from '../const';



const __ = {

	private: Symbol('private'),

}



const Flag = {

	CastShadow: 1 << 0,

	ReceiveShadow: 1 << 1

}



// Instanced drawing mode

const InstancedDrawingMode = {

	None: 1,

	Doing: 2,

	Done: 3,

};



const _callbackKeys = [

	'opacity',

	'color',

	'metalness',

	'roughness',

	'emissive'

];



const _defaultOptions = {};



const _alwaysOnTopLayerNumber = 2;



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

const autoHideIfObstructedEventTag = '__autoHideIfObstructedEventTag__';



// #region Private Functions



// Check whether need to create instanced drawing style from style group.

function _canCreateInstancedDrawingStyle(object, styleGroupPool) {

	let style = object.bodyNode.getStyle();



	// If we have already used instanced drawing mode then skip to recreate it again

	if (style.isEnabled('InstancedDrawing')) {

		return false;

	}



	// Check whether it's already using style from group

	if (styleGroupPool.hasStyle(style)) {

		return false;

	}



	return true;

}



// Convert 2d array matrix to 1d array matrix.

function _convertMatrices(matrices) {

	let _matrices = [];



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

		let m = matrices[i];



		for (let j = 0, jl = m.length; j < jl; j++) {

			_matrices.push(m[j]);

		}

	}



	return _matrices;

}



// #endregion



/**
 * RenderComponent - 渲染组件
 * 
 * 控制对象的渲染属性,如使用实例化绘图模式。
 * 
 * @class RenderComponent
 * @memberof THING
 * @extends THING.BaseComponent
 * @public
 */
class RenderComponent extends BaseComponent {



	static mustCopyWithInstance = true;



	/**

	 * 渲染组件类,用于控制对象的渲染属性,如使用实例化绘图模式。

	 */

	constructor() {

		super();



		this[__.private] = {};

		let _private = this[__.private];



		// Render attributes

		_private.renderOrder = 0;

		_private.initRenderLayer = null;

		_private.renderLayer = null;

		_private.flags = new Flags();

		_private.flags.set(Flag.CastShadow | Flag.ReceiveShadow);



		// Instanced drawing - Style

		_private.instancedDrawingMode = InstancedDrawingMode.None;

		_private.instanceGroupName = '';



		// Instanced drawing - Node

		_private.instancingNodeInfo = null;

		_private.instancingNode = null;



		// The callbacks

		_private.callbacks = null;



		_private.imageTexture = null;

		_private.modelInfo = null;



		_private.autoHideIfObstructed = false;

		_private.autoHideIfObstructedIntervalTime = 1;

		_private.autoHideIfObstructedTime = 0;

		_private.autoHideIfObstructedDepthEpsilon = 0.000001;



		_private.unloadImageTexture = () => {

			if (_private.imageTexture) {

				_private.imageTexture.release();

				_private.imageTexture = null;

			}

		}

	}



	// #region Private



	_setRenderAttribute(key, funcName, value, recursive) {

		let _private = this[__.private];



		_private[key] = value;



		// Get the object

		let object = this.object;



		// Update body node's render order

		let bodyNode = object.bodyNode;

		if (bodyNode[funcName]) {

			bodyNode[funcName](value);

		}



		// Change all children if needed

		if (recursive) {

			if (object.hasChildren()) {

				let children = object.children;

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

					const child = children[i];



					if (child.hasInherit(key)) {

						const inheritType = child.inherit[key];

						switch (inheritType) {

							case InheritType.Normal:

								child[funcName](value, true);

								break;



							case InheritType.Break:

								child[funcName](value, false);

								break;



							case InheritType.Jump:

								child.children.traverse(object => {

									object[funcName](value, true);

								});

								break;



							default:

								break;

						}

					}

					else {

						child[funcName](value, true);

					}

				}

			}

		}

	}



	_canMakeInstancedDrawing() {

		let _private = this[__.private];



		// Get the object

		let object = this.object;



		// Let other module to determine whether to process instanced drawing

		if (_private.callbacks) {

			let beforeMakeInstancedDrawingFunc = _private.callbacks.beforeMakeInstancedDrawing;

			if (beforeMakeInstancedDrawingFunc && beforeMakeInstancedDrawingFunc(object) === false) {

				return false;

			}

		}



		// Make sure instancing(node) is disable

		if (_private.instancingNodeInfo) {

			return false;

		}



		// Make sure it has instance group name or resource url

		if (!object.instanceGroupName && !object.resource.url) {

			return false;

		}



		// Make sure style is existing

		let style = object.body.style;

		if (!style) {

			return false;

		}



		// If some attribute use callback function then we can not enable instanced drawing

		for (let i = 0; i < _callbackKeys.length; i++) {

			let func = style[_callbackKeys[i]];



			// If it's function without returning constant value then skip to make instanced drawing

			if (Utils.isFunction(func) && !func.isConstantValue) {

				return false;

			}

		}



		// Make sure all animations are stopped

		// This component name (“_internalAnimation”) is a constant string

		if (object.hasComponent('_internalAnimation')) {

			if (object.animation.hasInited()) {

				if (!object.animation.isAllAnimationsReady()) {

					return false;

				}

			}

		}



		return true;

	}



	_makeInstancedDrawing(style, styleGroupPool, renderMode) {

		let object = this.object;

		let instancedDrawing = renderMode == 'InstancedRendering' ? true : false;



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

		if (!instancedDrawingStyle) {

			return;

		}



		let _private = this[__.private];



		style.initResource(instancedDrawingStyle);



		style.makeCurrentAsBaseValue();



		_private.instancedDrawingMode = InstancedDrawingMode.Done;

	}



	_tryMakeInstancedDrawing(style, renderMode) {

		let _private = this[__.private];



		// If it can not make instanced drawing

		if (!this._canMakeInstancedDrawing()) {

			// Enable instanced drawing failed, resume the state

			_private.instancedDrawingMode = InstancedDrawingMode.None;



			return false;

		}



		let object = this.object;

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



		// Check whether need to re-create style from group

		if (_canCreateInstancedDrawingStyle(object, styleGroupPool)) {

			// Wait copy operation finished first

			if (_private.copyPromise) {

				_private.copyPromise.then(() => {

					this._makeInstancedDrawing(style, styleGroupPool, renderMode);

				});

			}

			else {

				this._makeInstancedDrawing(style, styleGroupPool, renderMode);

			}

		}

		_private.instancedDrawingMode = InstancedDrawingMode.Done;



		return true;

	}



	async _autoHideIfObstructed() {

		const _private = this[__.private];

		const object = this.object;

		const app = this.app;

		const camera = app.camera;



		if (object.destroyed) {

			return;

		}



		if (_private.autoHideIfObstructedTime < _private.autoHideIfObstructedIntervalTime) {

			_private.autoHideIfObstructedTime += app.deltaTime;

			return;

		}



		await camera.waitForGPUUpdate();



		_private.autoHideIfObstructedTime = 0;



		if (camera.isChanging) {

			return;

		}



		const checkPosition = object.position;

		const screen = camera.worldToScreen(checkPosition);



		const oldPickType = camera.pickType;

		camera.pickType = PickType.GPUFast;

		const pickResult = camera.pick(screen[0], screen[1]);

		camera.pickType = oldPickType;



		let desiredVisible = true;

		if (pickResult) {

			// If the picked object is self or its descendant, it must be visible

			if (pickResult.object === object || pickResult.object.isChildOf(object)) {

				desiredVisible = true;

			}

			else {

				const pickDepth = camera.worldToScreen(pickResult.position)[2];

				const occluded = pickDepth + 1 + _private.autoHideIfObstructedDepthEpsilon < screen[2] + 1;

				desiredVisible = !occluded;

			}

		}

		else {

			console.log('gg')

		}



		object.visible = desiredVisible;

	}



	// #endregion



	// #region Common



	getRenderOrder() {

		let _private = this[__.private];



		return _private.renderOrder;

	}



	setRenderOrder(value, recursive) {

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

	}



	getCastShadow() {

		let _private = this[__.private];



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

	}



	setCastShadow(value, recursive) {

		let _private = this[__.private];



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



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

	}



	getReceiveShadow() {

		let _private = this[__.private];



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

	}



	setReceiveShadow(value, recursive) {

		let _private = this[__.private];



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



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

	}



	syncAttributes() {

		let bodyNode = this.object.bodyNode;

		if (bodyNode.isRenderableNode) {

			let _private = this[__.private];



			if (bodyNode.getCastShadow() != this.castShadow) {

				bodyNode.setCastShadow(this.castShadow);

			}



			if (bodyNode.getReceiveShadow() != this.receiveShadow) {

				bodyNode.setReceiveShadow(this.receiveShadow);

			}



			if (bodyNode.getRenderOrder() != _private.renderOrder) {

				bodyNode.setRenderOrder(_private.renderOrder);

			}



			let renderLayer = bodyNode.getRenderLayer();

			_private.initRenderLayer = renderLayer;



			if ((_private.renderLayer != null) && (renderLayer != _private.renderLayer)) {

				bodyNode.setRenderLayer(_private.renderLayer);

			}

		}

	}



	getGeometryInfo() {

		return this.object.node.getGeometryInfo();

	}



	get callbacks() {

		let _private = this[__.private];



		_private.callbacks = _private.callbacks || {

			beforeMakeInstancedDrawing: null

		};



		return _private.callbacks;

	}



	get renderOrder() {

		return this.getRenderOrder();

	}

	set renderOrder(value) {

		this.setRenderOrder(value, true);

	}



	get castShadow() {

		return this.getCastShadow();

	}

	set castShadow(value) {

		this.setCastShadow(value, true);

	}



	get receiveShadow() {

		return this.getReceiveShadow();

	}

	set receiveShadow(value) {

		this.setReceiveShadow(value, true);

	}



	get alwaysOnTop() {

		let _private = this[__.private];



		return _private.renderLayer == _alwaysOnTopLayerNumber;

	}

	set alwaysOnTop(value) {

		let _private = this[__.private];



		let bodyNode = this.object.bodyNode;

		if (!bodyNode.isRenderableNode) {

			return;

		}



		let alwaysOnTop = this.alwaysOnTop;

		if (value == alwaysOnTop) {

			return;

		}



		if (value) {

			_private.renderLayer = _alwaysOnTopLayerNumber;

			bodyNode.setRenderLayer(_alwaysOnTopLayerNumber);

		}

		else {

			_private.renderLayer = null;

			bodyNode.setRenderLayer(_private.initRenderLayer);

		}

	}



	// #endregion



	// #region Instanced Drawing - Style



	makeInstancedDrawing(value = true, options = _defaultOptions) {

		let _private = this[__.private];



		if (_private.instancedDrawingMode == InstancedDrawingMode.Doing) {

			return false;

		}



		// Get the object

		let object = this.object;



		// Get the current values of style, in order to update base values later

		let style = object.body.style;

		if (!style) {

			return false;

		}



		// If style instance equal value, return

		if (style.isInstancedDrawing == value) {

			_private.instancedDrawingMode = value ? InstancedDrawingMode.Done : InstancedDrawingMode.None;



			return true;

		}



		if (value) {

			// Get the render mode

			let renderMode = Utils.parseValue(options['renderMode'], 'InstancedRendering');



			if (object.loaded) {

				if (!this._tryMakeInstancedDrawing(style, renderMode)) {

					return false;

				}

			}

			else {

				// Wait for complete

				object.waitForComplete().then(

					// OK

					() => {

						this._tryMakeInstancedDrawing(style, renderMode);

					},

					// Error

					() => {

						// Enable instanced drawing mode failed due to object had been destroyed

						_private.instancedDrawingMode = InstancedDrawingMode.None;

					}

				);



				_private.instancedDrawingMode = InstancedDrawingMode.Doing;

			}

		}

		else {

			// Get the body node

			let bodyNode = object.bodyNode;



			// Disable instanced drawing

			_private.instancedDrawingMode = InstancedDrawingMode.None;



			// Create style without enable instanced drawing

			let resource = Utils.createObject('Style', { scene: object.app.scene.context, view: object.app.view });

			resource.copy(bodyNode.getStyle());

			resource.enable('InstancedDrawing', false);



			// Update style base values

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



			style.initResource(resource);

			style.updateBaseValues(values);

		}



		return true;

	}



	get isInstancedDrawing() {

		let _private = this[__.private];



		return _private.instancedDrawingMode != InstancedDrawingMode.None;

	}



	get instanceGroupName() {

		return this[__.private].instanceGroupName;

	}

	set instanceGroupName(value) {

		this[__.private].instanceGroupName = value;

	}



	// #endregion



	// #region Instanced Drawing - Node



	enableInstancing(options) {

		return new Promise((resolve, reject) => {

			let _private = this[__.private];



			if (!_private.instancingNodeInfo) {

				// Make sure object had been loaded

				this.object.waitForComplete().then(() => {

					// We can not use style instanced drawing at the same time

					if (this.isInstancedDrawing) {

						resolve(false);

						return false;

					}



					let instancingNode = this.object.bodyNode.getAttribute('Instancing');

					if (!instancingNode) {

						resolve(false);

						return false;

					}



					_private.instancingNode = instancingNode;



					_private.instancingNodeInfo = {

						maxNumber: 0,

						number: 0,

						matrices: [],

						pickedIds: []

					};



					if (options) {

						if (!this.setInstancing(options)) {

							resolve(false);

							return false;

						}

					}



					resolve(true);

				});

			}

		});

	}



	disableInstancing() {

		let _private = this[__.private];



		if (!_private.instancingNodeInfo) {

			return;

		}



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



		_private.instancingNodeInfo = null;

		_private.instancingNode = null;

	}



	setInstancing(options) {

		let _private = this[__.private];



		let instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return false;

		}



		let maxNumber = Utils.parseValue(options['maxNumber'], instancingNodeInfo.maxNumber);

		let number = Utils.parseValue(options['number'], instancingNodeInfo.number);

		let matrices = Utils.parseArray(options['matrices'], instancingNodeInfo.matrices);

		let pickedIds = Utils.parseArray(options['pickedIds'], instancingNodeInfo.pickedIds);



		if (number > maxNumber) {

			return false;

		}



		if (matrices.length != maxNumber) {

			return false;

		}



		instancingNodeInfo.maxNumber = maxNumber;

		instancingNodeInfo.number = number;

		instancingNodeInfo.matrices = matrices;

		instancingNodeInfo.pickedIds = pickedIds;



		let instancingNode = _private.instancingNode;



		instancingNode.begin();

		instancingNode.setMaxCount(instancingNodeInfo.maxNumber);

		instancingNode.setCount(instancingNodeInfo.number);

		instancingNode.setMatrices(_convertMatrices(instancingNodeInfo.matrices));



		if (pickedIds.length === maxNumber) {

			instancingNode.setPickedIds(instancingNodeInfo.pickedIds);

		}



		if (options['colors']) {

			let colors = Utils.parseArray(options['colors']);

			instancingNodeInfo.colors = colors;

			instancingNode.setColors(_convertMatrices(instancingNodeInfo.colors));

		}

		instancingNode.end();



		return true;

	}



	getInstancing() {

		let _private = this[__.private];



		return _private.instancingNodeInfo;

	}



	setInstancingColor(index, colors) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return false;

		}



		const instancingNode = _private.instancingNode;

		instancingNode.setColor(index, colors);



		return true;

	}



	getInstancingColor(index, target = []) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return null;

		}



		return _private.instancingNode.getColor(index, target);

	}



	setInstancingVisible(index, visible) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return false;

		}



		const instancingNode = _private.instancingNode;

		instancingNode.setVisible(index, visible);



		return true;

	}



	getInstancingVisible(index) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return null;

		}



		return _private.instancingNode.getVisible(index);

	}



	getInstancedCount() {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return 0;

		}



		return _private.instancingNode.getCount();

	}



	setInstancingMatrix(index, matrix) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return false;

		}



		const instancingNode = _private.instancingNode;

		instancingNode.setMatrix(index, matrix);



		return true;

	}



	getInstancingMatrix(index, target = []) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return null;

		}



		return _private.instancingNode.getMatrix(index, target);

	}



	setInstancingClearOutColor(index, value) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return;

		}



		_private.instancingNode.setClearOutColor(index, value);

	}



	getInstancingClearOutColor(index) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return;

		}



		return _private.instancingNode.getClearOutColor(index);

	}



	setInstancingDiffuseUVTransform(index, diffuseUVTransform) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return;

		}



		_private.instancingNode.setDiffuseUVTransform(index, diffuseUVTransform);

	}



	getInstancingDiffuseUVTransform(index, target) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return;

		}



		return _private.instancingNode.getDiffuseUVTransform(index, target);

	}



	setInstancingAttribute(name, value) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return;

		}



		_private.instancingNode.setAttribute(name, value);

	}



	getInstancingAttribute(name) {

		const _private = this[__.private];

		const instancingNodeInfo = _private.instancingNodeInfo;

		if (!instancingNodeInfo) {

			return null;

		}



		return _private.instancingNode.getAttribute(name);

	}



	// #endregion



	load(options, resolve, reject) {

		let object = this.object;

		if (object.destroyed) {

			reject(`The object had been destroyed, skip to load resources`);

			return;

		}



		let _private = this[__.private];



		let modelOptions = {

			lights: true

		};

		let objResource = object.resource;



		if (objResource.nodeName) {

			modelOptions.rootNode = object.body.getRootNode()

			modelOptions.nodeName = objResource.nodeName;

		}



		if (objResource.excludeNodeNames) {

			modelOptions.excludeNodeNames = objResource.excludeNodeNames;

		}



		if (objResource.inverseRotationMode) {

			modelOptions.inverseRotationMode = objResource.inverseRotationMode;

		}



		if (objResource.instanceStyle) {

			for (const key in objResource.instanceStyle) {

				modelOptions[key] = objResource.instanceStyle[key];

			}

		}



		const mapData = modelOptions.map;

		if (mapData) {

			_private.imageTexture = this.app.resourceManager.getTextureManager().load(mapData.url, _defaultTextureParam, {

				flipY: mapData.flipY

			});

			modelOptions['image'] = _private.imageTexture.getTextureResource();

			delete modelOptions['texture'];

		}



		const envapData = modelOptions.envMap;

		if (envapData) {

			modelOptions['envMap'] = envapData.getTextureResource();

		}



		if (objResource.instanceId) {

			modelOptions['id'] = objResource.instanceId;

			modelOptions['useInstance'] = true;

		}



		if (objResource.instanceCount) {

			modelOptions['instanceCount'] = objResource.instanceCount;

		}



		if (options.indexData) {

			modelOptions.indexData = options.indexData;

		}



		if (options.blobMap) {

			modelOptions.blobMap = options.blobMap;

		}



		const onBeforeSetup = modelOptions.onBeforeSetup;



		object.app.resourceManager.loadObjectResource(object, modelOptions, {

			onBeforeSetup: (ev) => {

				onBeforeSetup && onBeforeSetup(ev);

			},

			onSetup: (ev) => {

				let info = ev.info;



				_private.modelInfo = info;

			},

			onLoad: () => {

				if (_private.imageTexture) {

					_private.imageTexture.waitForComplete().then(resolve, reject);

				}

				else {

					resolve();

				}

			},

			onError: (ev) => {

				reject(ev);

			}

		});

	}



	onBeforeRemove() {

		let _private = this[__.private];

		_private.unloadImageTexture();

	}



	onUnloadResource() {

		let _private = this[__.private];

		_private.unloadImageTexture();

	}



	set autoHideIfObstructed(value) {

		const _private = this[__.private];

		_private.autoHideIfObstructed = value;



		if (value) {

			this.object.on('update', () => {

				this._autoHideIfObstructed();

			}, autoHideIfObstructedEventTag);

		}

		else {

			this.object.off('update', autoHideIfObstructedEventTag);

		}

	}

	get autoHideIfObstructed() {

		return this[__.private].autoHideIfObstructed;

	}



	get modelInfo() {

		let _private = this[__.private];

		return _private.modelInfo;

	}



}



export { RenderComponent }