/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */

import {
    BimVertexData,
    BimProductMeshDescriptor,
    BimRegion,
    BimTransform,
    BimIfcStyleDTO,
    BimTypeObjectDTO,
    BimObjectAttributeDTO
} from './bim-format-types';
import { BimIfcIndex } from './BimIfcIndex';
import { BimGeometricFeaturesRepository } from './bim-geometric-features-repository';
import { BimChangeIfc } from './bim-api-client';
import { BimVertexDataRepository, WriteBatchResult } from './bim-vertex-data-repository';
import { BimTransformsRepository } from './bim-transforms-repository';
import { BimIfcObject, BimIfcSpace, BimIfcBuildingStorey, BimIfcDoor } 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 { IBimIfcLoaderElement, type GeometryFormat } from './bim-ifc-loader-element';
import { IfcProductMesh } from './IfcProductMesh';
import { FastTransform } from '../math/FastTransform';
import { GeometryArrayOffset, GeometryArrays } from './GeometryBuffer';
import { Matrix, Vector3, Quaternion } from './babylonjs-maths-import';
import { setMax, setMin } from '../math';
import { BimTypeObjectRepository } from './BimTypeObjectRepository';
import { BimObjectAttribute } from './BimObjectAttribute';
import { Fail, Failure } from '../fail';
import { BimProductMesh } from './BimProductMesh';
import { BimProduct } from './BimProduct';
import { Writeable } from '../Types';
import { coordinateSystems } from './babylon-coordinate-system';
import { BimTypeObject } from './BimTypeObject';
import { HttpResponseType } from '../http';
import { digest } from '../digest';

import {
    readT8ClientModelWithFactories,
    readT8GIModelWithFactories,
    T8ClientProductDTO,
    T8ClientProjectDTO,
    T8ClientProductConnectionsDTO,
    T8ClientProjectConnectionsDTO,
    T8ClientEntityTypeDTO,
    T8ClientUnitDTO,
    T8Unit,
    T8PropertySet,
    T8QuantitySet,
    T8ClientModelDTO,
    T8GIMeshDescriptorDTO,
    T8GIStyleDTO,
    T8GIRegionDTO,
    T8GIModelGeneric,
    T8GITransform,
    T8GIEntityMeshLookup,
    ModelProductClass
} from '@twinfinity/t8-client-model';

export type T8BimIfcLoaderElementCreateFailure = Failure<
    {
        httpStatus: number;
        reason: string;
    } & { ifc: Pick<BimChangeIfc, 'id' | 'version' | 'url'> }
>;

type T8GIBimModel = T8GIModelGeneric<BimProductMeshDescriptor, BimIfcStyleDTO, BimRegion>;

function meshDescriptorFactory(dto: T8GIMeshDescriptorDTO): BimProductMeshDescriptor {
    const i = dto.index;
    const hash = dto.getHash();
    return {
        p: dto.vertexCounts[i],
        i: dto.indexCounts[i],
        h: digest.toBase64(hash),
        bh: hash.slice(),
        obd: 0,
        sbd: 0,
        bbox: [dto.bboxMinX[i], dto.bboxMinY[i], dto.bboxMinZ[i], dto.bboxMaxX[i], dto.bboxMaxY[i], dto.bboxMaxZ[i]]
    };
}

function styleFactory(dto: T8GIStyleDTO): BimIfcStyleDTO {
    const surface = dto.getSurfaceColor();
    const name = dto.getName();
    return {
        r: surface[0],
        g: surface[1],
        b: surface[2],
        a: surface[3],
        name,
        hash: name,
        diffuseFactor: dto.getDiffuseFactor() ?? 0,
        specularFactor: dto.getSpecularFactor() ?? 0,
        reflectionFactor: dto.getReflectionFactor() ?? 0,
        transmissionFactor: dto.getTransmissionFactor() ?? 0,
        diffuseTransmissionFactor: dto.getDiffuseTransmissionFactor() ?? 0,
        specularExponent: dto.getSpecularExponent()
    };
}

function regionFactory(dto: T8GIRegionDTO): BimRegion {
    const i = dto.index;
    return {
        bbox: [dto.bboxMinX[i], dto.bboxMinY[i], dto.bboxMinZ[i], dto.bboxMaxX[i], dto.bboxMaxY[i], dto.bboxMaxZ[i]],
        isMostPopulated: dto.isMostPopulated(),
        name: dto.getName(),
        population: dto.populations[i],
        worldCoordinateSystem: Array.from(dto.getWorldCoordinateSystem())
    };
}

const NO_CHILDREN: BimIfcObject[] = Object.freeze([] as BimIfcObject[]) as BimIfcObject[];

export const NAME_NOT_SET_SENTINEL = 'undefined';

