// This Source Code Form is subject to the terms of the Mozilla Public
// License, v2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/
import { getTimestampFromImage, } from "@foxglove/studio-base/panels/ThreeDeeRender/renderables/Images/ImageTypes";
import { normalizeCompressedImage, normalizeRawImage, normalizeRosCompressedImage, normalizeRosImage, } from "@foxglove/studio-base/panels/ThreeDeeRender/renderables/Images/imageNormalizers";
import { normalizeCameraInfo } from "@foxglove/studio-base/panels/ThreeDeeRender/renderables/projections";
import { normalizeAnnotations } from "./annotations/normalizeAnnotations";
import { IMAGE_MODE_HUD_GROUP_ID, WAITING_FOR_BOTH_MESSAGES_HUD_ID, WAITING_FOR_CALIBRATION_HUD_ID, WAITING_FOR_IMAGES_EMPTY_HUD_ID, WAITING_FOR_IMAGES_NOTICE_ID, WAITING_FOR_SYNC_EMPTY_HUD_ID, WAITING_FOR_SYNC_NOTICE_HUD_ID, } from "./constants";
import { t3D } from "../../t3D";
const syncToleranceSec = 0.15; // maximum acceptable time difference for selecting a “closest” annotation.
const maxStalenessSec = 1.1; // how long we can hold the last older annotation if no better match is available. 1s corresponds to 1 FPS + 100ms window for jitter.
const hysteresisSec = 0.1; // how much “stickiness” we allow before switching candidates.
const imageRetentionSec = 5; // keep small window of recent images
const maxImageBuffer = 150; // cap number of stored images
// Have constants for the HUD items so that they don't need to be recreated and GCed every message
export const WAITING_FOR_BOTH_HUD_ITEM = {
    id: WAITING_FOR_BOTH_MESSAGES_HUD_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForCalibrationAndImages"),
    displayType: "empty",
};
export const WAITING_FOR_CALIBRATION_HUD_ITEM = {
    id: WAITING_FOR_CALIBRATION_HUD_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForCalibration"),
    displayType: "empty",
};
export const WAITING_FOR_IMAGE_NOTICE_HUD_ITEM = {
    id: WAITING_FOR_IMAGES_NOTICE_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForImages"),
    displayType: "notice",
};
export const WAITING_FOR_IMAGE_EMPTY_HUD_ITEM = {
    id: WAITING_FOR_IMAGES_EMPTY_HUD_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForImages"),
    displayType: "empty",
};
export const WAITING_FOR_SYNC_NOTICE_HUD_ITEM = {
    id: WAITING_FOR_SYNC_NOTICE_HUD_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForSyncAnnotations"),
    displayType: "notice",
};
export const WAITING_FOR_SYNC_EMPTY_HUD_ITEM = {
    id: WAITING_FOR_SYNC_EMPTY_HUD_ID,
    group: IMAGE_MODE_HUD_GROUP_ID,
    getMessage: () => t3D("waitingForSyncAnnotations"),
    displayType: "empty",
};
/**
 * Processes and normalizes incoming messages and manages state of
 * messages to be rendered given the ImageMode config. A large part of this responsibility
 * is managing state in synchronized mode and ensuring that the a synchronized set of image and
 * annotations are handed off to the SceneExtension for rendering.
 */
