import * as THREE from '/res/gnr_express/three/three.module.js';
import { OrbitControls } from '/res/gnr_express/three/addons/OrbitControls.js';

export const ViewingMode = {
    imageView: 0,
    volumeView: 1
};
Object.seal(ViewingMode);

class LimImageViewControl {
    #view
    #target
    #onWheelHandler
    #onPointerDownHandler
    #onPointerMoveHandler
    #onPointerUpHandler
    #onPointerLeave

    constructor(view, target) {
        this.#view = view;
        this.#target = target;

        let isPan = false;
        let moveState = null;
        let panOriginalX = 0, panOriginalY = 0;

        this.#onWheelHandler = (ev) => {
            const delta = -1 * ev.deltaY;
            if (!isPan) {
                this.#view.onWheel(delta, ev.clientX, ev.clientY);
            }
            ev.preventDefault();
        };

        this.#onPointerDownHandler = (ev) => {
            ev.preventDefault();
            isPan = true;
            panOriginalX = ev.screenX;
            panOriginalY = ev.screenY;
            moveState = this.#view.onDragBegin();
        };

        this.#onPointerMoveHandler = (ev) => {
            if (!isPan) return;
            this.#view.onDragMove(moveState, ev.screenX - panOriginalX, ev.screenY - panOriginalY);
        };

        this.#onPointerUpHandler = () => {
            if (!isPan) return;
            this.#view.onDragEnd();
            isPan = false;
        };

        this.#onPointerLeave = () => {
            if (!isPan) return;
            this.#view.onDragEnd();
            isPan = false;
        }

        this.#target.addEventListener("wheel", this.#onWheelHandler);
        this.#target.addEventListener("pointerdown", this.#onPointerDownHandler);
        this.#target.addEventListener("pointermove", this.#onPointerMoveHandler);
        this.#target.addEventListener("pointerup", this.#onPointerUpHandler);
        this.#target.addEventListener("pointerleave", this.#onPointerLeave);
    }

    dispose() {
        this.#target.removeEventListener("wheel", this.#onWheelHandler);
        this.#target.removeEventListener("pointerdown", this.#onPointerDownHandler);
        this.#target.removeEventListener("pointermove", this.#onPointerMoveHandler);
        this.#target.removeEventListener("pointerup", this.#onPointerUpHandler);
        this.#target.removeEventListener("pointerleave", this.#onPointerLeave);
    }
}

export class LimImageView {
    #imageCanvas
    #statusbar
    #navigator
    #viewingMode
    #is3d
    #isRgb
    #channelIndex
    #binaryIndexes
    #zoomIndex
    #imageX
    #imageY
    #imageWidth
    #imageHeight
    #imageBpc
    #imageCalibration
    #imageZoomSizes
    #imageZoomTileCache
    #tilesBeingLoaded
    #tilesBeingLoaded3d
    #allChannelNames
    #allChannelColors
    #allChannelGains
    #allChannelGammas
    #allChannelOffsets
    #lutsEnabled
    #showSingleChannelInMono
    #allBinLayerNames
    #allBinLayerColors
    #allLoopIndexes
    #allLoopIndexesMax
    #currentLoopIndexes
    #imageViewControl
    #volumeViewRenderer
    #volumeViewScene
    #volumeViewCamera
    #volumeViewMaterial
    #volumeViewMesh
    #volumeViewControl
    #setZoomIndexLow
    #setImageModeLow
    #setVolumeModeLow
    #playingLoop
    #imageTileSize
    #volumeReqPower
    #initialLoopPos

    constructor(canvas, navigator, statusbar_) {
        this.#imageCanvas = canvas ?? document.createElement("canvas");
        this.#statusbar = statusbar_ ?? createLimStatusbar();
        this.#navigator = navigator ?? createLimNavigator();
        this.#viewingMode = ViewingMode.imageView;
        this.#is3d = false;
        this.#isRgb = false;
        this.#imageBpc = 0;
        this.#channelIndex = -1;
        this.#binaryIndexes = [];
        this.#zoomIndex = 0;
        this.#imageX = 0;
        this.#imageY = 0;
        this.#imageWidth = 0;
        this.#imageHeight = 0;
        this.#imageCalibration = 0.0;
        this.#imageZoomSizes = [];
        this.#tilesBeingLoaded = [];
        this.#tilesBeingLoaded3d = [];
        this.#imageZoomTileCache = [];
        this.#allChannelNames = [];
        this.#allChannelColors = [];
        this.#allChannelGains = [];
        this.#allChannelGammas = [];
        this.#allChannelOffsets = [];
        this.#allBinLayerNames = [];
        this.#allBinLayerColors = [];
        this.#allLoopIndexes = null;
        this.#allLoopIndexesMax = null;
        this.#currentLoopIndexes = null;
        this.#imageViewControl = new LimImageViewControl(this, this.#imageCanvas);
        this.#volumeViewRenderer = null;
        this.#volumeViewScene = null;
        this.#volumeViewCamera = null;
        this.#volumeViewMaterial = null;
        this.#volumeViewMesh = null;
        this.#volumeViewControl = null;
        this.#playingLoop = "";
        this.#imageTileSize = 256;
        this.#volumeReqPower = 9;
        this.#initialLoopPos = {};

        this.#setZoomIndexLow = (value, mouseX, mouseY) => {
            if (this.#zoomIndex === value || this.#imageZoomSizes?.[value] == undefined)
                return;
            const oldZoomIndex = this.#zoomIndex;
            this.#zoomIndex = value;
            this?.onzoomindexchanged?.();
            if (this.#viewingMode === ViewingMode.imageView)
                this.updateImage(oldZoomIndex, mouseX, mouseY);
        }

        this.#setImageModeLow = () => {
            let parent = null;
            this.#tilesBeingLoaded3d.forEach(item => {
                item.aborted = "";
                item.src = "";
            });
            if (this.#volumeViewRenderer && (parent = this.#volumeViewRenderer.domElement.parentElement)) {
                parent.replaceChild(this.#imageCanvas, this.#volumeViewRenderer.domElement);
            }
            this.#imageCanvas.style.display = "block";
            this.#imageCanvas.style.userSelect = "none";

            this.#volumeViewRenderer?.dispose?.();
            this.#volumeViewRenderer = null;
            this.#volumeViewScene?.dispose?.();
            this.#volumeViewScene = null;
            this.#volumeViewCamera?.dispose?.();
            this.#volumeViewCamera = null;
            this.#volumeViewMaterial?.dispose?.();
            this.#volumeViewMaterial = null;
            this.#volumeViewMesh?.dispose?.();
            this.#volumeViewMesh = null;
            this.#volumeViewControl?.dispose?.();
            this.#volumeViewControl = null;

            this.#imageViewControl = new LimImageViewControl(this, this.#imageCanvas);
            this.clearCanvas();
            this.updateImage();

            const znavtrack = document.getElementById("ndnav-track-z");
            if (znavtrack)
                znavtrack.classList.remove('lim-disabled');
            const znavctrl = document.getElementById("ndnav-ctrl-z");
            if (znavctrl)
                znavctrl.classList.remove('lim-disabled');
            const znavlabel = document.getElementById("ndnav-label-z");
            if (znavlabel && znavtrack)
                znavlabel.innerText = `Z-slice ${this.#currentLoopIndexes["z"]+1}/${znavtrack.alltickscount}`;
        }

        this.#setVolumeModeLow = () => {
            this.clearCache();
            this.#imageViewControl?.dispose?.();
            this.#imageViewControl = null;

            this.#volumeViewScene = new THREE.Scene();
            this.#volumeViewRenderer = new THREE.WebGLRenderer({ preserveDrawingBuffer: true });
            this.#volumeViewRenderer.setPixelRatio(window.devicePixelRatio);
            this.#volumeViewRenderer.setSize(this.#imageCanvas.width, this.#imageCanvas.height);
            this.#imageCanvas.style.display = "block";
            this.#imageCanvas.style.userSelect = "none";

            const h = 512;
            const aspect = this.#imageCanvas.width / this.#imageCanvas.height;
            this.#volumeViewCamera = new THREE.OrthographicCamera(-h * aspect / 2, h * aspect / 2, h / 2, -h / 2, 1, 2000);
            this.#volumeViewCamera.position.set(-160, -160, 160);
            this.#volumeViewCamera.up.set(0, 0, 1);

            this.#volumeViewControl = new OrbitControls(this.#volumeViewCamera, this.#volumeViewRenderer.domElement);
            this.#volumeViewControl.addEventListener('change', () => { this.renderVolume(); });
            this.#volumeViewControl.minZoom = 0.25;
            this.#volumeViewControl.maxZoom = 8.0;
            this.#volumeViewControl.enablePan = true;

            let parent = null;
            if (this.#imageCanvas && (parent = this.#imageCanvas.parentElement)) {
                parent.replaceChild(this.#volumeViewRenderer.domElement, this.#imageCanvas);
            }

            this.updateVolume();

            const znavtrack = document.getElementById("ndnav-track-z");
            if (znavtrack)
                znavtrack.classList.add('lim-disabled');
            const znavctrl = document.getElementById("ndnav-ctrl-z");
            if (znavctrl)
                znavctrl.classList.add('lim-disabled');
            const znavlabel = document.getElementById("ndnav-label-z");
            if (znavlabel && znavtrack)
                znavlabel.innerText = `Z-slice all ${znavtrack.alltickscount}`;
        }

