import { GeometryBuffer } from './GeometryBuffer';
import { VertexDataMerge } from './vertex-data-merge';
import { telemetry } from '../Telemetry';
import { StopWatch, yieldThreadFor } from './stopwatch';
import { MergeIdBuilder } from './MergeIdBuilder';
import { CubeCellMergeIdAlgorithm } from './CubeCellMergeIdAlgorithm';
import { BimIfcClass } from './bim-ifc-class';
/**
 * Represents the build event data for the {@link IfcGeometryBuilder}.
 */
export class GeometryBuilderBuildEventData {
    totalGeometryCount;
    totalIndiceCount;
    /** @hidden */
    _geometry;
    processedIndiceCount = 0;
    /**
     * Creates an instance of GeometryBuilderBuildEventData.
     * @param totalGeometryCount The total number of geometries being built.
     * @param totalIndiceCount The total number of indices being processed.
     */
    constructor(totalGeometryCount, totalIndiceCount) {
        this.totalGeometryCount = totalGeometryCount;
        this.totalIndiceCount = totalIndiceCount;
    }
    /**
     * Gets currently built geometry
     */
    get geometry() {
        return this._geometry;
    }
    /**
     * Gets the progress of the build operation as a percentage.
     */
    get progress() {
        return Math.floor((this.processedIndiceCount / this.totalIndiceCount) * 100);
    }
}
/**
 * Represents a builder for creating geometries from IFC product meshes.
 */
