/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */
import { HttpResponseType } from '../http';
import { BimVertexDataReader } from './bim-vertex-data-reader';
import { T8gdReader } from '@twinfinity/geometry';
import { GeometryMemoryBlock as GeometryMemoryBlock } from './GeometryMemoryBlock';
import { telemetry } from '../Telemetry';
import { loadMeshStore, WasmGeometryProcessor, decodeBase64Hash } from '@twinfinity/geometry';
import { NormalType } from '@twinfinity/geometry';
export class BimVertexDataRepository {
    _bimApiClient;
    _vertexDataBlock = new GeometryMemoryBlock();
    _requestedMeshDescriptorHashes = new Set();
    _v2FormatMeshHashes = new Set();
    _meshDescriptorByHash = new Map();
    _loadedUrls = new Map();
    _geometryProcessor;
    _geometryProcessorPromise;
    constructor(_bimApiClient, options) {
        this._bimApiClient = _bimApiClient;
        if (options?.geometryProcessor) {
            this._geometryProcessor = options.geometryProcessor;
        }
        else if (options?.wasmLoaderOptions) {
            this._geometryProcessorPromise = loadMeshStore(options.wasmLoaderOptions).then((result) => {
                if (result) {
                    this._geometryProcessor = new WasmGeometryProcessor(result.module, result.blobUrl);
                }
                return this._geometryProcessor;
            });
        }
    }
    /**
     * Reads edge indices for a mesh as a freshly-allocated Uint32Array (or undefined when
     * the mesh has no edges or is not loaded). The copy is intentional: callers are free
     * to retain the returned array across {@link releaseV2MeshSourcesFor} / {@link releaseV2MeshSources}
     * calls, which would otherwise detach an underlying view.
     */
    readEdgeIndices(hash) {
        // Non-WASM path: edges live in the retained V2 buffer.
        const fromBlock = this._vertexDataBlock.readEdgeIndices(hash);
        if (fromBlock)
            return new Uint32Array(fromBlock);
        // WASM path: edges are extracted from the WASM geometry buffer. The WASM side
        // probes the on-disk edge count to size the output buffer exactly — no heuristic.
        if (this._geometryProcessor && this._v2FormatMeshHashes.has(hash)) {
            const md = this._meshDescriptorByHash.get(hash);
            if (md?.bh) {
                return this._geometryProcessor.readEdges(md.bh);
            }
        }
        return undefined;
    }
    /** Releases retained V2 geometry buffers after all edges have been read. */
    releaseV2MeshSources() {
        this._vertexDataBlock.releaseV2MeshSources();
    }
    /**
     * Releases retained V2 geometry buffers for a specific set of mesh hashes.
     * Use this when several loader elements share one repository — each element releases
     * only the hashes it consumed, so concurrent peers can still resolve their edges.
     */
    releaseV2MeshSourcesFor(hashes) {
        this._vertexDataBlock.releaseV2MeshSourcesFor(hashes);
    }
    get hasBatchSupport() {
        return this._geometryProcessor !== undefined;
    }
    setGeometryProcessor(processor) {
        this._geometryProcessor = processor;
    }
    load(loaderElement) {
        if (!loaderElement.geometryUrl?.href) {
            return Promise.resolve(false);
        }
        const key = loaderElement.geometryUrl.href.toLowerCase();
        const existingPromise = this._loadedUrls.get(key);
        if (existingPromise) {
            return existingPromise;
        }
        const isV2Format = loaderElement.geometryFormat === 't8gd';
        const fetchPromise = (async () => {
            if (this._geometryProcessorPromise) {
                await this._geometryProcessorPromise;
            }
            let geometryArrayBuffer = await this.fetchGeometry(loaderElement);
            if (geometryArrayBuffer.byteLength === 0)
                return true;
            const allMeshDescriptors = loaderElement.index.meshDescriptors.filter((md) => md !== undefined);
            if (this._geometryProcessor) {
                // Backfill binary hashes for every descriptor — V1 and V2 alike — when WASM
                // is enabled. The WASM cache looks meshes up by binary hash, and several
                // entry points (V1+WASM mesh deflate, V2+WASM readEdges, IDX JSON loaders
                // that don't carry md.bh) all rely on this being set. Safe to run on every
                // load: decodeBase64Hash is cheap and idempotent.
                for (const md of allMeshDescriptors) {
                    if (md.bh === undefined) {
                        md.bh = decodeBase64Hash(md.h);
                    }
                }
                this.registerDescriptorsWithProcessor(geometryArrayBuffer, allMeshDescriptors, isV2Format);
                geometryArrayBuffer = undefined;
                this.registerTransformsWithProcessor(loaderElement, loaderElement.index.transforms);
            }
            else {
                const meshDescriptorsToLoad = allMeshDescriptors.filter((md) => {
                    if (this._requestedMeshDescriptorHashes.has(md.h)) {
                        const cached = this._vertexDataBlock.getVertexData(md);
                        if (cached) {
                            const cachedP = cached.positions.length / 3;
                            const cachedI = cached.indices.length;
                            if (md.p < cachedP) {
                                this._vertexDataBlock.removeVertexData(md.h);
                                return true;
                            }
                            if (md.p !== cachedP || md.i !== cachedI) {
                                md.p = cachedP;
                                md.i = cachedI;
                            }
                        }
                        return false;
                    }
                    this._requestedMeshDescriptorHashes.add(md.h);
                    return true;
                });
                if (isV2Format) {
                    const fileReader = new T8gdReader(geometryArrayBuffer);
                    if (!fileReader.isValid) {
                        throw new Error('Invalid T8GD file format');
                    }
                    await this._vertexDataBlock.importPackedVertexDataV2(fileReader, meshDescriptorsToLoad);
                }
                else {
                    const vertexReader = new BimVertexDataReader(geometryArrayBuffer);
                    await this._vertexDataBlock.importPackedVertexData(vertexReader, meshDescriptorsToLoad);
                }
            }
            return true;
        })();
        this._loadedUrls.set(key, fetchPromise);
        return fetchPromise;
    }
    get sizeInBytes() {
        return this._vertexDataBlock.byteSize;
    }
    get wasmMemoryUsage() {
        return this._geometryProcessor?.getMemoryUsage() ?? { used: 0, capacity: 0, allocated: 0, outputBuffers: 0 };
    }
    clear() {
        this._vertexDataBlock.reset();
        this._requestedMeshDescriptorHashes.clear();
        this._v2FormatMeshHashes.clear();
        this._meshDescriptorByHash.clear();
        this._loadedUrls.clear();
        this._geometryProcessor?.clearCache();
    }
    dispose() {
        this.clear();
        this._geometryProcessor?.dispose();
        this._geometryProcessor = undefined;
        this._geometryProcessorPromise = undefined;
    }
    static _emptyVertexData = {
        positions: new Float32Array(0),
        packedNormals: new Uint16Array(0),
        indices: new Uint32Array(0)
    };
    get(md) {
        if (md.i === 0 || md.p === 0) {
            return BimVertexDataRepository._emptyVertexData;
        }
        if (this._geometryProcessor) {
            const isV2 = this._v2FormatMeshHashes.has(md.h);
            const wasmData = this._geometryProcessor.deflate(md.bh, md.p, md.i, isV2);
            if (wasmData) {
                return {
                    positions: wasmData.positions,
                    packedNormals: wasmData.normals,
                    indices: wasmData.indices,
                    normalType: isV2 ? NormalType.PerFace : NormalType.PerVertex
                };
            }
        }
        const ret = this._vertexDataBlock.getVertexData(md);
        if (ret === undefined)
            throw new Error(`BimProductMeshDescriptor with hash ${md.h} is not registered.`);
        return ret;
    }
    write(ifcProductMesh, transform, destination, offset, aabb, options) {
        const includeNormals = options.includeNormals ?? false;
        const data = this.get(ifcProductMesh.descriptor);
        if (includeNormals && data.normalType === NormalType.PerFace) {
            return this.writePerFaceNormals(data, ifcProductMesh.colorTexturePixelIndex, transform, destination, offset, aabb);
        }
        const { colorTexturePixelIndex } = ifcProductMesh;
        let indiceOffset = offset.indices;
        let positionsOffset = offset.positions * 3;
        let uvOffset = offset.positions * 2;
        const posLen = data.positions.length;
        // This inlining of min/max calculation is intentional for performance reasons
        // Using a function to modify the aabb in the loop adds roughly 500ms in a larger building
        let minX = aabb.min.x;
        let minY = aabb.min.y;
        let minZ = aabb.min.z;
        let maxX = aabb.max.x;
        let maxY = aabb.max.y;
        let maxZ = aabb.max.z;
        for (let vI = 0, pNi = 0; vI < posLen; vI += 3, pNi++, positionsOffset += 3) {
            transform.transformInPlace(data.positions[vI], data.positions[vI + 1], data.positions[vI + 2], destination.positions, positionsOffset);
            destination.uvs[uvOffset++] = colorTexturePixelIndex;
            if (includeNormals) {
                transform.rotatePackedNormalInPlace(data.packedNormals[pNi], destination.uvs, uvOffset);
            }
            uvOffset++;
            const x = destination.positions[positionsOffset];
            const y = destination.positions[positionsOffset + 1];
            const z = destination.positions[positionsOffset + 2];
            if (minX > x)
                minX = x;
            if (minY > y)
                minY = y;
            if (minZ > z)
                minZ = z;
            if (maxX < x)
                maxX = x;
            if (maxY < y)
                maxY = y;
            if (maxZ < z)
                maxZ = z;
        }
        aabb.min.x = minX;
        aabb.min.y = minY;
        aabb.min.z = minZ;
        aabb.max.x = maxX;
        aabb.max.y = maxY;
        aabb.max.z = maxZ;
        for (let i = 0; i < data.indices.length; ++i) {
            destination.indices[indiceOffset] = offset.positions + data.indices[i];
            indiceOffset++;
        }
        return { vertexCount: posLen / 3, indexCount: data.indices.length };
    }
    writePerFaceNormals(data, colorTexturePixelIndex, transform, destination, offset, aabb) {
        const indices = data.indices;
        const indexCount = indices.length;
        const faceCount = indexCount / 3;
        if (faceCount === 0) {
            return { vertexCount: 0, indexCount: 0 };
        }
        // Deduplicate (vertexIndex, packedNormal) pairs, build remapped index buffer,
        // and write positions/UVs in a single pass.
        // Key encoding: vertexIndex * 65536 + packedNormal. Safe because packedNormal is Uint16 (max 65535)
        // and vertex indices below 2^37 keep the product within float64 safe integer range (2^53).
        const vertexNormalToOutputIndex = new Map();
        let nextOutputIndex = 0;
        let indiceOffset = offset.indices;
        let minX = aabb.min.x;
        let minY = aabb.min.y;
        let minZ = aabb.min.z;
        let maxX = aabb.max.x;
        let maxY = aabb.max.y;
        let maxZ = aabb.max.z;
        for (let face = 0; face < faceCount; face++) {
            const packedNormal = data.packedNormals[face];
            const baseIdx = face * 3;
            for (let v = 0; v < 3; v++) {
                const vertexIndex = indices[baseIdx + v];
                const key = vertexIndex * 65536 + packedNormal;
                let outputIndex = vertexNormalToOutputIndex.get(key);
                if (outputIndex === undefined) {
                    outputIndex = nextOutputIndex++;
                    vertexNormalToOutputIndex.set(key, outputIndex);
                    const srcOffset = vertexIndex * 3;
                    const dstPosOffset = (offset.positions + outputIndex) * 3;
                    let uvOffset = (offset.positions + outputIndex) * 2;
                    transform.transformInPlace(data.positions[srcOffset], data.positions[srcOffset + 1], data.positions[srcOffset + 2], destination.positions, dstPosOffset);
                    destination.uvs[uvOffset++] = colorTexturePixelIndex;
                    transform.rotatePackedNormalInPlace(packedNormal, destination.uvs, uvOffset);
                    const x = destination.positions[dstPosOffset];
                    const y = destination.positions[dstPosOffset + 1];
                    const z = destination.positions[dstPosOffset + 2];
                    if (minX > x)
                        minX = x;
                    if (minY > y)
                        minY = y;
                    if (minZ > z)
                        minZ = z;
                    if (maxX < x)
                        maxX = x;
                    if (maxY < y)
                        maxY = y;
                    if (maxZ < z)
                        maxZ = z;
                }
                destination.indices[indiceOffset++] = offset.positions + outputIndex;
            }
        }
        aabb.min.x = minX;
        aabb.min.y = minY;
        aabb.min.z = minZ;
        aabb.max.x = maxX;
        aabb.max.y = maxY;
        aabb.max.z = maxZ;
        return { vertexCount: nextOutputIndex, indexCount };
    }
    writeBatch(meshes, destination, offset) {
        if (!this._geometryProcessor || meshes.length === 0) {
            return undefined;
        }
        const center = meshes[0].centerOfParentIfcCube;
        if (!center) {
            return undefined;
        }
        const requests = [];
        for (const mesh of meshes) {
            if (mesh.descriptor.i === 0 || mesh.descriptor.p === 0) {
                continue;
            }
            const indexUrl = mesh.loaderElement.indexUrl?.href ?? '';
            const urlId = this._geometryProcessor.getOrCreateUrlId(indexUrl);
            requests.push({
                hash: mesh.descriptor.h,
                binaryHash: mesh.descriptor.bh,
                transformGroup: urlId,
                transformIndex: mesh.wt,
                colorIndex: mesh.colorTexturePixelIndex,
                centerX: center.x,
                centerY: center.y,
                centerZ: center.z
            });
        }
        const result = this._geometryProcessor.processBatchInto(requests, {
            positions: destination.positions,
            uvs: destination.uvs,
            indices: destination.indices,
            positionOffset: offset.positions * 3,
            uvOffset: offset.positions * 2,
            indexOffset: offset.indices,
            initialVertexOffset: offset.positions
        });
        if (!result) {
            return undefined;
        }
        return {
            aabb: result.aabb,
            vertexCount: result.vertexCount,
            indexCount: result.indexCount
        };
    }
    registerTransformsWithProcessor(loaderElement, transforms) {
        if (!this._geometryProcessor) {
            return;
        }
        const indexUrl = loaderElement.indexUrl?.href ?? '';
        this._geometryProcessor.registerTransforms(indexUrl, transforms);
    }
    registerDescriptorsWithProcessor(geometryArrayBuffer, meshDescriptors, isV2Format) {
        if (!this._geometryProcessor) {
            return;
        }
        if (isV2Format) {
            const newDescriptors = [];
            for (const md of meshDescriptors) {
                const existing = this._meshDescriptorByHash.get(md.h);
                if (!existing || md.p < existing.p) {
                    this._requestedMeshDescriptorHashes.add(md.h);
                    this._v2FormatMeshHashes.add(md.h);
                    this._meshDescriptorByHash.set(md.h, md);
                    newDescriptors.push({ hash: md.h, vertexCount: md.p, indexCount: md.i });
                }
            }
            this._geometryProcessor.addGeometryBuffer(geometryArrayBuffer, newDescriptors);
        }
        else {
            const descriptorsV1 = [];
            for (const md of meshDescriptors) {
                const existing = this._meshDescriptorByHash.get(md.h);
                if (existing && md.p >= existing.p) {
                    continue;
                }
                this._requestedMeshDescriptorHashes.add(md.h);
                this._meshDescriptorByHash.set(md.h, md);
                descriptorsV1.push({
                    hash: md.h,
                    byteOffset: md.obd,
                    byteSize: md.sbd,
                    vertexCount: md.p,
                    indexCount: md.i
                });
            }
            this._geometryProcessor.addGeometryBufferLegacy(geometryArrayBuffer, descriptorsV1);
        }
    }
    async fetchGeometry(loaderElement) {
        const properties = {
            url: loaderElement.ifc.metadata.serverRelativeUrl,
            version: loaderElement.ifc.version,
            id: loaderElement.ifc.id,
            discipline: loaderElement.ifc.discipline.short,
            httpStatus: -1,
            byteSize: -1
        };
        const event = telemetry.startTrackEvent('Fetched IFC geometry');
        try {
            const { status, value } = await this._bimApiClient.get(loaderElement.geometryUrl, HttpResponseType.arrayBuffer);
            properties.httpStatus = status;
            const geometryArrayBuffer = status === 200 ? await value : new ArrayBuffer(0);
            properties.byteSize = geometryArrayBuffer.byteLength;
            return geometryArrayBuffer;
        }
        finally {
            event.stop(properties);
        }
    }
}
//# sourceMappingURL=bim-vertex-data-repository.js.map