import { Utils, BaseTickableObject3D, MathUtils } from '@uino/thing';

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

/**
 * @class InstancedLOD
 * @summary 实例化 LOD(Level of Detail)对象,基于空间分割树(四叉树/八叉树)管理大量实例化物体的细节层次。
 * 通过注册不同 LOD 级别的模型资源,结合空间分割算法自动管理大量实例的显示与隐藏,
 * 根据摄像机距离动态切换细节级别,在保证视觉质量的同时大幅减少渲染开销。
 * 支持注册多个 LOD 资源、批量添加实例节点、缓存探测结果,
 * 适用于大规模植被、城市建筑群、粒子系统等需要大量相同/相似对象高效渲染的场景。
 * @public
 * @extends THING.BaseTickableObject3D
 * @memberof THING
 * @example
 * // 创建 InstancedLOD 对象
 * const lodSystem = new THING.EXTEND.InstancedLOD({
 *   boundary: {
 *     min: [-100, -10, -100],
 *     max: [100, 50, 100]
 *   },
 *   treeType: 'quadtree',
 *   maxDepth: 6
 * });
 * 
 * // 注册 LOD 资源(高、中、低三种精度)
 * lodSystem.registerResources('tree', {
 *   urls: [
 *     './models/tree_lod0.glb',
 *     './models/tree_lod1.glb',
 *     './models/tree_lod2.glb'
 *   ],
 *   distances: [50, 100, 200],
 *   maxCount: 1000,
 *   radius: 2
 * }).then(function() {
 *   // 批量添加实例
 *   lodSystem.addNodes('tree', {
 *     position: [xyz coordinates],
 *     angle: [ angles array],
 *     scale: [ scale array ]
 *   });
 * });
 */
class InstancedLOD extends BaseTickableObject3D {

	static DefaultPickId = 500000;

	/**
	 * constructor.
	 * @param {object} param
	 * @param {THING.Box3} param.boundary the bounding box of all LODs
	 * @param {string} param.treeType  the type of the space partition tree, either 'octree' or 'quadtree'
	 * @param {number} param.maxDepth the maximum depth of the space partition tree
	 * @param {boolean} param.strictHide whether to strictly hide LODs when update
	 * @param {number} param.distance the distance from the camera at which the LODs are shown
	 */
	constructor(param = {}) {
		super(param);
	}

	onSetupAttributes(param) {
		super.onSetupAttributes(param);
		this[__.private] = {};
		const _private = this[__.private];

		const external = param['extras'] || param['external'];
		if (external) {
			param.treeType = external.treeType;
			param.boundary = external.boundary;
			param.maxDepth = external.maxDepth;
			param.strictHide = external.strictHide;
			param.distance = external.distance;
		}

		_private.treeType = param.treeType || 'quadtree';
		const boundary = param.boundary || {};
		_private.boundary = {
			min: boundary.min || [-500, -100, -500],
			max: boundary.max || [500, 100, 500]
		};
		_private.maxDepth = param.maxDepth || 6;
	}

	onLoadResource(options, resolve, reject) {
		let dynamic = Utils.parseValue(options['dynamic'], false);
		if (!dynamic) {
			const _private = this[__.private];
			options.defaultPickId = InstancedLOD.DefaultPickId;
			const node = Utils.createObject('LODNode', { external: { uScene: this.node._uScene, param: options }});
			this.body.setNode(node);

			_private.resources = new Map();
			_private.camera = this.app.camera;

			const external = options['extras'] || options['external'];
			if (!external) {
				super.onLoadResource(options, resolve, reject);
			}
			else {
				const resources = external.resources;
				if (!resources) { resolve(); }
				const nodes = external.nodes;
				if (!nodes) { resolve(); }

				for (let i = 0, l = nodes.length; i < l; i++) {
					const count = nodes[i].position.length / 3;
					InstancedLOD.DefaultPickId += count;
				}

				const registers = resources.map(resource => {
					return this.registerResources(resource.name, resource);
				})

				const registersPromise = Promise.all(registers);
				registersPromise
					.then(() => {
						for (let i = 0, l = nodes.length; i < l; i++) {
							const node = nodes[i];
							const index = node.resource;
							const name = resources[index].name;

							this.addNodes(name, node);
						}

						resolve();
					})
					.catch((error) => {
						reject(error);
					});
			}
		}
		else {
			resolve();
		}
	}

