import { Mat4 } from '../../core/math/mat4.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Vec3 } from '../../core/math/vec3.js';
import { GSplatPlacement } from './gsplat-placement.js';

const _invWorldMat = new Mat4();
const _localCameraPos = new Vec3();
const _tempCompletedUrls = [];
class GSplatOctreeInstance {
		constructor(octree, placement, assetLoader){
				this.activePlacements = new Set();
				this.dirtyModifiedPlacements = false;
				this.pending = new Set();
				this.pendingDecrements = new Map();
				this.previousPosition = new Vec3();
				this.octree = octree;
				this.placement = placement;
				this.assetLoader = assetLoader;
				this.nodeLods = new Int32Array(octree.nodes.length);
				this.nodeLods.fill(-1);
				const numFiles = octree.files.length;
				this.filePlacements = new Array(numFiles).fill(null);
		}
		destroy() {
				this.pending.clear();
				this.pendingDecrements.clear();
				this.filePlacements.length = 0;
		}
		calculateNodeLod(localCameraPosition, nodeIndex, maxLod) {
				const node = this.octree.nodes[nodeIndex];
				const distance = localCameraPosition.distance(node.bounds.center);
				const lodDistances = [
						2,
						4,
						6,
						8,
						10
				];
				for(let lod = 0; lod < maxLod; lod++){
						if (distance < lodDistances[lod]) {
								return lod;
						}
				}
				return maxLod;
		}
		updateLod(cameraNode) {
				const worldCameraPosition = cameraNode.getPosition();
				const octreeWorldTransform = this.placement.node.getWorldTransform();
				_invWorldMat.copy(octreeWorldTransform).invert();
				const localCameraPosition = _invWorldMat.transformPoint(worldCameraPosition, _localCameraPos);
				const maxLod = this.octree.lodLevels - 1;
				const nodes = this.octree.nodes;
				for(let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex++){
						const node = nodes[nodeIndex];
						const newLodIndex = this.calculateNodeLod(localCameraPosition, nodeIndex, maxLod);
						const currentLodIndex = this.nodeLods[nodeIndex];
						if (newLodIndex !== currentLodIndex) {
								const pendingOldFileIndex = this.pendingDecrements.get(nodeIndex);
								if (pendingOldFileIndex !== undefined) {
										this.decrementFileRef(pendingOldFileIndex, nodeIndex);
										this.pendingDecrements.delete(nodeIndex);
								}
								this.nodeLods[nodeIndex] = newLodIndex;
								const wasVisible = currentLodIndex >= 0;
								const willBeVisible = newLodIndex >= 0;
								if (!wasVisible && willBeVisible) {
										const newFileIndex = node.lods[newLodIndex].fileIndex;
										this.incrementFileRef(newFileIndex, nodeIndex, newLodIndex);
								} else if (wasVisible && !willBeVisible) {
										const oldFileIndex = node.lods[currentLodIndex].fileIndex;
										this.decrementFileRef(oldFileIndex, nodeIndex);
								} else if (wasVisible && willBeVisible) {
										const newFileIndex = node.lods[newLodIndex].fileIndex;
										const oldFileIndex = node.lods[currentLodIndex].fileIndex;
										this.incrementFileRef(newFileIndex, nodeIndex, newLodIndex);
										const newPlacement = this.filePlacements[newFileIndex];
										if (newPlacement?.resource) {
												this.decrementFileRef(oldFileIndex, nodeIndex);
										} else {
												this.pendingDecrements.set(nodeIndex, oldFileIndex);
										}
								}
						}
				}
		}
		incrementFileRef(fileIndex, nodeIndex, lodIndex) {
				if (fileIndex === -1) return;
				let placement = this.filePlacements[fileIndex];
				if (!placement) {
						placement = new GSplatPlacement(null, this.placement.node, lodIndex);
						this.filePlacements[fileIndex] = placement;
						if (!this.addFilePlacement(fileIndex)) {
								const fileUrl = this.octree.files[fileIndex];
								this.octree.ensureFileResource(fileUrl, this.assetLoader);
								this.pending.add(fileIndex);
						}
				}
				const nodes = this.octree.nodes;
				const node = nodes[nodeIndex];
				const lod = node.lods[lodIndex];
				const interval = new Vec2(lod.offset, lod.offset + lod.count - 1);
				placement.intervals.set(nodeIndex, interval);
				this.dirtyModifiedPlacements = true;
		}
		decrementFileRef(fileIndex, nodeIndex) {
				if (fileIndex === -1) return;
				const placement = this.filePlacements[fileIndex];
				if (placement) {
						placement.intervals.delete(nodeIndex);
						this.dirtyModifiedPlacements = true;
						if (placement.intervals.size === 0) {
								if (placement.resource) {
										this.activePlacements.delete(placement);
								}
								this.filePlacements[fileIndex] = null;
								this.pending.delete(fileIndex);
						}
				}
		}
		addFilePlacement(fileIndex) {
				const fileUrl = this.octree.files[fileIndex];
				const res = this.octree.getFileResource(fileUrl);
				if (res) {
						const placement = this.filePlacements[fileIndex];
						if (placement) {
								placement.resource = res;
								placement.aabb.copy(res.aabb);
								this.activePlacements.add(placement);
								this.dirtyModifiedPlacements = true;
								return true;
						}
				}
				return false;
		}
		testMoved() {
				const position = this.placement.node.getPosition();
				const length = position.distance(this.previousPosition);
				if (length > 1) {
						this.previousPosition.copy(position);
						return true;
				}
				return false;
		}
		update() {
				if (this.pending.size) {
						for (const fileIndex of this.pending){
								const fileUrl = this.octree.files[fileIndex];
								this.octree.ensureFileResource(fileUrl, this.assetLoader);
								const placement = this.filePlacements[fileIndex];
								if (placement && placement.intervals.size > 0 && this.addFilePlacement(fileIndex)) {
										_tempCompletedUrls.push(fileIndex);
										for (const [nodeIndex] of placement.intervals){
												const oldFileIndex = this.pendingDecrements.get(nodeIndex);
												if (oldFileIndex !== undefined) {
														this.decrementFileRef(oldFileIndex, nodeIndex);
														this.pendingDecrements.delete(nodeIndex);
												}
										}
								}
						}
						for (const fileIndex of _tempCompletedUrls){
								this.pending.delete(fileIndex);
						}
						_tempCompletedUrls.length = 0;
				}
				const dirty = this.dirtyModifiedPlacements;
				this.dirtyModifiedPlacements = false;
				return dirty;
		}
}

export { GSplatOctreeInstance };