class T8BimIfcIndex implements BimIfcIndex {
    readonly modelFactors = {
        oneFoot: 304.8,
        oneInch: 25.4,
        oneKilometer: 1000000,
        oneMeter: 1000,
        oneMetre: 1000,
        oneMile: 1609344,
        oneMilliMeter: 1,
        oneMilliMetre: 1
    };

    readonly regions: BimRegion[] = [];
    readonly classes: string[] = [];
    readonly typeClasses: string[] = [];
    readonly texts: string[] = ['undefined'];
    readonly objectAttributes: BimObjectAttributeDTO[] = [];
    readonly types: BimTypeObjectDTO[] = [];
    readonly transforms: (BimTransform | undefined)[] = [];
    readonly meshDescriptors: (BimProductMeshDescriptor | undefined)[] = [];
    readonly styles: BimIfcStyleDTO[] = [];
    readonly project: BimProduct;

    constructor(
        public readonly discipline: Discipline,
        t8gi: T8GIBimModel | undefined,
        project: BimProduct
    ) {
        this.project = project;

        if (t8gi) {
            this.transforms = t8gi.transforms as T8GITransform[];
            this.meshDescriptors = t8gi.meshDescriptors;
            this.styles = t8gi.styles;
            this.regions = t8gi.regions;
        }

        this.styles.push({
            r: 50,
            g: 180,
            b: 50,
            a: 255,
            diffuseFactor: 0,
            diffuseTransmissionFactor: 0,
            hash: 'ifcSpaceColorHash',
            name: 'ifcSpaceColor',
            reflectionFactor: 0,
            specularFactor: 0,
            specularExponent: undefined,
            transmissionFactor: 0
        });
    }
}

interface T8SeparateChildren {
    _aggregates: BimIfcObject[];
    _contains: BimIfcObject[];
}

interface T8ProductContext {
    loaderElement: T8BimIfcLoaderElement;
    index: T8BimIfcIndex;
    t8gi: T8GIBimModel | undefined;
    entityMeshLookup: T8GIEntityMeshLookup | undefined;
    classIndexMap: Map<string, number>;
    productIndexMap: Map<number, BimIfcObject>;
    typeIndexMap: Map<number, BimTypeObject>;
}

export class T8BimIfcLoaderElement implements IBimIfcLoaderElement {
    public readonly transformsRepository: BimTransformsRepository;
    public readonly typeObjectRepository: BimTypeObjectRepository;
    public readonly project: BimIfcObject;
    public readonly spaces: BimIfcSpace[] = [];
    public readonly floors: BimIfcBuildingStorey[] = [];
    public readonly products: BimIfcObject[] = [];
    public readonly discipline: Discipline;
    public readonly styles: BimIfcStyle[] = [];
    public readonly classes: BimIfcClass[] = [];
    public readonly ifcName: string;
    public readonly materials: Materials;
    public readonly loadOptions?: BimApiIfcObjectsLoadOptions;
    public readonly transform?: Matrix;
    public readonly loader: BimIfcLoaderElementParent;
    public readonly index: BimIfcIndex;

    private _t8gi: T8GIBimModel | undefined;
    private _numberOfVertices = 0;
    private _numberOfIndices = 0;
    private _meshCount = 0;