	/**
	 * Registers an instanced resource.
	 * @param {string} name the name of the resource
	 * @param {object} options the resource
	 * @param {Array<string>} options.urls urls to model resources
	 * @param {Array<number>} options.distances the distance on which the classification levels are based
	 * @param {number} options.maxCount the maximum number of instances
	 * @param {number} options.radius the radius of LOD node
	 * @returns {Promise}
	 */
	registerResources(name, options) {
		const _private = this[__.private];
		if (_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has been registered.');
			return;
		}

		const app = this.app;
		const loadTask = options.urls.map(url => {
			const modelUrl = Utils.resolveURL(url)
			const load = app.resourceManager.loadModelAsync(modelUrl);
			return load;
		})

		const promise = Promise.all(loadTask);
		promise.then((result) => {
			this.bodyNode.registerResources(options, result);
			options.nodes = {
				position: [],
				angle: [],
				scale: [],
				scaleFactor: options.scaleFactor || 1
			};
			options.ids = [];
			_private.resources.set(name, options);
		});

		return promise;
	}

	/**
	 * Unregisters an instanced resource.
	 * @param {string} name the name of the resource
	 */
	unRegisterResource(name) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}

		const resources = _private.resources.get(name);
		resources.urls.forEach(url => {
			this.bodyNode.unRegisterResource(url);
		})
		_private.resources.delete(name);
	}

	/**
	 * Add an instanced LOD node.
	 * @param {string} name the resource name has been registered
	 * @param {object} data the data
	 * @param {Array<number>} data.position the position of node
	 * @param {Array<number>} data.angle the angle of node
	 * @param {Array<number>} data.scale the scale of node
	 * @return {number} the node id
	 */
	addNode(name, data) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}
		const worldMatrix = MathUtils.createMat4(data.position, data.angle, data.scale);
		const resourcesData = _private.resources.get(name);
		const radius = data.radius ? data.radius : resourcesData.radius;
		resourcesData.nodes.position.push(data.position);
		resourcesData.nodes.angle.push(data.angle);
		THING.MathUtils.vec3.scale(data.scale, data.scale, scaleFactor)
		resourcesData.nodes.scale.push(data.scale);

		const id = this.bodyNode.createInstancedLODNode({
			resources: resourcesData.urls,
			distances: resourcesData.distances,
			worldMatrix,
			radius
		})

		resourcesData.ids.push(id);
		return id;
	}

	/**
	 * destroy an instanced LOD node.
	 * @param {number} id the node id
	 */
	destroyNode(id) {
		this.bodyNode.destroyInstancedLODNode(id);
	}

	/**
	 * Add instanced LOD nodes.
	 * @param {string} name the resource name has been registered
	 * @param {object} data the data
	 * @param {Array<Array>} data.position the position array of nodes
	 * @param {Array<Array>} data.angle the angle array of nodes
	 * @param {Array<Array>} data.scale the scale array of nodes
	 * @return {Array<number>} the nodes id array
	 */
	addNodes(name, data) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}

		const resourcesData = _private.resources.get(name);
		let position = data.position || [];
		let angle = data.angle || [];
		let scale = data.scale || [];
		let scaleFactor = resourcesData.nodes.scaleFactor;

		if (!(position.length === angle.length && angle.length === scale.length)) {
			console.warn('The position angle and scale array lengths are not equal, and the shortest length will be used for calculation.')

			const length = MathUtils.min(position.length, angle.length, scale.length);
			position.length = length;
			angle.length = length;
			scale.length = length;
		}

		const maxCount = resourcesData.maxCount * 3;
		if (position.length > maxCount) {
			console.warn('The maximum value supported by this resource is ' + maxCount);

			position.length = maxCount;
			angle.length = maxCount;
			scale.length = maxCount;
		}

		resourcesData.nodes.position = position;
		resourcesData.nodes.angle = angle;
		resourcesData.nodes.scale = scale;

		const worldMatrices = [], radiuses = [];
		for (let i = 0, l = position.length; i < l; i += 3) {
			const vec3_position = [position[i], position[i + 1], position[i + 2]];
			const vec3_angle = [angle[i], angle[i + 1], angle[i + 2]];
			const vec3_scale = [scale[i] * scaleFactor, scale[i + 1] * scaleFactor, scale[i + 2] * scaleFactor];
			const worldMatrix = MathUtils.createMat4(vec3_position, vec3_angle, vec3_scale);
			worldMatrices.push(worldMatrix);

			const radius = data.radius ? data.radius[i] : resourcesData.radius;
			radiuses.push(radius);
		}

		const ids = this.bodyNode.createInstancedLODNodes({
			resources: resourcesData.urls,
			distances: resourcesData.distances,
			worldMatrices,
			radiuses
		});

		resourcesData.ids = ids;

		return ids;
	}

	destroyResourcesNodes(name) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}

		const resourcesData = _private.resources.get(name);
		this.bodyNode.destroyResourcesNodes(resourcesData.urls, resourcesData.ids);
		resourcesData.nodes.position.length = 0;
		resourcesData.nodes.angle.length = 0;
		resourcesData.nodes.scale.length = 0;
		resourcesData.nodes.scaleFactor = 1;
		resourcesData.ids.length = 0;
	}

	/**
	 * Set the camera on which to update the state of the LODs.
	 * @param {THING.Camera} camera the camera
	 */
	setCamera(camera) {
		const _private = this[__.private];
		if (!camera.isCamera) {
			console.warn('InstancedLOD: The setted is not a camera.');
			return;
		}
		_private.camera = camera;
	}

	onUpdate(deltaTime) {
		if (this.bodyNode) {
			const _private = this[__.private];
			this.bodyNode.update(_private.camera.node);
		}
	}

	onExportExternalData() {
		let external = Object.assign({}, super.onExportExternalData());
		const _private = this[__.private];

		external.boundary = _private.boundary;
		external.treeType = _private.treeType;
		external.maxDepth = _private.maxDepth;
		external.strictHide = this.strictHide;
		external.distance = this.distance;
		const resources = [];
		const nodes = [];

		const resourcesMap = _private.resources;
		for (let [key, value] of resourcesMap) {
			const index = resources.length;
			const nodeOptions = Object.assign({ resource: index }, value.nodes);
			delete nodeOptions.scaleFactor;
			nodes.push(nodeOptions);

			const resourceOption = Object.assign({ name: key }, value);
			resourceOption.scaleFactor = resourceOption.nodes.scaleFactor;
			delete resourceOption.nodes;
			delete resourceOption.ids;
			resources.push(resourceOption);
		}

		external.resources = resources;
		external.nodes = nodes;

		return external;
	}

	/**
	 * 获取/设置距离。
	 * @type {number}
	 * @public
	 */
	set distance(distance) {
		this.bodyNode.setDistance(distance);
	}
	get distance() {
		return this.bodyNode.getDistance();
	}

	/**
	 * 获取/设置严格隐藏。
	 * @type {boolean}
	 * @public
	 */
	set strictHide(strictHide) {
		this.bodyNode.setStrictHide(strictHide);
	}
	get strictHide() {
		return this.bodyNode.getStrictHide();
	}

	getScale(name) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}

		const resourcesData = _private.resources.get(name);
		if (resourcesData.node.scaleFactor) {
			return resourcesData.node.scaleFactor;
		}

		return 1;
	}

	setScale(name, value) {
		const _private = this[__.private];
		if (!_private.resources.has(name)) {
			console.warn('InstancedLOD: Resource has not been registered.');
			return;
		}

		const resourcesData = _private.resources.get(name);
		this.bodyNode.destroyResourcesNodes(resourcesData.urls, resourcesData.ids);
		resourcesData.nodes.scaleFactor = value;
		resourcesData.ids.length = 0;

		this.addNodes(name, resourcesData.nodes);
	}

	/**
	 * 获取资源。
	 * @type {Map}
	 * @public
	 */
	get resources() {
		const _private = this[__.private];
		return _private.resources;
	}

	/**
	 * 检查是否为 InstancedLOD 类型或继承自它。
	 * @type {boolean}
	 * @public
	 */
	get isInstancedLOD() {
		return true
	}

}

export { InstancedLOD };