
import * as THREE from "../../../../libs/three.js/build/three.module.js";
import {PointAttribute, PointAttributes, PointAttributeTypes} from "../../../loader/PointAttributes.js";
import {OctreeGeometry, OctreeGeometryNode} from "./OctreeGeometry.js";

// let loadedNodes = new Set();

export class NodeLoader{

	constructor(url){
		this.url = url;
	}

	async load(node){

		if(node.loaded || node.loading){
			return;
		}

		node.loading = true;
		Potree.numNodesLoading++;

		// console.log(node.name, node.numPoints);

		// if(loadedNodes.has(node.name)){
		// 	// debugger;
		// }
		// loadedNodes.add(node.name);

		try{
			if(node.nodeType === 2){
				await this.loadHierarchy(node);
			}

			let {byteOffset, byteSize} = node;


			let urlOctree = `${this.url}/../octree.bin`;

			let first = byteOffset;
			let last = byteOffset + byteSize - 1n;

			let buffer;

			if(byteSize === 0n){
				buffer = new ArrayBuffer(0);
				console.warn(`loaded node with 0 bytes: ${node.name}`);
			}else{
				let response = await fetch(urlOctree, {
					headers: {
						'content-type': 'multipart/byteranges',
						'Range': `bytes=${first}-${last}`,
					},
				});

				buffer = await response.arrayBuffer();
			}

			let workerPath;
			if(this.metadata.encoding === "BROTLI"){
				workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker_brotli.js';
			}else{
				workerPath = Potree.scriptPath + '/workers/2.0/DecoderWorker.js';
			}

			let worker = Potree.workerPool.getWorker(workerPath);

			worker.onmessage = function (e) {

				let data = e.data;
				let buffers = data.attributeBuffers;

				Potree.workerPool.returnWorker(workerPath, worker);

				let geometry = new THREE.BufferGeometry();
				
				for(let property in buffers){

					let buffer = buffers[property].buffer;

					if(property === "position"){
						geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(buffer), 3));
					}else if(property === "rgba"){
						geometry.setAttribute('rgba', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true));
					}else if(property === "NORMAL"){
						//geometry.setAttribute('rgba', new THREE.BufferAttribute(new Uint8Array(buffer), 4, true));
						geometry.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(buffer), 3));
					}else if (property === "INDICES") {
						let bufferAttribute = new THREE.BufferAttribute(new Uint8Array(buffer), 4);
						bufferAttribute.normalized = true;
						geometry.setAttribute('indices', bufferAttribute);
					}else{
						const bufferAttribute = new THREE.BufferAttribute(new Float32Array(buffer), 1);

						let batchAttribute = buffers[property].attribute;
						bufferAttribute.potree = {
							offset: buffers[property].offset,
							scale: buffers[property].scale,
							preciseBuffer: buffers[property].preciseBuffer,
							range: batchAttribute.range,
						};

						geometry.setAttribute(property, bufferAttribute);
					}

				}
				// indices ??

				node.density = data.density;
				node.geometry = geometry;
				node.loaded = true;
				node.loading = false;
				Potree.numNodesLoading--;
			};

			let pointAttributes = node.octreeGeometry.pointAttributes;
			let scale = node.octreeGeometry.scale;

			let box = node.boundingBox;
			let min = node.octreeGeometry.offset.clone().add(box.min);
			let size = box.max.clone().sub(box.min);
			let max = min.clone().add(size);
			let numPoints = node.numPoints;

			let offset = node.octreeGeometry.loader.offset;

			let message = {
				name: node.name,
				buffer: buffer,
				pointAttributes: pointAttributes,
				scale: scale,
				min: min,
				max: max,
				size: size,
				offset: offset,
				numPoints: numPoints
			};

			worker.postMessage(message, [message.buffer]);
		}catch(e){
			node.loaded = false;
			node.loading = false;
			Potree.numNodesLoading--;

			console.log(`failed to load ${node.name}`);
			console.log(e);
			console.log(`trying again!`);
		}
	}

	parseHierarchy(node, buffer){

		let view = new DataView(buffer);
		let tStart = performance.now();

		let bytesPerNode = 22;
		let numNodes = buffer.byteLength / bytesPerNode;

		let octree = node.octreeGeometry;
		// let nodes = [node];
		let nodes = new Array(numNodes);
		nodes[0] = node;
		let nodePos = 1;

		for(let i = 0; i < numNodes; i++){
			let current = nodes[i];

			let type = view.getUint8(i * bytesPerNode + 0);
			let childMask = view.getUint8(i * bytesPerNode + 1);
			let numPoints = view.getUint32(i * bytesPerNode + 2, true);
			let byteOffset = view.getBigInt64(i * bytesPerNode + 6, true);
			let byteSize = view.getBigInt64(i * bytesPerNode + 14, true);

			// if(byteSize === 0n){
			// 	// debugger;
			// }


			if(current.nodeType === 2){
				// replace proxy with real node
				current.byteOffset = byteOffset;
				current.byteSize = byteSize;
				current.numPoints = numPoints;
			}else if(type === 2){
				// load proxy
				current.hierarchyByteOffset = byteOffset;
				current.hierarchyByteSize = byteSize;
				current.numPoints = numPoints;
			}else{
				// load real node 
				current.byteOffset = byteOffset;
				current.byteSize = byteSize;
				current.numPoints = numPoints;
			}
			
			current.nodeType = type;

			if(current.nodeType === 2){
				continue;
			}

			for(let childIndex = 0; childIndex < 8; childIndex++){
				let childExists = ((1 << childIndex) & childMask) !== 0;

				if(!childExists){
					continue;
				}

				let childName = current.name + childIndex;

				let childAABB = createChildAABB(current.boundingBox, childIndex);
				let child = new OctreeGeometryNode(childName, octree, childAABB);
				child.name = childName;
				child.spacing = current.spacing / 2;
				child.level = current.level + 1;

				current.children[childIndex] = child;
				child.parent = current;

				// nodes.push(child);
				nodes[nodePos] = child;
				nodePos++;
			}

			// if((i % 500) === 0){
			// 	yield;
			// }
		}

		let duration = (performance.now() - tStart);

		// if(duration > 20){
		// 	let msg = `duration: ${duration}ms, numNodes: ${numNodes}`;
		// 	console.log(msg);
		// }
	}

	async loadHierarchy(node){

		let {hierarchyByteOffset, hierarchyByteSize} = node;
		let hierarchyPath = `${this.url}/../hierarchy.bin`;
		
		let first = hierarchyByteOffset;
		let last = first + hierarchyByteSize - 1n;

		let response = await fetch(hierarchyPath, {
			headers: {
				'content-type': 'multipart/byteranges',
				'Range': `bytes=${first}-${last}`,
			},
		});



		let buffer = await response.arrayBuffer();

		this.parseHierarchy(node, buffer);

		// let promise = new Promise((resolve) => {
		// 	let generator = this.parseHierarchy(node, buffer);

		// 	let repeatUntilDone = () => {
		// 		let result = generator.next();

		// 		if(result.done){
		// 			resolve();
		// 		}else{
		// 			requestAnimationFrame(repeatUntilDone);
		// 		}
		// 	};
			
		// 	repeatUntilDone();
		// });

		// await promise;

		



	}

}

