import { ComlinkWrap } from "@foxglove/den/worker";
import { uint8ArrayToUint16Array } from "./utils";
import { Logger } from "../../../utils/logger";
import { globalInputEventMetricsManager, } from "../../../utils/metrics-manager";
const logger = Logger.getLogger();
const WORKER_COUNT = 4;
const DEFAULT_TARGET_FPS = 9;
const DEFAULT_MAX_PROCESSED_QUEUE_SIZE = 25;
const DISPATCH_INTERVAL_MS = 100; // The interval in which the pool will dispatch frames. NOTE: Lower values may result in lag spikes.
const DISPATCH_INTERVAL_FRACTION = 1000 / DISPATCH_INTERVAL_MS;
const MAX_BATCH_SIZE = 3;
export class DepthToPointcloudWorkerPool {
    lastUsedWorkerIndex = 0;
    workerApis;
    #frameQueue = [];
    #processedFrameQueue = [];
    #isWebGpuSupported = false;
    #metrics;
    #maximumStorageBufferSize;
    #FRAME_BATCH_SIZE;
    #maxProcessedQueueSize;
    #prevColorFrameWidth;
    #prevDepthFrameWidth;
    #dispatchCallback;
    #targetDispatchIntervalMs;
    #isProcessing = false;
    #dispatchTimeoutId = undefined;
    constructor(callback, batchSize = MAX_BATCH_SIZE, targetFps = DEFAULT_TARGET_FPS, maxProcessedQueueSize = DEFAULT_MAX_PROCESSED_QUEUE_SIZE) {
        if (typeof navigator.gpu !== "undefined") {
            void navigator.gpu.requestAdapter().then(gpuAdapter => {
                if (!gpuAdapter) {
                    this.#isWebGpuSupported = false;
                    logger.warn("WebGPU adapter not found. Falling back to CPU for all frames.");
                }
                else {
                    this.#isWebGpuSupported = true;
                    this.#maximumStorageBufferSize = gpuAdapter.limits.maxStorageBufferBindingSize / 16;
                    logger.info("WebGPU potentially supported. Will attempt GPU processing.");
                }
            }).catch(err => {
                logger.warn("Error requesting WebGPU adapter. Falling back to CPU.", err);
                this.#isWebGpuSupported = false;
            });
        }
        else {
            logger.warn("navigator.gpu is undefined. WebGPU not supported. Falling back to CPU for all frames.");
            this.#isWebGpuSupported = false;
        }
        this.#maximumStorageBufferSize = -1;
        this.#prevDepthFrameWidth = 0;
        this.#prevColorFrameWidth = 0;
        this.#dispatchCallback = callback;
        this.#FRAME_BATCH_SIZE = Math.max(1, batchSize);
        this.#targetDispatchIntervalMs = DISPATCH_INTERVAL_MS / Math.max(1, Math.round(targetFps / DISPATCH_INTERVAL_FRACTION)) * this.#calculateBatchMultiplier();
        this.#maxProcessedQueueSize = Math.max(1, maxProcessedQueueSize);
        this.workerApis = new Array(WORKER_COUNT)
            .fill(undefined)
            .map(() => {
            const worker = new Worker(new URL("../pointcloud/pointcloudFromDepth.worker.ts", import.meta.url), {
                type: "module",
            });
            return ComlinkWrap(worker);
        });
        this.#metrics = globalInputEventMetricsManager;
        this.#metrics.registerEvent("pcl-pool.frameInput");
    }
    async queueDepthFrame(data) {
        // @ts-expect-error - Known
        // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
        if (!(data.colorFrame.message?.data instanceof Uint8Array)) {
            logger.warn("Skipping frame: colorFrame.message.data is not a Uint8Array", data.colorFrame);
            return;
        }
        if (data.width !== this.#prevDepthFrameWidth || data.i420Width !== this.#prevColorFrameWidth) {
            this.setMaxGpuQueueSize(data.width, data.height);
            this.#prevDepthFrameWidth = data.width;
            this.#prevColorFrameWidth = data.i420Width;
            this.#frameQueue = [];
        }
        if (data.width === data.i420Width && data.height === data.i420Height) {
            this.#frameQueue.push(data);
            this.tryProcessBatch();
        }
    }
    setMaxGpuQueueSize(width, height) {
        if (!this.#isWebGpuSupported) {
            return;
        }
        const numPixelsPerFrame = width * height;
        const currentOutSize = (numPixelsPerFrame * 16) * this.#FRAME_BATCH_SIZE;
        if (currentOutSize > this.#maximumStorageBufferSize) {
            logger.warn(`GPU queue size ${currentOutSize} exceeds maximum storage buffer size ${this.#maximumStorageBufferSize}. Reducing batch size.`);
            const frameBatchSize = Math.floor(this.#maximumStorageBufferSize / (numPixelsPerFrame * 16));
            this.#FRAME_BATCH_SIZE = Math.min(Math.max(1, frameBatchSize), MAX_BATCH_SIZE);
            if (frameBatchSize < 1) {
                // We can't fit one color and depth frame in GPU memory. So it's better to fallback to CPU
                logger.warn("Output buffer size is too small to process. Falling back to CPU.");
                this.#isWebGpuSupported = false;
            }
        }
    }
    // eslint-disable-next-line @foxglove/no-boolean-parameters
    tryProcessBatch(force = false) {
        this.#metrics?.registerEvent("pcl-pool.batchSent");
        const fps = this.#metrics?.calculate("pcl-pool.batchSent");
        if (fps && fps > 0) {
            this.#targetDispatchIntervalMs = (DISPATCH_INTERVAL_MS / Math.max(1, Math.round(fps / DISPATCH_INTERVAL_FRACTION))) * this.#calculateBatchMultiplier();
        }
        if ((this.#isProcessing || this.#frameQueue.length < this.#FRAME_BATCH_SIZE) && !force) {
            return;
        }
        this.#isProcessing = true;
        const batchToProcess = this.#frameQueue.splice(0, this.#FRAME_BATCH_SIZE);
        if (batchToProcess.length === 0) {
            this.#isProcessing = false;
            return;
        }
        // biome-ignore lint/style/noNonNullAssertion: <explanation>
        const firstFrame = batchToProcess[0];
        const width = firstFrame.width;
        const height = firstFrame.height;
        const fx = firstFrame.fx;
        const fy = firstFrame.fy;
        const cx = firstFrame.cx;
        const cy = firstFrame.cy;
        const i420Width = firstFrame.i420Width;
        const i420Height = firstFrame.i420Height;
        try {
            const depthDataArray = batchToProcess.map(f => uint8ArrayToUint16Array(f.depthFrame.data));
            // @ts-expect-error - ITS unknown
            // eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-member-access
            const i420DataArray = batchToProcess.map(f => f.colorFrame.message.data);
            const workerIndex = this.lastUsedWorkerIndex;
            // biome-ignore lint/style/noNonNullAssertion: <explanation>
            const workerApi = this.workerApis[workerIndex].remote;
            this.lastUsedWorkerIndex = (this.lastUsedWorkerIndex + 1) % WORKER_COUNT;
            workerApi.depthToPointcloudGPU(depthDataArray, width, height, fx, fy, cx, cy, i420DataArray, i420Width, i420Height, Number(localStorage.getItem("stereoDepthMaxValue") ?? "8000"), { hasGPU: this.#isWebGpuSupported })
                .then(results => {
                if (results.length === batchToProcess.length) {
                    for (let i = 0; i < results.length; i++) {
                        // biome-ignore lint/style/noNonNullAssertion: <explanation>
                        const originalFrame = batchToProcess[i];
                        const pointCloudResult = results[i];
                        if (!pointCloudResult) {
                            logger.warn(`Worker ${workerIndex}: Result ${i} is null or undefined. Passing null to callback.`);
                            this.#addToProcessedQueue({
                                originalColorEvent: originalFrame.colorFrame,
                                originalDepthFrame: originalFrame.depthFrame,
                                processedPointCloud: null,
                            });
                        }
                        else {
                            this.#addToProcessedQueue({
                                originalColorEvent: originalFrame.colorFrame,
                                originalDepthFrame: originalFrame.depthFrame,
                                processedPointCloud: pointCloudResult,
                            });
                        }
                    }
                }
                else {
                    logger.error(`Worker ${workerIndex}: Mismatch in result count (${results.length}) and batch size (${batchToProcess.length}). Discarding results for this batch.`);
                    for (const originalFrame of batchToProcess) {
                        this.#addToProcessedQueue({
                            originalColorEvent: originalFrame.colorFrame,
                            originalDepthFrame: originalFrame.depthFrame,
                            processedPointCloud: null,
                        });
                    }
                }
            })
                .catch(error => {
                logger.error(`Worker ${workerIndex}: Error processing batch:`, error);
                for (const originalFrame of batchToProcess) {
                    this.#addToProcessedQueue({
                        originalColorEvent: originalFrame.colorFrame,
                        originalDepthFrame: originalFrame.depthFrame,
                        processedPointCloud: null,
                    });
                }
            })
                .finally(() => {
                this.#isProcessing = false;
                this.tryProcessBatch();
            });
        }
        catch (error) {
            logger.error("Error preparing data for worker:", error);
            for (const originalFrame of batchToProcess) {
                this.#addToProcessedQueue({
                    originalColorEvent: originalFrame.colorFrame,
                    originalDepthFrame: originalFrame.depthFrame,
                    processedPointCloud: null,
                });
            }
            this.#isProcessing = false;
            this.tryProcessBatch();
        }
    }
    #calculateBatchMultiplier() {
        if (this.#processedFrameQueue.length > this.#maxProcessedQueueSize / 2) {
            return 1.2;
        }
        if (this.#processedFrameQueue.length >= this.#maxProcessedQueueSize - this.#maxProcessedQueueSize / 5) {
            return 0.75;
        }
        return 1.3;
    }
    #addToProcessedQueue(processedFrame) {
        if (this.#processedFrameQueue.length >= this.#maxProcessedQueueSize) {
            const fps = this.#metrics?.calculate("pcl-pool.batchSent");
            if (fps && fps > 0) {
                this.#targetDispatchIntervalMs = (DISPATCH_INTERVAL_MS / Math.max(1, Math.round(fps / DISPATCH_INTERVAL_FRACTION))) * this.#calculateBatchMultiplier();
            }
            this.#processedFrameQueue.shift();
            logger.warn(`Processed frame queue full (>= ${this.#maxProcessedQueueSize}). Dropping oldest frame.`);
        }
        this.#processedFrameQueue.push(processedFrame);
        if (this.#dispatchTimeoutId === undefined) {
            this.#scheduleNextDispatch();
        }
    }
    #scheduleNextDispatch() {
        if (this.#dispatchTimeoutId !== undefined) {
            clearTimeout(this.#dispatchTimeoutId);
        }
        const fps = this.#metrics?.calculate("pcl-pool.batchSent");
        if (fps && fps > 0) {
            const MIN_DISPATCH_INTERVAL_MS = 100;
            this.#targetDispatchIntervalMs = Math.min(MIN_DISPATCH_INTERVAL_MS, (DISPATCH_INTERVAL_MS / Math.max(1, Math.round(fps / DISPATCH_INTERVAL_FRACTION))) * this.#calculateBatchMultiplier());
        }
        this.#dispatchTimeoutId = setTimeout(this.#dispatchNextFrame, this.#targetDispatchIntervalMs);
        if (this.#processedFrameQueue.length <= 0) {
            this.#dispatchTimeoutId = undefined;
        }
    }
    #dispatchNextFrame = () => {
        const frameToDispatch = this.#processedFrameQueue.shift();
        if (frameToDispatch) {
            try {
                this.#dispatchCallback(frameToDispatch.originalColorEvent, frameToDispatch.originalDepthFrame, frameToDispatch.processedPointCloud);
            }
            catch (error) {
                logger.error("Error in dispatchCallback:", error);
            }
        }
        this.#dispatchTimeoutId = setTimeout(this.#dispatchNextFrame, this.#targetDispatchIntervalMs);
    };
    destroy() {
        if (this.#dispatchTimeoutId !== undefined) {
            clearTimeout(this.#dispatchTimeoutId);
            this.#dispatchTimeoutId = undefined;
        }
        this.#frameQueue = [];
        this.#processedFrameQueue = [];
        // Properly dispose of all workers and release Comlink proxies
        for (const workerWrapper of this.workerApis) {
            workerWrapper.dispose();
        }
        this.workerApis = [];
    }
}
//# sourceMappingURL=poitcloudPoolManager.js.map