import { BimVertexData, BimProductMeshDescriptor, SimpleBoundingBox } from './bim-format-types';
import { BimIfcIndex, IfcFileIndex } from './BimIfcIndex';
import { BimChangeIfc } from './bim-api-client';
import { BimVertexDataRepository, WriteBatchResult } from './bim-vertex-data-repository';
import { BimPropertySetRepository } from './bim-property-set-repository';
import { BimTransformsRepository } from './bim-transforms-repository';
import { BimIfcObject, BimIfcSpace, BimIfcBuildingStorey } from './bim-ifc-object';
import { Discipline } from './discipline';
import { Materials } from './materials';
import { BimIfcStyle } from './bim-ifc-style';
import { BimIfcClass } from './bim-ifc-class';
import { BimIfcLoaderElementParent } from './bim-ifc-loader';
import { BimApiIfcObjectsLoadOptions } from '../BimApiLoadOptions';
import { BimCoreApiClient } from './client/BimCoreApiClient';
import { IfcFileIndexReader, IfcFileIndexReadFailure } from './IfcFileIndexReader';
import { IfcProductMesh } from './IfcProductMesh';
import { FastTransform } from '../math/FastTransform';
import { GeometryArrayOffset, GeometryArrays } from './GeometryBuffer';
import { Matrix, Vector3 } from './babylonjs-maths-import';
import { BimTypeObjectRepository } from './BimTypeObjectRepository';
import { BimObjectAttribute } from './BimObjectAttribute';
import { BimObjectAttributeReader } from './BimObjectAttributeReader';
import { Fail, Failure, isFailure } from '../fail';
import { BimProductMesh } from './BimProductMesh';
import { BimGeometricFeaturesRepository } from './bim-geometric-features-repository';
import { T8BimIfcLoaderElement, T8BimIfcLoaderElementCreateFailure } from './T8BimIfcLoaderElement';

export type GeometryFormat = 'geom' | 't8gd';

/**
 * Represents an IFC file and provides access to its data.
 */
export interface IBimIfcLoaderElement {
    /**
     * The total number of vertices contained in all instances of {@link BimProductMesh}
     * in this IFC file.
     */
    readonly numberOfVertices: number;

    /**
     * The total number of indices contained in all instances of {@link BimProductMesh}
     * in this IFC file.
     */
    readonly numberOfIndices: number;

    /**
     * The total number of triangles contained in all instances of {@link BimProductMesh}
     * in this IFC file.
     */
    readonly numberOfTriangles: number;

    /**
     * All {@link BimIfcClass} instances referenced by {@link BimProduct}'s in the IFC file this instance represents.
     */
    readonly classes: BimIfcClass[];

    /**
     * The IfcProject instance of the IFC file this instance represents.
     */
    readonly project: BimIfcObject;

    /**
     * The format that the geometry is stored in.
     */
    readonly geometryFormat: GeometryFormat;

    /**
     * The total number of {@link BimProductMesh} instances in the IFC file this instance represents.
     * Same as {@link ifcProductsWithGeometryCount}.
     */
    readonly meshCount: number;

    /**
     * The total number of {@link BimProductMesh} instances in the IFC file this instance represents.
     * Same as {@link meshCount}.
     */
    readonly ifcProductsWithGeometryCount: number;
    /**
     * All IfcSpace ({@link BimIfcSpace}) instances in the IFC file this instance represents.
     */
    readonly spaces: BimIfcSpace[];

    /**
     * All IfcBuildingStorey ({@link BimIfcBuildingStorey}) instances in the IFC file this instance represents.
     */
    readonly floors: BimIfcBuildingStorey[];

    /**
     * All IFC products in the IFC file this instance represents. Here represented as a flat list.
     */
    readonly products: BimIfcObject[];

    /**
     * The discipline of the IFC file this instance represents.
     */
    readonly discipline: Discipline;