let tmpVec3 = new THREE.Vector3();
function createChildAABB(aabb, index){
	let min = aabb.min.clone();
	let max = aabb.max.clone();
	let size = tmpVec3.subVectors(max, min);

	if ((index & 0b0001) > 0) {
		min.z += size.z / 2;
	} else {
		max.z -= size.z / 2;
	}

	if ((index & 0b0010) > 0) {
		min.y += size.y / 2;
	} else {
		max.y -= size.y / 2;
	}
	
	if ((index & 0b0100) > 0) {
		min.x += size.x / 2;
	} else {
		max.x -= size.x / 2;
	}

	return new THREE.Box3(min, max);
}

let typenameTypeattributeMap = {
	"double": PointAttributeTypes.DATA_TYPE_DOUBLE,
	"float": PointAttributeTypes.DATA_TYPE_FLOAT,
	"int8": PointAttributeTypes.DATA_TYPE_INT8,
	"uint8": PointAttributeTypes.DATA_TYPE_UINT8,
	"int16": PointAttributeTypes.DATA_TYPE_INT16,
	"uint16": PointAttributeTypes.DATA_TYPE_UINT16,
	"int32": PointAttributeTypes.DATA_TYPE_INT32,
	"uint32": PointAttributeTypes.DATA_TYPE_UINT32,
	"int64": PointAttributeTypes.DATA_TYPE_INT64,
	"uint64": PointAttributeTypes.DATA_TYPE_UINT64,
}

