// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
// ┃ This file is part of the Perspective library, distributed under the terms ┃
// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

import type { WebGLContextManager } from "../../../webgl/context-manager";
import type { CandlestickChart } from "../candlestick";
import {
    createLineCornerBuffer,
    createQuadCornerBuffer,
    getInstancing,
} from "../../../webgl/instanced-attrs";
import { compileProgram } from "../../../webgl/program-cache";
import bodyVert from "../../../shaders/candlestick-body.vert.glsl";
import bodyFrag from "../../../shaders/candlestick-body.frag.glsl";
import lineVert from "../../../shaders/line-uniform.vert.glsl";
import lineFrag from "../../../shaders/line-uniform.frag.glsl";

type GL = WebGL2RenderingContext | WebGLRenderingContext;

interface BodyCache {
    program: WebGLProgram;
    quadBuffer: WebGLBuffer;
    instanceBuffer: WebGLBuffer;
    u_projection: WebGLUniformLocation | null;
    a_corner: number;
    a_x_center: number;
    a_half_width: number;
    a_y0: number;
    a_y1: number;
    a_color: number;
}

interface WickCache {
    program: WebGLProgram;
    cornerBuffer: WebGLBuffer;
    segmentBuffer: WebGLBuffer;
    u_projection: WebGLUniformLocation | null;
    u_color: WebGLUniformLocation | null;
    u_resolution: WebGLUniformLocation | null;
    u_line_width: WebGLUniformLocation | null;

    /**
     * `line-uniform` was extended to support the Y-Line `interpolate`
     * feature with a per-segment alpha multiplier driven by
     * `a_real_start * a_real_end` and `u_interp_alpha`. Wicks are
     * always "real" data — every segment renders fully — so we hold
     * these locations to neutralize them at draw time (uniform = 1.0,
     * constant attribute values = 1.0). Without that, an unset uniform
     * defaults to 0 and the fragment alpha collapses to 0, rendering
     * the wicks invisible.
     */
    u_interp_alpha: WebGLUniformLocation | null;
    a_corner: number;
    a_start: number;
    a_end: number;
    a_real_start: number;
    a_real_end: number;
}

interface ProgramCache {
    body: BodyCache;
    wick: WickCache;
}

/**
 * Persistent body + wick vertex buffer state. Built once per data load
 * by `rebuildBuffers`; pan/zoom redraws bind + dispatch with no uploads.
 */
interface BodyWickBuffers {
    bodyCount: number;
    upWickCount: number;
    downWickCount: number;

    /**
     * Persistent GPU buffer for up wicks. Layout: [x,low, x,high, ...].
     */
    upWickBuffer: WebGLBuffer;
    downWickBuffer: WebGLBuffer;
}

/**
 * Candlestick body + wick glyph. Owns the body and wick programs +
 * their corner/segment/instance buffers, and the persistent up/down
 * wick vertex buffers built per data load.
 */
export class BodyWickGlyph {
    private _program: ProgramCache | null = null;
    private _buffers: BodyWickBuffers | null = null;

    /**
     * Lazily compile the body and wick programs and create their static
     * GPU buffers (corner / quad). Cached for the lifetime of the chart.
     */
    private ensureProgram(glManager: WebGLContextManager): ProgramCache {
        if (this._program) {
            return this._program;
        }

        const gl = glManager.gl;

        const quadBuffer = createQuadCornerBuffer(gl);
        const bodyPartial = compileProgram<
            Omit<BodyCache, "quadBuffer" | "instanceBuffer">
        >(
            glManager,
            "candlestick-body",
            bodyVert,
            bodyFrag,
            ["u_projection"],
            [
                "a_corner",
                "a_x_center",
                "a_half_width",
                "a_y0",
                "a_y1",
                "a_color",
            ],
        );
        const body: BodyCache = {
            ...bodyPartial,
            quadBuffer,
            instanceBuffer: gl.createBuffer()!,
        };

        const cornerBuffer = createLineCornerBuffer(gl);
        const wickPartial = compileProgram<
            Omit<WickCache, "cornerBuffer" | "segmentBuffer">
        >(
            glManager,
            "line-uniform",
            lineVert,
            lineFrag,
            [
                "u_projection",
                "u_color",
                "u_resolution",
                "u_line_width",
                "u_interp_alpha",
            ],
            ["a_corner", "a_start", "a_end", "a_real_start", "a_real_end"],
        );
        const wick: WickCache = {
            ...wickPartial,
            cornerBuffer,
            segmentBuffer: gl.createBuffer()!,
        };

        this._program = { body, wick };
        return this._program;
    }

    /**
     * Drop persistent body + wick vertex buffers. Called from data-load
     * (before `rebuildBuffers`) and from chart-destroy paths.
     */
    invalidateBuffers(chart: CandlestickChart): void {
        const buf = this._buffers;
        if (!buf || !chart._glManager) {
            this._buffers = null;
            return;
        }

        const gl = chart._glManager.gl;
        gl.deleteBuffer(buf.upWickBuffer);
        gl.deleteBuffer(buf.downWickBuffer);
        this._buffers = null;
    }

