import {PointCloudTreeNode} from "../PointCloudTree.js";
import {PointAttributes, PointAttribute, PointAttributeTypes} from "../loader/PointAttributes.js";
import {EventDispatcher} from "../EventDispatcher.js";

export class PointCloudZappchaGeometryNode{

	constructor(){
		this.id = PointCloudZappchaGeometryNode.IDCount++;
		this.bytesPerAxis = 4;
		this.bytesPerVertex = 17;
		this.dataLocation = 0;
		this.children = [];
		this.left = null;
		this.right = null;
		this.boundingBox = null;
		this.number = null;
		this.geometry = null;
		this.loaded = false;
		this.numPoints = 0;
		this.originalNumPoints = 0;
		this.filter = 0;
		this.dataSize = 0;
		this.level = 0;
		this.signedURL = null;
		this.version = 0;
		this.oneTimeDisposeHandlers = [];
		this.maxInt32Bits = 0x1000000; // 2^24
	}

	isGeometryNode(){
		return true;
	}

	isTreeNode(){
		return false;
	}

	isLoaded(){
		return this.loaded;
	}

	getBoundingSphere(){
		return this.boundingSphere;
	}

	getChildren(){
		return this.children;
	}

	getBoundingBox(){
		return this.boundingBox;
	}

	getLevel(){
		return this.level;
	}
	
	completeLoad(successful){
		this.loaded = successful;
		this.loading = false;
		Potree.numNodesLoading--;
	}
	
	load(){
		if (Potree.numNodesLoading >= Potree.maxNodesLoading || this.loaded || this.loading) {
			return;
		}
		
		this.loading = true;
		Potree.numNodesLoading++;
		
		let node = this;
		let buffSize = node.numPoints * node.bytesPerVertex;
		if(this.version===18){
			buffSize = node.dataSize;
		}
		
		fetch(node.signedURL,{
			headers: {
				'range': 'bytes='+node.dataLocation+"-"+(node.dataLocation+BigInt(buffSize)),
				'Cache-Control': 'no-cache,no-store',
			},
		}).then(function(response){
			response.arrayBuffer().then((buffer) => {
				const workerPath = Potree.scriptPath + '/workers/ZappchaWorker.js';
				const worker = Potree.workerPool.getWorker(workerPath);
				
				worker.onmessage = (e) => {
					let position = new Float32Array(e.data.position);
					let color = new Uint8Array(e.data.color);
					let intensities = new Float32Array(e.data.intensities);
					let classifications = new Uint8Array(e.data.classifications);
					let indices = e.data.indices;

					node.geometry = new THREE.BufferGeometry();

					node.geometry.setAttribute('position', new THREE.BufferAttribute(position, 3));
					node.geometry.setAttribute('rgba', new THREE.BufferAttribute(color, 4, true));
					node.geometry.setAttribute('intensity', new THREE.BufferAttribute(intensities, 1));
					node.geometry.setAttribute('classification', new THREE.BufferAttribute(classifications, 1));
					node.geometry.setAttribute('indices', new THREE.BufferAttribute(new Uint8Array(indices), 4, true));
					
					Potree.workerPool.returnWorker(workerPath, worker);
					
					node.completeLoad(true);
				}
			
				let message = {
					version : node.version,
					filter : node.filter,
					bytesPerAxis: node.bytesPerAxis,
					bytesPerVertex: node.bytesPerVertex,
					buffer: buffer,
					numPoints: node.numPoints,
					originalNumPoints: node.originalNumPoints,
					minx: node.boundingBox.min.x,
					miny: node.boundingBox.min.y,
					minz: node.boundingBox.min.z,
					maxx: node.boundingBox.max.x,
					maxy: node.boundingBox.max.y,
					maxz: node.boundingBox.max.z
				};
				
				worker.postMessage(message);
			});
		}).catch(function(err){
			console.log("Something went wrong!", err);
			this.completeLoad(false);
		});
	}
	
	dispose(){
		if (this.geometry) {
			this.geometry.dispose();
			this.geometry = null;
			this.loaded = false;

			for (let i = 0; i < this.oneTimeDisposeHandlers.length; i++) {
				let handler = this.oneTimeDisposeHandlers[i];
				handler();
			}
			this.oneTimeDisposeHandlers = [];
		}
	}