        this.#imageCanvas.style.display = "block";
        this.#imageCanvas.style.userSelect = "none";
    }

    async loadImageMetadata() {
        const url = new URL(`/api/v1/image_metadata`, window.location.origin)
        url.searchParams.append("doc_id", this.docId ?? "");
        const response = await fetch(url.toString());
        return await response.json();
    }

    loadTile(seqindex, reqpower, x, y, selchannel, selbinlayers, luts, imageformat) {
        const url = new URL(`/api/v1/image_rgb_tile`, window.location.origin);
        url.searchParams.append("doc_id", this.docId ?? "");
        url.searchParams.append("seqindex", seqindex);
        url.searchParams.append("reqpower", reqpower);
        url.searchParams.append("tilex", x);
        url.searchParams.append("tiley", y);
        url.searchParams.append("selchannels", selchannel);
        url.searchParams.append("selbinlayers", selbinlayers);
        url.searchParams.append("luts", luts);
        url.searchParams.append("channelinmono", (this?.showSingleChannelInMono ?? 0) ? 1 : 0);
        url.searchParams.append("format", imageformat ?? "jpeg");
        return url.toString();
    }

    async autoLuts(seqindex, useLo, percentLo, useHi, percentHi) {
        const url = new URL(`/api/v1/image_auto_luts`, window.location.origin)
        url.searchParams.append("doc_id", this.docId ?? "");
        url.searchParams.append("seqindex", seqindex ?? this.currentSeqIndex);
        if (useLo)
            url.searchParams.append("loperc", percentLo ?? 0);
        if (useHi)
            url.searchParams.append("hiperc", percentHi ?? 0);
        const response = await fetch(url.toString());
        return await response.json();
    }

    async loadVolumeInfo(seqindex, reqpower) {
        const url = new URL(`/api/v1/volume_info`, window.location.origin)
        url.searchParams.append("doc_id", this.docId ?? "");
        url.searchParams.append("seqindex", seqindex);
        url.searchParams.append("reqpower", reqpower);
        const response = await fetch(url.toString());
        return await response.json();
    }

    onWheel(delta, mouseX, mouseY) {
        if (0 < delta && this.#zoomIndex < this.#imageZoomSizes.length-1) {
            this.#setZoomIndexLow(this.#zoomIndex + 1, mouseX, mouseY-40);
        }
        else if (delta < 0 && 0 < this.#zoomIndex) {
            this.#setZoomIndexLow(this.#zoomIndex - 1, mouseX, mouseY-40);
        }
    }

    onDragBegin() {
        return {
            imageX: this.#imageX,
            imageY: this.#imageY,
        };
    }

    onDragMove(originalState, x, y) {
        if (!this.#imageZoomSizes.length)
            return;

        const { s: scale, p: power, w: width, h: height } = this.#imageZoomSizes[this.#zoomIndex];
        let oldX = this.#imageX;
        let oldY = this.#imageY;
        if (this.#imageCanvas.width < width) {
            this.#imageX = originalState.imageX - x/scale;
            this.#imageX = Math.floor(Math.max(0, Math.min(this.#imageX, width-this.#imageCanvas.width)));
        }
        else if (this.#imageCanvas.width < width*scale) {
            const center = (width-this.#imageCanvas.width)/2;
            const margin = (width*scale - this.#imageCanvas.width)/2/scale;
            this.#imageX = Math.floor(Math.max(center-margin, Math.min(originalState.imageX - x/scale, center+margin)));
        }

        if (this.#imageCanvas.height < height) {
            this.#imageY = originalState.imageY - y/scale;
            this.#imageY = Math.floor(Math.max(0, Math.min(this.#imageY, height-this.#imageCanvas.height)));
        }
        else if (this.#imageCanvas.height < height*scale) {
            const center = (height - this.#imageCanvas.height)/2;
            const margin = (height*scale - this.#imageCanvas.height)/2/scale;
            this.#imageY = Math.floor(Math.max(center-margin, Math.min(originalState.imageY - y/scale, center+margin)));
        }

        if (oldX !== this.#imageX || oldY !== this.#imageY) {
            this.renderImage(undefined, undefined, false);
        }
    }

    onDragEnd() {
        this.renderImage(undefined, undefined, false);
    }

    get currentLoopIndexes() {
        return { ...this.#currentLoopIndexes };
    }

    set currentLoopIndexes(indexes) {
        if (JSON.stringify(indexes) === JSON.stringify(this.#currentLoopIndexes))
            return;

        const keys = new Set([...Object.getOwnPropertyNames(this.#currentLoopIndexes), ...Object.getOwnPropertyNames(indexes)])
        const change = [...keys.values()].filter(k => this.#currentLoopIndexes[k] !== indexes[k]);

        this.#currentLoopIndexes = { ...indexes };
        this?.onCurrentLoopIndexesChanged?.([ ...change ]);
        this.updateNavigator();
        this.clearCache();
        this.update();
    }

    get currentSeqIndex() {
        return 0 < this.#allLoopIndexes.length ? this.#allLoopIndexes.indexOf(JSON.stringify(this.#currentLoopIndexes)) : 0;
    }

    moveCurrentLoopIndex(loop, delta) {
        const loopIndexes = this.currentLoopIndexes;
        if (!Object.hasOwn(loopIndexes, loop) || (document.getElementById(`ndnav-track-${loop}`)?.classList?.contains?.('lim-disabled') ?? true))
            return false;
        if (delta) {
            loopIndexes[loop] += delta;
        } else {
            loopIndexes[loop] = this.#initialLoopPos[loop];
        }
        if (loopIndexes[loop] < 0 || this.#allLoopIndexesMax[loop] < loopIndexes[loop])
            return false;
        this.currentLoopIndexes = loopIndexes;
        return true;
    }

    bestFit() {
        if (0 <= this.#zoomIndex && this.#zoomIndex < this.#imageZoomSizes.length) {
            const { w: width, h: height } = this.#imageZoomSizes[this.#zoomIndex];
            this.#imageX = Math.floor((width - this.#imageCanvas.width) / 2);
            this.#imageY = Math.floor((height - this.#imageCanvas.height) / 2);
        }

        const diffs = this.#imageCanvas.height <= this.#imageCanvas.width
            ? this.#imageZoomSizes.map(item => Math.abs(this.#imageCanvas.height - item.h*item.s))
            : this.#imageZoomSizes.map(item => Math.abs(this.#imageCanvas.width  - item.w*item.s ));

        this.zoomIndex = diffs.indexOf(Math.min(...diffs))
    };

    updateNavigator() {
        const loopIndexes = this.#currentLoopIndexes;
        const ndtitles = { "w": "Well", "m": "Point", "t": "Time", "z": "Z-slice" };
        const ndtracks = this.#navigator.getElementsByClassName("ndnav-dim-track");
        const ndlabels = this.#navigator.getElementsByClassName("ndnav-dim-label");
        for (let i = 0; i < ndtracks.length; i++) {
            const ndtrack = ndtracks[i];
            const curindex = loopIndexes[ndtrack.loop];
            ndlabels[i].innerText = `${ndtitles[ndtrack.loop]} ${curindex+1}/${ndtrack.alltickscount}`;
            for (let seltick of ndtrack.getElementsByClassName("ndnav-tick selected"))
                seltick.classList.remove("selected");
            const ticks = ndtrack.getElementsByClassName("ndnav-tick");
            ticks[Math.floor(curindex * ticks.length / ndtrack.alltickscount)].classList.add("selected");
        }
    }

    updateCanvasSize(w, h) {
        this.updateStatusbarSize(w);
        this.updateNavigatorElement(w);
        switch (this.#viewingMode) {
            case ViewingMode.imageView:
                this.#imageCanvas.width = w;
                this.#imageCanvas.height = h;
                this.updateImage(this.#zoomIndex ?? -1);
                break;
            case ViewingMode.volumeView:
                if (this.#volumeViewCamera) {
                    this.#volumeViewRenderer.setSize(w, h);
                    const aspect = w / h;
                    const frustumHeight = this.#volumeViewCamera.top - this.#volumeViewCamera.bottom;
                    this.#volumeViewCamera.left = - frustumHeight * aspect / 2;
                    this.#volumeViewCamera.right = frustumHeight * aspect / 2;
                    this.#volumeViewCamera.updateProjectionMatrix();
                    this.renderVolume();
                }
                break;
        }
    }

    clearCanvas(){
        const ctx = this.#imageCanvas.getContext("2d");
        ctx.clearRect(0, 0, this.#imageCanvas.width, this.#imageCanvas.height);
    };

    clearCache() {
        if (!this.#imageZoomSizes.length)
            return;

        this.#tilesBeingLoaded.forEach(item => {
            item.onload = null;
            item.aborted = true;
            item.src = "";
        });
        this.#tilesBeingLoaded = [];

        this.#imageZoomTileCache = [];
        for (let i = 0; i < this.#imageZoomSizes.length; i++) {
            const { w: width, h: height } = this.#imageZoomSizes[i];
            this.#imageZoomTileCache.push(new Array(Math.ceil(width / this.#imageTileSize) * Math.ceil(height / this.#imageTileSize)));
        }

        this.#tilesBeingLoaded3d.forEach(item => {
            item.aborted = "";
            item.src = "";
        });

        this.triggerProgress(100);
    }

    update() {
        switch (this.#viewingMode) {
            case ViewingMode.imageView:
                this.updateImage();
                break;
            case ViewingMode.volumeView:
                this.updateVolume();
                break;
        }
    }

    updateImage(fromZoomIndex, mouseX, mouseY) {
        if (!this.#imageZoomSizes.length)
            return;

        const half_canvas_width = this.#imageCanvas.width / 2;
        const half_canvas_height = this.#imageCanvas.height / 2;
        const from_scale = this.#imageZoomSizes?.[fromZoomIndex]?.s ?? 1.0;
        const { s: scale, w: width, h: height } = this.#imageZoomSizes[this.#zoomIndex];
        if (typeof mouseX === "undefined") {
            mouseX = Math.round(half_canvas_width);
        }
        if (typeof mouseY === "undefined") {
            mouseY = Math.round(half_canvas_height);
        }

        if (width*scale < this.#imageCanvas.width || fromZoomIndex === -1) {
            this.#imageX = Math.floor((width - this.#imageCanvas.width) / 2);
            mouseX = Math.round(half_canvas_width);
        }
        else if (typeof fromZoomIndex === "number") {
            const srcMouseX = (mouseX - half_canvas_width)/from_scale + half_canvas_width;
            const dstMouseX = (mouseX - half_canvas_width)/scale + half_canvas_width;
            this.#imageX = Math.floor(((this.#imageX + srcMouseX) / this.#imageZoomSizes[fromZoomIndex].w * width) - dstMouseX);
            if (width < this.#imageCanvas.width) {
                const center = (width-this.#imageCanvas.width)/2;
                const margin = (width*scale - this.#imageCanvas.width)/2/scale;
                this.#imageX = Math.floor(Math.max(center-margin, Math.min(this.#imageX, center+margin)));
            }
            else {
                this.#imageX = Math.floor(Math.max(0, Math.min(this.#imageX, width-this.#imageCanvas.width)));
            }
        }

        if (height*scale < this.#imageCanvas.height || fromZoomIndex === -1) {
            this.#imageY = Math.floor((height - this.#imageCanvas.height) / 2);
            mouseY = Math.round(half_canvas_height);
        }
        else if (typeof fromZoomIndex === "number") {
            const srcMouseY = (mouseY - half_canvas_height)/from_scale + half_canvas_height;
            const dstMouseY = (mouseY - half_canvas_height)/scale + half_canvas_height;
            this.#imageY = Math.floor(((this.#imageY + srcMouseY) / this.#imageZoomSizes[fromZoomIndex].h * height) - dstMouseY);
            if (height < this.#imageCanvas.height) {
                const center = (height - this.#imageCanvas.height)/2;
                const margin = (height*scale - this.#imageCanvas.height)/2/scale;
                this.#imageY = Math.floor(Math.max(center-margin, Math.min(this.#imageY, center+margin)));
            }
            else {
                this.#imageY = Math.floor(Math.max(0, Math.min(this.#imageY, height-this.#imageCanvas.height)));
            }
        }

        this.renderImage(undefined, undefined, true);
    }

    async updateVolume() {
        const localNonce = this.updateVolumeNonce = new Object();

        const ch = this.#channelIndex;
        const luts = this.lutsForUrlParam;
        const sel_channel = (typeof ch === "undefined" || ch === -1) ? 'f' : (ch).toString(16);
        const sel_bin_layers = this.#binaryIndexes.reduce((p, c) => p|(1 << c), 0).toString(16);

        if (localNonce !== this.updateVolumeNonce)
            return;

        this.triggerProgress(0);
        const response = await this.loadVolumeInfo(this.currentSeqIndex, this.#volumeReqPower);
        if (localNonce !== this.updateVolumeNonce)
            return;

        const { xCount, yCount, zCount, slices } = response;
        const [ xSize, ySize, zSize ] = [ xCount, yCount, response.zSize*response.xCount/response.xSize ];

        let u_fmt = 0;
        const volume_format = 'RGBA8';
        const volume_data = new Uint8Array(xCount*yCount*zCount*4);

        const makeScene = async () => {
            const texture = new THREE.Data3DTexture(volume_data, xCount, yCount, zCount);
            if (volume_format === 'RGBA8') {
                u_fmt = 4;
                texture.internalFormat = 'RGBA8';
                texture.format = THREE.RGBAFormat;
                texture.type = THREE.UnsignedByteType;
            }
            else if (volume_format === 'R8') {
                u_fmt = 1;
                texture.internalFormat = 'R8';
                texture.format = THREE.RedFormat;
                texture.type = THREE.UnsignedByteType;
            }

            texture.minFilter = THREE.LinearFilter;
            texture.magFilter = THREE.LinearFilter;
            texture.unpackAlignment = 1;
            texture.needsUpdate = true;

            this.#volumeViewControl.target.set(xSize/2, ySize/2, zSize/2);
            this.#volumeViewControl.update();

            if (!this.#volumeViewMaterial) {

                this.#volumeViewMaterial = new THREE.RawShaderMaterial( {
                    glslVersion: THREE.GLSL3,
                    uniforms: {
                        u_fmt: { value: u_fmt },
                        u_data: { value: texture },
                        u_gray: { value: 0 },
                        u_size: { value: new THREE.Vector3(xSize, ySize, zSize) },
                        u_count: { value: new THREE.Vector3(xCount, yCount, zCount) },
                    },
                    vertexShader,
                    fragmentShader,
                    side: THREE.BackSide,
                });

                const geometry = new THREE.BoxGeometry(xSize, ySize, zSize);
                geometry.translate(xSize/2 - 0.5, ySize/2 - 0.5, zSize/2 - 0.5);

                this.#volumeViewMesh?.dispose?.();
                this.#volumeViewMesh = new THREE.Mesh(geometry, this.#volumeViewMaterial);
                this.#volumeViewScene.add(this.#volumeViewMesh);


                const vertices = [];

                vertices.push(0, 0, 0, xSize, 0, 0);
                vertices.push(0, 0, 0, 0, ySize, 0);
                vertices.push(0, 0, 0, 0, 0, zSize);

                vertices.push(0, 0, zSize, xSize, 0, zSize);
                vertices.push(0, 0, zSize, 0, ySize, zSize);

                vertices.push(xSize, 0, 0, xSize, ySize, 0);
                vertices.push(xSize, 0, 0, xSize, 0, zSize);

                vertices.push(0, ySize, 0, xSize, ySize, 0);
                vertices.push(0, ySize, 0, 0, ySize, zSize);

                vertices.push(0, ySize, zSize, xSize, ySize, zSize);
                vertices.push(xSize, 0, zSize, xSize, ySize, zSize);

                vertices.push(xSize, ySize, 0, xSize, ySize, zSize);

                const bbg = new THREE.BufferGeometry()
                bbg.setAttribute( 'position', new THREE.Float32BufferAttribute( vertices, 3 ) );

                const lineSegments = new THREE.LineSegments(bbg, new THREE.LineDashedMaterial({ color: 0xa0a0a0, depthTest: false, scale: 1, dashSize: 4, gapSize: 2 }));
                lineSegments.computeLineDistances();
                this.#volumeViewScene.add(lineSegments);

                this.#volumeViewScene.add(new THREE.LineSegments(bbg, new THREE.LineBasicMaterial( { color: 0xffffff } )));
            }
            else {
                this.#volumeViewMaterial.uniforms['u_fmt'].value = u_fmt;
                this.#volumeViewMaterial.uniforms['u_data'].value = texture;
                this.#volumeViewMaterial.uniforms['u_gray'].value = 0;
            }

            this.renderVolume();
        };

        if (!this.#playingLoop)
            makeScene();

        await (() => {
            return new Promise((resolve, reject) => {
                let slicesLoaded = 0;
                this.#tilesBeingLoaded3d = [];
                for (let i = 0; i < slices.length; i++) {
                    const slice = slices[i];
                    const tiles = slice.tiles;
                    const sliceSeqIndex = slice.seqIndex;
                    slice.tilesLoaded = 0;
                    for (let j = 0; j < tiles.length; j++) {
                        if (localNonce !== this.updateVolumeNonce) {
                            this.#tilesBeingLoaded3d.forEach(item => item.src = '');
                            return;
                        }

                        const tile = tiles[j];
                        const tileImage = new Image(tile.w, tile.h);
                        this.#tilesBeingLoaded3d.push(tileImage);
                        tileImage.onload = (e) => {
                            const img = e.target;
                            if (img?.aborted)
                                return;
                            if (!slice.ctx) {
                                slice.canvas = document.createElement('canvas');
                                slice.canvas.width = xCount; slice.canvas.height = yCount;
                                slice.ctx = slice.canvas.getContext("2d", { willReadFrequently: true });
                            }
                            slice.ctx.drawImage(img, tile.x, tile.y);
                            if (++slice.tilesLoaded == tiles.length) {
                                const img_data = slice.ctx.getImageData(0, 0, xCount, yCount);
                                volume_data.set(img_data.data, i*xCount*yCount*4);
                                slice.ctx = null; slice.canvas = null;
                                if (i % 8 == 7 && !this.#playingLoop) {
                                    makeScene();
                                }
                                if (++slicesLoaded == slices.length) {
                                    makeScene();
                                    this.renderVolumeDone(true);
                                }
                                this.triggerProgress(Math.floor(100 * slicesLoaded / slices.length));
                            }
                        };

                        tileImage.src = this.loadTile(sliceSeqIndex, this.#volumeReqPower, tile.xTile, tile.yTile, sel_channel, sel_bin_layers, luts, "png");
                    }
                }
            });
        })();
    }

    checkAndAdvancePlayingLoop() {
        if (this.#playingLoop) {
            const loopIndexes = this.currentLoopIndexes;
            if (++loopIndexes[this.#playingLoop] == (this.#allLoopIndexesMax[this.#playingLoop]+1))
                loopIndexes[this.#playingLoop] = 0;

            clearTimeout(this?.advancePlayingLoopEventId ?? -1);
            this.advancePlayingLoopEventId = setTimeout(() => {
                this.currentLoopIndexes = loopIndexes;
            }, 100);
        }
    }

    async renderVolumeDone(allTilesWereDrawn) {
        if (allTilesWereDrawn)
            this.checkAndAdvancePlayingLoop();
    }

    triggerProgress(percent) {
        if (!this.#playingLoop || percent === 100)
            this?.onLoadProgressChanged?.(percent);
    }

    renderImage(imageFormat, ctx, avoidClear) {
        if (!this.#imageZoomSizes.length)
            return;

        if (typeof imageFormat === "undefined")
            imageFormat = "jpeg";

        if (typeof avoidClear === "undefined")
            avoidClear = !!this.#playingLoop;

        const { s: scale, p: power, w: width, h: height } = this.#imageZoomSizes[this.#zoomIndex];

        const ctx_was_undefined = typeof ctx === "undefined";
        const _rendering_done = (allTilesDrawn) => {
            this.triggerProgress(100);

            if (ctx_was_undefined && 1 < scale) {
                const offscreenCanvas = new OffscreenCanvas(this.#imageCanvas.width, this.#imageCanvas.height);
                const offscreenCtx = offscreenCanvas.getContext("2d");
                this.renderImage(imageFormat, offscreenCtx);
                const bitmap = offscreenCanvas.transferToImageBitmap();
                const _ctx = this.#imageCanvas.getContext("2d");
                _ctx.setTransform(1, 0, 0, 1, 0, 0);
                _ctx.translate(this.#imageCanvas.width/2, this.#imageCanvas.height/2);
                _ctx.scale(scale, scale);
                _ctx.translate(-this.#imageCanvas.width/2, -this.#imageCanvas.height/2);
                _ctx.drawImage(bitmap, 0, 0);
                bitmap.close();
            }
            if (ctx_was_undefined && imageFormat !== 'png' && allTilesDrawn && !this.#playingLoop) {
                clearTimeout(this?._redraw_pngs_id ?? -1);
                this._redraw_pngs_id = setTimeout(() => {
                    this.renderImage('png');
                }, 2000);
            }
            this.renderImageDone(allTilesDrawn);
        }

        const ch = this.#channelIndex;
        const luts = this.lutsForUrlParam;
        const selchannel = (typeof ch === "undefined" || ch === -1) ? 'f' : (ch).toString(16);
        const selbinlayers = this.#binaryIndexes.reduce((p, c) => p|(1 << c), 0).toString(16);

        const tilesInX = Math.ceil(width / this.#imageTileSize);

        if (typeof ctx === "undefined") {
            ctx = this.#imageCanvas.getContext("2d");
            ctx.setTransform(1, 0, 0, 1, 0, 0);
            ctx.translate(this.#imageCanvas.width/2, this.#imageCanvas.height/2);
            ctx.scale(scale, scale);
            ctx.translate(-this.#imageCanvas.width/2, -this.#imageCanvas.height/2);
        }

        if (width*scale < this.#imageCanvas.width) {
            this.#imageX = Math.floor((width - this.#imageCanvas.width) / 2);
            ctx.clearRect(0, 0, -this.#imageX, this.#imageCanvas.height);
            ctx.clearRect(Math.ceil(this.#imageX + this.#imageCanvas.width + 0.5), 0, this.#imageCanvas.width, this.#imageCanvas.height);
        }
        if (height*scale < this.#imageCanvas.height) {
            this.#imageY = Math.floor((height - this.#imageCanvas.height) / 2);
            ctx.clearRect(0, 0, this.#imageCanvas.width, -this.#imageY);
            ctx.clearRect(0, Math.ceil(this.#imageY + this.#imageCanvas.height + 0.5), this.#imageCanvas.width, this.#imageCanvas.height);
        }

        const tilesToRender = [];
        const tilesToKeepLoading = new Set();
        for (let i = this.#imageY <= 0 ? 0 : Math.floor(this.#imageY / this.#imageTileSize); i < Math.ceil(Math.min(this.#imageY+this.#imageCanvas.height, height) / this.#imageTileSize); i++) {
            for (let j = this.#imageX <= 0 ? 0 : Math.floor(this.#imageX / this.#imageTileSize); j < Math.ceil(Math.min(this.#imageX+this.#imageCanvas.width, width) / this.#imageTileSize); j++) {
                tilesToRender.push([i, j, i * tilesInX + j]);
                tilesToKeepLoading.add(JSON.stringify({x: j*this.#imageTileSize, y: i*this.#imageTileSize, z: this.#zoomIndex}));
            }
        }

        // tady je potreba smazat jenom ty co se nebudou loadovat
        this.#tilesBeingLoaded.forEach((item, index) => {
            if (!tilesToKeepLoading.has(JSON.stringify({x: item.tilex, y: item.tiley, z: item.zoomIndex}))/* || (imageFormat !== "png" && item.imageFormat === "png")*/) {
                item.aborted = true;
                item.src = "";
            }
        });

        this.#tilesBeingLoaded = this.#tilesBeingLoaded.filter(item => !item.aborted);

        this.#imageZoomTileCache.forEach((cache) => {
            cache = cache.filter(item => (!(item?.aborted ?? false)));
        });

        this.triggerProgress(0);

        let tilesDrawn = 0;
        let tilesHandled = 0;
        for (let tileToRender of tilesToRender) {
            const [i, j, flatIndex] = tileToRender;
            const tilex = j*this.#imageTileSize;
            const tiley = i*this.#imageTileSize;
            let makeNewRequests = true;
            const img = this.#imageZoomTileCache?.[this.#zoomIndex]?.[flatIndex];
            if (img) {
                makeNewRequests = false;
                if (!img.aborted && img.complete) {
                    ctx.drawImage(img, this.#imageX < 0 ? -this.#imageX + img.tilex : img.tilex - this.#imageX, this.#imageY < 0 ? -this.#imageY + img.tiley : img.tiley - this.#imageY);
                    if (imageFormat === 'png' && img.imageFormat !== imageFormat)
                        makeNewRequests = true;
                    else {
                        tilesDrawn++;
                        tilesHandled++;
                        this.triggerProgress(Math.floor(100 * tilesHandled / tilesToRender.length));
                        if (tilesHandled === tilesToRender.length) {
                            _rendering_done(tilesDrawn === tilesHandled);
                        }
                    }
                }
                else {
                    if (!avoidClear)
                        ctx.clearRect(this.#imageX < 0 ? -this.#imageX + tilex : tilex - this.#imageX, this.#imageY < 0 ? -this.#imageY + tiley : tiley - this.#imageY, this.#imageTileSize, this.#imageTileSize);
                    if (img?.aborted) {
                        makeNewRequests = true;
                    }
                }
            }
            else if (!avoidClear)
                ctx.clearRect(this.#imageX < 0 ? -this.#imageX + tilex : tilex - this.#imageX, this.#imageY < 0 ? -this.#imageY + tiley : tiley - this.#imageY, this.#imageTileSize, this.#imageTileSize);

            if (makeNewRequests) {
                const z = this.#zoomIndex;
                this.#imageZoomTileCache[z][flatIndex] = new Image(this.#imageTileSize, this.#imageTileSize);
                this.#tilesBeingLoaded = this.#tilesBeingLoaded.filter((item) => tilex !== item.tilex || tiley !== item.tiley || z !== item.zoomIndex || imageFormat !== item.imageFormat);
                this.#tilesBeingLoaded.push(this.#imageZoomTileCache[z][flatIndex]);
                this.#imageZoomTileCache[z][flatIndex].onload = (event) => {
                    const img = event.target;
                    const indexInTilesBeingLoaded = this.#tilesBeingLoaded.indexOf(img);
                    if (0 <= indexInTilesBeingLoaded)
                        this.#tilesBeingLoaded.splice(indexInTilesBeingLoaded, 1);
                    if (!img?.aborted && img.zoomIndex === this.#zoomIndex) {
                        ctx.drawImage(img, this.#imageX < 0 ? -this.#imageX + img.tilex : img.tilex - this.#imageX, this.#imageY < 0 ? -this.#imageY + img.tiley : img.tiley - this.#imageY);
                        tilesDrawn++;
                    }
                    else if (!avoidClear){
                        ctx.clearRect(this.#imageX < 0 ? -this.#imageX + tilex : tilex - this.#imageX, this.#imageY < 0 ? -this.#imageY + tiley : tiley - this.#imageY, this.#imageTileSize, this.#imageTileSize);
                    }
                    tilesHandled++;
                    this.triggerProgress(Math.floor(100 * tilesHandled / tilesToRender.length));
                    if (tilesHandled === tilesToRender.length) {
                        _rendering_done(tilesDrawn === tilesHandled);
                    }
                };

                this.#imageZoomTileCache[z][flatIndex].imageFormat = imageFormat;
                this.#imageZoomTileCache[z][flatIndex].zoomIndex = z;
                this.#imageZoomTileCache[z][flatIndex].tilex = tilex;
                this.#imageZoomTileCache[z][flatIndex].tiley = tiley;
                this.#imageZoomTileCache[z][flatIndex].src = this.loadTile(this.currentSeqIndex, power, j, i, selchannel, selbinlayers, luts, imageFormat);
            }
        }
    }

    async renderImageDone(allTilesWereDrawn) {
        if (allTilesWereDrawn)
            this.checkAndAdvancePlayingLoop();
    }

    renderVolume() {
        this.#volumeViewRenderer.render(this.#volumeViewScene, this.#volumeViewCamera);
    }

    clearFile() {
        this.clearCache();

        this.#allLoopIndexes = [];
        this.#is3d = false;
        this.#isRgb = false;
        this.#imageBpc = 0;
        this.#imageWidth = 0;
        this.#imageHeight = 0;
        this.#imageCalibration = 0.0;
        this.#channelIndex = -1;
        this.#binaryIndexes = [];
        this.#imageZoomSizes = [];
        this.#allChannelNames = [];
        this.#allChannelColors = [];
        this.#allChannelGains = [];
        this.#allBinLayerNames = [];
        this.#allBinLayerColors = [];
        this.#allLoopIndexes = [];
        this.#allLoopIndexesMax = [];
        this.#currentLoopIndexes = [];
        this.#playingLoop = "";
        this.#imageTileSize = 256;
        this.#volumeReqPower = 9;

        this.viewingMode = ViewingMode.imageView;
        this.clearCanvas();
        this.updateStatusbarElement();
        this.updateNavigatorElement();
    }

    async loadFile() {
        this.clearCache();
        const meta = await this.loadImageMetadata();
        this.#channelIndex = -1;
        this.#binaryIndexes = [];
        this.#is3d = meta.is3d;
        this.#isRgb = meta.isRgb;
        this.#imageBpc = meta.bitdepth;
        this.#imageWidth = meta.width;
        this.#imageHeight = meta.height;
        this.#imageCalibration = meta.calibration;
        this.#imageZoomSizes = [];
        this.#allChannelNames = meta.channelNames;
        this.#allChannelColors = meta.channelColors;
        this.#allChannelGains = meta.channelNames.map(() => 1.0);
        this.#allChannelGammas = meta.channelNames.map(() => 1.0);
        this.#allChannelOffsets = meta.channelNames.map(() => 0);
        this.#allBinLayerNames = meta.binaryNames;
        this.#allBinLayerColors = meta.binaryColors;
        this.#allLoopIndexes = [];
        this.#allLoopIndexesMax = [];
        this.#currentLoopIndexes = [];
        this.#playingLoop = "";
        this.#imageTileSize = meta?.imageTileSize ?? 256;
        this.#volumeReqPower = meta?.volumeReqPower ?? 9;
        this.#initialLoopPos = meta?.initialLoopPos ?? {};

        const blpo2 = (v) => {
            v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;
            return v;
        };

        this.updateStatusbarElement();

        if (Array.isArray(meta.allLoopIndices) && 0 < meta.allLoopIndices.length) {
            const dims = Object.getOwnPropertyNames(meta.allLoopIndices[0]);
            this.#allLoopIndexesMax = Object.fromEntries(dims.map(value => [ value, 0 ]))
            for (let index of meta.allLoopIndices) {
                for (let dim of dims) {
                    const i = index[dim];
                    this.#allLoopIndexesMax[dim] = Math.max(this.#allLoopIndexesMax[dim], index[dim])
                }
                this.#allLoopIndexes.push(JSON.stringify(index));
            }
            this.#currentLoopIndexes = meta?.initialLoopPos ?? meta.allLoopIndices[0];
        }

        this.updateNavigatorElement();

        const imageFullPowerSize = blpo2(this.#imageHeight <= this.#imageWidth ? this.#imageWidth : this.#imageHeight);
        for (let i = 4; i <= 20; i++) {
            const k = 1 << i;
            let zoom = 100 * k / imageFullPowerSize;
            if (9 <= i || k === imageFullPowerSize) {
                const downsampledWidth = Math.floor(this.#imageWidth * k / imageFullPowerSize);
                const downsampledHeight = Math.floor(this.#imageHeight * k / imageFullPowerSize);

                const tileCache = new Array(Math.ceil(downsampledWidth / this.#imageTileSize) * Math.ceil(downsampledHeight / this.#imageTileSize))
                this.#imageZoomSizes.push({s:1.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                this.#imageZoomTileCache.push(tileCache);

                zoom *= 1.4142;
                this.#imageZoomSizes.push({s:1.4142, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                this.#imageZoomTileCache.push(tileCache);

                if (k === imageFullPowerSize) {
                    zoom = 200;
                    this.#imageZoomSizes.push({s:2.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                    this.#imageZoomTileCache.push(tileCache);
                    zoom *= 1.4142;
                    this.#imageZoomSizes.push({s:zoom/100.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                    this.#imageZoomTileCache.push(tileCache);
                    zoom = 400;
                    this.#imageZoomSizes.push({s:4.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                    this.#imageZoomTileCache.push(tileCache);
                    zoom *= 1.4142;
                    this.#imageZoomSizes.push({s:zoom/100.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                    this.#imageZoomTileCache.push(tileCache);
                    zoom = 800;
                    this.#imageZoomSizes.push({s:8.0, z: zoom.toFixed(zoom < 1 ? 2 : (zoom < 10 ? 1 : 0)), w: downsampledWidth, h: downsampledHeight, p: i});
                    this.#imageZoomTileCache.push(tileCache);
                    break;
                }
            }
        }

        this.#zoomIndex = -1; // to force render
        this.clearCache();
        this.triggerProgress(5);
        this.bestFit();
    }

    updateStatusbarElement() {
        this.#statusbar.innerText = "";
        if (this.#imageWidth && this.#imageHeight) {

            const createRectImg = (chcolor, round) => {
                const rr = round ? "50%" : "0";
                const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
                    <rect width="12" height="12" x="2" y="2" rx="${rr}" ry="${rr}" style="fill: ${chcolor}; stroke-width: 0.5; stroke: #111;"/>
                </svg>`;
                const base64Svg = btoa(unescape(encodeURIComponent(svgContent)));
                const img_rect = document.createElement('img');
                img_rect.src = `data:image/svg+xml;base64,${base64Svg}`;
                return img_rect;
            }
            const chcolors = [];
            for (let i = 0; i < (this.#allChannelNames?.length ?? 0); i++) {
                const chcolor = this.#allChannelColors[i];
                const r = Math.min(255, parseInt(chcolor.slice(1, 3), 16) + 128).toString(16);
                const g = Math.min(255, parseInt(chcolor.slice(3, 5), 16) + 128).toString(16);
                const b = Math.min(255, parseInt(chcolor.slice(5, 7), 16) + 128).toString(16);
                chcolors.push(`#${r}${g}${b}`);
            }

            // channels
            if (1 < (this.#allChannelNames?.length ?? 0)) {
                let svg_gradient = "";
                for (let k = 0; k < chcolors.length; k++) {
                    const i = this.isRgb ? 2 - k : k;
                    svg_gradient += `<stop offset="${(k*100/(chcolors.length-1)).toFixed(0)}%" stop-color="${chcolors[i]}" />`
                }

                const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
                    <defs>
                        <linearGradient id="grad-ch" x1="0%" x2="100%" y1="0%" y2="0%">${svg_gradient}</linearGradient>
                    </defs>
                    <rect width="12" height="12" x="2" y="2" style="fill: url(#grad-ch); stroke-width: 0.5; stroke: #111;"/>
                </svg>`;
                const base64Svg = btoa(unescape(encodeURIComponent(svgContent)));
                const img = document.createElement('img');
                img.src = `data:image/svg+xml;base64,${base64Svg}`;

                const btn = document.createElement("button");
                btn.className = 'lim-button-channel lim-tool-button lim-small-icon';
                btn.ariaPressed = "true";
                btn.title = "All channels";
                btn.appendChild(img);
                btn.appendChild(document.createTextNode("ALL"));
                btn.onclick = (e) => {
                    if (btn.ariaPressed !== "true") {
                        const buttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-channel');
                        for (let index = 0; index < buttons.length; index++) {
                            buttons[index].ariaPressed = index === 0 ? "true" : "false";
                        }
                        this.channelIndex = -1;
                    }
                }

                const underline = document.createElement("span");
                underline.style.background = this.isRgb ? "linear-gradient(0.25turn, #FF0000, #00FF00, #0000FF)"  : `linear-gradient(0.25turn, ${chcolors.join(", ")})`;
                btn.appendChild(underline);

                this.#statusbar.appendChild(btn);
            }

            for (let k = 0; k < this.#allChannelNames.length; k++) {
                const i = this.isRgb ? 2 - k : k;
                const chname = this.#allChannelNames[i];
                const btn = document.createElement("button");
                btn.className = 'lim-button-channel lim-tool-button lim-small-icon';
                btn.ariaPressed = 1 === this.#allChannelNames.length ? "true" : "false";
                btn.title = chname;
                const img = createRectImg(chcolors[i]);
                btn.appendChild(img);
                btn.appendChild(document.createTextNode(chname));
                btn.onclick = (e) => {
                    if (btn.ariaPressed !== "true") {
                        const buttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-channel');
                        for (let index = 0; index < buttons.length; index++) {
                            buttons[index].ariaPressed = index === i+1 ? "true" : "false";
                        }
                        this.channelIndex = k;
                    }
                }

                const underline = document.createElement("span");
                underline.style.background = `${chcolors[i]}`;
                btn.appendChild(underline);

                this.#statusbar.appendChild(btn);
            }

            // info in the middle
            let el = document.createElement("span");
            el.className = "lim-left-equalizer";
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.style.flexGrow = 1;
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.innerText = this.#imageCalibration <= 0.0 ? "uncalibrated" : `${this.#imageCalibration.toFixed(2) } µm/pixel`;
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.className = "lim-separator";
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.innerText = 32 == this.#imageBpc ? `${this.#allChannelNames.length} x float32` : `${this.#allChannelNames.length} x ${this.#imageBpc}bit`;
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.className = "lim-separator";
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.innerText = `${this.#imageWidth} x ${this.#imageHeight} pixels`;
            if (0 < this.#imageCalibration) {
                let w = this.#imageCalibration*this.#imageWidth;
                let h = this.#imageCalibration*this.#imageHeight;
                let u = "µm";
                if (1000 < w || 1000 < h) {
                    w = w/1000;
                    h = h/1000;
                    u = "mm";
                }
                el.title = `${w.toFixed(1)} x ${h.toFixed(1)} ${u}`;
            }
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.style.flexGrow = 1;
            this.#statusbar.appendChild(el);

            el = document.createElement("span");
            el.className = "lim-right-equalizer";
            this.#statusbar.appendChild(el);

            // binary layers
            for (let i = 0; i < (this.#allBinLayerNames?.length ?? 0); i++) {
                const binname = this.#allBinLayerNames[i];
                const bincolor = this.#allBinLayerColors[i];

                const r = Math.min(255, parseInt(bincolor.slice(1, 3), 16) + 128).toString(16);
                const g = Math.min(255, parseInt(bincolor.slice(3, 5), 16) + 128).toString(16);
                const b = Math.min(255, parseInt(bincolor.slice(5, 7), 16) + 128).toString(16);

                const btn = document.createElement("button");
                btn.className = 'lim-button-binary lim-tool-button lim-small-icon';
                btn.ariaPressed = "false";
                btn.title = binname;
                const img = createRectImg(`#${r}${g}${b}`, true);
                btn.appendChild(img);
                btn.appendChild(document.createTextNode(binname));
                btn.onclick = (e) => {
                    btn.ariaPressed = btn.ariaPressed !== "true" ? "true" : "false";
                    const selindexes = [];
                    const buttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-binary');
                    for (let index = 0; index < buttons.length; index++) {
                        if (buttons[index].ariaPressed === "true") {
                            selindexes.push(index)
                        }
                    }
                    this.binaryIndexes = selindexes;
                }

                const underline = document.createElement("span");
                underline.style.background = `#${r}${g}${b}`;
                btn.appendChild(underline);

                this.#statusbar.appendChild(btn);
            }
        }

        this.#statusbar.hidden = 0 == this.#statusbar.childElementCount;
    }

    updateStatusbarSize(w) {
        let channelButtonWidth = 0;
        const chbuttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-channel');
        for (let index = 0; index < chbuttons.length; index++) {
            chbuttons[index].classList.remove('lim-minimized');
        }
        for (let index = 0; index < chbuttons.length; index++) {
            const style = getComputedStyle(chbuttons[index]);
            channelButtonWidth += parseFloat(style.width);
        }

        let binaryButtonWidth = 0;
        const binbuttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-binary');
        for (let index = 0; index < binbuttons.length; index++) {
            binbuttons[index].classList.remove('lim-minimized');
        }
        for (let index = 0; index < binbuttons.length; index++) {
            const style = getComputedStyle(binbuttons[index]);
            binaryButtonWidth += parseFloat(style.width);
        }

        if (w/3 < channelButtonWidth) {
            for (let index = 0; index < chbuttons.length; index++) {
                chbuttons[index].classList.add('lim-minimized');
            }
            channelButtonWidth = 32*chbuttons.length;
        }

        if (w/3 < binaryButtonWidth) {
            for (let index = 0; index < binbuttons.length; index++) {
                binbuttons[index].classList.add('lim-minimized');
            }
            binaryButtonWidth = 32*binbuttons.length;
        }

        const leftEqualizer = document.querySelector('.lim-image-statusbar > .lim-left-equalizer');
        const rightEqualizer = document.querySelector('.lim-image-statusbar > .lim-right-equalizer');
        if (channelButtonWidth < binaryButtonWidth && leftEqualizer && rightEqualizer) {
            leftEqualizer.style.width = `${binaryButtonWidth-channelButtonWidth}px`;
            rightEqualizer.style.width = "0";
        }
        else if (leftEqualizer && rightEqualizer){
            leftEqualizer.style.width = "0";
            rightEqualizer.style.width = `${channelButtonWidth-binaryButtonWidth}px`;
        }
    }

    updateNavigatorElement(width) {
        this.#navigator.innerText = "";
        let maxTickCount = 60;
        if (typeof width === "number") {
            maxTickCount = Math.floor((width - 230) / 4);
        }
        if (Array.isArray(this.#allLoopIndexes) && 0 < this.#allLoopIndexes.length) {
            const ndtitles = { "w": "Well", "m": "Point", "t": "Time", "z": "Z-slice" };
            for (let loop of ["w", "m", "t", "z"]) {
                const maxindex = this.#allLoopIndexesMax[loop];
                const curindex = this.#currentLoopIndexes[loop];
                if (this.#allLoopIndexesMax.hasOwnProperty(loop) && 0 < maxindex) {
                    let el = null;
                    el = document.createElement("div");
                    el.id = `ndnav-label-${loop}`;
                    el.className = "ndnav-dim-label";
                    el.innerText = `${ndtitles[loop]} ${curindex+1}/${maxindex+1}`;
                    this.#navigator.appendChild(el);
                    el = document.createElement("div");
                    el.id = `ndnav-track-${loop}`;
                    el.className = "ndnav-dim-track";
                    el.loop = loop;
                    el.alltickscount = maxindex + 1;
                    const step = Math.ceil(el.alltickscount / Math.min(el.alltickscount, maxTickCount));
                    for (let i = 0; i <= maxindex; i += step) {
                        const tick = document.createElement("span");
                        tick.className = "ndnav-tick";
                        tick.loop = loop;
                        tick.index_in_loop = i;
                        tick.onclick = (e) => {
                            const that = e.target;
                            if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                                this.playingLoop = "";
                                const loopIndexes = this.currentLoopIndexes;
                                loopIndexes[that.loop] = that.index_in_loop;
                                this.currentLoopIndexes = loopIndexes;
                            }
                        }
                        if (Math.floor(i/step) == Math.floor(curindex/step))
                            tick.classList.add("selected");
                        el.appendChild(tick);
                    }
                    this.#navigator.appendChild(el);
                    el = document.createElement("div");
                    el.className = "ndnav-dim-ctrl";
                    el.id = `ndnav-ctrl-${loop}`

                    let btn = document.createElement("img");
                    btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_step_back.svg`
                    btn.loop = loop;
                    btn.onclick = (e) => {
                        const that = e.target;
                        if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                            this.playingLoop = "";
                            const loopIndexes = this.currentLoopIndexes;
                            loopIndexes[that.loop] = 0;
                            this.currentLoopIndexes = loopIndexes;
                        }
                    };
                    el.appendChild(btn);

                    btn = document.createElement("img");
                    btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_fast_back.svg`
                    btn.loop = loop;
                    btn.onclick = (e) => {
                        const that = e.target;
                        if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                            this.playingLoop = "";
                            const loopIndexes = this.currentLoopIndexes;
                            loopIndexes[that.loop] = Math.max(0, loopIndexes[that.loop] - 1);
                            this.currentLoopIndexes = loopIndexes;
                        }
                    };
                    el.appendChild(btn);

                    const play_btn = document.createElement("img");
                    play_btn.id = `ndnav-play-${loop}`;
                    play_btn.style.marginLeft = "4px";
                    play_btn.style.marginRight = "4px";
                    play_btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_play.svg`
                    play_btn.loop = loop;
                    play_btn.onclick = (e) => {
                        const that = e.target;
                        if (this.playingLoop === that.loop) {
                            this.playingLoop = "";
                        }
                        else if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                            this.playingLoop = that.loop;
                        }
                    }
                    play_btn.setPlayIcon = () => {
                        play_btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_play.svg`
                    };
                    play_btn.setPauseIcon = () => {
                        play_btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_pause.svg`
                    };
                    el.appendChild(play_btn);

                    btn = document.createElement("img");
                    btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_fast_forw.svg`
                    btn.loop = loop;
                    btn.onclick = (e) => {
                        const that = e.target;
                        if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                            this.playingLoop = "";
                            const loopIndexes = this.currentLoopIndexes;
                            loopIndexes[that.loop] = Math.min(loopIndexes[that.loop] + 1, maxindex);
                            this.currentLoopIndexes = loopIndexes;
                        }
                    };
                    el.appendChild(btn);

                    btn = document.createElement("img");
                    btn.src = `/res/gnr_core_gui/CoreGUI/Icons/base/playbar_step_forw.svg`
                    btn.loop = loop;
                    btn.onclick = (e) => {
                        const that = e.target;
                        if (that.parentElement && !that.parentElement.classList.contains('lim-disabled')) {
                            this.playingLoop = "";
                            const loopIndexes = this.currentLoopIndexes;
                            loopIndexes[that.loop] = maxindex;
                            this.currentLoopIndexes = loopIndexes;
                        }
                    };
                    el.appendChild(btn);

                    this.#navigator.appendChild(el);
                }
            }
        }

        this.#navigator.style.display = 0 == this.#navigator.childElementCount ? "none" : "grid";
    }

    get valid() {
        return 0 < this.#imageWidth * this.#imageHeight;
    }

    get is3d() {
        return this.#is3d;
    }

    get isRgb() {
        return this.#isRgb;
    }

    get channelIndex() {
        return this.#channelIndex;
    }

    set channelIndex(value) {
        if (this.#channelIndex === value)
            return;

        this.#channelIndex = value;

        const buttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-channel');
        for (let index = 0; index < buttons.length; index++) {
            buttons[index].ariaPressed = index === (this.#channelIndex + 1) ? "true" : "false";
        }

        this.clearCache();
        this.update();
    }

    get binaryIndexes() {
        return this.#binaryIndexes;
    }

    set binaryIndexes(value) {
        if (!Array.isArray(value) || JSON.stringify(this.#binaryIndexes) === JSON.stringify(value?.sort?.()))
            return;

        this.#binaryIndexes = [...value.sort()];

        const buttons = document.querySelectorAll('.lim-image-statusbar > .lim-button-binary');
        for (let index = 0; index < buttons.length; index++) {
            buttons[index].ariaPressed = this.#binaryIndexes.includes(index) ? "true" : "false";
        }

        this.clearCache();
        this.update();
    }

    get zoomIndex() {
        return this.#zoomIndex;
    }

    set zoomIndex(value) {
        this.#setZoomIndexLow(value);
    }

    get channelNames() {
        return this.#allChannelNames;
    }

    get channelColors() {
        return this.#allChannelColors;
    }

    get lutsEnabled() {
        return this.#lutsEnabled;
    }

    set lutsEnabled(val) {
        if (this.#lutsEnabled !== val) {
            this.#lutsEnabled = val;
            this.clearCache();
            this.update();
        }
    }

    get channelGains() {
        return this.#allChannelGains;
    }

    get channelGammas() {
        return this.#allChannelGammas;
    }

    get channelOffsets() {
        return this.#allChannelOffsets;
    }

    set channelGains(val) {
        this.#allChannelGains = val;
        if (this.#lutsEnabled) {
            this.clearCache();
            this.update();
        }
    }

    set channelGammas(val) {
        this.#allChannelGammas = val;
        if (this.#lutsEnabled) {
            this.clearCache();
            this.update();
        }
    }

    set channelOffsets(val) {
        this.#allChannelOffsets = val;
        if (this.#lutsEnabled) {
            this.clearCache();
            this.update();
        }
    }

    get lutsForUrlParam() {
        return btoa(JSON.stringify(this.#lutsEnabled
            ? this.#allChannelNames.map((name, index) => ({ gain: this.#allChannelGains?.[index] ?? 1.0, gamma: this.#allChannelGammas?.[index] ?? 1.0, offset: this.#allChannelOffsets?.[index] ?? 0 }))
            : this.#allChannelNames.map(() => ({ gain: 1.0, gamma: 1.0, offset: 0 }))));
    }

    setLuts(gains, gammas, offsets) {
        if (gains)
            this.#allChannelGains = gains;
        if (gammas)
            this.#allChannelGammas = gammas;
        if (offsets)
            this.#allChannelOffsets = offsets;
        if (this.#lutsEnabled) {
            this.clearCache();
            this.update();
        }
    }

    get showSingleChannelInMono() {
        return this.#showSingleChannelInMono;
    }

    set showSingleChannelInMono(val) {
        if (this.#showSingleChannelInMono === val)
            return;
        this.#showSingleChannelInMono = val;
        if (this.#channelIndex !== -1 || this.#allChannelNames.length === 1) {
            this.clearCache();
            this.update();
        }
    }

    get playingLoop() {
        return this.#playingLoop;
    }

    set playingLoop(val) {
        if (this.#playingLoop === val)
            return;

        if (this.#playingLoop) {
            document.getElementById(`ndnav-play-${this.#playingLoop}`).setPlayIcon();
            clearTimeout(this?.advancePlayingLoopEventId ?? -1);
        }
        this.#playingLoop = val;
        if (this.#playingLoop)
            document.getElementById(`ndnav-play-${this.#playingLoop}`).setPauseIcon();

        this.update();
    }

    get binaryNames() {
        return this.#allBinLayerNames;
    }

    get imageZoomSizes() {
        return this.#imageZoomSizes;
    }

    get imageCanvasDomElement() {
        return this.#imageCanvas;
    }

    get statusbarDomElement() {
        return this.#statusbar;
    }

    get navigatorDomElement() {
        return this.#navigator;
    }

    get viewingMode() {
        return this.#viewingMode;
    }

    set viewingMode(value) {
        if (this.#viewingMode === value)
            return;

        this.playingLoop = "";
        this.#viewingMode = value;
        switch (this.#viewingMode) {
            case ViewingMode.imageView:
                this.#setImageModeLow();
                break;
            case ViewingMode.volumeView:
                this.#setVolumeModeLow();
                break;
        }
    }
}

const createLimStatusbar = () => {
    const el = document.createElement("div");
    el.className = "lim-image-statusbar lim-flex-row lim-fill-width"
    el.hidden = true;
    el.style.padding = "3px";
    el.style.backgroundColor = "var(--color-window)";
    return el;
};

const createLimNavigator = () => {
    const el = document.createElement("div");
    el.class = "lim-fill-width"
    el.style.padding = "3px";
    el.style.display = "none";
    el.style.rowGap = "1px";
    el.style.gridTemplateColumns = "120px auto max-content";
    el.style.backgroundColor = "var(--color-window)";
    return el;
};

const vertexShader = /* glsl */`
in vec3 position;

uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;

out vec4 v_nearpos;
out vec4 v_farpos;
out vec3 v_position;

void main() {
    // Prepare transforms to map to "camera view". See also:
    // https://threejs.org/docs/#api/renderers/webgl/WebGLProgram
    mat4 viewtransformf = modelViewMatrix;
    mat4 viewtransformi = inverse(modelViewMatrix);

    // Project local vertex coordinate to camera position. Then do a step
    // backward (in cam coords) to the near clipping plane, and project back. Do
    // the same for the far clipping plane. This gives us all the information we
    // need to calculate the ray and truncate it to the viewing cone.
    vec4 position4 = vec4(position, 1.0);
    vec4 pos_in_cam = viewtransformf * position4;

    // Intersection of ray and near clipping plane (z = -1 in clip coords)
    pos_in_cam.z = -pos_in_cam.w;
    v_nearpos = viewtransformi * pos_in_cam;

    // Intersection of ray and far clipping plane (z = +1 in clip coords)
    pos_in_cam.z = pos_in_cam.w;
    v_farpos = viewtransformi * pos_in_cam;

    // Set varyings and output pos
    v_position = position;
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * position4;
}
`;

const fragmentShader = /* glsl */`
precision highp float;
precision mediump sampler3D;

uniform int u_fmt;
uniform int u_gray;
uniform vec3 u_size;
uniform vec3 u_count;
uniform sampler3D u_data;

in vec3 v_position;
in vec4 v_nearpos;
in vec4 v_farpos;

out vec4 color;

// The maximum distance through our rendering volume is sqrt(3).
const int MAX_STEPS = 887;	// 887 for 512^3, 1774 for 1024^3
const int REFINEMENT_STEPS = 4;
const float relative_step_size = 1.0;

void cast_mip1(vec3 start_loc, vec3 step, int nsteps, vec3 view_ray);
void cast_mip4(vec3 start_loc, vec3 step, int nsteps, vec3 view_ray);

void main() {
    // Normalize clipping plane info
    vec3 farpos = v_farpos.xyz / v_farpos.w;
    vec3 nearpos = v_nearpos.xyz / v_nearpos.w;

    // Calculate unit vector pointing in the view direction through this fragment.
    vec3 view_ray = normalize(nearpos.xyz - farpos.xyz);

    // Compute the (negative) distance to the front surface or near clipping plane.
    // v_position is the back face of the cuboid, so the initial distance calculated in the dot
    // product below is the distance from near clip plane to the back of the cuboid
    float distance = dot(nearpos - v_position, view_ray);
    distance = max(distance, min((-0.5 - v_position.x) / view_ray.x, (u_size.x - 0.5 - v_position.x) / view_ray.x));
    distance = max(distance, min((-0.5 - v_position.y) / view_ray.y, (u_size.y - 0.5 - v_position.y) / view_ray.y));
    distance = max(distance, min((-0.5 - v_position.z) / view_ray.z, (u_size.z - 0.5 - v_position.z) / view_ray.z));

    // Now we have the starting position on the front surface
    vec3 front = v_position + view_ray * distance;

    // Decide how many steps to take
    int nsteps = int(-distance / relative_step_size + 0.5);
    if (nsteps < 1)
        discard;

    // Get starting location and step vector in texture coordinates
    vec3 step = ((v_position - front) / u_size) / float(nsteps);
    vec3 start_loc = front / u_size;

    // For testing: show the number of steps. This helps to establish
    // whether the rays are correctly oriented
    // color = vec4(0.0, float(nsteps) / 1.0 / u_size.x, 1.0, 1.0);
    // return;

    if (1 == u_fmt)
        cast_mip1(start_loc, step, nsteps, view_ray);
    else if (4 == u_fmt)
        cast_mip4(start_loc, step, nsteps, view_ray);
    else
        discard;

    if (color.a < 0.05)
            discard;
}

void cast_mip4(vec3 start_loc, vec3 step, int nsteps, vec3 view_ray) {
    vec4 max_col = vec4(0, 0, 0, 255);
    vec3 loc = start_loc;
    for (int iter=0; iter<nsteps; iter++) {
        vec4 col = texture(u_data, loc);
        max_col.r = max(max_col.r, col.r);
        max_col.g = max(max_col.g, col.g);
        max_col.b = max(max_col.b, col.b);
        loc += step;
    }
    if (0 != u_gray) {
        color.r = color.g = color.b = max(max_col.r, max(max_col.g, max_col.b));
        color.a = 255.0f;
    }
    else
        color = max_col;
}

void cast_mip1(vec3 start_loc, vec3 step, int nsteps, vec3 view_ray) {
    float max_val = -1e6;
    int max_i = 100;
    vec3 loc = start_loc;
    for (int iter=0; iter<nsteps; iter++) {
        float val = texture(u_data, loc).r;
        if (val > max_val) {
            max_val = val;
            max_i = iter;
        }
        loc += step;
    }

    // Refine location, gives crispier images
    vec3 iloc = start_loc + step * (float(max_i) - 0.5);
    vec3 istep = step / float(REFINEMENT_STEPS);
    for (int i=0; i<REFINEMENT_STEPS; i++) {
        max_val = max(max_val, texture(u_data, iloc).r);
        iloc += istep;
    }

    // Resolve final color
    color.r = max_val;
    color.g = max_val;
    color.b = max_val;
    color.a = 255.0f;
}
`;


/*___________________________________________________________________________*/
export class LimLutsDialog extends HTMLElement {
    #rgb
    #state
    #settings
    #singleChannelInGray
    #shiftKeyState
    #abortListeners
    #resetImg

    constructor() {
        super();
    }

    static observedAttributes = [];

    setupChannels(chnames, chcolors, rgb, settings) {
        this.#state = [];
        for (let i = 0; i < chnames.length; i++) {
            this.#state.push({
                name: chnames[i],
                color: chcolors[i],
                gain: 1.0,
                gamma: 1.0,
                offset: 0.0,
            });
        }

        this.#rgb = rgb ?? false;

        this.#settings = {};

        this.#settings['useLo'] = settings?.useLo ?? true;
        document.getElementById(`${this.id}-settings-lo-check`).checked = this.#settings['useLo'];

        this.#settings['percentLo'] = Math.max(0, settings?.percentLo ?? 0.0);
        document.getElementById(`${this.id}-settings-lo-number`).value = this.#settings['percentLo'];

        this.#settings['useHi'] = settings?.useHi ?? true;
        document.getElementById(`${this.id}-settings-hi-check`).checked = this.#settings['useHi'];

        this.#settings['percentHi'] = Math.max(0, settings?.percentHi ?? 0.01);
        document.getElementById(`${this.id}-settings-hi-number`).value = this.#settings['percentHi'];

        this.#singleChannelInGray = false;

        this._recreateDOM();
    }

    updateValues(gains, gammas, offsets, singleChannelInGray) {
        for (let i = 0; i < (this.#state?.length ?? 0); i++) {
            if (typeof gains?.[i] !== "undefined")
                this.updateGain(i, gains[i]);
            if (typeof gammas?.[i] !== "undefined")
                this.updateGamma(i, gammas[i]);
            if (typeof offsets?.[i] !== "undefined")
                this.updateOffset(i, offsets[i]);
        }

        if (typeof singleChannelInGray !== "undefined") {
            this.#singleChannelInGray = singleChannelInGray;
            document.getElementById(`${this.id}-settings-channel-in-gray-check`).checked = this.#singleChannelInGray;
        }
    }

    get gains() {
        return Array.isArray(this.#state) ? this.#state.map(item => (item?.gain ?? 1.0)) : [];
    }

    get gammas() {
        return Array.isArray(this.#state) ? this.#state.map(item => (item?.gamma ?? 1.0)) : [];
    }

    get offsets() {
        return Array.isArray(this.#state) ? this.#state.map(item => (item?.offset ?? 0)) : [];
    }

    get singleChannelInGray() {
        return this.#singleChannelInGray;
    }

    get settings() {
        return this.#settings;
    }

    updateGain(i, value, senderId) {
        this.#state[i].gain = value;
        if (`${this.id}-gain-slider-${i}` !== senderId)
            document.getElementById(`${this.id}-gain-slider-${i}`).value = Math.log2(this.#state[i].gain);
        if (`${this.id}-gain-number-${i}` !== senderId)
            document.getElementById(`${this.id}-gain-number-${i}`).value = this.#state[i].gain.toFixed(2);
    }

    updateGamma(i, value, senderId) {
        this.#state[i].gamma = value;
        if (`${this.id}-gamma-slider-${i}` !== senderId)
            document.getElementById(`${this.id}-gamma-slider-${i}`).value = Math.log2(this.#state[i].gamma);
        if (`${this.id}-gamma-number-${i}` !== senderId)
            document.getElementById(`${this.id}-gamma-number-${i}`).value = this.#state[i].gamma.toFixed(3);
    }

    updateOffset(i, value, senderId) {
        this.#state[i].offset = value;
        if (`${this.id}-offset-number-${i}` !== senderId)
            document.getElementById(`${this.id}-offset-number-${i}`).value = this.#state[i].offset.toFixed(0);
    }

    _keyEventHandler(e) {
        this.#shiftKeyState = e.shiftKey;
    }

    _triggerLutsChangedEvent() {
        clearTimeout(this?.lutsChangedEventId ?? -1);
        this.lutsChangedEventId = setTimeout(() => {
            const evt = new Event("lutschanged", { bubbles: true, cancelable: false });
            this.dispatchEvent(evt);
        }, 250);
    }

    _triggerSingleChannelInGrayChangedEvent() {
        const evt = new Event("channelingraychanged", { bubbles: true, cancelable: false });
        this.dispatchEvent(evt);
    }

    _triggerSettingsChangedEvent() {
        const evt = new Event("settingschanged", { bubbles: true, cancelable: false });
        this.dispatchEvent(evt);
    }

    _recreateDOM() {

        const content = document.getElementById(`${this.id}-content`);

        content.innerText = "";

        const gainDataList = document.createElement("datalist");
        gainDataList.id = `${this.id}-gain-tick-values`;
        for (let val of [-1, 0.0000, 3.3219, 4.3219, 4.9069, 5.3219, 5.6439, 5.9069, 6.1293, 6.3219, 6.4919, 6.6439, 6.7814, 6.9069, 7.0224, 7.1293, 7.2288, 7.3219, 7.4094, 7.4919, 7.5699, 7.6439, 7.7142, 7.7814, 7.8455, 7.9069, 7.9658, 8.0000]) {
            const opt = document.createElement("option");
            opt.value = val;
            gainDataList.appendChild(opt);
        }
        content.appendChild(gainDataList);

        const gammaDataList = document.createElement("datalist");
        gammaDataList.id = `${this.id}-gamma-tick-values`;
        for (let val of [ 0 ]) {
            const opt = document.createElement("option");
            opt.value = val;
            gammaDataList.appendChild(opt);
        }
        content.appendChild(gammaDataList);

        let el = document.createElement("span");
        el.className = "lim-font-bold lim-align-self-start"
        el.innerText = "Channel";
        content.appendChild(el);

        el = document.createElement("span");
        el.className = "lim-font-bold lim-justify-center"
        el.innerText = "Gain";
        el.style.gridColumnStart = "span 2";
        content.appendChild(el);

        el = document.createElement("span");
        el.className = "lim-font-bold lim-justify-center"
        el.innerText = "Offset";
        content.appendChild(el);

        el = document.createElement("span");
        el.className = "lim-font-bold lim-justify-center"
        el.innerText = "Gamma";
        el.style.gridColumnStart = "span 2";
        content.appendChild(el);

        el = document.createElement("button");
        el.className = 'lim-tool-button lim-medium-icon';
        el.id = `${this.id}-global-reset`;
        el.name = `global-reset`;
        el.appendChild(this.#resetImg.cloneNode());
        el.onclick = () => {
            for (let i = 0; i < (this.#state?.length ?? 0); i++) {
                this.updateGain(i, 1.0);
                this.updateGamma(i, 1.0);
                this.updateOffset(i, 0.0);
            }
            this._triggerLutsChangedEvent();
        }
        content.appendChild(el);


        for (let k = 0; k < (this.#state?.length ?? 0); k++) {
            const i = this.#rgb ? (2 - k) : k;

            // 1. Name
            el = document.createElement("span");
            el.innerText = this.#state[i].name;
            content.appendChild(el);

            // 2. Gain Slider
            const color = this.#state[i].color;
            const r = Math.min(255, parseInt(color.slice(1, 3), 16) + 128).toString(16);
            const g = Math.min(255, parseInt(color.slice(3, 5), 16) + 128).toString(16);
            const b = Math.min(255, parseInt(color.slice(5, 7), 16) + 128).toString(16);

            el = document.createElement("input");
            el.id = `${this.id}-gain-slider-${i}`;
            el.name = `gain-slider-${i}`;
            el.type = "range";
            el.value = Math.log2(this.#state[i].gain);
            el.min = -1;
            el.max = 8;
            el.step = "any";
            el.style.accentColor = `#${r}${g}${b}`;
            el.style.userSelect = "none";
            el.style.marginLeft = "0.3em";
            el.setAttribute("list", gainDataList.id);
            el.oninput = (e) => {
                const logval = parseFloat(e.target.value);
                const all = this.#rgb ? !this.#shiftKeyState : this.#shiftKeyState;
                if (all)
                    this.#state.forEach((item, index) => this.updateGain(index, Math.pow(2, logval), e.target.id));
                else
                    this.updateGain(i, Math.pow(2, logval), e.target.id);
                this._triggerLutsChangedEvent();
            };
            el.ondblclick = (e) => {
                const all = this.#rgb ? !this.#shiftKeyState : this.#shiftKeyState;
                if (all)
                    this.#state.forEach((item, index) => this.updateGain(index, 1.0));
                else
                    this.updateGain(i, 1.0);
                this._triggerLutsChangedEvent();
            };
            content.appendChild(el);

            // 3. Gain Number
            el = document.createElement("input");
            el.id = `${this.id}-gain-number-${i}`;
            el.name = `gain-number-${i}`;
            el.type = "number";
            el.min = 0.5;
            el.step = 0.01;
            el.value = this.#state[i].gain.toFixed(2);
            el.style.textAlign = "right";
            el.style.marginRight = "0.3em";
            el.setAttribute("required", "true");
            el.oninput = (e) => {
                if (e.target.validity.valid) {
                    this.updateGain(i, parseFloat(e.target.value), e.target.id);
                    this._triggerLutsChangedEvent();
                }
            };
            content.appendChild(el);

            // 4. Offset Number
            el = document.createElement("input");
            el.id = `${this.id}-offset-number-${i}`;
            el.name = `offset-number-${i}`;
            el.type = "number";
            el.step = 1;
            el.value = this.#state[i].offset.toFixed(0);
            el.style.textAlign = "right";
            el.style.marginLeft = "0.3em";
            el.style.marginRight = "0.3em";
            el.setAttribute("required", "true");
            el.oninput = (e) => {
                if (e.target.validity.valid) {
                    this.updateOffset(i, parseFloat(e.target.value), e.target.id);
                    this._triggerLutsChangedEvent();
                }
            };
            content.appendChild(el);

            // 5. Gamma Slider
            el = document.createElement("input");
            el.id = `${this.id}-gamma-slider-${i}`;
            el.name = `gain-slider-${i}`;
            el.type = "range";
            el.value = Math.log2(this.#state[i].gamma);
            el.min = -3;
            el.max = 3;
            el.step = "any";
            el.style.marginLeft = "0.3em";
            el.style.userSelect = "none";
            el.setAttribute("list", gammaDataList.id);
            el.oninput = (e) => {
                const logval = parseFloat(e.target.value);
                const all = this.#rgb ? !this.#shiftKeyState : this.#shiftKeyState;
                if (all)
                    this.#state.forEach((item, index) => this.updateGamma(index, Math.pow(2, logval), e.target.id));
                else
                    this.updateGamma(i, Math.pow(2, logval), e.target.id);
                this._triggerLutsChangedEvent();
            };
            el.ondblclick = (e) => {
                const all = this.#rgb ? !this.#shiftKeyState : this.#shiftKeyState;
                if (all)
                    this.#state.forEach((item, index) => this.updateGamma(index, 1.0));
                else
                    this.updateGamma(i, 1.0);
                this._triggerLutsChangedEvent();
            }
            content.appendChild(el);

            // 6. Gamma Number
            el = document.createElement("input");
            el.id = `${this.id}-gamma-number-${i}`;
            el.type = "numeric";
            el.name = `gamma-number-${i}`;
            el.value = this.#state[i].gamma.toFixed(3);
            el.style.textAlign = "right";
            el.style.padding = "0.3em";
            el.style.marginRight = "0.3em";
            el.setAttribute("required", "true");
            el.setAttribute("pattern", "([0-9]*[.])?[0-9]+");
            el.oninput = (e) => {
                if (e.target.validity.valid) {
                    this.updateGamma(i, parseFloat(e.target.value), e.target.id);
                    this._triggerLutsChangedEvent();
                }
            };
            content.appendChild(el);

            // 7. Reset
            el = document.createElement("button");
            el.className = 'lim-tool-button lim-small-icon';
            el.name = `gain-reset-${i}`;
            el.appendChild(this.#resetImg.cloneNode());
            el.onclick = () => {
                this.updateGain(i, 1.0);
                this.updateGamma(i, 1.0);
                this.updateOffset(i, 0.0);
                this._triggerLutsChangedEvent();
            }
            content.appendChild(el);
        }

        el = document.createElement("span");
        el.className = "lim-font-small"
        el.innerText = this.#rgb ? `Hold the SHIFT key to control single component slider.` : `Hold the SHIFT key to move all sliders at once.`;
        el.style.padding = "0.3em";
        el.style.gridColumnStart = "2";
        content.appendChild(el);

        // SETTINGS line

        const settings_left = document.createElement("span");
        settings_left.className = "lim-flex-row lim-small-gap";
        settings_left.style.gridRowStart = `${(this.#state?.length ?? 0) + 4}`;
        settings_left.style.gridColumnStart = "span 7";

        // 1. title "Auto Scale"
        el = document.createElement("span");
        el.innerText = "Auto Scale"
        settings_left.appendChild(el);

        // 2. lo checkbox
        el = document.createElement("input");
        el.id = `${this.id}-settings-lo-check`;
        el.type = "checkbox";
        el.value = "Low"
        el.checked = this.#settings?.useLo ?? false;
        el.style.marginLeft = "1.2em";
        el.oninput = (e) => {
            this.#settings.useLo = e.target.checked;
            this._triggerSettingsChangedEvent();
        };
        settings_left.appendChild(el);

        el = document.createElement("label");
        el.style.alignSelf = "center";
        el.htmlFor = `${this.id}-settings-lo-check`;
        el.innerText = "Low";
        settings_left.appendChild(el);

        // 3. lo edit
        el = document.createElement("input");
        el.id = `${this.id}-settings-lo-number`;
        el.type = "number";
        el.min = 0
        el.max = 100
        el.step = 0.01
        el.value = this.#settings?.percentLo ?? 0;
        el.style.width = "6em";
        el.style.textAlign = "right";
        el.style.marginLeft = "0.3em";
        el.style.marginRight = "0.3em";
        el.setAttribute("required", "true");
        el.oninput = (e) => {
            if (e.target.validity.valid) {
                this.#settings.percentLo = parseFloat(e.target.value);
                this._triggerSettingsChangedEvent();
            }
        };
        settings_left.appendChild(el);

        // 4. hi checkbox
        el = document.createElement("input");
        el.id = `${this.id}-settings-hi-check`
        el.type = "checkbox";
        el.checked = this.#settings?.useHi ?? false;
        el.style.marginLeft = "1.2em";
        el.oninput = (e) => {
            this.#settings.useHi = e.target.checked;
            this._triggerSettingsChangedEvent();
        };
        settings_left.appendChild(el);

        el = document.createElement("label");
        el.style.alignSelf = "center";
        el.htmlFor = `${this.id}-settings-hi-check`;
        el.innerText = "High";
        settings_left.appendChild(el);

        // 5. hi edit
        el = document.createElement("input");
        el.id = `${this.id}-settings-hi-number`;
        el.type = "number";
        el.min = 0
        el.max = 100
        el.step = 0.01
        el.value = this.#settings?.percentHi ?? 0;
        el.style.width = "6em";
        el.style.textAlign = "right";
        el.style.marginLeft = "0.3em";
        el.style.marginRight = "0.3em";
        el.setAttribute("required", "true");
        el.oninput = (e) => {
            if (e.target.validity.valid) {
                this.#settings.percentHi = parseFloat(e.target.value);
                this._triggerSettingsChangedEvent();
            }
        };
        settings_left.appendChild(el);

        content.appendChild(settings_left);

        el = document.createElement("span");
        el.style.flexGrow = 1;
        settings_left.appendChild(el);

        // 6. channel in gray checkbox
        el = document.createElement("input");
        el.id = `${this.id}-settings-channel-in-gray-check`
        el.type = "checkbox";
        el.checked = this.#singleChannelInGray ?? false;
        el.style.marginLeft = "1.2em";
        el.oninput = (e) => {
            this.#singleChannelInGray = e.target.checked;
            this._triggerSingleChannelInGrayChangedEvent();
        };
        settings_left.appendChild(el);

        el = document.createElement("label");
        el.style.alignSelf = "center";
        el.htmlFor = `${this.id}-settings-channel-in-gray-check`;
        el.innerText = "Single channel in gray";
        settings_left.appendChild(el);
    }

    connectedCallback() {

        this.#resetImg = document.createElement("img");
        this.#resetImg.src = "/res/gnr_core_gui/CoreGUI/Icons/base/close.svg";

        const titleBar = document.createElement("div");
        titleBar.id = `${this.id}-titlebar`
        titleBar.className = "lim-flex-row lim-fill-width lim-titlebar lim-titlebar-active";

        let el = document.createElement("span");
        el.style.marginLeft = "6px";
        el.innerText = "LUTs";
        titleBar.appendChild(el);

        el = document.createElement("span");
        el.style.flexGrow = 1;
        titleBar.appendChild(el);

        el = document.createElement("button");
        el.appendChild(this.#resetImg.cloneNode());
        el.onclick = () => {
            const evt = new Event("close", { bubbles: true, cancelable: false });
            this.dispatchEvent(evt);
        }
        titleBar.appendChild(el);

        titleBar.appendChild(el);

        this.appendChild(titleBar);

        el = document.createElement("div");
        el.id = `${this.id}-content`
        el.className = "lim-content lim-fill-width lim-fill-height";
        this.appendChild(el);

        this._recreateDOM();

        this.#abortListeners = new AbortController();
        addEventListener("keydown", (e) => this._keyEventHandler(e), { signal: this.#abortListeners.signal });
        addEventListener("keyup", (e) => this._keyEventHandler(e), { signal: this.#abortListeners.signal });
    }

    disconnectedCallback() {
        this.#abortListeners.abort();
    }

    adoptedCallback() {
    }

    attributeChangedCallback(name, oldValue, newValue) {
    }

    close() {

    }
}

customElements.define('lim-luts-dialog', LimLutsDialog);