// 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 { t } from "i18next";
import * as _ from "lodash-es";
import Logger from "@foxglove/log";
import { RenderableLineList } from "./markers/RenderableLineList";
import { Renderable } from "../Renderable";
import { SceneExtension } from "../SceneExtension";
import { stringToRgba } from "../color";
import { vec3TupleApproxEquals } from "../math";
import { MarkerAction, MarkerType, TIME_ZERO } from "../ros";
import { PRECISION_DEGREES, PRECISION_DISTANCE } from "../settings";
import { makePose, xyzrpyToPose } from "../transforms";
const log = Logger.getLogger(__filename);
const LAYER_ID = "foxglove.Grid";
const DEFAULT_SIZE = 10;
const DEFAULT_DIVISIONS = 10;
const DEFAULT_LINE_WIDTH = 1;
const DEFAULT_COLOR = "#248eff";
const MAX_DIVISIONS = 4096; // The JS heap size is a limiting factor
const LINE_OPTIONS = { worldUnits: false };
const DEFAULT_SETTINGS = {
    visible: true,
    frameLocked: true,
    label: "Grid",
    instanceId: "invalid",
    layerId: LAYER_ID,
    frameId: undefined,
    size: DEFAULT_SIZE,
    divisions: DEFAULT_DIVISIONS,
    lineWidth: DEFAULT_LINE_WIDTH,
    color: DEFAULT_COLOR,
    position: [0, 0, 0],
    rotation: [0, 0, 0],
};
export class GridRenderable extends Renderable {
    dispose() {
        this.userData.lineList.dispose();
        super.dispose();
    }
}
export class Grids extends SceneExtension {
    static extensionId = "foxglove.Grids";
    constructor(renderer, name = Grids.extensionId) {
        super(name, renderer);
        renderer.addCustomLayerAction({
            layerId: LAYER_ID,
            label: t("threeDee:addGrid"),
            icon: "Grid",
            handler: this.#handleAddGrid,
        });
        renderer.on("transformTreeUpdated", this.#handleTransformTreeUpdated);
        // Load existing grid layers from the config
        for (const [instanceId, entry] of Object.entries(renderer.config.layers)) {
            if (entry?.layerId === LAYER_ID) {
                this.#updateGrid(instanceId, entry);
            }
        }
    }
    dispose() {
        this.renderer.off("transformTreeUpdated", this.#handleTransformTreeUpdated);
        super.dispose();
    }
    removeAllRenderables() {
        // no-op
    }
    settingsNodes() {
        const handler = this.handleSettingsAction;
        const entries = [];
        for (const [instanceId, layerConfig] of Object.entries(this.renderer.config.layers)) {
            if (layerConfig?.layerId !== LAYER_ID) {
                continue;
            }
            const config = layerConfig;
            const frameIdOptions = [
                { label: "<Display frame>", value: undefined },
                ...this.renderer.coordinateFrameList,
            ];
            // prettier-ignore
            const fields = {
                frameId: { label: t("threeDee:frame"), input: "select", options: frameIdOptions, value: config.frameId }, // options is extended in `settings.ts:buildTopicNode()`
                size: { label: t("threeDee:size"), input: "number", min: 0, step: 0.5, precision: PRECISION_DISTANCE, value: config.size, placeholder: String(DEFAULT_SIZE) },
                divisions: { label: t("threeDee:divisions"), input: "number", min: 1, max: MAX_DIVISIONS, step: 1, precision: 0, value: config.divisions, placeholder: String(DEFAULT_DIVISIONS) },
                lineWidth: { label: t("threeDee:lineWidth"), input: "number", min: 0, step: 0.5, precision: 1, value: config.lineWidth, placeholder: String(DEFAULT_LINE_WIDTH) },
                color: { label: t("threeDee:color"), input: "rgba", value: config.color ?? DEFAULT_COLOR },
                position: { label: t("threeDee:position"), input: "vec3", labels: ["X", "Y", "Z"], precision: PRECISION_DISTANCE, value: config.position ?? [0, 0, 0] },
                rotation: { label: t("threeDee:rotation"), input: "vec3", labels: ["R", "P", "Y"], precision: PRECISION_DEGREES, value: config.rotation ?? [0, 0, 0] },
            };
            entries.push({
                path: ["layers", instanceId],
                node: {
                    label: config.label ?? t("threeDee:grid"),
                    icon: "Grid",
                    fields,
                    visible: config.visible ?? DEFAULT_SETTINGS.visible,
                    actions: [{ type: "action", id: "delete", label: t("threeDee:delete") }],
                    order: layerConfig.order,
                    handler,
                },
            });
            // Create renderables for new grid layers
            if (!this.renderables.has(instanceId)) {
                this.#updateGrid(instanceId, config);
            }
        }
        return entries;
    }
    startFrame(currentTime, renderFrameId, fixedFrameId) {
        // Set the `frameId` to use for `updatePose()`
        for (const renderable of this.renderables.values()) {
            renderable.userData.frameId = renderable.userData.settings.frameId ?? renderFrameId;
        }
        super.startFrame(currentTime, renderFrameId, fixedFrameId);
    }
    handleSettingsAction = (action) => {
        const path = action.payload.path;
        // Handle menu actions (delete)
        if (action.action === "perform-node-action") {
            if (path.length === 2 && action.payload.id === "delete") {
                const instanceId = path[1];
                // Remove this instance from the config
                this.renderer.updateConfig((draft) => {
                    delete draft.layers[instanceId];
                });
                // Remove the renderable
                this.#updateGrid(instanceId, undefined);
                // Update the settings tree
                this.updateSettingsTree();
                this.renderer.updateCustomLayersCount();
            }
            return;
        }
        if (path.length !== 3) {
            return; // Doesn't match the pattern of ["layers", instanceId, field]
        }
        this.saveSetting(path, action.payload.value);
        const instanceId = path[1];
        const settings = this.renderer.config.layers[instanceId];
        this.#updateGrid(instanceId, settings);
    };
    #handleAddGrid = (instanceId) => {
        log.info(`Creating ${LAYER_ID} layer ${instanceId}`);
        const config = { ...DEFAULT_SETTINGS, instanceId };
        // Add this instance to the config
        this.renderer.updateConfig((draft) => {
            const maxOrderLayer = _.maxBy(Object.values(draft.layers), (layer) => layer?.order);
            const order = 1 + (maxOrderLayer?.order ?? 0);
            draft.layers[instanceId] = { ...config, order };
        });
        // Add a renderable
        this.#updateGrid(instanceId, config);
        // Update the settings tree
        this.updateSettingsTree();
    };
    #handleTransformTreeUpdated = () => {
        this.updateSettingsTree();
    };
    #updateGrid(instanceId, settings) {
        let renderable = this.renderables.get(instanceId);
        // Handle deletes
        if (settings == undefined) {
            if (renderable != undefined) {
                renderable.userData.lineList.dispose();
                this.remove(renderable);
                this.renderables.delete(instanceId);
            }
            return;
        }
        const newSettings = { ...DEFAULT_SETTINGS, ...settings };
        if (!renderable) {
            renderable = this.#createRenderable(instanceId, newSettings);
            renderable.userData.pose = xyzrpyToPose(newSettings.position, newSettings.rotation);
        }
        const prevSettings = renderable.userData.settings;
        const markersEqual = newSettings.size === prevSettings.size &&
            newSettings.divisions === prevSettings.divisions &&
            newSettings.frameId === prevSettings.frameId &&
            newSettings.lineWidth === prevSettings.lineWidth &&
            newSettings.color === prevSettings.color;
        renderable.userData.settings = newSettings;
        // If the marker settings changed, generate a new marker and update the renderable
        if (!markersEqual) {
            const marker = createMarker(newSettings);
            renderable.userData.lineList.update(marker, undefined);
        }
        // Update the pose if it changed
        if (!vec3TupleApproxEquals(newSettings.position, prevSettings.position) ||
            !vec3TupleApproxEquals(newSettings.rotation, prevSettings.rotation)) {
            renderable.userData.pose = xyzrpyToPose(newSettings.position, newSettings.rotation);
        }
    }
    #createRenderable(instanceId, settings) {
        const marker = createMarker(settings);
        const lineListId = `${instanceId}:LINE_LIST`;
        const lineList = new RenderableLineList(lineListId, marker, undefined, this.renderer, LINE_OPTIONS);
        const renderable = new GridRenderable(instanceId, this.renderer, {
            receiveTime: 0n,
            messageTime: 0n,
            frameId: "", // This will be updated in `startFrame()`
            pose: makePose(),
            settingsPath: ["layers", instanceId],
            settings,
            lineList,
        });
        renderable.add(lineList);
        this.add(renderable);
        this.renderables.set(instanceId, renderable);
        return renderable;
    }
}
function createMarker(settings) {
    const { size, divisions, color: colorStr } = settings;
    const step = size / divisions;
    const halfSize = size / 2;
    const points = [];
    // Create a grid of line segments centered around <0, 0>
    for (let i = 0; i <= divisions; i++) {
        const x = -halfSize + i * step;
        points.push({ x, y: -halfSize, z: 0 });
        points.push({ x, y: halfSize, z: 0 });
        points.push({ x: -halfSize, y: x, z: 0 });
        points.push({ x: halfSize, y: x, z: 0 });
    }
    const color = { r: 1, g: 1, b: 1, a: 1 };
    stringToRgba(color, colorStr);
    return {
        header: {
            frame_id: "", // unused, settings.frameId is used instead
            stamp: TIME_ZERO,
        },
        ns: "",
        id: 0,
        type: MarkerType.LINE_LIST,
        action: MarkerAction.ADD,
        pose: makePose(),
        scale: { x: settings.lineWidth, y: 1, z: 1 },
        color,
        lifetime: TIME_ZERO,
        frame_locked: true,
        points,
        colors: [],
        text: "",
        mesh_resource: "",
        mesh_use_embedded_materials: false,
    };
}
//# sourceMappingURL=Grids.js.map