export class IfcGeometryBuilder {
    geometryBuildId;
    loaderElements;
    _mergeIds = [];
    _ifcProductMeshCount = 0;
    _geometryCount = 0;
    _deadline = new StopWatch(true);
    _totalIndiceCount = 0;
    _geometryRefs = [];
    _renderControl;
    static _categoriesToExcludeFromCameraMoveCulling = new Set([
        'beam',
        'columns',
        'walls',
        'roof',
        'floor',
        'space',
        'stairs',
        'windows',
        'doors',
        'covering'
    ]);
    /**
     * Creates an instance of IfcGeometryBuilder.
     * @param geometryBuildId The ID of the geometry build.
     * @param loaderElements The loader elements to build geometries from.
     * @param options The options for the geometry builder.
     */
    constructor(geometryBuildId, loaderElements, renderControl) {
        this.geometryBuildId = geometryBuildId;
        this.loaderElements = loaderElements;
        // If renderControl is not provided, we create a default one that does nothing.
        this._renderControl = renderControl ?? { isPaused: false, render: () => { } };
    }
    /**
     * Options to control for how long the geometry builder behavior.
     */
    static options = { deadlineMs: 200, sleepInMs: 4 };
    /**
     * Creates an instance of {@link IfcGeometryBuilder}.
     * @remarks
     * The create method will traverse all {@link BimProduct} instances provided by
     * `o.loaderElements.entries()`.
     * Each {@link BimProduct} that has geometry, not all products do, has N {@link BimProductMesh} instances.
     * The {@link create} method will visit each such instance and assign a `mergeId` to each of them. This `mergeId`
     * will be identical for {@link BimProductMesh} instances that shall be merged into the same {@link Geometry3d} instance when {@link build} is called.
     * {@link BimProductMesh} instances that alredy have a `mergeId` will be ignored as it is assumed that has already been processed
     * by another {@link IfcGeometryBuilder} instance. It is also possible to ignore certain {@link BimProduct} instances by providing a
     * predicate function in `o.predicate`. This will allow the caller to filter out products that should not be included in the geometry creation.
     * For example, one might want to only include IFC products that are of a certain type or have a certain property set. Specifying a predicate function allows for this.
     *
     * When a {@link IfcGeometryBuilder} instance has been created, the caller can call {@link build} to start the geometry creation process. This is a very
     * CPU intensive process.
     *
     * One should always attempt to use as few {@link IfcGeometryBuilder} instances, to create {@link Geometry3d} instances, as possible.
     * The more {@link Geometry3d} instances that we add to {@link TwinfinityViewer}, by calling {@link TwinfinityViewer.addOrReplaceMesh}, the worse rendering
     * performance we will get. This is because the more geometries we have the more draw calls we will have. The more draw calls we have the worse performance we will get.
     * There is no fixed number of geometries that is the limit. It depends on the hardware and the complexity of the geometries.
     * @param o - The options for creating the geometry builder.
     * @returns A promise that resolves to an instance of `IfcGeometryBuilder`.
     */
    static async create(o) {
        const { loaderElements, predicate } = o;
        o = { ...o };
        o.cullOnCameraMovePredicate ??= IfcGeometryBuilder.defaultCullOnCameraMovePredicate;
        telemetry.trackTrace({ message: `Fetch vertexdata and build merge groups...` });
        const fetchAndBuildEvent = telemetry.startTrackEvent('Fetched vertex data and build merge groups');
        const loaderElementsWithVertexData = new Map();
        // Calculate a set of mergedMeshDescriptors. Basically we just group all items that should be merged
        // into one mergedMeshDescriptor. Ie we get one descriptor per merge group.
        // Only keep data where products have not yet been loaded.
        const mergeGroupEvent = telemetry.startTrackEvent('Merge groups built');
        const mergeIdBuilder = new MergeIdBuilder((id) => new CubeCellMergeIdAlgorithm(id));
        const geometryBuilder = new IfcGeometryBuilder(mergeIdBuilder.id, loaderElements, o.renderControl);
        try {
            for (const loaderElement of loaderElements) {
                loaderElementsWithVertexData.getOrAdd(loaderElement, (lE) => lE.ensureVertexDataLoaded());
            }
            geometryBuilder._mergeIds = await mergeIdBuilder.mergeIds({
                ...IfcGeometryBuilder.options,
                cullOnCameraMovePredicate: o.cullOnCameraMovePredicate,
                loaderElements,
                predicate
            });
            geometryBuilder._geometryCount = geometryBuilder._mergeIds.length;
            for (const mergeId of geometryBuilder._mergeIds) {
                // Guaranteed that mergeIds returned always have > 0 meshes.
                geometryBuilder._ifcProductMeshCount += mergeId.ifcProductMeshes.length;
                geometryBuilder._totalIndiceCount += mergeId.indiceCount;
            }
        }
        finally {
            mergeGroupEvent.stop({
                _ifcProductMeshCount: geometryBuilder._ifcProductMeshCount,
                bjsMeshCount: geometryBuilder._mergeIds.length
            });
        }
        if (loaderElementsWithVertexData.size > 0) {
            await Promise.all([...loaderElementsWithVertexData.values()]);
        }
        fetchAndBuildEvent.stop();
        return geometryBuilder;
    }
    /**
     * Default {@link CullOnCameraMovePredicate} implementation.
     *
     * Keeps structurally important meshes (walls, floors, beams, etc.) and
     * low-polygon objects visible during camera movement. These exemptions
     * only apply when the mesh has no distance-based culling — meshes that
     * are already distance-culled are always movement-culled as well.
     */
    static defaultCullOnCameraMovePredicate(mesh, context) {
        const triangleCountCutoff = 1000;
        return !((IfcGeometryBuilder._categoriesToExcludeFromCameraMoveCulling.has(mesh.ifcProduct.class.type) ||
            (mesh.descriptor.i / 3 < triangleCountCutoff &&
                mesh.ifcProduct.class.type !== 'flow' &&
                mesh.ifcProduct.class !== BimIfcClass.predefined.electricDistributionPoint)) &&
            (context?.distance ?? -1) < 0);
    }
    /**
     * The number of IFC product meshes this builder will build geometries for.
     * @returns The number of meshes.
     */
    get ifcProductMeshCount() {
        return this._ifcProductMeshCount;
    }
    /**
     * The number of geometries that will be built when {@link build} is called.
     * @returns The number of geometries.
     */
    get geometryCount() {
        return this._geometryCount;
    }
    /**
     * Gets the number of indices in the geometry. The number of triangles in the geometry is this number divided by 3.
     * @returns The total number of indices.
     */
    get indiceCount() {
        return this._totalIndiceCount;
    }
    /**
     * Builds {@link Geometry3d} instance from all {@link BimProduct} instances that was included during the call to {@link create}.
     * This is a very CPU intensive operation.
     *
     * @remarks
     * Only access the {@link Geometry3d} instance inside the action callback. The {@link Geometry3d} instance, and all data accessable trhoug it,
     * will be invalid after the action callback has been invoked.
     * Each {@link Geometry3d} instance will contain the geometry from all {@link BimProductMesh} instances that share the same `mergeId`.
     *
     * @param action - The action to be invoked for each built {@link Geometry3d} instance. Call {@link TwinfinityViewer.addOrReplaceMesh} to add the geometry to the viewer.
     * The `eventData` parameter contains both the current {@link Geometry3d} instance that was built and information such as the current progress of the build operation.
     * See {@link GeometryBuilderBuildEventData} for more information.
     * @returns A promise that resolves to an array of {@link Geometry3dHandle} objects. These can be used to unload the geometries from {@link TwinfinityViewer}
     * by calling {@link TwinfinityViewer.removeMesh}.
     */
    async build(action) {
        if (this._ifcProductMeshCount === 0 || this._mergeIds.length === 0) {
            this.loaderElements.forEach((lE) => lE.transformsRepository.clear());
            return this._geometryRefs;
        }
        let totalIfcProductMeshCount = 0;
        const meshMergeSw = new StopWatch(false);
        const actionInvokeSw = new StopWatch(false);
        telemetry.trackTrace({ message: `Mesh merging...` });
        const event = telemetry.startTrackEvent('Mesh merging');
        const { deadlineMs: deadline, sleepInMs } = IfcGeometryBuilder.options;
        // Calculate approximately how many deadlines we need to pass before we force a render to show progress.
        const forceRenderAfterNDeadlines = 2000.0 / Math.max(1, deadline);
        this._deadline.resetAndStart();
        const eventData = new GeometryBuilderBuildEventData(this.geometryCount, this._totalIndiceCount);
        const vertexDataMergeOptionsTemplate = {
            // We don't need normals for ifc product meshes because
            // we calculate them in the fragment shader. This is a huge performance boost
            // since we can skip transforming them.
            includeNormals: false,
            // We want to use the transform cache for better performance. There are often many repeated transforms.
            // and a lot of geometry to transform.
            noTransformCache: false
        };
        const wasPaused = this._renderControl.isPaused;
        this._renderControl.isPaused = true;
        let elapsedDeadlinesCount = 0;
        try {
            let processedGeometryCount = 0;
            // Allocate a single GeometryBuffer instance which represents the largest geometry that will
            // be created. This instance will be reused when creating all geometries to avoid unnessecary memory allocation
            // which reduces performance drastically. maxPrimitive/IndiceCount is calculated before
            // the geometryMemoryBlockSingletonFactory is called by VertexDataMerge (happens when VertexDataMerge.merge() is called)
            const geometryBuffer = new GeometryBuffer();
            for (const mergeId of this._mergeIds) {
                geometryBuffer.maximizeLength(mergeId.primitiveCount, mergeId.indiceCount);
            }
            // Method to allow UI thread to breathe to avoid blocking it for too long which would result in a frozen UI.
            // This is especially important when building large geometries.
            const yieldUiThreadAndRenderIfNeeded = async (deadline) => {
                // Let UI thread breathe to avoid blocking it for too long which would result in a frozen UI.
                // This is especially important when building large geometries.
                await yieldThreadFor(sleepInMs);
                deadline.resetAndStart();
                // Periodically render to show progress. This is especially important when building large geometries
                // to avoid the appearance of a frozen 3D viewer.
                if (elapsedDeadlinesCount++ % forceRenderAfterNDeadlines === 0) {
                    this._renderControl.render();
                }
            };
            for (const mergeId of this._mergeIds) {
                meshMergeSw.resume();
                const vDm = new VertexDataMerge(mergeId.id, geometryBuffer, mergeId.ifcProductMeshes, {
                    ...vertexDataMergeOptionsTemplate,
                    cullOnCameraMove: mergeId.cullOnCameraMove
                });
                const geometry = vDm.merge(); // Super CPU intensive operation
                meshMergeSw.stop();
                if (this._deadline.elapsed > deadline) {
                    await yieldUiThreadAndRenderIfNeeded(this._deadline);
                }
                totalIfcProductMeshCount += mergeId.ifcProductMeshes.length;
                actionInvokeSw.resume();
                eventData.processedIndiceCount += geometry.indices.length;
                eventData._geometry = geometry;
                if (eventData.totalGeometryCount === ++processedGeometryCount) {
                    eventData.processedIndiceCount = eventData.totalIndiceCount;
                }
                action(eventData); // Could be a very slow operation. We dont know so we measure it.
                this._geometryRefs.push(geometry.toHandle());
                actionInvokeSw.stop();
                if (this._deadline.elapsed > deadline) {
                    await yieldUiThreadAndRenderIfNeeded(this._deadline);
                }
                mergeId.ifcProductMeshes.length = 0; // Free up memory
            }
            this._mergeIds.length = 0; // Free up memory
        }
        finally {
            this._renderControl.isPaused = wasPaused;
            event.stop({
                geometryCount: this.geometryCount,
                ifcProductCount: totalIfcProductMeshCount,
                geometryMergeDuration: meshMergeSw.totalElapsed,
                actionInvokeDuration: actionInvokeSw.totalElapsed
            });
        }
        // Get rid of all transforms. We do not need them now and they take up memory.
        this.loaderElements.forEach((lE) => lE.transformsRepository.clear());
        return this._geometryRefs;
    }
}
//# sourceMappingURL=IfcGeometryBuilder.js.map