    private constructor(
        public readonly ifcHttpResource: BimChangeIfc,
        materials: Materials,
        private readonly _vertexDataRepository: BimVertexDataRepository,
        loader: BimIfcLoaderElementParent,
        t8cmData: ArrayBuffer | ArrayBuffer[],
        t8giData: ArrayBuffer | undefined,
        loadOptions?: BimApiIfcObjectsLoadOptions,
        transform?: Matrix
    ) {
        this.materials = materials;
        this.loader = loader;
        this.loadOptions = loadOptions;
        this.transform = transform;
        this.discipline = ifcHttpResource.discipline;
        this.ifcName = ifcHttpResource.name;

        this._t8gi = t8giData
            ? readT8GIModelWithFactories(t8giData, meshDescriptorFactory, styleFactory, regionFactory)
            : undefined;

        const emptyProject: BimProduct = {
            c: 0,
            n: 0,
            d: 0,
            gid: '',
            e: 0,
            m: [],
            pd: {}
        };

        const t8Index = new T8BimIfcIndex(this.discipline, this._t8gi, emptyProject);
        this.index = t8Index;

        const indexStyles = t8Index.styles;
        const styleCount = indexStyles.length;
        const styles = new Array<BimIfcStyle>(styleCount);
        for (let i = 0; i < styleCount; i++) {
            styles[i] = new BimIfcStyle(indexStyles[i]);
        }
        this.styles = styles;

        this.typeObjectRepository = new T8BimTypeObjectRepository(this.index, []);
        this.project = this.parseT8CMAndCreateObjects(t8cmData, t8Index);

        this.transformsRepository = new BimTransformsRepository(this);

        this.processTransforms(transform);

        const products = this.products;
        const productCount = products.length;
        const discipline = this.discipline;
        let meshCount = 0;
        let disciplineObjectsWithGeometryCount = 0;
        const leafNodesWithNoGeometry = new Set<BimIfcObject>();

        for (let i = 0; i < productCount; i++) {
            const o = products[i];
            const oClass = o.class;

            discipline.addReferencedClass(oClass);
            oClass.addReferencedClass(discipline);
            (oClass.objectCount as number) = oClass.objectCount + 1;

            if (o.hasGeometry) {
                const meshLen = o.productMeshes.length;
                meshCount += meshLen;
                (oClass.objectsWithGeometryCount as number) = oClass.objectsWithGeometryCount + 1;
                disciplineObjectsWithGeometryCount++;
                const enclosingFloor = o.enclosingFloor;
                if (enclosingFloor) {
                    (enclosingFloor.objectsWithGeometryCount as number) = enclosingFloor.objectsWithGeometryCount + 1;
                }
            } else if (loadOptions?.removeLeafIfcObjectsWithNoGeometry && o.childCount === 0 && !o.hasGeometry) {
                leafNodesWithNoGeometry.add(o);
            }
        }

        this.deleteLeafNodesWithNoGeometry(leafNodesWithNoGeometry);

        // Release T8GI model to allow the ArrayBuffer to be GC'd.
        // All needed data (transforms, meshDescriptors, styles, regions) has been
        // extracted into independent copies. hash.slice() in meshDescriptorFactory
        // ensures mesh descriptor hashes are independent of the buffer.
        this._t8gi = undefined;

        this._meshCount = meshCount;
        (discipline.objectCount as number) = discipline.objectCount + productCount;
        (discipline.objectsWithGeometryCount as number) =
            discipline.objectsWithGeometryCount + disciplineObjectsWithGeometryCount;
    }

    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);
                }
            }
        }
    }

    public static async create(
        bimApi: BimCoreApiClient,
        materials: Materials,
        vertexDataRepository: BimVertexDataRepository,
        loader: BimIfcLoaderElementParent,
        ifcHttpResource: BimChangeIfc,
        loadOptions?: BimApiIfcObjectsLoadOptions,
        transform?: Matrix
    ): Promise<T8BimIfcLoaderElement | T8BimIfcLoaderElementCreateFailure> {
        const { id, version, url, resourceUrl } = ifcHttpResource;

        if (!resourceUrl.t8cm) {
            return Fail({
                httpStatus: 404,
                reason: 'No T8CM resource URL available.',
                ifc: { id, version, url }
            });
        }

        // Fetch T8CM and T8GI in parallel for better performance
        // T8GI is optional - if it fails to load, we continue without geometry info
        const t8cmPromise = bimApi.get(resourceUrl.t8cm, HttpResponseType.arrayBuffer);
        const t8giPromise = resourceUrl.t8gi
            ? bimApi.get(resourceUrl.t8gi, HttpResponseType.arrayBuffer).catch((error) => {
                  console.warn('Failed to fetch T8GI file, continuing without geometry info:', error);
                  return undefined;
              })
            : Promise.resolve(undefined);

        const [t8cmResponse, t8giResponse] = await Promise.all([t8cmPromise, t8giPromise]);

        if (t8cmResponse.status !== 200) {
            return Fail({
                httpStatus: t8cmResponse.status,
                reason: 'Failed to fetch T8CM file.',
                ifc: { id, version, url }
            });
        }

        let t8giData: ArrayBuffer | undefined;
        if (t8giResponse && t8giResponse.status === 200) {
            try {
                t8giData = await t8giResponse.value;
            } catch (error) {
                console.warn('Failed to read T8GI response, continuing without geometry info:', error);
            }
        }

        return new T8BimIfcLoaderElement(
            ifcHttpResource,
            materials,
            vertexDataRepository,
            loader,
            await t8cmResponse.value,
            t8giData,
            loadOptions,
            transform
        );
    }

    public get ifc(): BimChangeIfc {
        return this.ifcHttpResource;
    }

    public get ifcUrl(): URL {
        return new URL(this.ifcHttpResource.url);
    }

    public get numberOfVertices(): number {
        return this._numberOfVertices;
    }

    public get numberOfIndices(): number {
        return this._numberOfIndices;
    }

    public get numberOfTriangles(): number {
        return this._numberOfIndices / 3;
    }

    public get meshCount(): number {
        return this._meshCount;
    }

    public get ifcProductsWithGeometryCount(): number {
        return this._meshCount;
    }

    public get indexUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl.t8cm;
    }

    public get geometryUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl.t8gd;
    }

    public get geometryFormat(): GeometryFormat {
        return 't8gd'; // T8GI format implies T8GD geometry format
    }

    public get propertiesUrl(): URL | undefined {
        return undefined;
    }

    public get geometricFeaturesUrl(): URL | undefined {
        return this.ifcHttpResource.resourceUrl['geom-feat'];
    }

    public get hasBatchSupport(): boolean {
        return this._vertexDataRepository.hasBatchSupport;
    }

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

    public ensureVertexDataLoaded(): Promise<boolean> {
        return this._vertexDataRepository.load(this);
    }

    public getVertexData(productMeshDescriptor: BimProductMeshDescriptor): BimVertexData {
        return this._vertexDataRepository.get(productMeshDescriptor);
    }

    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);
    }

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

    public async loadPropertySets(): Promise<boolean> {
        return true;
    }

    public get geometricFeaturesRepository(): BimGeometricFeaturesRepository {
        return this._geometricFeaturesRepository;
    }
    // T8 loader reads edges from the t8gd geometry buffer directly and injects them via
    // setDecodedFeatures — the BimCoreApiClient is only needed by the legacy path.
    private readonly _geometricFeaturesRepository = new BimGeometricFeaturesRepository();
    private _geometricFeaturesPromise: Promise<boolean> | undefined;

    public loadGeometricFeatures(): Promise<boolean> {
        if (!this._geometricFeaturesPromise) {
            this._geometricFeaturesPromise = this.doLoadGeometricFeatures().catch((err) => {
                // Evict the cached rejection so a subsequent call can retry instead of
                // replaying the same failure forever.
                this._geometricFeaturesPromise = undefined;
                throw err;
            });
        }
        return this._geometricFeaturesPromise;
    }

    private async doLoadGeometricFeatures(): Promise<boolean> {
        // Ensure vertex data (and thus the V2 geometry buffer) is loaded before
        // attempting to read edges, since readEdgeIndices depends on it.
        await this.ensureVertexDataLoaded();
        this._geometricFeaturesRepository.populateFromVertexDataRepository(this.products, this._vertexDataRepository);
        return true;
    }

    public tryCreateOrExtendAABBFromRegions(
        min: Vector3,
        max: Vector3,
        o?: { onlyMostPopulatedRegions?: boolean }
    ): boolean {
        const options = { onlyMostPopulatedRegions: true, ...(o ?? {}) };
        let isValid = false;

        for (const region of this.index.regions) {
            if (options.onlyMostPopulatedRegions && !region.isMostPopulated) {
                continue;
            }

            const { bbox } = region;
            min.minimizeInPlaceFromFloats(bbox[0], bbox[1], bbox[2]);
            max.maximizeInPlaceFromFloats(bbox[3], bbox[4], bbox[5]);
            isValid = true;
        }

        return isValid;
    }

    private parseT8CMAndCreateObjects(t8cmData: ArrayBuffer | ArrayBuffer[], t8Index: T8BimIfcIndex): BimIfcObject {
        const ctx: T8ProductContext = {
            loaderElement: this,
            index: t8Index,
            t8gi: this._t8gi,
            entityMeshLookup: this._t8gi?.entityMeshLookup,
            classIndexMap: new Map<string, number>(),
            productIndexMap: new Map<number, BimIfcObject>(),
            typeIndexMap: new Map<number, BimTypeObject>()
        };

        let rootProject: BimIfcObject | undefined;
        let productCount = 0;

        const projectFactory = (dto: T8ClientProjectDTO<T8Unit>): BimIfcObject => {
            const classIndex = this.getOrAddClassIndex(ctx, dto.getType());

            const rawProduct: BimProduct = {
                c: classIndex,
                n: 0,
                d: 0,
                gid: dto.getGlobalId(),
                e: dto.localId,
                m: [],
                pd: {}
            };

            const obj = this.createT8BimIfcObject(
                ctx,
                String(dto.localId),
                rawProduct,
                [],
                dto.getPropertySets(),
                {},
                dto.getQuantitySets(),
                undefined,
                dto.getName(),
                dto.getDescription()
            );

            rootProject = obj;
            ctx.productIndexMap.set(dto.localId, obj);
            return obj;
        };

        const entityMeshLookup = ctx.entityMeshLookup;
        const entityMeshesMap = entityMeshLookup ? undefined : this._t8gi?.entityMeshes;
        const emptyMeshes: BimProductMesh[] = [];

        const productFactory = (dto: T8ClientProductDTO<T8Unit>): BimIfcObject => {
            const localId = dto.localId;
            const classIndex = this.getOrAddClassIndex(ctx, dto.getType());

            let meshes: BimProductMesh[];
            if (entityMeshLookup) {
                const meshCount = entityMeshLookup.getMeshCount(localId);
                meshes = meshCount > 0 ? new Array<BimProductMesh>(meshCount) : emptyMeshes;
            } else {
                meshes = (entityMeshesMap?.get(localId) ?? emptyMeshes) as BimProductMesh[];
            }

            const rawProduct: BimProduct = {
                c: classIndex,
                n: 0,
                d: 0,
                gid: dto.getGlobalId(),
                e: localId,
                m: meshes,
                pd: dto.getData()
            };

            const obj = this.createT8BimIfcObject(
                ctx,
                String(localId),
                rawProduct,
                [],
                dto.getPropertySets(),
                dto.getComputedPropertySets(),
                dto.getElementQuantities(),
                undefined,
                dto.getName(),
                dto.getDescription()
            );

            ctx.productIndexMap.set(localId, obj);
            productCount++;
            return obj;
        };

        const entityTypeFactory = (dto: T8ClientEntityTypeDTO<T8Unit>): BimTypeObject => {
            const type = new BimTypeObject(
                dto.entityTypeIndex,
                dto.getGlobalId(),
                dto.getType(),
                dto.getTag(),
                dto.getName(),
                dto.getDescription(),
                dto.getData()
            );

            this.convertT8PropertySetsToType(type, dto.getPropertySets(), dto.getElementQuantities());

            ctx.typeIndexMap.set(dto.entityTypeIndex, type);
            return type;
        };

        const unitFactory = (dto: T8ClientUnitDTO, dataModel: T8ClientModelDTO): T8Unit => {
            return T8Unit.create(dto, dataModel);
        };

        const connectProject = (dto: T8ClientProjectConnectionsDTO<BimIfcObject>, project: BimIfcObject): void => {
            const agg = dto.getAggregates();
            if (agg) {
                (project as unknown as T8SeparateChildren)._aggregates = agg;
                this.assignParentAndFloor(project, agg);
            }
        };

        const typeRepo = this.typeObjectRepository as T8BimTypeObjectRepository;
        const connectProduct = (
            dto: T8ClientProductConnectionsDTO<BimIfcObject, BimTypeObject>,
            product: BimIfcObject
        ): void => {
            const aggregates = dto.getAggregates();
            const contains = dto.getContains();

            if (aggregates) {
                (product as unknown as T8SeparateChildren)._aggregates = aggregates;
                this.assignParentAndFloor(product, aggregates);
            }
            if (contains) {
                (product as unknown as T8SeparateChildren)._contains = contains;
                this.assignParentAndFloor(product, contains);
            }

            const definedByType = dto.getDefinedByType();
            if (definedByType) {
                (product.rawProduct as Writeable<BimProduct>).t = definedByType.id;
                typeRepo.addType(definedByType);
            }
        };

        readT8ClientModelWithFactories(
            t8cmData instanceof Array ? t8cmData : [t8cmData],
            projectFactory,
            productFactory,
            entityTypeFactory,
            unitFactory,
            connectProject,
            connectProduct
        );

        (t8Index as Writeable<T8BimIfcIndex>).project = rootProject?.rawProduct ?? t8Index.project;

        // Pre-allocate products array with known capacity
        const products = this.products as BimIfcObject[];
        products.length = 0;

        // Track visited products using a simple counter for faster orphan detection
        let visitedCount = 0;

        if (rootProject) {
            // Use stack-based iteration instead of generator for better performance
            const stack: BimIfcObject[] = [rootProject];
            while (stack.length > 0) {
                const obj = stack.pop()!;
                products.push(obj);
                visitedCount++;

                if (obj instanceof BimIfcSpace) {
                    this.spaces.push(obj);
                } else if (obj instanceof BimIfcBuildingStorey) {
                    this.floors.push(obj);
                }

                const children = obj.children;
                for (let i = children.length - 1; i >= 0; i--) {
                    stack.push(children[i]);
                }
            }
        }

        // Only check for orphans if tree traversal didn't find all products
        // productCount tracks products created via productFactory (excludes project)
        if (visitedCount <= productCount) {
            const productsSet = new Set(products);
            for (const product of ctx.productIndexMap.values()) {
                if (!productsSet.has(product)) {
                    products.push(product);
                    if (product instanceof BimIfcSpace) {
                        this.spaces.push(product);
                    } else if (product instanceof BimIfcBuildingStorey) {
                        this.floors.push(product);
                    }
                }
            }
        }

        if (!rootProject) {
            throw new Error('T8CM data does not contain a project. Cannot create loader element.');
        }
        return rootProject;
    }

    private assignParentAndFloor(parent: BimIfcObject, children: readonly BimIfcObject[]): void {
        const enclosingFloor = parent instanceof BimIfcBuildingStorey ? parent : parent.enclosingFloor;
        for (let i = 0, n = children.length; i < n; i++) {
            const child = children[i];
            (child.parents as BimIfcObject[]).push(parent);
            (child as Writeable<BimIfcObject>).enclosingFloor = enclosingFloor;
        }
    }

    private createT8BimIfcObject(
        ctx: T8ProductContext,
        entityLabelInIfc: string,
        rawProduct: BimProduct,
        parents: BimIfcObject[],
        propertySets: Record<string, T8PropertySet<T8Unit>>,
        computedPropertySets: Record<string, T8PropertySet<T8Unit>>,
        quantitySets: Record<string, T8QuantitySet<T8Unit>>,
        enclosingFloor: BimIfcBuildingStorey | undefined,
        name: string | undefined,
        description: string | undefined
    ): BimIfcObject {
        const classIdx = rawProduct.c ?? 0;
        const ifcClass = ModelProductClass.getOrAdd(this.index.classes[classIdx]);

        let obj: BimIfcObject;
        if (ifcClass === ModelProductClass.predefined.space) {
            obj = new T8BimIfcSpace(entityLabelInIfc, this, rawProduct, parents, enclosingFloor, name, description);
        } else if (ifcClass === ModelProductClass.predefined.buildingStorey) {
            obj = new T8BimIfcBuildingStorey(
                entityLabelInIfc,
                this,
                rawProduct,
                parents,
                enclosingFloor,
                name,
                description
            );
        } else if (ifcClass === ModelProductClass.predefined.door) {
            obj = new T8BimIfcDoor(entityLabelInIfc, this, rawProduct, parents, enclosingFloor, name, description);
        } else {
            obj = new T8BimIfcObject(entityLabelInIfc, this, rawProduct, parents, enclosingFloor, name, description);
        }

        this.populateProperties(obj, propertySets, computedPropertySets, quantitySets);

        const meshes = rawProduct.m;
        if (meshes.length > 0) {
            const objClass = obj.class;
            const stylesLen = this.styles.length;
            const isSpace = objClass === ModelProductClass.predefined.space;
            const spaceStyleIdx = stylesLen - 1;
            const localId = rawProduct.e;
            const lookup = ctx.entityMeshLookup;

            if (lookup) {
                for (let idx = 0, len = meshes.length; idx < len; ++idx) {
                    const si = isSpace ? spaceStyleIdx : lookup.getStyleIndex(localId, idx);
                    meshes[idx] = new IfcProductMesh(
                        obj,
                        lookup.getMeshDescriptorIndex(localId, idx),
                        0,
                        si,
                        lookup.getTransformIndex(localId, idx)
                    );
                }
            } else {
                for (let idx = 0, len = meshes.length; idx < len; ++idx) {
                    const dto = meshes[idx];
                    const si = isSpace ? spaceStyleIdx : (dto.si ?? 0);
                    meshes[idx] = new IfcProductMesh(obj, dto.mi ?? 0, dto.r ?? 0, si, dto.wt ?? 0);
                }
            }
        }

        return obj;
    }

    private populateProperties(
        obj: BimIfcObject,
        propertySets: Record<string, T8PropertySet<T8Unit>>,
        computedPropertySets: Record<string, T8PropertySet<T8Unit>>,
        quantitySets: Record<string, T8QuantitySet<T8Unit>>
    ): void {
        (obj as Writeable<BimIfcObject>).propertySets = propertySets;
        (obj as Writeable<BimIfcObject>).computedPropertySets = computedPropertySets;
        (obj as Writeable<BimIfcObject>).quantitySets = quantitySets;
    }

    private convertT8PropertySetsToType(
        type: BimTypeObject,
        propertySets: Record<string, T8PropertySet<T8Unit>>,
        quantitySets: Record<string, T8QuantitySet<T8Unit>>
    ): void {
        (type as Writeable<BimTypeObject>).propertySets = propertySets;
        (type as Writeable<BimTypeObject>).quantitySets = quantitySets;
    }

    private getOrAddClassIndex(ctx: T8ProductContext, className: string): number {
        let idx = ctx.classIndexMap.get(className);
        if (idx === undefined) {
            idx = ctx.index.classes.length;
            (ctx.index.classes as string[]).push(className);
            ctx.classIndexMap.set(className, idx);
            this.classes.push(BimIfcClass.getOrAdd(className));
        }
        return idx;
    }

    private processTransforms(transform?: Matrix): void {
        const transforms = this.index.transforms;
        const transformCount = transforms.length;

        if (transformCount === 0) {
            return;
        }

        const tmpScale = Vector3.Zero();
        const tmpRotation = Quaternion.Identity();
        const tmpTranslate = Vector3.Zero();
        const tmpMatrix = Matrix.Identity();
        const tmpPoints = [
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero(),
            Vector3.Zero()
        ] as [Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3];
        const tmpMin = Vector3.Zero();
        const tmpMax = Vector3.Zero();

        // T8 format: geometry is always in millimeters, convert to meters
        const toMeterScale = 1.0 / this.index.modelFactors.oneMeter;

        for (let i = 0; i < transformCount; i++) {
            const wt = transforms[i];
            if (!wt) continue;
            const mutableWt = wt as Writeable<BimTransform>;

            // Apply meter scaling to position and scale (same as IDX format)
            mutableWt.x = (wt.x ?? 0) * toMeterScale;
            mutableWt.y = (wt.y ?? 0) * toMeterScale;
            mutableWt.z = (wt.z ?? 0) * toMeterScale;
            mutableWt.sx = (wt.sx ?? 1) * toMeterScale;
            mutableWt.sy = (wt.sy ?? 1) * toMeterScale;
            mutableWt.sz = (wt.sz ?? 1) * toMeterScale;

            const ifcTransform = this.transformsRepository.convertBimTransformToMatrixToRef(wt, tmpMatrix);
            const bjsTransform = coordinateSystems.convertIfcTransformToBabylonTransformInPlace(ifcTransform);

            if (transform) {
                bjsTransform.multiplyToRef(transform, bjsTransform);
            }

            bjsTransform.decompose(tmpScale, tmpRotation, tmpTranslate);

            mutableWt.x = tmpTranslate.x;
            mutableWt.y = tmpTranslate.y;
            mutableWt.z = tmpTranslate.z;

            mutableWt.qx = tmpRotation.x;
            mutableWt.qy = tmpRotation.y;
            mutableWt.qz = tmpRotation.z;
            mutableWt.qw = tmpRotation.w;

            mutableWt.sx = tmpScale.x;
            mutableWt.sy = tmpScale.y;
            mutableWt.sz = tmpScale.z;
        }

        const tmp = {
            points: tmpPoints,
            min: tmpMin,
            max: tmpMax
        };

        // Process region bounding boxes: convert from IFC to Babylon coordinates
        for (const region of this.index.regions) {
            this.convertIfcBboxToBabylonToRef(
                { x: region.bbox[0], y: region.bbox[1], z: region.bbox[2] },
                { x: region.bbox[3], y: region.bbox[4], z: region.bbox[5] },
                tmp.points,
                toMeterScale,
                transform
            );

            setMax(tmp.min);
            setMin(tmp.max);

            for (const point of tmp.points) {
                tmp.min.minimizeInPlace(point);
                tmp.max.maximizeInPlace(point);
            }

            region.bbox[0] = tmp.min.x;
            region.bbox[1] = tmp.min.y;
            region.bbox[2] = tmp.min.z;

            region.bbox[3] = tmp.max.x;
            region.bbox[4] = tmp.max.y;
            region.bbox[5] = tmp.max.z;
        }

        // NOTE: Mesh descriptor bboxes are intentionally NOT scaled here.
        // The transform's scale component (sx, sy, sz) already includes the unit conversion
        // (scaled by toMeterScale). When aabb() transforms the local bbox, the scale is applied,
        // converting from mm to meters. Scaling the bbox here would cause double-scaling.
    }

    private convertIfcBboxToBabylonToRef(
        min: { x: number; y: number; z: number },
        max: { x: number; y: number; z: number },
        out: [Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3, Vector3],
        scale = 1,
        transform?: Matrix
    ): void {
        // Convert IFC coordinates (Y-up) to Babylon coordinates (Z-up swapped)
        // IFC: (x, y, z) -> Babylon: (x, z, y)
        out[0].set(min.x, min.z, min.y).scaleInPlace(scale);
        out[1].set(max.x, min.z, min.y).scaleInPlace(scale);
        out[2].set(min.x, min.z, max.y).scaleInPlace(scale);
        out[3].set(min.x, max.z, min.y).scaleInPlace(scale);
        out[4].set(max.x, min.z, max.y).scaleInPlace(scale);
        out[5].set(max.x, max.z, min.y).scaleInPlace(scale);
        out[6].set(min.x, max.z, max.y).scaleInPlace(scale);
        out[7].set(max.x, max.z, max.y).scaleInPlace(scale);

        if (transform) {
            for (const p of out) {
                Vector3.TransformCoordinatesToRef(p, transform, p);
            }
        }
    }
}

