text: import * as mat4 from "./glmatrix/mat4.js";
  import * as mat3 from "./glmatrix/mat3.js";
  import * as vec3 from "./glmatrix/vec3.js";
  import * as vec4 from "./glmatrix/vec4.js";
  
  import {RenderLayer} from "./renderlayer.js";
  import {Octree} from "./octree.js";
  import {Frustum} from "./frustum.js";
  import {LineBoxGeometry} from "./lineboxgeometry.js";
  import {BufferManagerTransparencyOnly} from "./buffermanagertransparencyonly.js";
  import {BufferManagerPerColor} from "./buffermanagerpercolor.js";
  import {Utils} from "./utils.js";
  import {TileLoader} from "./tileloader.js";
  import {ReuseLoader} from "./reuseloader.js";
  
  const RED = [1, 0, 0, 1];
  const GREEN = [0, 1, 0, 1];
  const BLUE = [0, 0, 1, 1];
  const GRAY = [0.5, 0.5, 0.5, 1];
  const PURPLE = [1, 0, 1, 1];
  
  /**
   * A specific type of RenderLayer, which uses Tiling to achieve better render performance, but also minimizes the amount of data that needs to be loaded of the line.
   * 
   */
  export class TilingRenderLayer extends RenderLayer {
  	constructor(viewer, geometryDataToReuse, bounds) {
  		super(viewer, geometryDataToReuse);
  
  		this.octree = new Octree(viewer, bounds, viewer.globalTranslationVector, viewer.settings.maxOctreeDepth);
  		this.lineBoxGeometry = new LineBoxGeometry(viewer, viewer.gl);
  
  		this.loaderToNode = {};
  
  		this.drawTileBorders = this.viewer.settings.realtimeSettings.drawTileBorders;
  
  		this._frustum = new Frustum();
  		
  		window.tilingRenderLayer = this;
  		
  		this.enabled = false;
  		
  		this.show = "none";
  		this.initialLoad = "none";
  		
  		// TODO unregister
  		this.viewer.camera.listeners.push(() => {
  			this._frustum.init(this.viewer.camera.viewMatrix, this.viewer.camera.projMatrix);
  		});
  	}
  	
  	showAll() {
  		this.show = "all";
  		this.viewer.dirty = 2;
  	}
  
  	load(bimServerApi, densityThreshold, roids, fieldsToInclude, progressListener) {
  		var reuseLowerThreshold = this.settings.loaderSettings.reuseThreshold;
  		if (!this.settings.loaderSettings.tilingLayerReuse) {
  			reuseLowerThreshold = -1;
  		}
  		this.tileLoader = new TileLoader(this, this.viewer, bimServerApi, densityThreshold, reuseLowerThreshold, this.geometryDataToReuse, roids, fieldsToInclude);
  		if (this.settings.loaderSettings.tilingLayerReuse) {
  			this.reuseLoader = new ReuseLoader(this.viewer, reuseLowerThreshold, bimServerApi, fieldsToInclude, roids, this.tileLoader.quantizationMap, this.geometryCache, this.geometryDataToReuse);
  		}
  
  		var promise = new Promise((resolve, reject) => {
  			var init = this.tileLoader.initialize().then(() => {
  				this.enabled = true;
  				// Ugly way of triggering an octree-visibility update
  				this.lastViewMatrix = null;
  				if (this.initialLoad == "all") {
  					return this.tileLoader.loadAll(progressListener);
  				}
  				resolve();
  			});
  		});
  		return promise;
  	}
  
  	cull(node) {
  		// 1. Are we always showing all objects?
  		if (this.show === "all") {
  			return false;
  		}
  
  		// 2. Is the complete Tile outside of the view frustum?
  		if (this._frustum.intersectsWorldAABB(node.minimalBox.minmax) === Frustum.OUTSIDE_FRUSTUM) {
  			return true;
  		}
  		
  		// 3. Is the tile too far away?
  		var cameraEye = this.viewer.camera.eye;
  		var tileCenter = node.minimalBox.normalizedCenter;
  		var closestPotentialDistanceMm = Math.abs(vec3.distance(cameraEye, tileCenter) - node.minimalBox.radius);
  		
  //		console.log(closestPotentialDistanceMm);
  		
  		// Project the biggest face of the node to 2D and determine it's area in pixels
  		
  		const vFOV = this.viewer.camera.perspective.fov * Math.PI / 180;
  		const pixelWidth = 1000 * Math.tan(vFOV / 2) / closestPotentialDistanceMm; // far-plane distance
  
  		const factor = 100000 / pixelWidth;
  		
  		if (node.gpuBufferManager != null) {
  			// A tile is already loaded, we need to determine how much of it to show
  			node.stats.trianglesDrawing = 0;
  			var totalTriangles = 0;
  			for (var transparent of [false, true]) {
  				for (var twoSidedTriangles of [false, true]) {
  					var buffers = node.gpuBufferManager.getBuffers(transparent, twoSidedTriangles, false);
  					for (var buffer of buffers) {
  						buffer.nrTrianglesToDraw = Math.floor(Math.min(buffer.nrIndices, Math.floor(buffer.nrIndices * factor)) / 3);
  						totalTriangles += buffer.nrIndices / 3;
  						node.stats.trianglesDrawing += buffer.nrTrianglesToDraw;
  					}
  				}
  			}
  			for (var transparent of [false, true]) {
  				for (var twoSidedTriangles of [false, true]) {
  					var buffers = node.gpuBufferManager.getBuffers(transparent, twoSidedTriangles, true);
  					for (var buffer of buffers) {
  						buffer.nrTrianglesToDraw = Math.floor(Math.min(buffer.nrIndices, Math.floor(buffer.nrIndices * factor)) / 3) * buffer.numInstances;
  						totalTriangles += (buffer.nrIndices / 3) * buffer.numInstances;
  						node.stats.trianglesDrawing += buffer.nrTrianglesToDraw;
  					}
  				}
  			}
  			node.normalizedDistanceFactor = node.stats.trianglesDrawing / totalTriangles;
  			if (node.stats.trianglesDrawing == 0) {
  				return true;
  			}
  		} else {
  			// This bit determines whether a tile will be loaded or not
  			if (pixelWidth > 0.004) {
  				return false;
  			} else {
  				node.normalizedDistanceFactor = 0;
  				if (node.stats != null) {
  					node.stats.trianglesDrawing = 0;
  				}
  				return true;
  			}
  		}
  		// Default response
  		return false;
  	}
  	
  	prepareRender(reason) {
  		// This only needs to be recalculated if the camera has changed, so we keep track of the last view matrix
  		
  		// TODO To correctly update the stats, this also needs to run whenever new data was loaded
  		if (this.lastViewMatrix == null || this.octree.size != this.lastOctreeSize || !mat4.equals(this.lastViewMatrix, this.viewer.camera.viewMatrix) || reason == 2) {
  			this.lastViewMatrix = mat4.clone(this.viewer.camera.viewMatrix);
  
  			var renderingTiles = 0;
  			var renderingTriangles = 0;
  			var drawCalls = 0;
  			
  			this.octree.traverseBreathFirst((node) => {
  				if (node.parent != null && node.parent.visibilityStatus == 0) {
  					node.visibilityStatus = 0;
  					return false;
  				}
  				if (this.cull(node)) {
  					node.visibilityStatus = 0;
  					return false;
  				} else {
  					node.visibilityStatus = 1;
  					if (node.loadingStatus == 0) {
  						this.tileLoader.loadTile(node);
  					} else {
  						if (node.stats != null) {
  							renderingTiles++;
  							renderingTriangles += (node.stats.trianglesDrawing ? node.stats.trianglesDrawing : 0);
  							drawCalls += node.stats.drawCallsPerFrame;
  						}
  					}
  				}
  			});
  			
  			this.viewer.stats.setParameter("Drawing", "Draw calls per frame (L2)", drawCalls);
  			this.viewer.stats.setParameter("Drawing", "Triangles to draw (L2)", renderingTriangles);
  			this.viewer.stats.setParameter("Tiling", "Rendering", renderingTiles);
  		}
  	}
  	
  	renderBuffers(transparency, twoSidedTriangles, reuse, lines, visibleElements) {
  		// TODO when navigation is active (rotating, panning etc...), this would be the place to decide to for example not-render anything in this layer, or maybe apply more aggressive culling
  		// if (this.viewer.navigationActive) {
  		// 	return;
  		// }
  		
  		// TODO would be nicer if this was passed as an integer argument, indicating the iteration count of this frame
  		let picking = visibleElements.pass === 'pick';
  
  		var programInfo = this.viewer.programManager.getProgram(this.viewer.programManager.createKey(reuse, picking));
  
  		this.gl.useProgram(programInfo.program);
  
  		if (!picking) {
  			// TODO find out whether it's possible to do this binding before the program is used (possibly just once per frame, and better yet, a different location in the code)
  			this.viewer.lighting.render(programInfo.uniformBlocks.LightData);
  			this.gl.uniformMatrix3fv(programInfo.uniformLocations.viewNormalMatrix, false, this.viewer.camera.viewNormalMatrix);
  		}
  
  		this.gl.uniformMatrix4fv(programInfo.uniformLocations.projectionMatrix, false, this.viewer.camera.projMatrix);
  		this.gl.uniformMatrix4fv(programInfo.uniformLocations.viewMatrix, false, this.viewer.camera.viewMatrix);
  		this.gl.uniform3fv(programInfo.uniformLocations.postProcessingTranslation, this.postProcessingTranslation);
  		this.gl.uniform4fv(programInfo.uniformLocations.sectionPlane, this.viewer.sectionPlanes.buffer);
  
  //		if (this.settings.quantizeVertices) {
  //			this.gl.uniformMatrix4fv(programInfo.uniformLocations.vertexQuantizationMatrix, false, this.viewer.vertexQuantization.getTransformedInverseVertexQuantizationMatrix());
  //		}
  
  		programInfo.lastUnquantizationMatrixUsed = null; // This ony is used for "caching", need to reset it otherwise it won't be set
  		
  		this.octree.traverse((node) => {
  			// TODO at the moment a list (of non-empty tiles) is used to do traverseBreathFirst, but since a big optimization is possible by automatically culling 
  			// child nodes of parent nodes that are culled, we might have to reconsider this and go back to tree-traversal, where returning false would indicate to 
  			// skip the remaining child nodes
  			
  			if (node.visibilityStatus == 1) {
  				if (node.gpuBufferManager == null) {
  					// Not initialized yet
  					return;
  				}
  				var buffers = node.gpuBufferManager.getBuffers(transparency, twoSidedTriangles, reuse);
  				this.renderFinalBuffers(buffers, programInfo, visibleElements);
  			} else {
  				return false;
  			}
  		});
  	}
  
  	traverseFunction(node, level, lineBoxGeometry) {
  		var color = null;
  		if (node.loadingStatus === 0) {
  			// No visualisation, node is not empty (or parent node)
  		} else if (node.loadingStatus === 1) {
  			// Node is waiting to start loading
  			color = RED;
  		} else if (node.loadingStatus === 2) {
  			// Node is loading
  		} else if (node.loadingStatus === 3) {
  			// Node is loaded
  			if (node.visibilityStatus === 0) {
  				color = GREEN;
  			} else if (node.visibilityStatus === 1) {
  				if (node.normalizedDistanceFactor === 1) {
  					// Uncomment for debugging tile borders
  //					color = PURPLE;
  				} else {
  					color = BLUE;
  					// This changes (content of) the constant, but the constant is only used for this, so it's fine
  					color[3] = 1 - node.normalizedDistanceFactor;
  				}
  			}
  		} else if (node.loadingStatus === 4) {
  			// To be documented
  			color = GRAY;
  		} else if (node.loadingStatus === 5) {
  			// Node has been tried to load, but no objects were returned
  		}
  		if (color != null) {
  			lineBoxGeometry.render(color, node.minimalBox.normalizedMatrix, 0.001);
  		}
  	}
  	
  	renderTileBorders() {
  		if (this.drawTileBorders) {
  			// The lines are rendered in the transparency-phase only
  			this.lineBoxGeometry.renderStart(this.viewer, this);
  			this.octree.traverse(this.traverseFunction, false, 0, this.lineBoxGeometry);
  			this.lineBoxGeometry.renderStop();
  		}
  	}
  
  	addGeometry(loaderId, geometry, object) {
  		var sizes = {
  			vertices: geometry.positions.length,
  			normals: geometry.normals.length,
  			indices: geometry.indices.length,
  			lineIndices: geometry.lineIndices.length,
  			colors: (geometry.colors != null ? geometry.colors.length : 0),
  			pickColors: geometry.positions.length
  		};
  
  		var node = this.loaderToNode[loaderId];
  
  		// TODO some of this is duplicate code, also in defaultrenderlayer.js
  		if (geometry.reused > 1 && this.geometryDataToReuse != null && this.geometryDataToReuse.has(geometry.id)) {
  			geometry.matrices.push(object.matrix);
  			geometry.objects.push(object);
  
  			this.viewer.stats.inc("Drawing", "Triangles to draw", geometry.indices.length / 3);
  
  			return;
  		}
  		
  		if (node.bufferManager == null) {
  			if (this.settings.useObjectColors) {
  				node.bufferManager = new BufferManagerPerColor(this.viewer, this.viewer.settings, this, this.viewer.bufferSetPool);
  			} else {
  				node.bufferManager = new BufferManagerTransparencyOnly(this.viewer, this.viewer.settings, this, this.viewer.bufferSetPool);
  			}
  		}
  		var buffer = node.bufferManager.getBufferSet(geometry.hasTransparency, geometry.color, sizes);
  		buffer.node = node;
  		
  		super.addGeometry(loaderId, geometry, object, buffer, sizes);
  	}
  	
  	dump() {
  		console.log(this.tileLoader.executor);
  	}
  
  	createObject(loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb) {
  		var loader = this.getLoader(loaderId);
  		var node = this.loaderToNode[loaderId];
  		return super.createObject(loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb, node.gpuBufferManager, node);
  	}
  
  	addGeometryReusable(geometry, loader, gpuBufferManager) {
  		super.addGeometryReusable(geometry, loader, gpuBufferManager);
  		var node = this.loaderToNode[loader.loaderId];
  		node.stats.triangles += ((geometry.indices.length / 3) * (geometry.matrices.length));
  		node.stats.drawCallsPerFrame++;
  		
  		this.viewer.stats.inc("Drawing", "Draw calls per frame (L2)");
  	}
  
  	done(loaderId) {
  		var loader = this.getLoader(loaderId);
  		var node = this.loaderToNode[loaderId];
  		
  		// When a new tile has been loaded and the viewer is not moving, we need to force an update of the culling of the node
  		if (this.cull(node)) {
  			node.visibilityStatus = 0;
  		} else {
  			node.visibilityStatus = 1;
  		}
  		
  		for (var geometry of loader.geometries.values()) {
  			if (geometry.isReused) {
  				this.addGeometryReusable(geometry, loader, node.gpuBufferManager);
  			}
  		}
  		
  		var bufferManager = node.bufferManager;
  		if (bufferManager != null) {
  			for (var buffer of bufferManager.getAllBuffers()) {
  				this.flushBuffer(buffer);
  			}
  			bufferManager.clear();
  			node.bufferManager = null;
  		}
  
  		for (var object of loader.objects.values()) {
  			object.add = null;
  		}
  
  		if (this.settings.autoCombineGpuBuffers) {
  			var savedBuffers = node.gpuBufferManager.combineBuffers();
  			this.viewer.stats.dec("Drawing", "Draw calls per frame (L2)", savedBuffers);
  			this.viewer.stats.dec("Buffers", "Buffer groups", savedBuffers);
  			node.stats.drawCallsPerFrame -= savedBuffers;
  		}
  
  		this.viewer.dirty = 2;
  		
  		this.removeLoader(loaderId);
  	}
  
  	flushAllBuffers() {
  		this.octree.traverse((node) => {
  			var bufferManager = node.bufferManager;
  			if (bufferManager != null) {
  				for (var buffer of bufferManager.getAllBuffers()) {
  					this.flushBuffer(buffer);
  				}
  				if (this.settings.useObjectColors) {
  					// When using object colors, it makes sense to sort the buffers by color, so we can potentially skip a few uniform binds
  					// It might be beneficiary to do this sorting on-the-lfy and not just when everything is loaded
  
  					// TODO disabled for now since we are testing combining buffer, which makes this obsolete
  //					this.sortBuffers(node.liveBuffers);
  				}
  			}
  		}, false);
  	}
  
  	renderSelectionOutlines(ids, width) {
  		for (var oid of ids) {
  			// TODO this is already much more efficient than iterating over all octree nodes, but can be made more efficient for large selections by first 
  			// organizing the oid's per node
  			var viewObject = this.viewer.getViewObject(oid);
  			super.renderSelectionOutlines(ids, width, viewObject.node);
  		}
  	}
  	
  	addCompleteBuffer(buffer, gpuBufferManager) {
  		var newBuffer = super.addCompleteBuffer(buffer, gpuBufferManager);
  		
  		const node = this.loaderToNode[buffer.loaderId];
  		newBuffer.node = node;
  		node.stats.triangles += buffer.nrIndices / 3;
  		node.stats.drawCallsPerFrame++;
  		
  		return newBuffer;
  	}
  	
  	flushBuffer(buffer) {
  		var node = buffer.node;
  		let gpuBuffer = super.flushBuffer(buffer, node.gpuBufferManager);
  
  		if (gpuBuffer == null) {
  			debugger;
  		}
  		gpuBuffer.node = node;
  
  		node.stats.triangles += buffer.nrIndices / 3;
  		node.stats.drawCallsPerFrame++;
  
  		if (node.bufferManager) {
  			node.bufferManager.resetBuffer(buffer);
  		}
  		this.viewer.dirty = 2;
  
  		return gpuBuffer;
  	}
  
  	completelyDone() {
  		this.flushAllBuffers();
  		this.viewer.dirty = 2;
  	}
  }