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

import { BimIfcPropertiesResponse } from './bim-format-types';

import '../MapExtensions';
import { BimIfcObject } from './bim-ifc-object';
import { T8Property, T8PropertySet, T8Quantity, T8QuantitySet, T8Unit } from '@twinfinity/t8-client-model';

export interface DecodedPropertySet {
    readonly name: string;
    readonly type: 'properties' | 'quantities';
    readonly value: T8PropertySet<T8Unit> | T8QuantitySet<T8Unit>;
    readonly gidPropertyNames?: string[];
}

const GID_SENTINEL = 'useGlobalIdHere_ee5c4789-0e4a-4e6c-8beb-3e2ca239b4a8';

function inferType(value: boolean | number | string): string {
    return typeof value === 'boolean' ? 'IfcBoolean' : typeof value === 'number' ? 'IfcReal' : 'IfcText';
}

export class BimPropertySetReader {
    readonly _decodedPropertySetByHashCache = new Map<string, DecodedPropertySet>();

    public clear(): void {
        this._decodedPropertySetByHashCache.clear();
    }

    public decodePropertySets(bimIfcPropertiesResponse: BimIfcPropertiesResponse): readonly DecodedPropertySet[] {
        const decodedPropertySets: DecodedPropertySet[] = [];

        for (const psetDto of bimIfcPropertiesResponse.propertySets) {
            let psetName = bimIfcPropertiesResponse.propertySetNames[psetDto.s];
            const psetType: DecodedPropertySet['type'] = psetName.startsWith('q.') ? 'quantities' : 'properties';
            psetName = psetName.slice(2);

            const cached = this._decodedPropertySetByHashCache.get(psetDto.h);
            if (cached !== undefined) {
                decodedPropertySets.push(cached);
                continue;
            }

            const { propertyNames, propertyValues, propertyUnits } = bimIfcPropertiesResponse;
            const pLen = psetDto.n.length;

            const accumulatedProps: Record<string, T8Property<T8Unit>[]> = {};
            const accumulatedUnits: Record<string, T8Unit | undefined> = {};
            let gidPropertyNames: string[] | undefined;

            for (let i = 0; i < pLen; ++i) {
                const pName = propertyNames[psetDto.n[i]];
                const value = propertyValues[psetDto.v[i]];
                const type = inferType(value);

                let unit: T8Unit | undefined;
                if (propertyUnits != null && psetDto.u != null && i < psetDto.u.length) {
                    const pUnitIdx = psetDto.u[i];
                    if (pUnitIdx >= 0 && pUnitIdx < propertyUnits.length) {
                        const pu = propertyUnits[pUnitIdx];
                        if (pu != null && pu.name && pu.type) {
                            unit = new T8Unit(pu.name, pu.type);
                        }
                    }
                }

                (accumulatedProps[pName] ??= []).push({ value, type, unit });
                if (unit !== undefined) {
                    accumulatedUnits[pName] = unit;
                }
            }

            // Unwrap singleton arrays and detect GID sentinel properties
            const properties: Record<string, T8Property<T8Unit> | T8Property<T8Unit>[]> = {};
            for (const [propName, propArray] of Object.entries(accumulatedProps)) {
                if (propArray.length === 1) {
                    if (propArray[0].value === GID_SENTINEL) {
                        gidPropertyNames ??= [];
                        gidPropertyNames.push(propName);
                    }
                    properties[propName] = propArray[0];
                } else {
                    properties[propName] = propArray;
                }
            }

            let decodedValue: T8PropertySet<T8Unit> | T8QuantitySet<T8Unit>;
            if (psetType === 'quantities') {
                const quantities: Record<string, T8Quantity<T8Unit>> = {};
                for (const [propName, prop] of Object.entries(properties)) {
                    const single = Array.isArray(prop) ? prop[0] : prop;
                    quantities[propName] = { value: single.value!, type: single.type, unit: single.unit };
                }
                decodedValue = { name: psetName, quantities };
            } else {
                decodedValue = { name: psetName, properties, sets: {} };
            }

            const decoded: DecodedPropertySet = {
                name: psetName,
                value: decodedValue,
                type: psetType,
                ...(gidPropertyNames ? { gidPropertyNames } : {})
            };

            this._decodedPropertySetByHashCache.set(psetDto.h, decoded);
            decodedPropertySets.push(decoded);
        }
        return decodedPropertySets;
    }

    public getT8PropertySetForProduct(ifcObject: BimIfcObject, decoded: DecodedPropertySet): T8PropertySet<T8Unit> {
        if (!decoded.gidPropertyNames) return decoded.value as T8PropertySet<T8Unit>;

        const base = decoded.value as T8PropertySet<T8Unit>;
        const overrides: Record<string, T8Property<T8Unit>> = {};
        for (const propName of decoded.gidPropertyNames) {
            const existing = base.properties[propName];
            const baseProp = Array.isArray(existing) ? existing[0] : existing;
            overrides[propName] = { value: ifcObject.gid, type: baseProp?.type ?? 'IfcText', unit: baseProp?.unit };
        }
        return {
            name: base.name,
            properties: { ...base.properties, ...overrides },
            sets: base.sets
        };
    }
}
