text: import * as vec4 from "./glmatrix/vec4.js";
  import * as mat4 from "./glmatrix/mat4.js";
  import * as mat3 from "./glmatrix/mat3.js";
  
  import {Utils} from "./utils.js";
  import {DataInputStream} from "./datainputstream.js";
  
  import {AvlTree} from "./collections/avltree.js";
  
  const PROTOCOL_VERSIONS = new Set([20, 21]);
  
  /**
   * This class is supposed to be and stay BIMserver-free.
   */
  
  export class GeometryLoader {
  	constructor(loaderId, renderLayer, loaderSettings, vertexQuantizationMatrices, stats, settings, geometryCache, gpuBufferManager, usePreparedBuffers) {
  		this.renderLayer = renderLayer;
  		this.settings = settings;
  		
  		this.loaderSettings = loaderSettings;
  
  		this.loaderId = loaderId;
  		this.vertexQuantizationMatrices = vertexQuantizationMatrices;
  		this.stats = stats;
  		this.geometryCache = geometryCache;
  
  		this.state = {};
  		this.objectAddedListeners = [];
  		this.geometryIds = new Map();
  		this.dataToInfo = new Map();
  		
  		this.gpuBufferManager = gpuBufferManager;
  
  		if (usePreparedBuffers) {
  			this.createdTransparentObjects = new Map(); // object id -> object info
  			this.createdOpaqueObjects = new Map(); // object id -> object info
  		}
  		
  		// temporary for emergency quantization
  		this.v4 = vec4.create();
  		
  		this.promise = new Promise((resolve, reject) => {
  			this.resolve = resolve;
  		});
  
  		// Object IDs need to be stored so that references
  		// to the GPU BufferSet can be made later, which is
  		// only constructed at the end of geometry loading.
  		this.uniqueIdsLoaded = [];
  	}
  	
  	processPreparedBufferInit(stream, hasTransparancy, hasTwoSidedTriangles) {
  		this.preparedBuffer = {};
  
  		this.preparedBuffer.nrObjects = stream.readInt();
  		this.preparedBuffer.nrIndices = stream.readInt();
  		
  		this.preparedBuffer.nrLineIndices = stream.readInt();
  		this.preparedBuffer.positionsIndex = stream.readInt();
  		this.preparedBuffer.normalsIndex = stream.readInt();
  		this.preparedBuffer.colorsIndex = stream.readInt();
  		
  		this.preparedBuffer.indicesRead = 0;
  		this.preparedBuffer.lineIndicesRead = 0;
  		this.preparedBuffer.positionsRead = 0;
  		this.preparedBuffer.normalsRead = 0;
  		this.preparedBuffer.colorsRead = 0;
  
  		this.preparedBuffer.nrObjectsRead = 0;
  
  		this.preparedBuffer.nrColors = this.preparedBuffer.positionsIndex * 4 / 3;
  
  		this.preparedBuffer.indices = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrIndices, this.renderLayer.gl.ELEMENT_ARRAY_BUFFER, 3, WebGL2RenderingContext.UNSIGNED_INT, "Uint32Array");
  		if (this.loaderSettings.generateLineRenders) {
  			this.preparedBuffer.lineIndices = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrLineIndices, this.renderLayer.gl.ELEMENT_ARRAY_BUFFER, 2, WebGL2RenderingContext.UNSIGNED_INT, "Uint32Array");
  		}
  		this.preparedBuffer.colors = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrColors, this.renderLayer.gl.ARRAY_BUFFER, 4, WebGL2RenderingContext.UNSIGNED_BYTE, "Uint8Array");
  		this.preparedBuffer.vertices = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.positionsIndex, this.renderLayer.gl.ARRAY_BUFFER, 3, this.settings.quantizeVertices ? WebGL2RenderingContext.SHORT : WebGL2RenderingContext.FLOAT, this.settings.quantizeVertices ? "Int16Array" : "Float32Array");
  		this.preparedBuffer.normals = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.normalsIndex, this.renderLayer.gl.ARRAY_BUFFER, this.settings.quantizeNormals ? (this.settings.loaderSettings.octEncodeNormals ? 2 : 3) : 3, this.settings.quantizeNormals ? WebGL2RenderingContext.BYTE : WebGL2RenderingContext.FLOAT, this.settings.quantizeNormals ? "Int8Array" : "Float32Array");
  		this.preparedBuffer.pickColors = Utils.createEmptyBuffer(this.renderLayer.gl, this.preparedBuffer.nrColors, this.renderLayer.gl.ARRAY_BUFFER, 4, WebGL2RenderingContext.UNSIGNED_BYTE, "Uint8Array");
  
  		this.preparedBuffer.uniqueIdToIndex = new AvlTree(this.renderLayer.viewer.inverseUniqueIdCompareFunction);
  		this.preparedBuffer.uniqueIdSet = new Set();
  		
  		this.preparedBuffer.loaderId = this.loaderId;
  		this.preparedBuffer.hasTransparency = hasTransparancy;
  		this.preparedBuffer.hasTwoSidedTriangles = hasTwoSidedTriangles;
  
  		if (this.loaderSettings.quantizeVertices) {
  			this.preparedBuffer.unquantizationMatrix = this.unquantizationMatrix;
  		}
  
  		this.preparedBuffer.bytes = Utils.calculateBytesUsed(this.settings, this.preparedBuffer.positionsIndex, this.preparedBuffer.nrColors, this.preparedBuffer.nrIndices, this.preparedBuffer.nrLineIndices, this.preparedBuffer.normalsIndex);
  
  		stream.align8();
  	}
  
  	processPreparedBuffer(stream, hasTransparancy, hasTwoSidedTriangles) {
  		const loadedViewObjects = [];
  
  		const nrObjects = stream.readInt();
  		const totalNrIndices = stream.readInt();
  		const totalNrLineIndices = stream.readInt();
  		const positionsIndex = stream.readInt();
  		const normalsIndex = stream.readInt();
  		const colorsIndex = stream.readInt();
  
  		if (this.preparedBuffer.nrIndices == 0) {
  			return;
  		}
  		
  		this.preparedBuffer.indicesRead += totalNrIndices;
  		this.preparedBuffer.lineIndicesRead += totalNrLineIndices;
  		this.preparedBuffer.positionsRead += positionsIndex;
  		this.preparedBuffer.normalsRead += normalsIndex;
  		this.preparedBuffer.colorsRead += colorsIndex;
  		
  		const previousStartIndex = this.preparedBuffer.indices.writePosition / 4;
  		const previousLineIndexStart = this.loaderSettings.generateLineRenders ? this.preparedBuffer.lineIndices.writePosition / 4 : 0;
  		const previousColorIndex = this.preparedBuffer.colors.writePosition;
  		if (!this.settings.fakeLoading) {
  			Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.indices, stream.dataView, stream.pos, totalNrIndices);
  		}
  		stream.pos += totalNrIndices * 4;
  
  		if (this.loaderSettings.generateLineRenders) {
  			Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.lineIndices, stream.dataView, stream.pos, totalNrLineIndices);
  			stream.pos += totalNrLineIndices * 4;
  		}
  
  		var nrColors = positionsIndex * 4 / 3;
  		var colors = new Uint8Array(nrColors);
  		var colors32 = new Uint32Array(colors.buffer);
  		var createdObjects = null;
  
  		if (hasTransparancy) {
  			createdObjects = this.createdTransparentObjects;
  		} else {
  			createdObjects = this.createdOpaqueObjects;
  		}
  
  		var pickColors = new Uint8Array(positionsIndex * 4 / 3);
  		var pickColors32 = new Uint32Array(pickColors.buffer);
  		pickColors.i = 0;
  
  		var currentColorIndex = 0;
  
  		const collectedMetaObjects = [];
  		
  		for (var i = 0; i < nrObjects; i++) {
  			var uniqueId = this.readAndCreateUniqueId(stream);
  			this.uniqueIdsLoaded.push(uniqueId);
  			this.preparedBuffer.uniqueIdSet.add(uniqueId);
  
  			var startIndex = stream.readInt();
  			var startLineIndex = stream.readInt();
  			var nrIndices = stream.readInt();
  			var nrLineIndices = stream.readInt();
  			var nrVertices = stream.readInt();
  			var minIndex = stream.readInt();
  			var maxIndex = stream.readInt();
  			
  			if (this.loaderSettings.generateLineRenders) {
  				var minLineIndex = stream.readInt();
  				var maxLineIndex = stream.readInt();
  			}
  			var nrObjectColors = nrVertices / 3 * 4;
  
  			if (!createdObjects) {
  				loadedViewObjects.push(uniqueId);
  				const viewObject = {
  					type: "Annotation",
  					pickId: uniqueId
  				}
  				this.renderLayer.viewer.addViewObject(uniqueId, viewObject);				
  			}
  
  			var viewObject = this.renderLayer.viewer.viewObjects.get(uniqueId);
  			var pickColor = this.renderLayer.viewer.getPickColorForPickId(viewObject.pickId);
  			var color32 = pickColor[0] + pickColor[1] * 256 + pickColor[2] * 256 * 256 + pickColor[3] * 256 * 256 * 256;
  			var lenObjectPickColors = nrObjectColors;
  			pickColors32.fill(color32, pickColors.i / 4, (pickColors.i + lenObjectPickColors) / 4);
  			pickColors.i += lenObjectPickColors;
  
  			const density = stream.readFloat();
  
  			var colorPackSize = stream.readInt();
  			if (createdObjects) {
  				var object = createdObjects.get(uniqueId);
  				object.density = density;
  			}
  			
  			const meta = {
  				start: previousStartIndex + startIndex,
  				length: nrIndices,
  				lineIndicesStart: previousLineIndexStart + startLineIndex,
  				lineIndicesLength: nrLineIndices,
  				color: previousColorIndex + currentColorIndex,
  				colorLength: nrObjectColors,
  				minIndex: minIndex,
  				maxIndex: maxIndex,
  				minLineIndex: minLineIndex,
  				maxLineIndex: maxLineIndex
  			};
  			
  			this.preparedBuffer.uniqueIdToIndex.set(uniqueId, [meta]);
  			collectedMetaObjects.push(meta);
  			if (colorPackSize == 0) {
  				// Generate default colors for this object
  				var defaultColor = this.renderLayer.viewer.defaultColors[object.type.toUpperCase()];
  				if (defaultColor == null) {
  					defaultColor = this.renderLayer.viewer.defaultColors.DEFAULT;
  				}
  				if (defaultColor.asInt == null) {
  					// Cache the integer version
  					var color = new Uint8Array(4);
  					color[0] = defaultColor.r * 255;
  					color[1] = defaultColor.g * 255;
  					color[2] = defaultColor.b * 255;
  					color[3] = defaultColor.a * 255;
  					defaultColor.asInt = color[0] + color[1] * 256 + color[2] * 65536 + color[3] * 16777216;
  				}
  				colors32.fill(defaultColor.asInt, currentColorIndex / 4, (currentColorIndex + nrObjectColors) / 4);
  				currentColorIndex += nrObjectColors;
  			}
  			for (var j = 0; j < colorPackSize; j++) {
  				var count = stream.readInt();
  				var color = stream.readUnsignedByteArray(4);
  				var color32 = color[0] + color[1] * 256 + color[2] * 65536 + color[3] * 16777216;
  				colors32.fill(color32, (currentColorIndex / 4), (currentColorIndex + count) / 4);
  				currentColorIndex += count;
  			}
  		}
  		Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.colors, colors, 0, nrColors);
  
  		if (this.loaderSettings.quantizeVertices) {
  			if (!this.settings.quantizeVertices) {
  				// we need to do some alignment here
  				const floats = new Float32Array(positionsIndex);
  				const aligned_u8 = new Uint8Array(floats.buffer);
  				const unaligned_u8 = new Uint8Array(stream.dataView.buffer, stream.pos, aligned_u8.length);
  				stream.pos += aligned_u8.length;
  				aligned_u8.set(unaligned_u8);
  				
  				// now we can quantize the input
  				// admittedly there was code for this in BufferTransformer, but it was commented out?
  				const m4 = this.vertexQuantizationMatrices.vertexQuantizationMatrix;
  				for (var i = 0; i < floats.length; i += 3) {
  					this.v4.set(floats.subarray(i, i + 3));
  					this.v4[3] = 1.0;
  					vec4.transformMat4(this.v4, this.v4, m4);
  					floats.set(this.v4.subarray(0, 3), i);
  				}
  				const quantized = new Int16Array(floats);
  
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, quantized, 0, quantized.length);
  			} else {
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, stream.dataView, stream.pos, positionsIndex);
  				stream.pos += positionsIndex * 2;
  			}		
  		} else {
  			const floats = new Float32Array(stream.dataView.buffer, stream.pos, positionsIndex);
  			if (this.settings.quantizeVertices) {
  				const quantized = new Int16Array(floats.length);
  				const m4 = this.vertexQuantizationMatrices.vertexQuantizationMatrix;
  				for (var i = 0; i < floats.length; i += 3) {
  					this.v4.set(floats.subarray(i, i + 3));
  					this.v4[3] = 1.0;
  					vec4.transformMat4(this.v4, this.v4, m4);
  					quantized.set(this.v4.subarray(0, 3), i);
  				}
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, quantized, 0, quantized.length);
  			} else {
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.vertices, floats, 0, floats.length);
  			}
  			stream.pos += positionsIndex * 4;
  
  			// @todo a bit ugly, but only in this case the AABB for the on-demand loaded object
  			// is computed.
  			if (loadedViewObjects.length) {
  				// @todo update this in the annotation generator
  				collectedMetaObjects.forEach((elem)=>{
  					elem.start *= 3;
  					elem.length *= 3;
  				});
  				for (var i = 0; i < collectedMetaObjects.length; ++i) {
  					const meta = collectedMetaObjects[i];
  					// TODO use uniqueId
  					const oid = loadedViewObjects[i];
  					const aabb = new Float32Array(6);
  					aabb.fill(Infinity);
  					aabb.subarray(3).fill(-Infinity);
  					for (var j = meta.minIndex; j <= meta.maxIndex; j += 3) {
  						const xyz = floats.subarray(3*j, 3*j+3);
  						for (let k = 0; k < 3; ++k) {
  							if (xyz[k] < aabb[k]) {
  								aabb[k] = xyz[k];
  							}
  							if (xyz[k] > aabb[k+3]) {
  								aabb[k+3] = xyz[k];
  							}
  						}
  					}
  					
  					var globalizedAabb;
  					if (this.renderLayer.viewer.globalTranslationVector) {
  						globalizedAabb = Utils.transformBounds(aabb, this.renderLayer.viewer.globalTranslationVector);
  					} else {
  						globalizedAabb = aabb;
  					}
  					const viewobj = this.renderLayer.viewer.getViewObject(oid);
  					viewobj.renderLayer = this.renderLayer;
  					viewobj.aabb = aabb;
  					viewobj.globalizedAabb = globalizedAabb;
  					this.renderLayer.viewer.setModelBounds(aabb);
  				}
  			}
  		}
  		
  		if (this.settings.quantizeNormals) {
  			// Debugging oct-encoding
  //			var octNormals = new Int8Array(stream.dataView.buffer, stream.pos, ((normalsIndex / 3) * 2));
  //			for (var i=0; i<octNormals.length; i+=2) {
  //				console.log(octNormals[i], octNormals[i+1]);
  //				let normal = Utils.octDecodeVec2([octNormals[i], octNormals[i+1]]);
  ////				if (this.lastNormal == null || this.lastNormal.toString() != normal.toString()) {
  ////					console.log(normal);
  ////				}
  //				this.lastNormal = normal;
  //			}
  
  			if (this.settings.loaderSettings.octEncodeNormals) {
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.normals, stream.dataView, stream.pos, ((normalsIndex / 3) * 2), true);
  				stream.pos += ((normalsIndex / 3) * 2);
  			} else {
  				Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.normals, stream.dataView, stream.pos, normalsIndex, true);
  				stream.pos += normalsIndex;
  			}
  		} else {
  			Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.normals, stream.dataView, stream.pos, normalsIndex, true);
  			stream.pos += normalsIndex * 4;
  		}
  
  		Utils.updateBuffer(this.renderLayer.gl, this.preparedBuffer.pickColors, pickColors, 0, pickColors.i);
  
  		this.preparedBuffer.nrObjectsRead += nrObjects;
  		
  		if (this.preparedGpuBuffer == null) {
  			this.preparedGpuBuffer = this.renderLayer.addCompleteBuffer(this.preparedBuffer, this.gpuBufferManager);
  		}
  
  		if (!this.settings.fakeLoading) {
  			this.preparedGpuBuffer.update(this.preparedBuffer.indicesRead, this.preparedBuffer.positionsRead, this.preparedBuffer.normalsRead, this.preparedBuffer.colorsRead);
  			this.renderLayer.viewer.dirty = 1;
  			
  			if (this.preparedBuffer.nrObjectsRead == this.preparedBuffer.nrObjects) {
  				this.preparedGpuBuffer.finalize();
  				
  				for (let uniqueId of this.uniqueIdsLoaded) {
  					this.renderLayer.viewer.uniqueIdToBufferSet.set(uniqueId, [this.preparedGpuBuffer]);
  				}
  				
  				this.uniqueIdsLoaded.length = 0;
  				this.preparedGpuBuffer = null;
  				this.preparedBuffer = null;
  			}
  		}
  		this.renderLayer.incLoadedPrimitives(totalNrIndices / 3, totalNrLineIndices / 2);
  
  		stream.align8();
  
  		return loadedViewObjects;
  	}
  	
  	processMessage(stream) {
  		var messageType = stream.readByte();
  		
  		if (messageType == 0) {
  			if (!this.readStart(stream)) {
  				// An error occured, usually version mismatch or missing serializer
  				return false;
  			}
  		} else if (messageType == 6) {
  			this.readEnd(stream);
  		} else {
  			this.readObject(stream, messageType);
  		}
  		stream.align8();
  		return stream.remaining() > 0;
  	}
  
  	endOfStream() {
  		if (this.dataToInfo.size > 0) {
  			// We need to tell the renderlayer that not all data has been loaded
  			this.renderLayer.storeMissingGeometry(this, this.dataToInfo);
  		}
  		if (this.dataToInfo.size == 0) {
  			// Only resolve (and cleanup this loader) when all has been loaded
  			this.resolve();
  		}
  	}
  	
  	binaryDataListener(data) {
  		this.stats.inc("Network", "Bytes OTL", data.byteLength);
  		var stream = new DataInputStream(data);
  		var channel = stream.readLong();
  		var type = stream.readLong();
  		if (type == 0) {
  			while (this.processMessage(stream)) {
  				
  			}
  		} else if (type == 1) {
  			this.endOfStream();
  			// End of stream
  		}
  	}
  	
  	geometryDataIdResolved(geometryDataId) {
  		this.dataToInfo.delete(geometryDataId);
  		if (this.dataToInfo.size == 0) {
  			// Only resolve (and cleanup this loader) when all has been loaded
  			this.resolve();
  		}
  	}
  	
  	readObject(stream, geometryType) {
  		var geometryId;
  		var numGeometries;
  		var numParts;
  		var objectBounds;
  		var numIndices;
  		var indices;
  		var numPositions;
  		var positions;
  		var numNormals;
  		var normals;
  		var numColors;
  		var colors = null;
  		
  		if (geometryType == 1) {
  			// Geometry
  			var reused = stream.readInt();
  			
  			var type = stream.readUTF8();
  			stream.align8();
  			var roid = stream.readLong();
  			var uniqueModelId = this.readAndCreateUniqueModelId(stream);
  			let hasTransparencyValue = stream.readLong();
  			var hasTransparency = hasTransparencyValue == 1;
  			if (this.protocolVersion == 21) {
  				var hasTwoSidedTriangles = stream.readLong() == 1;
  			} else {
  				var hasTwoSidedTriangles = true;
  			}
  			var geometryDataId = stream.readLong();
  			this.readGeometry(stream, roid, uniqueModelId, geometryDataId, geometryDataId, hasTransparency, hasTwoSidedTriangles, reused, type, true);
  			if (this.dataToInfo.has(geometryDataId)) {
  				// There are objects that have already been loaded, that are waiting for this GeometryData
  				var oids = this.dataToInfo.get(geometryDataId);
  				for (var oid of oids) {
  					var ob = this.renderLayer.getObject(this.loaderId, oid);
  					if (ob == null) {
  						console.error("Object with oid not found", oid)
  					} else {
  						ob.add(geometryDataId, oid);
  					}
  				}
  				// Now we can clean it up, nobody is waiting anymore
  				this.dataToInfo.delete(geometryDataId);
  			}
  		} else if (geometryType == 5) {
  			// Object
  			var inPreparedBuffer = stream.readByte() == 1;
  			var uniqueId = this.readAndCreateUniqueId(stream);
  			var type = stream.readUTF8();
  			var nrColors = stream.readInt();
  			stream.align8();
  			var roid = stream.readLong();
  			var geometryInfoOid = stream.readLong();
  			var hasTransparency = stream.readLong() == 1;
  
  			// Making copies here because otherwise we are potentially referring to very big buffers for as long as the viewer lives
  			var objectBounds = stream.readDoubleArrayCopy(6);
  			var matrix = stream.readDoubleArrayCopy(16);
  			
  			var geometryDataOid = stream.readLong();
  			var geometryDataOidFound = geometryDataOid;
  			if (inPreparedBuffer) {
  				if (hasTransparency) {
  					this.createdTransparentObjects.set(uniqueId, {
  						nrColors: nrColors,
  						type: type
  					});
  				} else {
  					this.createdOpaqueObjects.set(uniqueId, {
  						nrColors: nrColors,
  						type: type
  					});
  				}
  			}
  			if (!inPreparedBuffer) {
  				if (!this.geometryIds.has(geometryDataOid)) {
  					if (this.geometryCache != null && this.geometryCache.has(geometryDataOid)) {
  						// We know it's cached
  					} else {
  						geometryDataOidFound = null;
  						// We don't have the data yet, it might come in this stream, or maybe in a later stream
  						var list = this.dataToInfo.get(geometryDataOid);
  						if (list == null) {
  							list = [uniqueId];
  							this.dataToInfo.set(geometryDataOid, list);
  						} else {
  							list.push(uniqueId);
  						}
  					}
  				}
  			} else {
  				geometryDataOidFound = null;
  			}
  			
  			this.createObject(roid, uniqueId, geometryDataOidFound == null ? [] : [geometryDataOidFound], matrix, hasTransparency, type, objectBounds, inPreparedBuffer);
  		} else if (geometryType == 9) {
  			// Minimal object
  			var uniqueId = this.readAndCreateUniqueId(stream);
  			var type = stream.readUTF8();
  			var nrColors = stream.readInt();
  			var roid = stream.readLong();
  			var geometryInfoOid = stream.readLong();
  			var hasTransparency = stream.readLong() == 1;
  			
  			stream.align8();
  			var objectBounds = stream.readDoubleArrayCopy(6);
  
  			var geometryDataOid = stream.readLong();
  			var geometryDataOidFound = geometryDataOid;
  			if (hasTransparency) {
  				this.createdTransparentObjects.set(uniqueId, {
  					nrColors: nrColors,
  					type: type
  				});
  			} else {
  				this.createdOpaqueObjects.set(uniqueId, {
  					nrColors: nrColors,
  					type: type
  				});
  			}
  			
  			this.createObject(roid, uniqueId, [], null, hasTransparency, type, objectBounds, true);
  		} else if (geometryType == 7) {
  			this.processPreparedBuffer(stream, true, false);
  		} else if (geometryType == 8) {
  			this.processPreparedBuffer(stream, false, false);
  		} else if (geometryType == 10) {
  			this.processPreparedBufferInit(stream, true, false);
  		} else if (geometryType == 11) {
  			this.processPreparedBufferInit(stream, false, false);
  		} else if (geometryType == 12) {
  			this.processPreparedBuffer(stream, true, true);
  		} else if (geometryType == 13) {
  			this.processPreparedBuffer(stream, false, true);
  		} else if (geometryType == 14) {
  			this.processPreparedBufferInit(stream, true, true);
  		} else if (geometryType == 15) {
  			this.processPreparedBufferInit(stream, false, true);
  		} else {
  			console.error("Unsupported geometry type: " + geometryType);
  			return;
  		}
  
  		this.state.nrObjectsRead++;
  	}
  	
  	readAndCreateUniqueId(stream) {
  		if (this.loaderSettings.useUuidAndRid) {
  			let oid = stream.readInt();
  			let mid = stream.readLong();
  			return oid + "-" + mid;
  		} else {
  			var uniqueId = stream.readLong();
  			if (uniqueId === 0) {
  				uniqueId = this.renderLayer.viewer.oidCounter ++;
  			}
  			return uniqueId;
  		}
  	}
  	
  	readAndCreateUniqueModelId(stream) {
  		return stream.readLong(); // On BIMserver, this is croid
  	}
  
  	readGeometry(stream, roid, uniqueModelId, geometryId, geometryDataOid, hasTransparency, hasTwoSidedTriangles, reused, type, useIntForIndices) {
  		var numIndices = stream.readInt();
  		if (useIntForIndices) {
  			var indices = stream.readIntArray(numIndices);
  		} else {
  			var indices = stream.readShortArray(numIndices);
  		}
  		if (this.loaderSettings.generateLineRenders) {
  			var numLineIndices = stream.readInt();
  			if (useIntForIndices) {
  				var lineIndices = stream.readIntArray(numLineIndices);
  			} else {
  				var lineIndices = stream.readShortArray(numLineIndices);
  			}
  		}
  		var color = this.readColors(stream, type);
  		var numPositions = stream.readInt();
  		if (this.loaderSettings.quantizeVertices) {
  			var positions = stream.readShortArray(numPositions);
  			stream.align8();
  		} else {
  			var positions = stream.readFloatArray(numPositions);
  		}
  		var numNormals = stream.readInt();
  		if (this.loaderSettings.quantizeNormals) {
  			var normals = stream.readByteArray(numNormals);
  			stream.align8();
  		} else {
  			var normals = stream.readFloatArray(numNormals);
  		}
  		var numColors = stream.readInt();
  		if (numColors > 0) {
  			if (this.loaderSettings.quantizeColors) {
  				var colors = stream.readUnsignedByteArray(numColors);
  				stream.align8();
  			} else {
  				var colors = stream.readFloatArray(numColors);
  			}
  		} else if (color != null && !this.settings.useObjectColors) {
  			// When we are generating this data anyways, we might as well make sure it ends up in the format required by the GPU
  			if (this.settings.quantizeColors) {
  				var size = (4 * numPositions) / 3;
  				var colors = new Uint8Array(size);
  				var quantizedColor = new Uint8Array(4);
  				quantizedColor[0] = color.r * 255;
  				quantizedColor[1] = color.g * 255;
  				quantizedColor[2] = color.b * 255;
  				quantizedColor[3] = color.a * 255;
  				for (var i=0; i < size / 4; i++) {
  					colors.set(quantizedColor, i * 4);
  				}
  			} else {
  				var size = (4 * numPositions) / 3;
  				var colors = new Float32Array(size);
  				var nonQuantizedColor = new Float32Array(4);
  				nonQuantizedColor[0] = color.r;
  				nonQuantizedColor[1] = color.g;
  				nonQuantizedColor[2] = color.b;
  				nonQuantizedColor[3] = color.a;
  				for (var i=0; i < size / 4; i++) {
  					colors.set(nonQuantizedColor, i * 4);
  				}
  			}
  		}
  		if (this.settings.useObjectColors) {
  			colors = null;
  		}
  		if (!this.geometryIds.has(geometryDataOid)) {
  			this.geometryIds.set(geometryDataOid, true);
  		}
  		if (colors.length == 0) {
  			debugger;
  		}
  		this.renderLayer.createGeometry(this.loaderId, roid, uniqueModelId, geometryDataOid, positions, normals, colors, color, indices, lineIndices, hasTransparency, hasTwoSidedTriangles, reused);
  	}
  
  	readColors(stream, type) {
  		var b = stream.readInt();
  		if (b == 1) {
  			var color = {r: stream.readFloat(), g: stream.readFloat(), b: stream.readFloat(), a: stream.readFloat()};
  		} else {
  			var defaultColor = this.renderLayer.viewer.defaultColors[type.toUpperCase()];
  			if (defaultColor == null) {
  				var color = {
  					r: 0,
  					g: 1,
  					b: 0,
  					a: 1
  				};
  			} else {
  				color = defaultColor;
  			}
  		}
  		stream.align4();
  		return color;
  	}
  
  	createObject(roid, uniqueId, geometryIds, matrix, hasTransparency, type, aabb, inCompleteBuffer) {
  		if (this.state.mode == 0) {
  			console.log("Mode is still 0, should be 1");
  			return;
  		}
  		if (!inCompleteBuffer) {
  			if (this.multiplierToMm < 0.99 || this.multiplierToMm > 1.01) {
  				// We need to change the matrix because the operations in the matrix
  				// are based on the original geometry (e.a. not scaled to the right
  				// unit: mm)
  				var scaleMatrix = mat4.create();
  				mat4.scale(scaleMatrix, scaleMatrix, [this.multiplierToMm, this.multiplierToMm, this.multiplierToMm]);
  				var invertedScaleMatrix = mat4.create();
  				mat4.invert(invertedScaleMatrix, scaleMatrix);
  				
  				var totalMatrix = mat4.create();
  				// Read} from bottom to top
  				
  				// 3. Apply the scaling again
  				mat4.multiply(totalMatrix, totalMatrix, scaleMatrix);
  				
  				// 2. Apply the matrix
  				mat4.multiply(totalMatrix, totalMatrix, matrix);
  				
  				// 1. Server has already scaled the vertices, scale it back to the
  				// original values
  				mat4.multiply(totalMatrix, totalMatrix, invertedScaleMatrix);
  				
  				matrix = totalMatrix;
  			}
  			var normalMatrix = mat3.create();
  			mat3.fromMat4(normalMatrix, matrix);
  			mat3.invert(normalMatrix, normalMatrix);
  			mat3.transpose(normalMatrix, normalMatrix);
  		}
  		this.renderLayer.createObject(this.loaderId, roid, uniqueId, geometryIds, matrix, normalMatrix, scaleMatrix, hasTransparency, type, aabb);
  	}
  	
  	readStart(data) {
  		var start = data.readUTF8();
  
  		if (start != "BGS") {
  			console.error("Data does not start with BGS (" + start + ")");
  			return false;
  		}
  
  		this.protocolVersion = data.readByte();
  
  		if (!PROTOCOL_VERSIONS.has(this.protocolVersion)) {
  			console.error("Unimplemented version (protocol: " + this.protocolVersion + ", implemented: " + PROTOCOL_VERSIONS + ").\nUsually this means you need to either:\n\t- Update the BinarySerializers plugin bundle in BIMserver\n\t- Update your version of BIMsurfer 3");
  			return false;
  		}
  
  		this.multiplierToMm = data.readFloat();
  		data.align8();
  
  		var boundary = data.readDoubleArray(6);
  
  		this.state.mode = 1;
  		
  		return true;
  	}
  
  	initiateDownload() {
  		this.state = {
  			mode: 0,
  			nrObjectsRead: 0,
  			nrObjects: 0
  		};
  	}
  	
  	start() {
  		if (this.renderLayer.progressListener != null) {
  			this.renderLayer.progressListener(0, 0);
  		}
  		
  		if (this.onStart != null) {
  			this.onStart();
  		}
  		var obj = [];
  
  		this.initiateDownload();
  		
  		var loaderSettings = JSON.parse(JSON.stringify(this.loaderSettings));
  
  		return this.promise;
  	}
  	
  	readEnd(data) {
  		// This is the end of the binary stream, but there is one more message on the line, which is the end of the wrapping stream..., so that's when we close it all
  	}
  	
  	// This promise is fired as soon as the GeometryLoader is done
  	getPromise() {
  		return this.promise;
  	}
  }