interface T8BimIfcFields {
    _aggregates: BimIfcObject[];
    _contains: BimIfcObject[];
    _cachedChildren: BimIfcObject[];
    _t8Name: string | undefined;
    _t8Description: string | undefined;
}

function initT8Fields(obj: BimIfcObject, name: string | undefined, description: string | undefined): void {
    const t8 = obj as unknown as T8BimIfcFields;
    t8._aggregates = NO_CHILDREN;
    t8._contains = NO_CHILDREN;
    t8._cachedChildren = NO_CHILDREN;
    t8._t8Name = name;
    t8._t8Description = description;
}

const t8ChildrenDescriptors: PropertyDescriptorMap = {
    hasStructuredRelationships: {
        value: true,
        configurable: true
    },
    name: {
        get(this: T8BimIfcFields) {
            return this._t8Name ?? 'undefined';
        },
        configurable: true
    },
    description: {
        get(this: T8BimIfcFields) {
            return this._t8Description ?? 'undefined';
        },
        configurable: true
    },
    aggregates: {
        get(this: T8BimIfcFields) {
            return this._aggregates;
        },
        configurable: true
    },
    contains: {
        get(this: T8BimIfcFields) {
            return this._contains;
        },
        configurable: true
    },
    children: {
        get(this: T8BimIfcFields) {
            if (this._contains === NO_CHILDREN) return this._aggregates;
            if (this._aggregates === NO_CHILDREN) return this._contains;
            if (this._cachedChildren === NO_CHILDREN) {
                this._cachedChildren = [...this._aggregates, ...this._contains];
            }
            return this._cachedChildren;
        },
        configurable: true
    }
};

