// 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 * as _ from "lodash-es";
import * as THREE from "three";
import { assert } from "ts-essentials";
import Logger from "@foxglove/log";
import { toNanoSec } from "@foxglove/rostime";
import { Renderable } from "@foxglove/studio-base/panels/ThreeDeeRender/Renderable";
import { stringToRgba } from "@foxglove/studio-base/panels/ThreeDeeRender/color";
import { WorkerImageDecoder } from "@foxglove/studio-base/panels/ThreeDeeRender/renderables/Images/WorkerImageDecoder";
import { projectPixel } from "@foxglove/studio-base/panels/ThreeDeeRender/renderables/projections";
import { decodeCompressedImageToBitmap } from "./decodeImage";
import { DECODE_IMAGE_ERR_KEY, IMAGE_TOPIC_PATH } from "../ImageMode/constants";
const WORKER_COUNT = 4;
const log = Logger.getLogger(__filename);
const DEFAULT_DISTANCE = 1;
const DEFAULT_PLANAR_PROJECTION_FACTOR = 0;
export const IMAGE_RENDERABLE_DEFAULT_SETTINGS = {
    visible: false,
    frameLocked: true,
    cameraInfoTopic: undefined,
    distance: DEFAULT_DISTANCE,
    planarProjectionFactor: DEFAULT_PLANAR_PROJECTION_FACTOR,
    color: "#ffffff",
};
export class ImageRenderable extends Renderable {
    // Make sure that everything is build the first time we render
    // set when camera info or image changes
    #geometryNeedsUpdate = true;
    // set when geometry or material reference changes
    #meshNeedsUpdate = true;
    // set when image changes
    #textureNeedsUpdate = true;
    // set when material or texture changes
    #materialNeedsUpdate = true;
    #renderBehindScene = false;
    #isUpdating = false;
    #decodedImage;
    decoderPool;
    #lastUsedDecoderIndex = 0;
    #receivedImageSequenceNumber = 0;
    #displayedImageSequenceNumber = 0;
    #showingErrorImage = false;
    #disposed = false;
    constructor(topicName, renderer, userData) {
        super(topicName, renderer, userData);
        this.decoderPool = new Array(WORKER_COUNT).fill(undefined).map(() => new WorkerImageDecoder());
    }
    isDisposed() {
        return this.#disposed;
    }
    getDecodedImage() {
        return this.#decodedImage;
    }
    dispose() {
        this.#disposed = true;
        // Close ImageBitmap if it exists to free GPU memory
        if (this.#decodedImage instanceof ImageBitmap) {
            this.#decodedImage.close();
        }
        this.#decodedImage = undefined;
        this.userData.texture?.dispose();
        this.userData.material?.dispose();
        this.userData.geometry?.dispose();
        // Terminate all workers in the pool
        if (this.decoderPool) {
            for (const decoder of this.decoderPool) {
                decoder.terminate();
            }
            this.decoderPool.length = 0;
        }
        super.dispose();
    }
    updateHeaderInfo() {
        assert(this.userData.image, "updateHeaderInfo called without image");
        // If there is camera info, the frameId comes from the camera info since the user may have
        // selected camera info with a different frame than our image frame.
        //
        // If there is no camera info, we fall back to the image's frame
        const image = this.userData.image;
        const rawFrameId = this.userData.cameraInfo?.header.frame_id ??
            ("header" in image ? image.header.frame_id : image.frame_id);
        this.userData.frameId =
            typeof rawFrameId === "string" ? this.renderer.normalizeFrameId(rawFrameId) : rawFrameId;
        this.userData.messageTime = toNanoSec("header" in image ? image.header.stamp : image.timestamp);
    }
    details() {
        return { image: this.userData.image, camera_info: this.userData.cameraInfo };
    }
    setRenderBehindScene() {
        this.#renderBehindScene = true;
        this.#materialNeedsUpdate = true;
        this.#meshNeedsUpdate = true;
    }
    // Renderable should only need to care about the model
    setCameraModel(cameraModel) {
        this.#geometryNeedsUpdate ||= this.userData.cameraModel !== cameraModel;
        this.userData.cameraModel = cameraModel;
    }
    setSettings(newSettings) {
        const prevSettings = this.userData.settings;
        if (prevSettings.cameraInfoTopic !== newSettings.cameraInfoTopic) {
            // clear mesh since it is no longer showing userData accurately
            if (this.userData.mesh != undefined) {
                this.remove(this.userData.mesh);
            }
            this.userData.mesh = undefined;
            this.#geometryNeedsUpdate = true;
        }
        if (prevSettings.distance !== newSettings.distance ||
            newSettings.planarProjectionFactor !== prevSettings.planarProjectionFactor) {
            this.#geometryNeedsUpdate = true;
        }
        if (newSettings.color !== prevSettings.color) {
            this.#materialNeedsUpdate = true;
        }
        if (prevSettings.colorMode !== newSettings.colorMode ||
            prevSettings.flatColor !== newSettings.flatColor ||
            !_.isEqual(prevSettings.gradient, newSettings.gradient) ||
            prevSettings.colorMap !== newSettings.colorMap ||
            prevSettings.minValue !== newSettings.minValue ||
            prevSettings.maxValue !== newSettings.maxValue) {
            this.userData.settings = newSettings;
            // Decode the current image again, which takes into account the new options
            const image = this.userData.image;
            if (image) {
                this.setImage(image);
            }
            return;
        }
        this.userData.settings = newSettings;
    }
    setImage(image, resizeWidth, onDecoded) {
        this.userData.image = image;
        const seq = ++this.#receivedImageSequenceNumber;
        const decodePromise = this.decodeImage(image, resizeWidth);
        decodePromise
            .then((result) => {
            if (this.isDisposed()) {
                // Clean up the decoded result if it's an ImageBitmap
                if (result instanceof ImageBitmap) {
                    result.close();
                }
                return;
            }
            // prevent displaying an image older than the one currently displayed
            if (this.#displayedImageSequenceNumber > seq) {
                // Clean up the old result if it's an ImageBitmap
                if (result instanceof ImageBitmap) {
                    result.close();
                }
                return;
            }
            this.#displayedImageSequenceNumber = seq;
            this.#decodedImage = result;
            this.#textureNeedsUpdate = true;
            this.update();
            this.#showingErrorImage = false;
            onDecoded?.();
            this.removeError(DECODE_IMAGE_ERR_KEY);
            this.renderer.queueAnimationFrame();
        })
            .catch((err) => {
            // Ignore errors from terminated workers
            if (err.message === "Worker terminated") {
                return;
            }
            log.error(err);
            if (this.isDisposed()) {
                return;
            }
            // avoid needing to recreate error image if it already shown
            if (!this.#showingErrorImage) {
                void this.#setErrorImage(seq, onDecoded);
            }
            this.addError(DECODE_IMAGE_ERR_KEY, `Error decoding image: ${err.message}`);
        });
    }
    async #setErrorImage(seq, onDecoded) {
        const errorBitmap = await getErrorImage(64, 64);
        if (this.isDisposed()) {
            errorBitmap.close();
            return;
        }
        if (this.#displayedImageSequenceNumber > seq) {
            errorBitmap.close();
            return;
        }
        this.#decodedImage = errorBitmap;
        this.#textureNeedsUpdate = true;
        this.update();
        this.#showingErrorImage = true;
        // call ondecoded to display the error image when calibration is None
        onDecoded?.();
        this.renderer.queueAnimationFrame();
    }
    async decodeImage(image, resizeWidth) {
        if ("format" in image) {
            return await decodeCompressedImageToBitmap(image, resizeWidth);
        }
        const decoder = this.decoderPool?.at(this.#lastUsedDecoderIndex);
        this.#lastUsedDecoderIndex = ((this.#lastUsedDecoderIndex + 1) % this.decoderPool.length);
        return await decoder.decode(image, this.userData.settings);
    }
    update() {
        if (this.#isUpdating) {
            return;
        }
        this.#isUpdating = true;
        if (this.#textureNeedsUpdate && this.#decodedImage) {
            this.#updateTexture();
            this.#textureNeedsUpdate = false;
        }
        if (this.userData.image) {
            this.updateHeaderInfo();
        }
        if (this.#geometryNeedsUpdate && this.userData.cameraModel) {
            this.#rebuildGeometry();
            this.#geometryNeedsUpdate = false;
        }
        if (this.#materialNeedsUpdate) {
            this.#updateMaterial();
            this.#materialNeedsUpdate = false;
        }
        if (this.#meshNeedsUpdate &&
            this.userData.texture &&
            this.userData.geometry &&
            this.userData.material) {
            this.#updateMesh();
            this.#meshNeedsUpdate = false;
        }
        this.#isUpdating = false;
    }
    #rebuildGeometry() {
        assert(this.userData.cameraModel, "Camera model must be set before geometry can be updated");
        // Dispose of the current geometry if the settings have changed
        this.userData.geometry?.dispose();
        this.userData.geometry = undefined;
        const geometry = createGeometry(this.userData.cameraModel, this.userData.settings);
        this.userData.geometry = geometry;
        this.#meshNeedsUpdate = true;
    }
    #updateTexture() {
        assert(this.#decodedImage, "Decoded image must be set before texture can be updated or created");
        const decodedImage = this.#decodedImage;
        // Create or update the bitmap texture
        if (decodedImage instanceof ImageBitmap) {
            const canvasTexture = this.userData.texture;
            if (canvasTexture == undefined ||
                // instanceof check allows us to switch from a raw image (DataTexture) to a compressed image (CanvasTexture)
                !(canvasTexture instanceof THREE.CanvasTexture) ||
                !bitmapDimensionsEqual(decodedImage, canvasTexture.image)) {
                // Close the old bitmap if it exists and isn't the error image
                if (canvasTexture?.image instanceof ImageBitmap && !this.#showingErrorImage) {
                    canvasTexture.image.close();
                }
                canvasTexture?.dispose();
                this.userData.texture = createCanvasTexture(decodedImage);
            }
            else {
                // Close the old bitmap before replacing it
                if (canvasTexture.image instanceof ImageBitmap && canvasTexture.image !== decodedImage && !this.#showingErrorImage) {
                    canvasTexture.image.close();
                }
                canvasTexture.image = decodedImage;
                canvasTexture.needsUpdate = true;
            }
        }
        else {
            let dataTexture = this.userData.texture;
            if (dataTexture == undefined ||
                // instanceof check allows us to switch from a compressed image (CanvasTexture) to a raw image (DataTexture)
                !(dataTexture instanceof THREE.DataTexture) ||
                dataTexture.image.width !== decodedImage.width ||
                dataTexture.image.height !== decodedImage.height) {
                dataTexture?.dispose();
                dataTexture = createDataTexture(decodedImage);
                this.userData.texture = dataTexture;
            }
            else {
                dataTexture.image = decodedImage;
                dataTexture.needsUpdate = true;
            }
        }
        this.#materialNeedsUpdate = true;
    }
    #updateMaterial() {
        if (!this.userData.material) {
            this.#initMaterial();
            this.#meshNeedsUpdate = true;
        }
        const material = this.userData.material;
        const texture = this.userData.texture;
        if (texture) {
            material.map = texture;
        }
        tempColor = stringToRgba(tempColor, this.userData.settings.color);
        const transparent = tempColor.a < 1;
        const color = new THREE.Color(tempColor.r, tempColor.g, tempColor.b);
        material.color.set(color);
        material.opacity = tempColor.a;
        material.transparent = transparent;
        material.depthWrite = !transparent;
        if (this.#renderBehindScene) {
            material.depthWrite = false;
            material.depthTest = false;
        }
        else {
            material.depthTest = true;
        }
        material.needsUpdate = true;
    }
    #initMaterial() {
        stringToRgba(tempColor, this.userData.settings.color);
        const transparent = tempColor.a < 1;
        const color = new THREE.Color(tempColor.r, tempColor.g, tempColor.b);
        this.userData.material = new THREE.MeshBasicMaterial({
            name: `${this.userData.topic}:Material`,
            color,
            side: THREE.DoubleSide,
            opacity: tempColor.a,
            transparent,
            depthWrite: !transparent,
        });
    }
    #updateMesh() {
        assert(this.userData.geometry, "Geometry must be set before mesh can be updated or created");
        assert(this.userData.material, "Material must be set before mesh can be updated or created");
        if (!this.userData.mesh) {
            this.userData.mesh = new THREE.Mesh(this.userData.geometry, this.userData.material);
            this.add(this.userData.mesh);
        }
        else {
            this.userData.mesh.geometry = this.userData.geometry;
            this.userData.mesh.material = this.userData.material;
        }
        if (!this.#renderBehindScene) {
            this.userData.mesh.renderOrder = 0;
            return;
        }
        this.userData.mesh.renderOrder = -1 * Number.MAX_SAFE_INTEGER;
    }
    addError(key, message) {
        if (this.isDisposed()) {
            return;
        }
        // must account for if the renderable is part of `ImageMode` or `Images` scene extension
        this.renderer.settings.errors.add(IMAGE_TOPIC_PATH, key, message);
        this.renderer.settings.errors.addToTopic(this.userData.topic, key, message);
    }
    removeError(key) {
        this.renderer.settings.errors.remove(IMAGE_TOPIC_PATH, key);
        this.renderer.settings.errors.removeFromTopic(this.userData.topic, key);
    }
}
let tempColor = { r: 0, g: 0, b: 0, a: 0 };
function createCanvasTexture(bitmap) {
    const texture = new THREE.CanvasTexture(bitmap, THREE.UVMapping, THREE.ClampToEdgeWrapping, THREE.ClampToEdgeWrapping, THREE.NearestFilter, THREE.LinearFilter, THREE.RGBAFormat, THREE.UnsignedByteType);
    texture.generateMipmaps = false;
    texture.colorSpace = THREE.SRGBColorSpace;
    return texture;
}
function createDataTexture(imageData) {
    const dataTexture = new THREE.DataTexture(imageData.data, imageData.width, imageData.height, THREE.RGBAFormat, THREE.UnsignedByteType, THREE.UVMapping, THREE.ClampToEdgeWrapping, THREE.ClampToEdgeWrapping, THREE.NearestFilter, THREE.LinearFilter, 1, THREE.SRGBColorSpace);
    dataTexture.needsUpdate = true; // ensure initial image data is displayed
    return dataTexture;
}
function createGeometry(cameraModel, settings) {
    const WIDTH_SEGMENTS = 10;
    const HEIGHT_SEGMENTS = 10;
    const width = cameraModel.width;
    const height = cameraModel.height;
    const geometry = new THREE.PlaneGeometry(1, 1, WIDTH_SEGMENTS, HEIGHT_SEGMENTS);
    const gridX1 = WIDTH_SEGMENTS + 1;
    const gridY1 = HEIGHT_SEGMENTS + 1;
    const size = gridX1 * gridY1;
    const segmentWidth = width / WIDTH_SEGMENTS;
    const segmentHeight = height / HEIGHT_SEGMENTS;
    // Use a slight offset to avoid z-fighting with the CameraInfo wireframe
    const EPS = 1e-3;
    // Rebuild the position buffer for the plane by iterating through the grid and
    // projecting each pixel space x/y coordinate into a 3D ray and casting out by
    // the user-configured distance setting. UV coordinates are rebuilt so the
    // image is not vertically flipped
    const pixel = { x: 0, y: 0 };
    const p = { x: 0, y: 0, z: 0 };
    const vertices = new Float32Array(size * 3);
    const uvs = new Float32Array(size * 2);
    for (let iy = 0; iy < gridY1; iy++) {
        for (let ix = 0; ix < gridX1; ix++) {
            const vOffset = (iy * gridX1 + ix) * 3;
            const uvOffset = (iy * gridX1 + ix) * 2;
            pixel.x = ix * segmentWidth;
            pixel.y = iy * segmentHeight;
            projectPixel(p, pixel, cameraModel, settings);
            vertices[vOffset + 0] = p.x;
            vertices[vOffset + 1] = p.y;
            vertices[vOffset + 2] = p.z - EPS;
            uvs[uvOffset + 0] = ix / WIDTH_SEGMENTS;
            uvs[uvOffset + 1] = iy / HEIGHT_SEGMENTS;
        }
    }
    geometry.setAttribute("position", new THREE.BufferAttribute(vertices, 3));
    geometry.setAttribute("uv", new THREE.BufferAttribute(uvs, 2));
    geometry.attributes.position.needsUpdate = true;
    geometry.attributes.uv.needsUpdate = true;
    return geometry;
}
const bitmapDimensionsEqual = (a, b) => a?.width === b?.width && a?.height === b?.height;
async function getErrorImage(width, height) {
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    if (!ctx) {
        throw Error("Could not instantiate 2D canvas context");
    }
    canvas.width = width;
    canvas.height = height;
    // Draw outline
    ctx.strokeStyle = "red";
    ctx.lineWidth = 2;
    ctx.strokeRect(0, 0, width, height);
    // Draw X
    ctx.strokeStyle = "red";
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(0, 0);
    ctx.lineTo(width, height);
    ctx.moveTo(width, 0);
    ctx.lineTo(0, height);
    ctx.stroke();
    // Get the updated image data
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const bitmap = await createImageBitmap(imageData, { resizeWidth: width });
    return bitmap;
}
//# sourceMappingURL=ImageRenderable.js.map