// 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 { debouncePromise } from "@foxglove/den/async";
import { fromMillis, isGreaterThan, isLessThan } from "@foxglove/rostime";
import { estimateObjectSize } from "@foxglove/studio-base/players/messageMemoryEstimation";
import { PlayerCapabilities, PlayerPresence } from "@foxglove/studio-base/players/types";
import { DepthToPointcloudWorkerPool } from "../messaging/deserialization/pointcloud/poitcloudPoolManager.js";
import { createMessageHandler } from "../messaging/message-handler.js";
import { protobufsBySchema, protobufSchemaNameToType } from "../messaging/protobuf";
import { RateLimiter } from "../messaging/rate-limiter";
import { FrameStore } from "../utils/frame-store";
import { getIMUEventEmitter } from "../utils/imu-manager.js";
import { constructKeyForTopicRenderMetrics, globalDecodeMetricsManager, globalInputEventMetricsManager, globalThroughputMetricsManager } from "../utils/metrics-manager.js";
import { PoitCloudSyncFramework } from "../utils/poitcloud-sync.js";
class ProblemManager {
    problems = [];
    addProblem(problem) {
        this.problems.push(problem);
    }
    clearProblems() {
        this.problems = [];
    }
    getProblems() {
        return this.problems;
    }
}
export class FoxgloveConnection {
    listener;
    id;
    // player options
    playerPresence = PlayerPresence.NOT_PRESENT;
    playerCapabilities = [PlayerCapabilities.playbackControl];
    // state options
    messageEvents = [];
    lastEncodedFrame;
    receivedBytes = 0;
    startTime;
    endTime;
    parameters = new Map();
    publishedTopics = new Map();
    subscribedTopics = new Map();
    unsupportedTopics = new Set();
    advertisedServices = new Map();
    problems = new ProblemManager();
    resolutions = new Map();
    inputMetrics = globalInputEventMetricsManager;
    decodeMetrics = globalDecodeMetricsManager;
    throughputMetrics = globalThroughputMetricsManager;
    PoitCloudSyncFramework = new PoitCloudSyncFramework();
    depthToPointcloudWorkerPool = new DepthToPointcloudWorkerPool((event, frame, message) => {
        this.submitPointcloudFrameCallback(event, frame, message);
    });
    IMUEventEmitter = getIMUEventEmitter();
    // message handler
    messageHandler;
    rateLimitModCoefficient = 1;
    rateLimitCounter = {
        image: 0,
        pointCloud: 0,
        annotations: 0,
        imu: 0,
    };
    rateLimiter = new RateLimiter([
        { delayCap: 100, moduloCoefficient: 2 },
        { delayCap: 200, moduloCoefficient: 3 },
        { delayCap: 300, moduloCoefficient: 4 },
        { delayCap: 400, moduloCoefficient: 5 },
        { delayCap: 500, moduloCoefficient: 10 },
    ], (newValue) => {
        console.warn(`Setting rate limiting coefficient to ${newValue}`);
        this.rateLimitModCoefficient = newValue;
    });
    topics = {};
    constructor(id) {
        this.id = id;
        this.messageHandler = createMessageHandler(this.handleParsedMessage.bind(this));
    }
    setListener(listener) {
        this.listener = listener;
        this.emitState();
    }
    getTopicMetrics(topic) {
        return {
            incomingMessagesFps: this.inputMetrics.calculate(constructKeyForTopicRenderMetrics("incomingImageFrame", topic)),
            decodedMessagesFps: this.decodeMetrics.calculate(topic),
            messageThroughput: this.throughputMetrics.calculate(topic),
            resolutions: this.resolutions,
        };
    }
    getCurrentTime() {
        return fromMillis(Date.now());
    }
    onMessagePacket(event, errorCallback) {
        const [imageFrameMetricsKey, annotationsFrameMetricsKey, pointCloudFrameMetricsKey] = [
            constructKeyForTopicRenderMetrics("incomingImageFrame", event.data.topic),
            constructKeyForTopicRenderMetrics("incomingAnnotationsFrame", event.data.topic),
            constructKeyForTopicRenderMetrics("incomingPointCloudFrame", event.data.topic),
        ];
        const specificProtobuf = protobufsBySchema[event.data.schema]?.type;
        if (!specificProtobuf) {
            throw new Error("Unsupported protobuf schema: " + event.data.schema);
        }
        const packetType = protobufSchemaNameToType(specificProtobuf);
        const rateLimiterCounterForType = this.rateLimitCounter[packetType];
        this.rateLimitCounter[packetType] =
            (rateLimiterCounterForType + 1) % this.rateLimitModCoefficient;
        // If the rate limiting coefficient is not 1 we want to skip messages which
        if (this.isMessageTypeDominant(packetType, event.data.topic) &&
            this.rateLimitCounter[packetType] !== 0) {
            console.warn(`Skipping message of type ${packetType} due to rate limiting in topic ${event.data.topic}.`);
            return;
        }
        this.inputMetrics.registerEvent(packetType === "image"
            ? imageFrameMetricsKey
            : packetType === "pointCloud"
                ? pointCloudFrameMetricsKey
                : annotationsFrameMetricsKey);
        this.throughputMetrics.registerEvent(event.data.topic, event.data.buffer.byteLength);
        this.messageHandler.handle(event.data, errorCallback);
    }
    submitPointcloudFrameCallback(coloredEvent, poitcloudFrame, message) {
        coloredEvent.topic = "Point Cloud";
        coloredEvent.schemaName = "foxglove.PointCloud";
        coloredEvent.message = poitcloudFrame;
        // @ts-expect-error - ITS unknow
        // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
        coloredEvent.message.data = message;
        const messageSize = estimateObjectSize(coloredEvent.message);
        coloredEvent.sizeInBytes = messageSize;
        FrameStore.getInstance().registerFrame("Point Cloud", coloredEvent);
        this.messageEvents.push(coloredEvent);
        this.emitState();
    }
    async handleParsePointcloudMessage(coloredEvent) {
        const poitcloudFrame = this.PoitCloudSyncFramework.getFrame(coloredEvent.receiveTime.nsec);
        const { width: colorWith, height: colorHeight } = this.parseResolutionFromMessage(coloredEvent.message, coloredEvent.topic);
        if (poitcloudFrame) {
            const cameraIntrinsicsString = localStorage.getItem("neuralCameraIntrinsics");
            if (!cameraIntrinsicsString) {
                console.warn("Cannot parse pointcloud frame, missing camera intrinsics");
                return;
            }
            const neuralCameraIntrinsics = JSON.parse(cameraIntrinsicsString);
            await this.depthToPointcloudWorkerPool.queueDepthFrame({
                depthFrame: poitcloudFrame,
                width: poitcloudFrame.width ?? 0,
                height: poitcloudFrame.height ?? 0,
                fx: neuralCameraIntrinsics.right.focalLenght.x,
                fy: neuralCameraIntrinsics.right.focalLenght.y,
                cx: neuralCameraIntrinsics.right.principalPoint.x,
                cy: neuralCameraIntrinsics.right.principalPoint.y,
                colorFrame: coloredEvent,
                i420Width: colorWith ?? poitcloudFrame.width ?? 0,
                i420Height: colorHeight ?? poitcloudFrame.height ?? 0,
            });
        }
    }
    parseResolutionFromMessage(message, topic) {
        // Check if usual width/height attributes are present
        // @ts-expect-error - ITS unknow
        if ("width" in message && "height" in message) {
            this.resolutions.set(topic, `${message.width}x${message.height}`);
            return { width: message.width, height: message.height };
        }
        return { width: undefined, height: undefined };
    }
    handleParsedMessage(event) {
        this.decodeMetrics.registerEvent(event.topic);
        // FIXME: This isn't pretty but PC frames are transformed inside submitPointcloudFrameCallback
        // FIXME: and we only want to register them to FrameStore when they are transformed.
        if (event.topic !== "Point Cloud") {
            FrameStore.getInstance().registerFrame(event.topic, event);
        }
        if (event.schemaName === "foxglove.IMU") {
            this.IMUEventEmitter.push(event.message);
            return;
        }
        this.parseResolutionFromMessage(event.message, event.topic);
        if (event.schemaName === "foxglove.PointCloud") {
            this.PoitCloudSyncFramework.addFrame(event.message);
            return;
        }
        if (event.schemaName === "foxglove.PointClouds.Colored") {
            event.schemaName = "foxglove.PointClouds";
        }
        const messageSize = estimateObjectSize(event.message);
        event.sizeInBytes = messageSize;
        this.messageEvents.push(event);
        this.emitState();
        const poitcloudColorFrameName = this.subscribedTopics.get("_Point Cloud Color")
            ? "_Point Cloud Color"
            : "H.264 Stream";
        if (event.topic === poitcloudColorFrameName) {
            /* Good for performance testing
            const date = Date.now();
            event.publishTime= {
              sec: Math.floor(date / 1000),
              nsec: date *10 ** 6,
            };
            */
            void this.handleParsePointcloudMessage(event);
        }
    }
    emitState = debouncePromise(async () => {
        const messages = this.messageEvents;
        this.messageEvents = [];
        if (!this.listener || messages.length === 0) {
            await Promise.resolve();
            return;
        }
        const currentTime = this.getCurrentTime();
        if (!this.startTime || isLessThan(currentTime, this.startTime)) {
            this.startTime = currentTime;
        }
        if (!this.endTime || isGreaterThan(currentTime, this.endTime)) {
            this.endTime = currentTime;
        }
        const problems = this.problems.getProblems();
        this.problems.clearProblems();
        for (const message of messages) {
            this.topics[message.topic] = {
                name: message.topic,
                schemaName: message.schemaName,
            };
        }
        const newPlayerState = {
            name: this.id,
            presence: this.playerPresence,
            progress: {},
            capabilities: this.playerCapabilities,
            profile: undefined,
            playerId: "internal-player",
            problems,
            urlState: undefined,
            activeData: {
                messages,
                totalBytesReceived: this.receivedBytes,
                startTime: this.startTime,
                endTime: this.endTime,
                currentTime,
                isPlaying: true,
                repeatEnabled: false,
                speed: 1,
                lastSeekTime: 0,
                topics: Object.values(this.topics),
                topicStats: new Map(),
                datatypes: new Map(),
                parameters: this.parameters,
                publishedTopics: this.publishedTopics,
                subscribedTopics: this.subscribedTopics,
                unsupportedTopics: this.unsupportedTopics,
                services: this.advertisedServices,
            },
        };
        await this.listener(newPlayerState);
    });
    isMessageTypeDominant(packetType, topic) {
        const fpsPerPacketTypeList = [
            {
                packetType: "image",
                incomingFps: globalInputEventMetricsManager.calculate(constructKeyForTopicRenderMetrics("incomingImageFrame", topic)),
            },
            {
                packetType: "annotations",
                incomingFps: globalInputEventMetricsManager.calculate(constructKeyForTopicRenderMetrics("incomingAnnotationsFrame", topic)),
            },
            {
                packetType: "pointCloud",
                incomingFps: globalInputEventMetricsManager.calculate(constructKeyForTopicRenderMetrics("incomingPointCloudFrame", topic)),
            },
        ].sort((a, b) => (b.incomingFps ?? 0) - (a.incomingFps ?? 0));
        return fpsPerPacketTypeList.at(0)?.packetType === packetType;
    }
}
//# sourceMappingURL=foxglove-connection.js.map