class T8BimIfcObject extends BimIfcObject {
    constructor(
        entityLabelInIfc: string,
        ifcLoaderElement: IBimIfcLoaderElement,
        rawProduct: BimProduct,
        parents: BimIfcObject[],
        enclosingFloor: BimIfcBuildingStorey | undefined,
        name: string | undefined,
        description: string | undefined
    ) {
        super(entityLabelInIfc, ifcLoaderElement, rawProduct, parents, enclosingFloor);
        initT8Fields(this, name, description);
    }
}

class T8BimIfcSpace extends BimIfcSpace {
    constructor(
        entityLabelInIfc: string,
        ifcLoaderElement: IBimIfcLoaderElement,
        rawProduct: BimProduct,
        parents: BimIfcObject[],
        enclosingFloor: BimIfcBuildingStorey | undefined,
        name: string | undefined,
        description: string | undefined
    ) {
        super(entityLabelInIfc, ifcLoaderElement, rawProduct, parents, enclosingFloor);
        initT8Fields(this, name, description);
    }
}

class T8BimIfcBuildingStorey extends BimIfcBuildingStorey {
    constructor(
        entityLabelInIfc: string,
        ifcLoaderElement: IBimIfcLoaderElement,
        rawProduct: BimProduct,
        parents: BimIfcObject[],
        enclosingFloor: BimIfcBuildingStorey | undefined,
        name: string | undefined,
        description: string | undefined
    ) {
        super(entityLabelInIfc, ifcLoaderElement, rawProduct, parents, enclosingFloor);
        initT8Fields(this, name, description);
    }
}