export class MessageHandler {
    /** settings that should reflect image mode config */
    #config;
    /** Allows message handler push messages to overlay on top of the canvas */
    #hud;
    /** last state passed to listeners */
    #oldRenderState;
    /** internal state of last received messages */
    #lastReceivedMessages;
    /** listener functions that are called when the state changes. */
    #listeners = [];
    /** Holds what annotations are currently available on the given source. These are needed because annotations
     * that are marked as visible may be present in the layout/config, but are not present on the source.
     * This can cause synchronized annotations to never resolve if the source does not have the annotation topic
     * with no indication to the user that the annotation is not available.
     */
    #onAnnotationReceivedEvent;
    #annotationsFpsRefreshInterval = null;
    lastImage;
    // New: keep a buffer of recent images for annotation-prioritized synchronization
    imagesBuffer = [];
    // Store a small buffer of recent annotations per topic with computed seconds timestamps
    annotationsMap = new Map();
    // Track last selected annotation per topic to add hysteresis and reduce flicker
    lastSelectedByTopic = new Map();
    availableAnnotationTopics;
    /**
     * Create a MessageHandler
     * @param {Immutable<MessageHandlerConfig>} config - subset of ImageMode settings required for message handling
     * @param {HUDItemManager} hud - HUD manager used to display informational messages on the canvas
     */
    constructor(config, hud) {
        this.#config = config;
        this.#hud = hud;
        this.#lastReceivedMessages = {
            annotationsByTopic: new Map(),
        };
        this.availableAnnotationTopics = new Set();
    }
    dispose() {
        if (this.#annotationsFpsRefreshInterval) {
            clearInterval(this.#annotationsFpsRefreshInterval);
        }
    }
    addListener(listener) {
        this.#listeners.push(listener);
    }
    addAnnotationReceivedEventHandler(handler) {
        this.#onAnnotationReceivedEvent = handler;
    }
    removeListener(listener) {
        this.#listeners = this.#listeners.filter((fn) => fn !== listener);
    }
    handleRosRawImage = (messageEvent) => {
        this.handleImage(messageEvent, normalizeRosImage(messageEvent.message));
    };
    handleRosCompressedImage = (messageEvent) => {
        this.handleImage(messageEvent, normalizeRosCompressedImage(messageEvent.message));
    };
    handleRawImage = (messageEvent) => {
        this.handleImage(messageEvent, normalizeRawImage(messageEvent.message));
    };
    handleCompressedImage = (messageEvent) => {
        this.handleImage(messageEvent, normalizeCompressedImage(messageEvent.message));
    };
    handleImage(message, image) {
        this.lastImage = {
            ...message,
            message: image,
        };
        // Compute timestamp seconds for buffer storage (prefer image stamp, then receiveTime, then wall clock)
        const stamp = getTimestampFromImage(image);
        let stampTimeSec;
        const stampSec = stamp?.sec;
        const stampNsec = stamp?.nsec;
        if (typeof stampSec === "number" && typeof stampNsec === "number") {
            stampTimeSec = stampSec + stampNsec / 1e9;
        }
        else {
            const recv = message.receiveTime;
            const recvSec = recv?.sec;
            const recvNsec = recv?.nsec;
            if (typeof recvSec === "number" && typeof recvNsec === "number") {
                stampTimeSec = recvSec + recvNsec / 1e9;
            }
        }
        const arrivalSec = performance.now() / 1000;
        this.imagesBuffer.push({ image: this.lastImage, stampTimeSec, arrivalSec });
        // Trim by arrival time and length (do not use stamped time for retention)
        const nowSec = performance.now() / 1000;
        const cutoffArrival = nowSec - imageRetentionSec;
        this.imagesBuffer = this.imagesBuffer.filter((e) => e.arrivalSec >= cutoffArrival);
        if (this.imagesBuffer.length > maxImageBuffer) {
            this.imagesBuffer.splice(0, this.imagesBuffer.length - maxImageBuffer);
        }
        this.#emitState();
    }
    handleCameraInfo = (message) => {
        this.#lastReceivedMessages.cameraInfo = normalizeCameraInfo(message.message);
        this.#emitState();
    };
    handleAnnotations = (messageEvent) => {
        const list = normalizeAnnotations(messageEvent.message, messageEvent.schemaName);
        const arrivalSec = performance.now() / 1000;
        if (list.length > 0 && messageEvent.topic) {
            this.#onAnnotationReceivedEvent?.();
            // Prefer timestamps embedded in the annotations themselves
            const stampTimes = [];
            for (const a of list) {
                const sec = a.stamp?.sec;
                const nsec = a.stamp?.nsec;
                if (typeof sec === "number" && typeof nsec === "number") {
                    stampTimes.push(sec + nsec / 1e9);
                }
            }
            const timeSec = stampTimes.length > 0 ? Math.max(...stampTimes) : arrivalSec;
            const entry = {
                annotation: {
                    originalMessage: messageEvent,
                    annotations: list,
                },
                timeSec,
                arrivalSec,
            };
            const topic = messageEvent.topic;
            const buffer = this.annotationsMap.get(topic) ?? [];
            buffer.push(entry);
            // Keep unlimited buffer length, but drop entries older than 20 seconds based on arrival time
            const retentionSec = 20;
            const nowSec = performance.now() / 1000;
            const cutoffArrival = nowSec - retentionSec;
            const trimmed = buffer.filter((e) => e.arrivalSec >= cutoffArrival);
            this.annotationsMap.set(topic, trimmed);
        }
        // Always emit; render selection logic will pick appropriate annotations for the last image
        this.#emitState();
        if (this.#config.synchronize === false) {
            return;
        }
        // synchronized mode does selection in #getRenderState
    };
    setConfig(newConfig) {
        let changed = false;
        if (newConfig.synchronize != undefined && newConfig.synchronize !== this.#config.synchronize) {
            this.#oldRenderState = undefined;
            changed = true;
        }
        if ("imageTopic" in newConfig && this.#config.imageTopic !== newConfig.imageTopic) {
            this.#lastReceivedMessages.image = undefined;
            changed = true;
        }
        if (this.#config.calibrationTopic !== newConfig.calibrationTopic) {
            this.#lastReceivedMessages.cameraInfo = undefined;
            changed = true;
        }
        if (newConfig.annotations != undefined &&
            this.#config.annotations &&
            this.#config.annotations !== newConfig.annotations) {
            const newVisibleTopics = new Set();
            for (const [topic, settings] of Object.entries(newConfig.annotations)) {
                if (settings?.visible === true) {
                    newVisibleTopics.add(topic);
                }
            }
            for (const topic of this.#lastReceivedMessages.annotationsByTopic.keys()) {
                if (!newVisibleTopics.has(topic)) {
                    this.#lastReceivedMessages.annotationsByTopic.delete(topic);
                    changed = true;
                }
            }
        }
        this.#config = newConfig;
        if (changed) {
            this.#emitState();
        }
    }
    setAvailableAnnotationTopics(topicNames) {
        this.availableAnnotationTopics = new Set(topicNames);
        this.#emitState();
    }
    clear() {
        this.#lastReceivedMessages = {
            annotationsByTopic: new Map(),
        };
        this.annotationsMap.clear();
        this.lastSelectedByTopic.clear();
        this.lastImage = undefined;
        this.imagesBuffer = [];
        this.#oldRenderState = undefined;
        this.#emitState();
    }
    #emitState() {
        const state = this.getRenderStateAndUpdateHUD();
        this.#listeners.forEach((fn) => {
            fn(state, this.#oldRenderState);
        });
        this.#oldRenderState = state;
    }
    /** Do not use. only public for testing */
    getRenderStateAndUpdateHUD() {
        const state = this.#getRenderState();
        this.#updateHUDFromState(state);
        return state;
    }
    #updateHUDFromState(state) {
        const calibrationRequired = this.#config.calibrationTopic != undefined;
        const waitingForImage = this.#lastReceivedMessages.image == undefined && state.image == undefined;
        const waitingForCalibration = calibrationRequired && state.cameraInfo == undefined;
        const waitingForBoth = waitingForImage && waitingForCalibration;
        this.#hud.displayIfTrue(waitingForBoth, WAITING_FOR_BOTH_HUD_ITEM);
        this.#hud.displayIfTrue(waitingForCalibration && !waitingForBoth, WAITING_FOR_CALIBRATION_HUD_ITEM);
        this.#hud.displayIfTrue(waitingForImage && !calibrationRequired && !waitingForBoth, WAITING_FOR_IMAGE_EMPTY_HUD_ITEM);
        this.#hud.displayIfTrue(waitingForImage && calibrationRequired, WAITING_FOR_IMAGE_NOTICE_HUD_ITEM);
    }
    #getRenderState() {
        if (this.imagesBuffer.length === 0) {
            return {
                annotationsByTopic: new Map(),
                presentAnnotationTopics: undefined,
                missingAnnotationTopics: undefined,
            };
        }
        // Visible topics
        const visibleTopics = new Set();
        if (this.#config.annotations) {
            for (const [topic, settings] of Object.entries(this.#config.annotations)) {
                if (settings?.visible)
                    visibleTopics.add(topic);
            }
        }
        // If there are no visible annotation topics, just render the latest image
        if (visibleTopics.size === 0) {
            const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
            return {
                image: newestImageEntry.image,
                annotationsByTopic: new Map(),
                presentAnnotationTopics: undefined,
                missingAnnotationTopics: undefined,
            };
        }
        // Fast-path: if none of the visible topics has any annotation data yet, render image only
        let hasAnyAnnotationForVisible = false;
        for (const t of visibleTopics) {
            const buf = this.annotationsMap.get(t);
            if (buf && buf.length > 0) {
                hasAnyAnnotationForVisible = true;
                break;
            }
        }
        if (!hasAnyAnnotationForVisible) {
            const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
            return {
                image: newestImageEntry.image,
                annotationsByTopic: new Map(),
                presentAnnotationTopics: undefined,
                missingAnnotationTopics: undefined,
            };
        }
        // Required topics (available & visible)
        const requiredTopics = [];
        for (const t of visibleTopics) {
            if (this.availableAnnotationTopics.size === 0 || this.availableAnnotationTopics.has(t)) {
                requiredTopics.push(t);
            }
        }
        // New policy: always start from newest image by arrival (do not freeze on older frame).
        const newestImageEntry = this.imagesBuffer[this.imagesBuffer.length - 1];
        const imageTimestampSec = typeof newestImageEntry.stampTimeSec === "number"
            ? newestImageEntry.stampTimeSec
            : newestImageEntry.arrivalSec;
        const selectedAnnotations = new Map();
        const presentTopics = [];
        const missingTopics = [];
        for (const topic of visibleTopics) {
            const buffer = this.annotationsMap.get(topic);
            if (!buffer || buffer.length === 0) {
                // No annotations yet for this topic
                if (requiredTopics.includes(topic)) {
                    // waiting, but we still render image
                    // do not classify as missing because no candidate arrived yet
                }
                continue;
            }
            // Find preferred (newest <= image time within staleness)
            let preferred;
            for (let i = buffer.length - 1; i >= 0; i--) {
                const entry = buffer[i];
                const age = imageTimestampSec - entry.timeSec;
                if (age >= 0 && age <= maxStalenessSec) {
                    preferred = entry;
                    break;
                }
            }
            // Fallback: closest within tolerance window
            let closest = buffer[0];
            let closestDelta = Math.abs(closest.timeSec - imageTimestampSec);
            for (let i = 1; i < buffer.length; i++) {
                const entry = buffer[i];
                const delta = Math.abs(entry.timeSec - imageTimestampSec);
                if (delta < closestDelta) {
                    closest = entry;
                    closestDelta = delta;
                }
            }
            let candidate = preferred ?? closest;
            if (!candidate)
                continue;
            const candidateDelta = Math.abs(candidate.timeSec - imageTimestampSec);
            const candidateAge = imageTimestampSec - candidate.timeSec;
            const acceptable = (preferred && candidateAge >= 0 && candidateAge <= maxStalenessSec) ||
                (!preferred && candidateDelta <= syncToleranceSec);
            const notFutureFar = candidate.timeSec <= imageTimestampSec + syncToleranceSec;
            if (!acceptable || !notFutureFar) {
                // can't use for now
                continue;
            }
            // Hysteresis: keep previous if close
            const prevTime = this.lastSelectedByTopic.get(topic);
            if (prevTime != undefined) {
                const prevEntry = buffer.find((e) => Math.abs(e.timeSec - prevTime) < 1e-6);
                if (prevEntry) {
                    const prevDelta = Math.abs(prevEntry.timeSec - imageTimestampSec);
                    const prevAge = imageTimestampSec - prevEntry.timeSec;
                    const prevValid = (prevAge >= 0 && prevAge <= maxStalenessSec) || prevDelta <= syncToleranceSec;
                    if (prevValid && Math.abs(prevEntry.timeSec - candidate.timeSec) <= hysteresisSec) {
                        candidate = prevEntry;
                    }
                }
            }
            selectedAnnotations.set(topic, candidate.annotation);
            presentTopics.push(topic);
            this.lastSelectedByTopic.set(topic, candidate.timeSec);
        }
        // Determine missing topics (we have at least some annotation messages for them but could not align one this frame)
        for (const t of requiredTopics) {
            if (!presentTopics.includes(t)) {
                const buf = this.annotationsMap.get(t);
                if (buf && buf.length > 0) {
                    // We received data but could not synchronize within tolerance -> mark missing
                    missingTopics.push(t);
                }
            }
        }
        // Always return newest image; do not block rendering if annotations incomplete
        return {
            image: newestImageEntry.image,
            annotationsByTopic: selectedAnnotations,
            // Provide mismatch info only if we have both present and missing; otherwise undefined to reduce HUD noise
            presentAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? presentTopics.sort() : undefined,
            missingAnnotationTopics: presentTopics.length > 0 && missingTopics.length > 0 ? missingTopics.sort() : undefined,
        };
    }
}
//# sourceMappingURL=MessageHandler.js.map