    /**
     * The IFC index (.idx file)  of the IFC file this instance represents. This describes (in a very raw format)
     * the content of the IFC file. It does not include any geometry or property sets.
     */
    readonly index: BimIfcIndex;

    /**
     * Url where the IFC index (.idx file) will be retrieved from.
     */
    readonly indexUrl: URL | undefined;

    /**
     * Url where the IFC geometry (.geom file) will be retrieved from.
     */
    readonly geometryUrl: URL | undefined;

    /**
     * Url where the IFC property set (.prop file) will be retrieved from.
     */
    readonly propertiesUrl: URL | undefined;

    /**
     * Url where the IFC geometric features file (.geom-feature file) will be retrieved from.
     */
    readonly geometricFeaturesUrl: URL | undefined;

    /**
     * Reference to the transform repository this instance uses when
     * transforming IFC product geometries.
     */
    readonly transformsRepository: BimTransformsRepository;

    /**
     * Reference to the type object repository this instance uses for type properties.
     */
    readonly typeObjectRepository: BimTypeObjectRepository;

    /**
     * Reference to a {@link Materials} instance.
     */
    readonly materials: Materials;

    /**
     * All IFC styles that the {@link BimProductMesh} instances in the IFC file refers to.
     * The styles are used to color the meshes.
     */
    readonly styles: BimIfcStyle[];

    /**
     * The url where the contents of the IFC file can be retrieved from.
     */
    readonly ifcUrl: URL;

    /**
     * The name of the IFC file this instance represents.
     */
    readonly ifcName: string;

    /**
     * The {@link BimApiIfcObjectsLoadOptions} options (if any) that was used when this instance was created.
     */
    readonly loadOptions?: BimApiIfcObjectsLoadOptions;

    /**
     * The loader this instance belongs to.
     */
    readonly loader: BimIfcLoaderElementParent;

    /**
     * The IFC change (file) this instance represents.
     */
    readonly ifc: BimChangeIfc;

    /**
     * An optional transform for the IFC file. This transform will be applied to all IFC product geometries
     */
    readonly transform?: Matrix;

    /**
     * Ensures that the vertex data is loaded.
     * @returns A promise that resolves to a boolean. See it as a signal. I will be true if the call actualy loaded the data. Otherwhise it will be false (data loading is either in progress or has completed).
     */
    ensureVertexDataLoaded(): Promise<boolean>;

    /**
     * Gets the vertex data for a given product mesh descriptor.
     * @param productMeshDescriptor - The product mesh descriptor.
     * @returns The vertex data.
     */
    getVertexData(productMeshDescriptor: BimProductMeshDescriptor): BimVertexData;

    /**
     * Writes the vertex data to the specified destination.
     * @param ifcProductMesh - The IFC product mesh.
     * @param transform - The transformation matrix.
     * @param destination - The destination geometry arrays.
     * @param offset - The offset in the geometry arrays.
     * @param aabb - The axis-aligned bounding box.
     * @returns The actual vertex and index counts written.
     */
    writeVertexData(
        ifcProductMesh: BimProductMesh,
        transform: FastTransform,
        destination: GeometryArrays,
        offset: GeometryArrayOffset,
        aabb: { min: Vector3; max: Vector3 },
        options: { includeNormals?: boolean }
    ): { vertexCount: number; indexCount: number };

    /**
     * Whether batch vertex data writing is supported via WASM.
     */
    readonly hasBatchSupport: boolean;

    /**
     * WASM memory usage in bytes.
     */
    readonly wasmMemoryUsage: { used: number; capacity: number; allocated: number; outputBuffers: number };

    /**
     * Writes vertex data for multiple meshes in a single batch operation using WASM.
     * Falls back to undefined if batch support is not available.
     * @param meshes - The meshes to write.
     * @param destination - The destination geometry arrays.
     * @param offset - The offset in the geometry arrays.
     * @returns The batch result containing AABB and counts, or undefined if batch is not available.
     */
    writeBatch(
        meshes: BimProductMesh[],
        destination: GeometryArrays,
        offset: GeometryArrayOffset
    ): WriteBatchResult | undefined;