    /**
     * Pre-build the per-instance body buffer (interleaved
     * [xCenter, halfWidth, y0, y1, r, g, b]) and the up/down wick
     * line-segment buffers. Single GPU upload per buffer per data load.
     */
    rebuildBuffers(
        chart: CandlestickChart,
        glManager: WebGLContextManager,
    ): void {
        const candles = chart._candles;
        const cache = this.ensureProgram(glManager);
        const gl = glManager.gl;
        const xOrigin = chart._categoryOrigin;

        if (candles.count === 0) {
            const upBuf = gl.createBuffer()!;
            const downBuf = gl.createBuffer()!;
            this._buffers = {
                bodyCount: 0,
                upWickCount: 0,
                downWickCount: 0,
                upWickBuffer: upBuf,
                downWickBuffer: downBuf,
            };
            return;
        }

        //  Body buffer: 7 floats per candle (interleaved).
        const data = new Float32Array(candles.count * 7);
        let upCount = 0;
        let downCount = 0;
        const xC = candles.xCenter;
        const hw = candles.halfWidth;
        const open = candles.open;
        const close = candles.close;
        const isUp = candles.isUp;
        const upColor = chart._upColor;
        const downColor = chart._downColor;
        for (let i = 0; i < candles.count; i++) {
            const o = open[i];
            const c = close[i];
            const bodyLow = o < c ? o : c;
            const bodyHigh = o < c ? c : o;
            const up = isUp[i] !== 0;
            const col = up ? upColor : downColor;
            const off = i * 7;
            data[off + 0] = xC[i] - xOrigin;
            data[off + 1] = hw[i] * 0.7;
            data[off + 2] = bodyLow;
            data[off + 3] = bodyHigh;
            data[off + 4] = col[0];
            data[off + 5] = col[1];
            data[off + 6] = col[2];

            if (up) {
                upCount++;
            } else {
                downCount++;
            }
        }

        gl.bindBuffer(gl.ARRAY_BUFFER, cache.body.instanceBuffer);
        gl.bufferData(gl.ARRAY_BUFFER, data, gl.STATIC_DRAW);

        //  Wick buffers: per-color, packed [x,low, x,high] segments.
        const upWick = new Float32Array(upCount * 4);
        const downWick = new Float32Array(downCount * 4);
        let upW = 0;
        let downW = 0;
        const lows = candles.low;
        const highs = candles.high;
        for (let i = 0; i < candles.count; i++) {
            const x = xC[i] - xOrigin;
            const lo = lows[i];
            const hi = highs[i];
            if (isUp[i] !== 0) {
                upWick[upW + 0] = x;
                upWick[upW + 1] = lo;
                upWick[upW + 2] = x;
                upWick[upW + 3] = hi;
                upW += 4;
            } else {
                downWick[downW + 0] = x;
                downWick[downW + 1] = lo;
                downWick[downW + 2] = x;
                downWick[downW + 3] = hi;
                downW += 4;
            }
        }

        // Reuse existing wick GL buffers when available; otherwise allocate.
        const prev = this._buffers;
        const upBuf = prev?.upWickBuffer ?? gl.createBuffer()!;
        const downBuf = prev?.downWickBuffer ?? gl.createBuffer()!;
        gl.bindBuffer(gl.ARRAY_BUFFER, upBuf);
        gl.bufferData(gl.ARRAY_BUFFER, upWick, gl.STATIC_DRAW);
        gl.bindBuffer(gl.ARRAY_BUFFER, downBuf);
        gl.bufferData(gl.ARRAY_BUFFER, downWick, gl.STATIC_DRAW);

        this._buffers = {
            bodyCount: candles.count,
            upWickCount: upCount,
            downWickCount: downCount,
            upWickBuffer: upBuf,
            downWickBuffer: downBuf,
        };
    }

    draw(
        chart: CandlestickChart,
        gl: GL,
        glManager: WebGLContextManager,
        projection: Float32Array,
    ): void {
        const buf = this._buffers;
        if (!buf || buf.bodyCount === 0) {
            return;
        }

        const cache = this.ensureProgram(glManager);
        drawBodies(gl, glManager, cache.body, buf.bodyCount, projection);
        drawWicks(chart, gl, glManager, cache.wick, buf, projection);
    }

    /**
     * Free program-local GPU buffers + persistent vertex buffers. The
     * shader programs themselves are owned by `WebGLContextManager.shaders`
     * and not freed here.
     */
    destroy(chart: CandlestickChart): void {
        const gl = chart._glManager?.gl;
        if (gl) {
            this.invalidateBuffers(chart);
            const cache = this._program;
            if (cache) {
                gl.deleteBuffer(cache.body.quadBuffer);
                gl.deleteBuffer(cache.body.instanceBuffer);
                gl.deleteBuffer(cache.wick.cornerBuffer);
                gl.deleteBuffer(cache.wick.segmentBuffer);
            }
        }

        this._program = null;
        this._buffers = null;
    }
}

/**
 * Bind the persistent body buffer and issue one instanced draw.
 */
