import type { CullOnCameraMovePredicate } from './CullOnCameraMovePredicate';
import { MergeId, MergeIdBuilderAlgorithm } from './MergeId';

import { type IBimIfcLoaderElement } from './bim-ifc-loader-element';
import { type BimIfcObject } from './bim-ifc-object';
import { StopWatch, yieldThreadFor } from './stopwatch';

/**
 * Builder class for creating and optimizing {@link MergeId} instances from IFC loader elements.
 *
 * Each builder instance has a unique ID used as a prefix for generated merge IDs.
 * The builder processes meshes asynchronously, yielding periodically to avoid blocking the main thread.
 */
export class MergeIdBuilder {
    private static _id = -1; // TODO: Does not have to be static if we have a reusable instance.

    /** Unique identifier for this builder instance, used as prefix in generated merge IDs. */
    public readonly id: number;

    /** Creates a new MergeIdBuilder with a unique auto-incrementing ID. */
    public constructor(readonly _mergeIdAlgorithmFactory: (id: string) => MergeIdBuilderAlgorithm) {
        MergeIdBuilder._id++;
        this.id = MergeIdBuilder._id;
    }

    /**
     * Processes all meshes from the given loader elements and generates optimized {@link MergeId} instances.
     *
     * Meshes are spatially grouped into grid cells and then hierarchically merged to optimize
     * rendering performance. The operation yields periodically to prevent blocking the main thread.
     *
     * @param options - Configuration options for the merge operation.
     * @param options.loaderElements - The IFC loader elements containing products and meshes to process.
     * @param options.predicate - Filter predicate to determine which IFC products to include.
     * @param options.cullOnCameraMovePredicate - Predicate to determine if a mesh should be culled on camera move.
     * @param options.deadlineMs - Maximum time in milliseconds before yielding to the main thread.
     * @param options.sleepInMs - Duration in milliseconds to sleep when yielding.
     * @returns Promise resolving to an array of optimized {@link MergeId} instances.
     */
    public async mergeIds({
        loaderElements,
        predicate,
        deadlineMs,
        sleepInMs,
        cullOnCameraMovePredicate
    }: {
        loaderElements: IBimIfcLoaderElement[];
        predicate: (ifcProduct: BimIfcObject) => unknown;
        cullOnCameraMovePredicate: CullOnCameraMovePredicate;
        deadlineMs: number;
        sleepInMs: number;
    }): Promise<MergeId[]> {
        const mergeIds = this._mergeIdAlgorithmFactory(this.id.toString());
        const sW = new StopWatch();
        let count = 0;
        for (const loaderElement of loaderElements) {
            for (const ifcProduct of loaderElement.products) {
                if (!predicate(ifcProduct)) continue;
                for (const mesh of ifcProduct.productMeshes) {
                    if (mesh.isOnGpu) continue;
                    mergeIds.add(mesh, cullOnCameraMovePredicate);
                    count++;
                }

                // NOTE: Use of count because it does cost a little bit to access sW.elapsed. So we only do it
                // every 1000 meshes to reduce overhead. This optimization matters when processing millions of meshes.
                if (count % 1000 === 0 && sW.elapsed > deadlineMs) {
                    await yieldThreadFor(sleepInMs);
                    sW.resetAndStart();
                }
            }
        }

        return mergeIds.build();
    }
}