    /**
     * Loads the property sets and assigns them to the IFC products contained within this instance.
     * @returns A promise that resolves to a boolean indicating whether the property set was loaded
     */
    loadPropertySets(): Promise<boolean>;

    /**
     * Loads the geometric feature data and assigns them to the IFC product meshes contained within this instance.
     * @returns A promise that resolves to a boolean indicating whether the geometric feature data was loaded
     *
     * @internal
     * @hidden
     */
    loadGeometricFeatures(): Promise<boolean>;

    /**
     * Access the geometric features repository for per-mesh edge resolution.
     * @internal
     * @hidden
     */
    readonly geometricFeaturesRepository: BimGeometricFeaturesRepository;

    /**
     * Calculates, or extends a existing, axis-aligned bounding box (AABB) of the regions in the IFC file this instance represents.
     * The provided min, and max values will be updated to contain the min and max values of the AABB.
     * @param min Will be set to min vector of the AABB after call. If not part of existing AABB it must be set to (Number.MAX_VALUE, Number.MAX_VALUE, Number.MAX_VALUE)
     * @param max Will be set to max vector of the AABB after call. If not part of existing AABB it must be set to (Number.MIN_VALUE, Number.MIN_VALUE, Number.MIN_VALUE)
     * @param o Optional. Options object. If `onlyMostPopulatedRegions` is set to `true` then only the most populated regions will be considered.
     * @returns `true` if a valid AABB was calculated. Otherwise `false`. If `false` is returned then the min and max values will not be updated.
     * @example
     * ```typescript
     * // Example of how to calculate a AABB that encompasses the most populated regions of two IFC files.
     * const min = setMax(Vector3.Zero());
     * const max = setMin(Vector3.Zero());
     * // min and max will now contain the min and max values of the AABB encompassing the mostly populated region in loaderElementA.
     * loaderElementA.tryCreateOrExtendAABBFromRegions(min, max, { onlyMostPopulatedRegions: true});
     * // min and max will now contain the min and max values of the AABB that encompasses both loaderElementA and loaderElementB.
     * loaderElementB.tryCreateOrExtendAABBFromRegions(min, max, { onlyMostPopulatedRegions: true});
     * ```
     */
    tryCreateOrExtendAABBFromRegions(min: Vector3, max: Vector3, o?: { onlyMostPopulatedRegions?: boolean }): boolean;
}

/**
 * Represents a predicate function used to query BIM products.
 * The function takes a `loaderElement` of type {@link BimIfcLoaderElement} and a `bimIfcObject` of type {@link BimIfcObject} as parameters,
 * and returns a boolean value indicating whether the object satisfies the query condition.
 */
export interface BimProductQueryPredicate {
    (loaderElement: BimIfcLoaderElement, bimIfcObject: BimIfcObject): boolean;
}

/**
 * Represents the failure result when creating a {@link BimIfcLoaderElement} fails.
 */
export type BimIfcLoaderElementCreateFailure = Failure<
    {
        /**
         * Status of the HTTP request that failed.
         */
        httpStatus: number;
        /**
         * Gives the reason for the failure.
         */
        reason: string;
    } & { ifc: Pick<BimChangeIfc, 'id' | 'version' | 'url'> }
>;

/**
 * Represents an IFC file and provides access to its data.
 */
export class BimIfcLoaderElement implements IBimIfcLoaderElement {
    private _numberOfVertices = 0;
    private _numberOfIndices = 0;
    private _ifcUrl: URL;
    private readonly _objectAttributes: BimObjectAttribute[];

    /** @inheritDoc */
    public readonly transformsRepository: BimTransformsRepository;

    /** @inheritDoc */
    public readonly typeObjectRepository: BimTypeObjectRepository;

