/* *
 *
 *  (c) 2010-2026 Highsoft AS
 *  Author: Torstein Hønsi
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 * */
/// <reference types="@webgpu/types" />
'use strict';
import Color from '../../Core/Color/Color.js';
import ContourPoint from './ContourPoint.js';
import contourShader from './ContourShader.js';
import ContourSeriesDefaults from './ContourSeriesDefaults.js';
import CrossSymbol from '../CrossSymbol.js';
import Delaunay from '../../Core/Delaunay.js';
import SeriesRegistry from '../../Core/Series/SeriesRegistry.js';
import { diffObjects, extend, merge, normalizeTickInterval } from '../../Shared/Utilities.js';
import { error } from '../../Core/Utilities.js';
const { seriesTypes: { scatter: ScatterSeries } } = SeriesRegistry;
/* *
 *
 *  Class
 *
 * */
class ContourSeries extends ScatterSeries {
    static compose(SVGRendererClass) {
        CrossSymbol.compose(SVGRendererClass);
    }
    /* Uniforms:
     * - extremesUniform,
     * - valueExtremesUniform,
     * - contourInterval,
     * - contourOffset,
     * - smoothColoring,
     * - lineWidth,
     * - contourLineColor
     * - colorAxisStops
     * - colorAxisStopsCount
     * - isInverted
     */
    /* *
     *
     * Methods
     *
     * */
    getContourData() {
        const points = this.points, len = points.length, points3d = new Float32Array(len * 3), points2d = new Float64Array(len * 2);
        for (let i = 0; i < len; i++) {
            const { x, y = 0, value } = points[i], index2d = i * 2, index3d = i * 3;
            points2d[index2d] = x;
            points2d[index2d + 1] = y;
            points3d[index3d] = x;
            points3d[index3d + 1] = y;
            points3d[index3d + 2] = value ?? 0;
        }
        return [new Delaunay(points2d).triangles, points3d];
    }
    update(options, redraw) {
        options = diffObjects(options, this.userOptions);
        const uniformOptions = [
            'smoothColoring',
            'contourInterval',
            'contourOffset',
            'lineColor',
            'lineWidth'
        ];
        const isUniformOption = (key) => (uniformOptions.includes(key));
        const hasNonUniformOptions = Object.keys(options).some((key) => !isUniformOption(key));
        // Only fetch plotOptions if all options are uniform related.
        const allOptions = (hasNonUniformOptions ?
            void 0 :
            this.setOptions(merge(this.userOptions, options))), hasNonUniformPlotOptions = allOptions ?
            Object.keys(diffObjects(allOptions, this.options)).some((key) => !isUniformOption(key)) :
            false;
        if (hasNonUniformOptions || hasNonUniformPlotOptions) {
            super.update(options, redraw);
        }
        else {
            this.options = allOptions;
            // If only uniform-related options changed, avoid full series
            // reconstruction and update uniforms only.
            this.setUniforms();
        }
    }
    drawPoints() {
        const { group } = this;
        if (!group) {
            return;
        }
        if (!this.canvas) {
            this.foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
            group.element.appendChild(this.foreignObject);
            this.canvas = document.createElement('canvas');
            this.foreignObject.appendChild(this.canvas);
        }
        const { canvas, xAxis, yAxis } = this, foreignObject = this.foreignObject, oldWidth = foreignObject.width.baseVal.value, oldHeight = foreignObject.height.baseVal.value, { devicePixelRatio: dpr } = window;
        let width = xAxis.len, height = yAxis.len;
        if (this.chart.inverted) {
            [width, height] = [height, width];
        }
        if (oldWidth !== width) {
            foreignObject.setAttribute('width', width);
            canvas.width = width * dpr;
            canvas.style.width = width + 'px';
        }
        if (oldHeight !== height) {
            foreignObject.setAttribute('height', height);
            canvas.height = height * dpr;
            canvas.style.height = height + 'px';
        }
        if (this.renderFrame) {
            this.renderFrame();
        }
        else {
            /* eslint-disable @typescript-eslint/no-floating-promises */
            this.run();
        }
    }
    async run() {
        const series = this, chart = series.chart, renderer = chart.renderer, canvas = series.canvas, gpu = navigator.gpu, context = series.context = canvas.getContext('webgpu');
        if (!gpu || !context) {
            error(37, false, chart);
            return;
        }
        renderer.asyncCounter += 1;
        if (context) {
            let device = this.device;
            if (!this.adapter) {
                this.adapter = await gpu.requestAdapter();
            }
            if (!device && this.adapter) {
                device = this.device = await this.adapter.requestDevice();
            }
            const canvasFormat = gpu.getPreferredCanvasFormat();
            if (device) {
                context.configure({
                    device: device,
                    format: canvasFormat,
                    colorSpace: 'display-p3',
                    alphaMode: 'premultiplied',
                    usage: (GPUTextureUsage.RENDER_ATTACHMENT |
                        GPUTextureUsage.COPY_SRC)
                });
                const [indices, vertices] = this.getContourData();
                // WebGPU Buffers grouped under a single object
                const buffers = this.buffers = {
                    vertex: device.createBuffer({
                        size: vertices.byteLength,
                        usage: GPUBufferUsage.VERTEX | GPUBufferUsage.COPY_DST
                    }),
                    index: device.createBuffer({
                        size: indices.byteLength,
                        usage: GPUBufferUsage.INDEX | GPUBufferUsage.COPY_DST
                    }),
                    extremesUniform: device.createBuffer({
                        size: Float32Array.BYTES_PER_ELEMENT * 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    valueExtremesUniform: device.createBuffer({
                        size: Float32Array.BYTES_PER_ELEMENT * 2,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    contourIntervalUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    contourOffsetUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    smoothColoringUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    lineWidthUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    contourLineColor: device.createBuffer({
                        size: 12,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    colorAxisStopsCountUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    }),
                    colorAxisStopsUniform: device.createBuffer({
                        size: Float32Array.BYTES_PER_ELEMENT * 64,
                        usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST
                    }),
                    isInvertedUniform: device.createBuffer({
                        size: 4,
                        usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
                    })
                };
                const { vertex: vertexBuffer, index: indexBuffer, extremesUniform: extremesUniformBuffer, valueExtremesUniform: valueExtremesUniformBuffer, contourIntervalUniform: contourIntervalUniformBuffer, contourOffsetUniform: contourOffsetUniformBuffer, smoothColoringUniform: smoothColoringUniformBuffer, lineWidthUniform: lineWidthUniformBuffer, contourLineColor: contourLineColorBuffer, colorAxisStopsCountUniform: colAxisStopsCountUniformBuffer, colorAxisStopsUniform: colorAxisStopsUniformBuffer, isInvertedUniform: isInvertedUniformBuffer } = buffers;
                device.queue.writeBuffer(vertexBuffer, 0, vertices);
                device.queue.writeBuffer(indexBuffer, 0, indices);
                const vertexBufferLayout = {
                    arrayStride: 12,
                    attributes: [{
                            format: 'float32x3',
                            offset: 0,
                            shaderLocation: 0
                        }]
                };
                const shaderModule = device.createShaderModule({
                    code: contourShader
                });
                const pipeline = device.createRenderPipeline({
                    layout: 'auto',
                    vertex: {
                        module: shaderModule,
                        entryPoint: 'vertexMain',
                        buffers: [vertexBufferLayout]
                    },
                    fragment: {
                        module: shaderModule,
                        entryPoint: 'fragmentMain',
                        targets: [{
                                format: canvasFormat
                            }]
                    },
                    primitive: {
                        topology: 'triangle-list'
                    }
                });
                const bindGroup = device.createBindGroup({
                    layout: pipeline.getBindGroupLayout(0),
                    entries: [{
                            binding: 0,
                            resource: {
                                buffer: extremesUniformBuffer,
                                label: 'extremesUniformBuffer'
                            }
                        }, {
                            binding: 1,
                            resource: {
                                buffer: valueExtremesUniformBuffer,
                                label: 'valueExtremesUniformBuffer'
                            }
                        }, {
                            binding: 2,
                            resource: {
                                buffer: colorAxisStopsUniformBuffer,
                                label: 'colorAxisStopsBuffer'
                            }
                        }, {
                            binding: 3,
                            resource: {
                                buffer: colAxisStopsCountUniformBuffer,
                                label: 'colorAxisStopsCountBuffer'
                            }
                        }, {
                            binding: 4,
                            resource: {
                                buffer: contourIntervalUniformBuffer,
                                label: 'contourIntervalUniformBuffer'
                            }
                        }, {
                            binding: 5,
                            resource: {
                                buffer: contourOffsetUniformBuffer,
                                label: 'contourOffsetUniformBuffer'
                            }
                        }, {
                            binding: 6,
                            resource: {
                                buffer: smoothColoringUniformBuffer,
                                label: 'smoothColoringUniformBuffer'
                            }
                        }, {
                            binding: 7,
                            resource: {
                                buffer: lineWidthUniformBuffer,
                                label: 'lineWidthUniformBuffer'
                            }
                        }, {
                            binding: 8,
                            resource: {
                                buffer: contourLineColorBuffer,
                                label: 'contourLineColorBuffer'
                            }
                        }, {
                            binding: 9,
                            resource: {
                                buffer: isInvertedUniformBuffer,
                                label: 'isInvertedUniformBuffer'
                            }
                        }]
                });
                this.renderFrame = function () {
                    this.setUniforms(false);
                    const encoder = device.createCommandEncoder(), currentTexture = context.getCurrentTexture(), pass = encoder.beginRenderPass({
                        colorAttachments: [{
                                view: currentTexture.createView(),
                                loadOp: 'clear',
                                clearValue: [0, 0, 0, 0],
                                storeOp: 'store'
                            }]
                    });
                    pass.setPipeline(pipeline);
                    pass.setVertexBuffer(0, vertexBuffer);
                    pass.setIndexBuffer(indexBuffer, 'uint32');
                    pass.setBindGroup(0, bindGroup);
                    pass.drawIndexed(indices.length);
                    pass.end();
                    device.queue.submit([encoder.finish()]);
                };
                this.renderFrame();
            }
        }
        renderer.asyncCounter--;
        if (!renderer.asyncCounter && chart && !chart.hasLoaded) {
            chart.onload();
        }
    }
    destroy() {
        // Remove the foreign object. The canvas will be removed with it.
        // For some reason, `series.update` calls `series.destroy` even if
        // update does not trigger a rerender. This causes the canvas to be
        // removed here (unnecessarily) and that causes the flickering effect
        // when updating.
        this.canvas?.parentElement?.remove();
        super.destroy();
    }
    drawGraph() {
        // Do nothing
    }
    /**
     * Set all the updateable uniforms.
     *
     * @param {boolean} renderFrame
     * Whether to rerender the series' context after setting the uniforms.
     * Defaults to `true`.
     */
    setUniforms(renderFrame = true) {
        this.setFrameExtremesUniform(false);
        this.setValueExtremesUniform(false);
        this.setColorAxisStopsUniforms(false);
        this.setContourIntervalUniform(false);
        this.setContourOffsetUniform(false);
        this.setSmoothColoringUniform(false);
        this.setLineWidthUniform(false);
        this.setContourLineColorUniform(false);
        this.setIsInvertedUniform(renderFrame);
    }
    /**
     * Set the contour interval uniform according to the series options.
     *
     * @param {boolean} renderFrame
     * Whether to rerender the series' context after setting the uniform.
     * Defaults to `true`.
     */
    setContourIntervalUniform(renderFrame = true) {
        if (this.device && this.buffers?.contourIntervalUniform) {
            this.device.queue.writeBuffer(this.buffers.contourIntervalUniform, 0, new Float32Array([this.getContourInterval()]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the contour offset uniform according to the series options.
     */
    setContourOffsetUniform(renderFrame = true) {
        if (this.device && this.buffers?.contourOffsetUniform) {
            this.device.queue.writeBuffer(this.buffers.contourOffsetUniform, 0, new Float32Array([this.getContourOffset()]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the smooth coloring uniform according to the series options.
     */
    setSmoothColoringUniform(renderFrame = true) {
        if (this.device && this.buffers?.smoothColoringUniform) {
            this.device.queue.writeBuffer(this.buffers.smoothColoringUniform, 0, new Float32Array([this.getSmoothColoring()]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the line width uniform according to the series options.
     */
    setLineWidthUniform(renderFrame = true) {
        if (this.device && this.buffers?.lineWidthUniform) {
            this.device.queue.writeBuffer(this.buffers.lineWidthUniform, 0, new Float32Array([this.getLineWidth()]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the contour line color uniform according to the series options.
     */
    setContourLineColorUniform(renderFrame = true) {
        if (this.device && this.buffers?.contourLineColor) {
            this.device.queue.writeBuffer(this.buffers.contourLineColor, 0, new Float32Array(this.getContourLineColor()));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the frame extremes uniform according to the series options.
     */
    setFrameExtremesUniform(renderFrame = true) {
        if (this.device && this.buffers?.extremesUniform) {
            this.device.queue.writeBuffer(this.buffers.extremesUniform, 0, new Float32Array(this.getFrameExtremes()));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the value extremes uniform according to the series data.
     */
    setValueExtremesUniform(renderFrame = true) {
        if (this.device && this.buffers?.valueExtremesUniform) {
            this.device.queue.writeBuffer(this.buffers.valueExtremesUniform, 0, new Float32Array(this.getValueAxisExtremes()));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the color axis stops uniforms according to the color axis options.
     */
    setColorAxisStopsUniforms(renderFrame = true) {
        const stopsBuffer = this.buffers?.colorAxisStopsUniform;
        const countBuffer = this.buffers?.colorAxisStopsCountUniform;
        if (this.device && stopsBuffer && countBuffer) {
            const { array, length } = this.getColorAxisStopsData();
            // Write the stops to the buffer
            this.device.queue.writeBuffer(stopsBuffer, 0, array);
            // Write the count to the buffer
            this.device.queue.writeBuffer(countBuffer, 0, new Uint32Array([length]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Set the is inverted uniform according to the series options.
     */
    setIsInvertedUniform(renderFrame = true) {
        if (this.device && this.buffers?.isInvertedUniform) {
            this.device.queue.writeBuffer(this.buffers.isInvertedUniform, 0, new Uint32Array([this.chart.inverted ? 1 : 0]));
            if (renderFrame) {
                this.renderFrame?.();
            }
        }
    }
    /**
     * Returns the contour interval from the series options in format of the
     * WebGPU uniform.
     */
    getContourInterval() {
        const interval = this.options.contourInterval ?? (() => {
            const [min, max] = this.getValueAxisExtremes(), range = max - min;
            return normalizeTickInterval(range / 10);
        })();
        if (isNaN(interval) || interval <= 0) {
            return -1;
        }
        return interval;
    }
    /**
     * Returns the contour offset from the series options in format of the
     * WebGPU uniform.
     */
    getContourOffset() {
        const offset = this.options.contourOffset ?? 0;
        if (isNaN(offset) || offset <= 0) {
            return 0;
        }
        return offset;
    }
    /**
     * Returns the smooth coloring from the series options in format of the
     * WebGPU uniform.
     */
    getSmoothColoring() {
        return this.options.smoothColoring ? 1 : 0;
    }
    /**
     * Returns the lineWidth from the series options, which controls the
     * visibility of contour lines, in format of the WebGPU uniform.
     */
    getLineWidth() {
        return this.userOptions.lineWidth ?? 1;
    }
    /**
     * Returns the contour line color from the series options in format of the
     * WebGPU uniform.
     */
    getContourLineColor() {
        const { lineColor = '#000000' } = this.options;
        return ContourSeries.rgbaAsFrac(new Color(lineColor).rgba);
    }
    /**
     * Returns the extremes of the x and y axes in format of the WebGPU uniform.
     */
    getFrameExtremes() {
        const { xAxis, yAxis } = this;
        return [
            xAxis.toValue(0, true),
            xAxis.toValue(xAxis.len, true),
            yAxis.toValue(yAxis.len, true),
            yAxis.toValue(0, true)
        ];
    }
    /**
     * Returns the extremes of the data in format of the WebGPU uniform.
     */
    getValueAxisExtremes() {
        const series = this;
        let min = series.valueMin;
        if (isNaN(min || NaN)) {
            min = series.colorAxis?.min;
            if (isNaN(min || NaN)) {
                min = Math.min(...series.points.map((point) => point.value || 0));
            }
        }
        let max = series.valueMax;
        if (isNaN(max || NaN)) {
            max = series.colorAxis?.max;
            if (isNaN(max || NaN)) {
                max = Math.max(...series.points.map((point) => point.value || 0));
            }
        }
        return [min || 0, max || 0];
    }
    getColorAxisStopsData() {
        const colorAxisStops = this.colorAxis?.stops;
        let flattenedData;
        if (colorAxisStops) {
            flattenedData = [];
            for (const stop of colorAxisStops) {
                const rgba = stop?.color?.rgba;
                if (rgba) {
                    flattenedData.push(stop[0], ...ContourSeries.rgbaAsFrac(rgba));
                }
            }
        }
        return {
            array: new Float32Array(flattenedData ?? [
                0, 0, 0, 0,
                1, 1, 1, 1
            ]),
            length: colorAxisStops?.length || 2
        };
    }
    /* *
     *
     * Static Methods
     *
     * */
    /**
     * Returns the RGBA color as a fraction of the 255 range.
     */
    static rgbaAsFrac(rgba) {
        return [
            rgba[0],
            rgba[1],
            rgba[2]
        ].map((val) => val / 255);
    }
}
/* *
 *
 * Static Properties
 *
 * */
ContourSeries.defaultOptions = merge(ScatterSeries.defaultOptions, ContourSeriesDefaults);
export default ContourSeries;
extend(ContourSeries.prototype, {
    pointClass: ContourPoint,
    pointArrayMap: ['y', 'value'],
    keysAffectYAxis: ['y'],
    invertible: false
});
SeriesRegistry.registerSeriesType('contour', ContourSeries);