class T8BimIfcDoor extends BimIfcDoor {
    constructor(
        entityLabelInIfc: string,
        ifcLoaderElement: IBimIfcLoaderElement,
        rawProduct: BimProduct,
        parents: BimIfcObject[],
        enclosingFloor: BimIfcBuildingStorey | undefined,
        name: string | undefined,
        description: string | undefined
    ) {
        super(entityLabelInIfc, ifcLoaderElement, rawProduct, parents, enclosingFloor);
        initT8Fields(this, name, description);
    }
}

Object.defineProperties(T8BimIfcObject.prototype, t8ChildrenDescriptors);
Object.defineProperties(T8BimIfcSpace.prototype, t8ChildrenDescriptors);
Object.defineProperties(T8BimIfcBuildingStorey.prototype, t8ChildrenDescriptors);
Object.defineProperties(T8BimIfcDoor.prototype, t8ChildrenDescriptors);

class T8BimTypeObjectRepository extends BimTypeObjectRepository {
    private readonly _typeIdMap = new Map<number, BimTypeObject>();

    constructor(index: BimIfcIndex, objectAttributes: BimObjectAttribute[]) {
        super(index, objectAttributes);
    }

    public addType(type: BimTypeObject): void {
        if (!this._typeIdMap.has(type.id)) {
            this._typeIdMap.set(type.id, type);
            (this.types as BimTypeObject[]).push(type);
        }
    }

    public override has(id: number): boolean {
        return this._typeIdMap.has(id);
    }

    public override get(id: number): BimTypeObject {
        const type = this._typeIdMap.get(id);
        if (type === undefined) {
            throw new Error(`BimTypeObject with id ${id} not found`);
        }
        return type;
    }
}