    /** @inheritDoc */
    public readonly project: BimIfcObject;

    /** @inheritDoc */
    public readonly spaces: BimIfcSpace[] = [];

    /** @inheritDoc */
    public readonly floors: BimIfcBuildingStorey[] = [];

    /** @inheritDoc */
    public readonly products: BimIfcObject[] = [];

    /** @inheritDoc */
    public readonly meshCount: number;

    /** @inheritDoc */
    public readonly discipline: Discipline;

    /** @inheritDoc */
    public readonly styles: BimIfcStyle[] = [];

    /** @inheritDoc */
    public readonly classes: BimIfcClass[] = [];

    /** @inheritDoc */
    public readonly ifcName: string;

    private _propertySetLoadResult?: boolean;
    private _geometricFeaturesPromise?: Promise<boolean>;

    private constructor(
        public readonly loader: BimIfcLoaderElementParent,
        public readonly ifcHttpResource: BimChangeIfc,
        public readonly materials: Materials,
        public readonly index: BimIfcIndex,
        private readonly _propertySetRepository: BimPropertySetRepository,
        private readonly _geometricFeaturesRepository: BimGeometricFeaturesRepository,
        private readonly _vertexDataRepository: BimVertexDataRepository,
        public readonly loadOptions?: BimApiIfcObjectsLoadOptions,
        public readonly transform?: Matrix
    ) {
        this.discipline = ifcHttpResource.discipline;
        this.ifcName = ifcHttpResource.name;
        this.styles = index.styles.map((s) => new BimIfcStyle(s));
        this.classes = index.classes.map((classId) => BimIfcClass.getOrAdd(classId));
        this.transformsRepository = new BimTransformsRepository(this);

        this._ifcUrl = new URL(ifcHttpResource.url);

        this._objectAttributes = new BimObjectAttributeReader().read(this.index);
        this.typeObjectRepository = new BimTypeObjectRepository(this.index, this._objectAttributes);
        this.project = BimIfcObject.create(this);
        const referencedMeshDescriptors = new Set<number>();
        const referencedTransforms = new Set<number>();
        const leafNodesWithNoGeometry = new Set<BimIfcObject>();
        let meshCount = 0;

        for (const o of this.project.entries()) {
            let mD: BimProductMeshDescriptor;
            const len = o.productMeshes?.length ?? 0;
            for (let idx = 0; idx < len; ++idx) {
                const m = IfcProductMesh.from(o, o.productMeshes[idx]);
                o.productMeshes[idx] = m;

                mD = o.meshDescriptor(m);
                // Keep track of all meshdescriptors and transforms that actually are referenced by
                // meshes. We use this to trim away all meshdescriptors and transforms that have no references
                // later on. This is especialy useful in cases where we forcefully set some ifc objects
                // as not having a geometry. That means that we can clean out all geometries that referenced those
                // objects and save RAM.
                referencedMeshDescriptors.add(m.mi);
                referencedTransforms.add(m.wt);
                this._numberOfIndices += mD.i;
                this._numberOfVertices += mD.p;
            }

            if (loadOptions?.removeLeafIfcObjectsWithNoGeometry && o.childCount === 0 && !o.hasGeometry) {
                leafNodesWithNoGeometry.add(o);
            }

            this.discipline.addReferencedClass(o.class);
            (this.discipline.objectCount as number) = this.discipline.objectCount + 1;
            o.class.addReferencedClass(o.discipline);
            (o.class.objectCount as number) = o.class.objectCount + 1;

            if (o.hasGeometry) {
                meshCount += o.productMeshes.length;
                (o.class.objectsWithGeometryCount as number) = o.class.objectsWithGeometryCount + 1;
                (this.discipline.objectsWithGeometryCount as number) = this.discipline.objectsWithGeometryCount + 1;
                if (o.enclosingFloor) {
                    (o.enclosingFloor.objectsWithGeometryCount as number) =
                        o.enclosingFloor.objectsWithGeometryCount + 1;
                }
            }

            this.products.push(o);
            if (o instanceof BimIfcSpace) {
                this.spaces.push(o);
            }
            if (o instanceof BimIfcBuildingStorey) {
                this.floors.push(o);
            }
        }

        this.meshCount = meshCount;

        // Trim away leafnodes that has no geometry and no children
        this.deleteLeafNodesWithNoGeometry(leafNodesWithNoGeometry);

        // Check if we need to remove unreferenced mesh descriptors.
        this.deleteUnreferencedMeshDescriptors(referencedMeshDescriptors);

        this.deleteUnreferencedTransforms(referencedTransforms);
    }