function drawBodies(
    gl: GL,
    glManager: WebGLContextManager,
    cache: BodyCache,
    instanceCount: number,
    projection: Float32Array,
): void {
    if (instanceCount === 0) {
        return;
    }

    const stride = 7 * Float32Array.BYTES_PER_ELEMENT;

    gl.useProgram(cache.program);
    gl.uniformMatrix4fv(cache.u_projection, false, projection);

    const instancing = getInstancing(glManager);
    const { setDivisor } = instancing;

    // Per-vertex corner.
    gl.bindBuffer(gl.ARRAY_BUFFER, cache.quadBuffer);
    gl.enableVertexAttribArray(cache.a_corner);
    gl.vertexAttribPointer(cache.a_corner, 2, gl.FLOAT, false, 0, 0);
    setDivisor(cache.a_corner, 0);

    // Per-instance attributes from the persistent interleaved buffer.
    gl.bindBuffer(gl.ARRAY_BUFFER, cache.instanceBuffer);
    const f = Float32Array.BYTES_PER_ELEMENT;
    const bind = (loc: number, size: number, offset: number) => {
        gl.enableVertexAttribArray(loc);
        gl.vertexAttribPointer(loc, size, gl.FLOAT, false, stride, offset);
        setDivisor(loc, 1);
    };

    bind(cache.a_x_center, 1, 0);
    bind(cache.a_half_width, 1, 1 * f);
    bind(cache.a_y0, 1, 2 * f);
    bind(cache.a_y1, 1, 3 * f);
    bind(cache.a_color, 3, 4 * f);

    instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, instanceCount);

    setDivisor(cache.a_x_center, 0);
    setDivisor(cache.a_half_width, 0);
    setDivisor(cache.a_y0, 0);
    setDivisor(cache.a_y1, 0);
    setDivisor(cache.a_color, 0);
}

/**
 * Bind the persistent up/down wick buffers and dispatch one
 * instanced draw per color group. No per-frame allocations.
 */
function drawWicks(
    chart: CandlestickChart,
    gl: GL,
    glManager: WebGLContextManager,
    cache: WickCache,
    buf: BodyWickBuffers,
    projection: Float32Array,
): void {
    if (buf.upWickCount === 0 && buf.downWickCount === 0) {
        return;
    }

    const dpr = glManager.dpr;
    gl.useProgram(cache.program);
    gl.uniformMatrix4fv(cache.u_projection, false, projection);
    gl.uniform2f(cache.u_resolution, gl.canvas.width, gl.canvas.height);
    gl.uniform1f(cache.u_line_width, chart._pluginConfig.wick_width_px * dpr);

    // `line-uniform` was extended for the Y-Line interpolate feature
    // with a per-segment alpha multiplier; neutralize it here.
    // Constant attribute values (used when the array is disabled) and
    // uniform are stable for every draw, so set once after
    // `useProgram`. Disabling the arrays first guards against a prior
    // Y-Line draw that left them enabled at the same attribute index
    // (locations are shared because both programs link from the same
    // source).
    gl.disableVertexAttribArray(cache.a_real_start);
    gl.disableVertexAttribArray(cache.a_real_end);
    gl.vertexAttrib1f(cache.a_real_start, 1.0);
    gl.vertexAttrib1f(cache.a_real_end, 1.0);
    gl.uniform1f(cache.u_interp_alpha, 1.0);

    const instancing = getInstancing(glManager);
    const { setDivisor } = instancing;

    gl.bindBuffer(gl.ARRAY_BUFFER, cache.cornerBuffer);
    gl.enableVertexAttribArray(cache.a_corner);
    gl.vertexAttribPointer(cache.a_corner, 1, gl.FLOAT, false, 0, 0);
    setDivisor(cache.a_corner, 0);

    drawWickGroup(
        gl,
        instancing,
        cache,
        buf.upWickBuffer,
        buf.upWickCount,
        chart._upColor,
    );
    drawWickGroup(
        gl,
        instancing,
        cache,
        buf.downWickBuffer,
        buf.downWickCount,
        chart._downColor,
    );

    setDivisor(cache.a_start, 0);
    setDivisor(cache.a_end, 0);
}

function drawWickGroup(
    gl: GL,
    instancing: ReturnType<typeof getInstancing>,
    cache: WickCache,
    segmentBuffer: WebGLBuffer,
    count: number,
    color: [number, number, number],
): void {
    if (count === 0) {
        return;
    }

    const stride = 2 * Float32Array.BYTES_PER_ELEMENT;
    const { setDivisor } = instancing;

    gl.uniform4f(cache.u_color, color[0], color[1], color[2], 1.0);
    gl.bindBuffer(gl.ARRAY_BUFFER, segmentBuffer);
    gl.enableVertexAttribArray(cache.a_start);
    gl.vertexAttribPointer(cache.a_start, 2, gl.FLOAT, false, 2 * stride, 0);
    setDivisor(cache.a_start, 1);
    gl.enableVertexAttribArray(cache.a_end);
    gl.vertexAttribPointer(cache.a_end, 2, gl.FLOAT, false, 2 * stride, stride);
    setDivisor(cache.a_end, 1);

    instancing.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, count);
}