export class OctreeLoader{

	static parseAttributes(jsonAttributes){

		let attributes = new PointAttributes();

		let replacements = {
			"rgb": "rgba",
		};

		for (const jsonAttribute of jsonAttributes) {
			let {name, description, size, numElements, elementSize, min, max} = jsonAttribute;

			let type = typenameTypeattributeMap[jsonAttribute.type];

			let potreeAttributeName = replacements[name] ? replacements[name] : name;

			let attribute = new PointAttribute(potreeAttributeName, type, numElements);

			if(numElements === 1){
				attribute.range = [min[0], max[0]];
			}else{
				attribute.range = [min, max];
			}

			if (name === "gps-time") { // HACK: Guard against bad gpsTime range in metadata, see potree/potree#909
				if (attribute.range[0] === attribute.range[1]) {
					attribute.range[1] += 1;
				}
			}

			attribute.initialRange = attribute.range;

			attributes.add(attribute);
		}

		{
			// check if it has normals
			let hasNormals = 
				attributes.attributes.find(a => a.name === "NormalX") !== undefined &&
				attributes.attributes.find(a => a.name === "NormalY") !== undefined &&
				attributes.attributes.find(a => a.name === "NormalZ") !== undefined;

			if(hasNormals){
				let vector = {
					name: "NORMAL",
					attributes: ["NormalX", "NormalY", "NormalZ"],
				};
				attributes.addVector(vector);
			}
		}

		return attributes;
	}

	static async load(url){

		let response = await fetch(url);
		let metadata = await response.json();

		let attributes = OctreeLoader.parseAttributes(metadata.attributes);

		let loader = new NodeLoader(url);
		loader.metadata = metadata;
		loader.attributes = attributes;
		loader.scale = metadata.scale;
		loader.offset = metadata.offset;

		let octree = new OctreeGeometry();
		octree.url = url;
		octree.spacing = metadata.spacing;
		octree.scale = metadata.scale;

		// let aPosition = metadata.attributes.find(a => a.name === "position");
		// octree

		let min = new THREE.Vector3(...metadata.boundingBox.min);
		let max = new THREE.Vector3(...metadata.boundingBox.max);
		let boundingBox = new THREE.Box3(min, max);

		let offset = min.clone();
		boundingBox.min.sub(offset);
		boundingBox.max.sub(offset);

		octree.projection = metadata.projection;
		octree.boundingBox = boundingBox;
		octree.tightBoundingBox = boundingBox.clone();
		octree.boundingSphere = boundingBox.getBoundingSphere(new THREE.Sphere());
		octree.tightBoundingSphere = boundingBox.getBoundingSphere(new THREE.Sphere());
		octree.offset = offset;
		octree.pointAttributes = OctreeLoader.parseAttributes(metadata.attributes);
		octree.loader = loader;

		let root = new OctreeGeometryNode("r", octree, boundingBox);
		root.level = 0;
		root.nodeType = 2;
		root.hierarchyByteOffset = 0n;
		root.hierarchyByteSize = BigInt(metadata.hierarchy.firstChunkSize);
		root.hasChildren = false;
		root.spacing = octree.spacing;
		root.byteOffset = 0;

		octree.root = root;

		loader.load(root);

		let result = {
			geometry: octree,
		};

		return result;

	}

};