    /**
     * Creates a new instance of the BimIfcLoaderElement class.
     *
     * @param bimApi - The {@link BimCoreApiClient} instance.
     * @param materials - The {@link Materials} instance.
     * @param vertexDataRepository - The {@link BimVertexDataRepository} instance.
     * @param propertySetRepository - The {@link BimPropertySetRepository} instance.
     * @param geometricFeaturesRepository - The {@link BimGeometricFeaturesRepository} instance.
     * @param parent - The {@link BimIfcLoaderElementParent} instance.
     * @param ifcHttpResource - The {@link BimChangeIfc} instance.
     * @param options - Optional. The {@link BimApiIfcObjectsLoadOptions} instance.
     * @param transform - Optional. The `Matrix` instance that will be applied to all transformations in the IFC.
     * @returns A Promise that resolves to a {@link BimIfcLoaderElement} instance or a {@link BimIfcLoaderElementCreateFailure} instance.
     */
    public static async create(
        bimApi: BimCoreApiClient,
        materials: Materials,
        vertexDataRepository: BimVertexDataRepository,
        propertySetRepository: BimPropertySetRepository,
        geometricFeaturesRepository: BimGeometricFeaturesRepository,
        parent: BimIfcLoaderElementParent,
        ifcHttpResource: BimChangeIfc,
        options?: BimApiIfcObjectsLoadOptions,
        transform?: Matrix
    ): Promise<IBimIfcLoaderElement | BimIfcLoaderElementCreateFailure | T8BimIfcLoaderElementCreateFailure> {
        const { id, version, url, resourceUrl } = ifcHttpResource;

        if (resourceUrl.t8cm) {
            return T8BimIfcLoaderElement.create(
                bimApi,
                materials,
                vertexDataRepository,
                parent,
                ifcHttpResource,
                options,
                transform
            );
        }

        let indexResult: IfcFileIndex | IfcFileIndexReadFailure;
        if (resourceUrl.idx) {
            const indexReader = new IfcFileIndexReader(bimApi);
            indexResult = await indexReader.read(resourceUrl.idx, ifcHttpResource.discipline, transform);
        } else {
            return Fail({
                httpStatus: 404,
                reason: 'No resource url.',
                ifc: { id, version, url }
            });
        }

        if (isFailure(indexResult)) {
            return Fail({
                httpStatus: indexResult.httpStatus,
                reason: indexResult.reason,
                ifc: { id, version, url }
            });
        }

        return new BimIfcLoaderElement(
            parent,
            ifcHttpResource,
            materials,
            indexResult,
            propertySetRepository,
            geometricFeaturesRepository,
            vertexDataRepository,
            options,
            transform
        );
    }

