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

import { HttpResponseType } from '../http';
import '../MapExtensions';
import { BimCoreApiClient } from './client/BimCoreApiClient';

import { BimGeometricFeaturesReader, DecodedGeometricFeatures } from './bim-geometric-features-reader';
import { Vector3 } from '@babylonjs/core';
import type { FlatSegment3, Vec3 } from '@twinfinity/geometry';
import { BimProductMesh } from './BimProductMesh';
import { BimIfcObject } from './bim-ifc-object';
import { BimVertexDataRepository } from './bim-vertex-data-repository';

export class BimGeometricFeaturesRepository {
    private static readonly _temp = Vector3.Zero();
    readonly _geometricFeaturesReader = new BimGeometricFeaturesReader();
    private readonly _decodedFeaturesByHash = new Map<string, DecodedGeometricFeatures>();
    readonly _isGeometricFeaturesUrlAdded = new Map<string, Promise<boolean>>();

    public constructor(private readonly _bimApi?: BimCoreApiClient) {}

    /** Inject pre-decoded feature data for a mesh hash (used by the T8 loader which reads edges from t8gd). */
    public setDecodedFeatures(hash: string, features: DecodedGeometricFeatures): void {
        this._decodedFeaturesByHash.set(hash, features);
    }

    /**
     * Populate the repository by reading edge indices out of the t8gd buffer managed by
     * the vertex data repository. Edges in t8gd reference the position-deduplicated vertex
     * array — the same array exposed by `BimVertexDataRepository.get` for V2 format — so
     * the indices match the positions used at render time.
     *
     * Use this in place of {@link assignGeometricFeaturesToProducts} whenever the geometry
     * resource is `.t8gd`. The `.geom-feat` file's edge indices are in the welded-vertex
     * space which does NOT match the t8gd deduplicated layout.
     */
    public populateFromVertexDataRepository(
        products: readonly BimIfcObject[],
        vertexDataRepository: BimVertexDataRepository
    ): void {
        // Track only the hashes consumed by this call. Several loader elements may share
        // a single repository, so we must not release V2 sources globally — that would
        // wipe edges out from under any concurrent peer that has not yet read its meshes.
        const consumed = new Set<string>();
        try {
            for (const product of products) {
                for (const mesh of product.productMeshes) {
                    const hash = mesh.descriptor.h;
                    if (consumed.has(hash)) continue;
                    consumed.add(hash);

                    const edges = vertexDataRepository.readEdgeIndices(hash);
                    if (edges) {
                        this._decodedFeaturesByHash.set(hash, { edges });
                    }
                }
            }
        } finally {
            vertexDataRepository.releaseV2MeshSourcesFor(consumed);
        }
    }

    public assignGeometricFeaturesToProducts(geometricFeaturesUrl: URL): Promise<boolean> {
        if (!this._bimApi) {
            return Promise.resolve(false);
        }
        const key = geometricFeaturesUrl.href.toLowerCase();
        const loadingPromise = this._isGeometricFeaturesUrlAdded.get(key);
        if (loadingPromise) {
            return loadingPromise;
        }
        const p = this.fetchAndDecodeGeometricFeatures(geometricFeaturesUrl);
        this._isGeometricFeaturesUrlAdded.set(key, p);
        return p;
    }

    /**
     * Resolve feature edges for a single mesh on demand.
     * Looks up the mesh's geometry hash in the decoded feature data,
     * resolves vertex indices to world-space coordinates, and caches the
     * result on the mesh as {@link FlatSegment3} tuples — the format
     * consumed directly by `CreateGreasedLine`.
     *
     * Returns the edges, or undefined if no feature data exists for this mesh.
     */
    public resolveEdgesForMesh(mesh: BimProductMesh): FlatSegment3[] | undefined {
        if (mesh.edges) return mesh.edges;

        const geometricFeature = this._decodedFeaturesByHash.get(mesh.descriptor.h);
        if (geometricFeature == null) {
            return undefined;
        }

        const vertexData = mesh.vertexData;
        const matrix = mesh.transform();
        const indices = geometricFeature.edges;
        const vertices = new Map<number, Vec3>();
        for (const i of indices) {
            if (vertices.has(i)) continue;
            BimGeometricFeaturesRepository._temp.set(
                vertexData.positions[3 * i],
                vertexData.positions[3 * i + 1],
                vertexData.positions[3 * i + 2]
            );
            vertices.set(
                i,
                Vector3.TransformCoordinates(BimGeometricFeaturesRepository._temp, matrix).asArray() as Vec3
            );
        }

        const edges: FlatSegment3[] = [];
        for (let i = 0; i + 1 < indices.length; i += 2) {
            const a = vertices.get(indices[i])!;
            const b = vertices.get(indices[i + 1])!;
            edges.push([a[0], a[1], a[2], b[0], b[1], b[2]]);
        }

        (mesh as { edges?: FlatSegment3[] }).edges = edges;
        return edges;
    }

    public clear(): void {
        this._isGeometricFeaturesUrlAdded.clear();
        // Note: _decodedFeaturesByHash is intentionally NOT cleared here.
        // With lazy edge resolution, the decoded feature data must remain
        // available for resolveEdgesForMesh() calls after loading completes.
    }

    private async fetchAndDecodeGeometricFeatures(geometricFeaturesUrl: URL): Promise<boolean> {
        if (!this._bimApi) {
            return false;
        }
        const { status, value } = await this._bimApi.get(geometricFeaturesUrl, HttpResponseType.arrayBuffer);
        if (status !== 200) {
            return false;
        }
        const geometryArrayBuffer = await value;
        const decoded = this._geometricFeaturesReader.decodeGeometricFeatures(geometryArrayBuffer);

        // Store decoded index data (compact) — edges are resolved per-mesh on demand via resolveEdgesForMesh
        for (const [hash, features] of decoded) {
            this._decodedFeaturesByHash.set(hash, features);
        }

        return true;
    }
}