	getNumPoints(){
		return this.numPoints;
	}
};


PointCloudZappchaGeometryNode.IDCount = 0;


export class PointCloudZappchaGeometry extends EventDispatcher{

	constructor(){
		super();
		this.numPoints = 0;
		this.version = 0;
		this.boundingBox = null;
		this.numNodes = 0;
		this.name = null;
		this.provider = null;
		this.url = null;
		this.root = null;
		this.levels = 0;
		this._spacing = null;
		this.signedURL = null;
		
		{
			const attributes = new PointAttributes();

			attributes.add(PointAttribute.POSITION_CARTESIAN);
			attributes.add(new PointAttribute("rgba", PointAttributeTypes.DATA_TYPE_UINT8, 4));
			attributes.add(new PointAttribute("intensity", PointAttributeTypes.DATA_TYPE_UINT16, 1));
			attributes.add(new PointAttribute("classification", PointAttributeTypes.DATA_TYPE_UINT8, 1));

			this.pointAttributes = attributes;
		}
	}
	
	static async load(key, callback) {
		let signedURL = key;
		let response = await fetch(signedURL, {
			headers: {
				'Range': 'bytes=0-123',
				'Cache-Control':'no-cache,no-store',
			},
		});
		
		let buffer = await response.arrayBuffer();

		try {
			let sourceView = new DataView(buffer);
			const geometry = new PointCloudZappchaGeometry();
			geometry.key = key;
			geometry.provider = "Zappcha";
			geometry.numNodes = sourceView.getInt32(8,true);
			geometry.numPoints = sourceView.getBigInt64(12,true);
			geometry.version =sourceView.getInt32(4,true);
			geometry.signedURL = signedURL;
			let minX = sourceView.getFloat64(20,true);
			let minY = sourceView.getFloat64(28,true);
			let minZ = sourceView.getFloat64(36,true);
			let maxX = sourceView.getFloat64(44,true);
			let maxY = sourceView.getFloat64(52,true);
			let maxZ = sourceView.getFloat64(60,true);
			geometry.boundingBox = new THREE.Box3(
				new THREE.Vector3(minX,minY,minZ),
				new THREE.Vector3(maxX,maxY,maxZ)
			);
			
			let offset = geometry.boundingBox.min.clone().multiplyScalar(-1);

			geometry.boundingBox.min.add(offset);
			geometry.boundingBox.max.add(offset);
			geometry.offset = offset;
			
			console.log(geometry.boundingBox);

			let center = geometry.boundingBox.getCenter(new THREE.Vector3());
			let radius = geometry.boundingBox.getSize(new THREE.Vector3()).length() / 2;
			geometry.boundingSphere = new THREE.Sphere(center, radius);
			
			console.log(minX+" "+minY+" "+minZ);
			
			geometry.loadHierarchy(key);
			callback(geometry);
		
		} catch (e) {
			console.error(e.message);
			callback(null);
		}

	}
	
	
	async loadHierarchy(key){
		let vpc = this;
		
		let nodeSize = 14;
		if(vpc.version===18)
			nodeSize = 21;
		
		var end = 124+(nodeSize * this.numNodes);
		let response = await fetch(vpc.signedURL, {
			headers: {
				'Range': 'bytes=124-'+end,
				'Cache-Control':'no-cache,no-store',
			},
		});
		let buffer = await response.arrayBuffer();
		let view = new DataView(buffer);
		
		let stack = [];
		let root = null;

		let levels = 0;

		// TODO Debug: let start = new Date().getTime();
		// read hierarchy
		for (let i = 0; i < vpc.numNodes; i++) {
			let mask = view.getUint8(i * nodeSize, true);
			let hasLeft = (mask & 1) > 0;
			let hasRight = (mask & 2) > 0;
			let splitX = (mask & 4) > 0;
			let splitY = (mask & 8) > 0;
			let splitZ = (mask & 16) > 0;
			let split = null;
			if (splitX) {
				split = 'X';
			} else if (splitY) {
				split = 'Y';
			} if (splitZ) {
				split = 'Z';
			}

			let node = new PointCloudZappchaGeometryNode();
			node.version = vpc.version;
			node.signedURL = vpc.signedURL;
			node.hasLeft = hasLeft;
			node.hasRight = hasRight;
			node.split = split;
			node.isLeaf = !hasLeft && !hasRight;
			node.number = i;
			node.level = stack.length;
			if(node.version===18){
				node.dataSize = view.getInt32(i * nodeSize + 1, true);
				node.filter = view.getInt32(i*nodeSize+5,true) / 10000.0;
				node.numPoints = view.getInt16(i * nodeSize + 9, true);
				node.originalNumPoints = view.getInt16(i * nodeSize + 11, true);
				node.dataLocation = view.getBigInt64(i * nodeSize + 13, true);
			}
			else{	
				node.numPoints = view.getInt16(i * nodeSize + 2, true);
				node.dataSize = view.getInt16(i * nodeSize + 4, true);
				node.bytesPerVertex = view.getInt8(i * nodeSize + 1, true);
				node.dataLocation = view.getBigInt64(i * nodeSize + 6, true);
			}
			if(node.dataLocation==-1)
				node.dataLocation=BigInt(0);
			node.bytesPerAxis = (node.bytesPerVertex-5)/3;
			
			levels = Math.max(levels, node.level);
			
			if (stack.length > 0) {
				let parent = stack[stack.length - 1];
				node.boundingBox = parent.boundingBox.clone();
				let parentBBSize = parent.boundingBox.getSize(new THREE.Vector3());

				if (parent.hasLeft && !parent.left) {
					parent.left = node;
					parent.children.push(node);
					if (parent.split === 'X') {
						node.boundingBox.max.x = node.boundingBox.min.x + parentBBSize.x / 2;
					} else if (parent.split === 'Y') {
						node.boundingBox.max.y = node.boundingBox.min.y + parentBBSize.y / 2;
					} else if (parent.split === 'Z') {
						node.boundingBox.max.z = node.boundingBox.min.z + parentBBSize.z / 2;
					}

					let center = node.boundingBox.getCenter(new THREE.Vector3());
					let radius = node.boundingBox.getSize(new THREE.Vector3()).length() / 2;
					node.boundingSphere = new THREE.Sphere(center, radius);
				} else {
					parent.right = node;
					parent.children.push(node);
					if (parent.split === 'X') {
						node.boundingBox.min.x = node.boundingBox.min.x + parentBBSize.x / 2;
					} else if (parent.split === 'Y') {
						node.boundingBox.min.y = node.boundingBox.min.y + parentBBSize.y / 2;
					} else if (parent.split === 'Z') {
						node.boundingBox.min.z = node.boundingBox.min.z + parentBBSize.z / 2;
					}

					let center = node.boundingBox.getCenter(new THREE.Vector3());
					let radius = node.boundingBox.getSize(new THREE.Vector3()).length() / 2;
					node.boundingSphere = new THREE.Sphere(center, radius);
				}
			} else {
				root = node;
				root.boundingBox = vpc.boundingBox.clone();
				let center = root.boundingBox.getCenter(new THREE.Vector3());
				let radius = root.boundingBox.getSize(new THREE.Vector3()).length() / 2;
				root.boundingSphere = new THREE.Sphere(center, radius);
			}

			let bbSize = node.boundingBox.getSize(new THREE.Vector3());
			node.spacing = ((bbSize.x + bbSize.y + bbSize.z) / 3) / 75;
			node.estimatedSpacing = node.spacing;

			stack.push(node);

			if (node.isLeaf) {
				let done = false;
				while (!done && stack.length > 0) {
					stack.pop();

					let top = stack[stack.length - 1];

					done = stack.length > 0 && top.hasRight && top.right == null;
				}
			}
		}
		// TODO Debug:
		// let end = new Date().getTime();
		// let parseDuration = end - start;
		// let msg = parseDuration;
		// document.getElementById("lblDebug").innerHTML = msg;

		vpc.root = root;
		vpc.levels = levels;
		console.log(vpc.root);
		
		vpc.dispatchEvent({type: 'hierarchy_loaded'});
	};

	get spacing(){
		if (this._spacing) {
			return this._spacing;
		} else if (this.root) {
			return this.root.spacing;
		} else {
			// TODO ???: null;
		}
	}

	set spacing(value){
		this._spacing = value;
	}

};