    /** @inheritDoc */
    public get ifc(): BimChangeIfc {
        return this.ifcHttpResource;
    }
    /** @inheritDoc */
    public get ifcUrl(): URL {
        return this._ifcUrl;
    }
    /** @inheritDoc */
    public get numberOfVertices(): number {
        return this._numberOfVertices;
    }
    /** @inheritDoc */
    public get numberOfIndices(): number {
        return this._numberOfIndices;
    }
    /** @inheritDoc */
    public get numberOfTriangles(): number {
        return this._numberOfIndices / 3;
    }
    /** @inheritDoc */
    public get ifcProductsWithGeometryCount(): number {
        return this.meshCount;
    }
    /** @inheritDoc */
    public get indexUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl.idx;
    }
    /** @inheritDoc */
    public get geometryUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl.t8gd ?? this.ifcHttpResource.resourceUrl.geom;
    }
    /** @inheritDoc */
    public get propertiesUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl.prop;
    }
    /** @inheritDoc */
    public get geometricFeaturesRepository(): BimGeometricFeaturesRepository {
        return this._geometricFeaturesRepository;
    }

    /** @inheritDoc */
    public get geometricFeaturesUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl['geom-feat'];
    }
    /** @inheritDoc */
    public ensureVertexDataLoaded(): Promise<boolean> {
        return this._vertexDataRepository.load(this);
    }
    /** @inheritDoc */
    public getVertexData(productMeshDescriptor: BimProductMeshDescriptor): BimVertexData {
        return this._vertexDataRepository.get(productMeshDescriptor);
    }

    public get geometryFormat(): GeometryFormat {
        return this.ifcHttpResource.resourceUrl.t8gd != null ? 't8gd' : 'geom';
    }

    /** @inheritDoc */
    public tryCreateOrExtendAABBFromRegions(
        min: Vector3,
        max: Vector3,
        o?: { onlyMostPopulatedRegions?: boolean }
    ): boolean {
        const options = { onlyMostPopulatedRegions: true, ...(o ?? {}) };
        let isValid = false;
        const tmpMin = min.clone();
        const tmpMax = max.clone();
        for (const region of this.index.regions) {
            if (options.onlyMostPopulatedRegions && !region.isMostPopulated) {
                continue;
            }

            const { bbox } = region;
            if (!this.isValidAABB(bbox)) {
                continue; // Ignore all invalid AABBs
            }

            tmpMin.minimizeInPlaceFromFloats(bbox[0], bbox[1], bbox[2]);
            tmpMax.maximizeInPlaceFromFloats(bbox[3], bbox[4], bbox[5]);

            isValid = true;
        }

        if (isValid) {
            min.copyFrom(tmpMin);
            max.copyFrom(tmpMax);
        }
        return isValid;
    }

    /** @inheritDoc */
    public writeVertexData(
        ifcProductMesh: BimProductMesh,
        transform: FastTransform,
        destination: GeometryArrays,
        offset: GeometryArrayOffset,
        aabb: { min: Vector3; max: Vector3 },
        options: { includeNormals?: boolean }
    ): { vertexCount: number; indexCount: number } {
        return this._vertexDataRepository.write(ifcProductMesh, transform, destination, offset, aabb, options);
    }

    /** @inheritDoc */
    public get hasBatchSupport(): boolean {
        return this._vertexDataRepository.hasBatchSupport;
    }

    /** @inheritDoc */
    public get wasmMemoryUsage(): { used: number; capacity: number; allocated: number; outputBuffers: number } {
        return this._vertexDataRepository.wasmMemoryUsage;
    }

    /** @inheritDoc */
    public writeBatch(
        meshes: BimProductMesh[],
        destination: GeometryArrays,
        offset: GeometryArrayOffset
    ): WriteBatchResult | undefined {
        return this._vertexDataRepository.writeBatch(meshes, destination, offset);
    }

    /** @inheritDoc */
    public async loadPropertySets(clearCache = false): Promise<boolean> {
        try {
            if (!this.propertiesUrl) {
                return false;
            }

            if (this._propertySetLoadResult !== undefined) {
                return this._propertySetLoadResult;
            }

            this._propertySetLoadResult = await this._propertySetRepository.assignPropertySetsToProducts(
                this.propertiesUrl,
                this.products,
                this.typeObjectRepository.types
            );
            return this._propertySetLoadResult;
        } finally {
            if (clearCache) {
                this._propertySetRepository.clear();
            }
        }
    }

    /**
     * @internal
     * @hidden
     */
    public loadGeometricFeatures(clearCache = false): Promise<boolean> {
        // Cache the in-flight promise so concurrent callers share one load. Without this,
        // each caller passes the not-loaded check and races into a duplicate load.
        if (!this._geometricFeaturesPromise) {
            this._geometricFeaturesPromise = this.doLoadGeometricFeatures().catch((err) => {
                // Drop the failed promise so a subsequent call can retry.
                this._geometricFeaturesPromise = undefined;
                throw err;
            });
        }

        if (clearCache) {
            return this._geometricFeaturesPromise.finally(() => this._geometricFeaturesRepository.clear());
        }
        return this._geometricFeaturesPromise;
    }

    private async doLoadGeometricFeatures(): Promise<boolean> {
        // t8gd carries its own edge section whose indices match the position-deduplicated
        // vertex array we expose at render time. The standalone `.geom-feat` file is
        // indexed against the welded vertex array, which does NOT match t8gd's layout
        // and produces garbage edges when combined with t8gd positions.
        if (this.geometryFormat === 't8gd') {
            await this.ensureVertexDataLoaded();
            this._geometricFeaturesRepository.populateFromVertexDataRepository(
                this.products,
                this._vertexDataRepository
            );
            return true;
        }

        if (!this.geometricFeaturesUrl) {
            return false;
        }

        return this._geometricFeaturesRepository.assignGeometricFeaturesToProducts(this.geometricFeaturesUrl);
    }

    private deleteUnreferencedTransforms(referencedTransforms: Set<number>): void {
        const transformsLen = this.index.transforms.length;
        if (transformsLen !== referencedTransforms.size) {
            for (let transformIdx = 0; transformIdx < transformsLen; transformIdx++) {
                if (!referencedTransforms.has(transformIdx)) {
                    // Set element in meshDescriptors to undefined. This allows GC to collect it
                    // it will also ensure that bim-vertex-data-repository.ts does not keep geometry
                    // relatited to it
                    this.index.transforms[transformIdx] = undefined;
                }
            }
        }
    }

    private deleteUnreferencedMeshDescriptors(referencedMeshDescriptors: Set<number>): void {
        const meshDescriptorLen = this.index.meshDescriptors.length;
        if (meshDescriptorLen !== referencedMeshDescriptors.size) {
            for (let meshDescriptorIdx = 0; meshDescriptorIdx < meshDescriptorLen; meshDescriptorIdx++) {
                if (!referencedMeshDescriptors.has(meshDescriptorIdx)) {
                    // Set element in meshDescriptors to undefined. This allows GC to collect it
                    // it will also ensure that bim-vertex-data-repository.ts does not keep geometry
                    // relatited to it
                    this.index.meshDescriptors[meshDescriptorIdx] = undefined;
                }
            }
        }
    }

    private deleteLeafNodesWithNoGeometry(leafNodesWithNoGeometry: Set<BimIfcObject>): void {
        while (leafNodesWithNoGeometry.size > 0) {
            for (const o of leafNodesWithNoGeometry) {
                o.remove();
                leafNodesWithNoGeometry.delete(o);
                if (o.parent && o.parent.childCount === 0 && !o.parent.hasGeometry) {
                    leafNodesWithNoGeometry.add(o.parent);
                }
            }
        }
    }

    private isValidAABB(bbox: SimpleBoundingBox): boolean {
        const deltaX = bbox[3] - bbox[0];
        const deltaY = bbox[4] - bbox[1];
        const deltaZ = bbox[5] - bbox[2];
        const isAtleast2D = [deltaX > 0, deltaY > 0, deltaZ > 0].filter(Boolean).length > 1;
        return bbox.length >= 6 && deltaX >= 0 && deltaY >= 0 && deltaZ >= 0 && isAtleast2D;
    }
}
