// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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, CandlestickAutoFitCache } from "./candlestick";
import { PlotLayout } from "../../layout/plot-layout";
import { sampleGradient } from "../../theme/gradient";
import { renderInPlotFrame } from "../../webgl/plot-frame";
import { renderCanvasTooltip } from "../../interaction/tooltip-controller";
import { computeNiceTicks } from "../../layout/ticks";
import { type AxisDomain } from "../../axis/numeric-axis";
import {
    renderBarAxesChrome,
    renderBarGridlines,
    type BarCategoryAxis,
} from "../../axis/bar-axis";
import {
    measureCategoricalAxisHeight,
    type CategoricalDomain,
} from "../../axis/categorical-axis";
import { buildCandlestickTooltipLines } from "./candlestick-interact";
import {
    computeVisibleExtent,
    type VisibleExtent,
} from "../common/visible-extent";

/**
 * Resolve up/down body colors from `theme.gradientStops`. Cached on the
 * chart via `_upDownColorKey` (reference identity of the stops array)
 * — only `restyle()` (which clears the theme cache via
 * `invalidateTheme`) or a data load with a fresh theme triggers
 * resampling. Legacy code re-sampled every frame.
 */
export function ensureUpDownColors(chart: CandlestickChart): void {
    const theme = chart._resolveTheme();
    const stops = theme.gradientStops;
    if (chart._upDownColorKey === stops) {
        return;
    }

    const upSample = sampleGradient(stops, 1.0);
    const downSample = sampleGradient(stops, 0.0);
    chart._upColor = [upSample[0], upSample[1], upSample[2]];
    chart._downColor = [downSample[0], downSample[1], downSample[2]];
    chart._upDownColorKey = stops;
}

/**
 * Drop persistent body / wick / OHLC vertex buffers. Subsequent draws
 * no-op until the next {@link rebuildGlyphBuffers} call.
 */
export function invalidateGlyphBuffers(chart: CandlestickChart): void {
    chart._glyphs.bodyWick.invalidateBuffers(chart);
    chart._glyphs.ohlc.invalidateBuffers(chart);
}

/**
 * Rebuild the persistent body / wick / OHLC vertex buffers. Reads
 * `_candles` (columnar) plus the cached `_upColor` / `_downColor` to
 * populate the GPU buffers exactly once per data load. Subsequent pan/
 * zoom redraws bind + dispatch with no uploads.
 */
export function rebuildGlyphBuffers(
    chart: CandlestickChart,
    glManager: WebGLContextManager,
): void {
    chart._glyphs.bodyWick.rebuildBuffers(chart, glManager);
    chart._glyphs.ohlc.rebuildBuffers(chart, glManager);
}

export function renderCandlestickFrame(
    chart: CandlestickChart,
    glManager: WebGLContextManager,
): void {
    const gl = glManager.gl;
    const dpr = glManager.dpr;
    const cssWidth = gl.canvas.width / dpr;
    const cssHeight = gl.canvas.height / dpr;
    if (cssWidth <= 0 || cssHeight <= 0) {
        return;
    }

    if (chart._numCategories === 0) {
        return;
    }

    const theme = chart._resolveTheme();

    // Up/down colors sampled at the extremes of the theme gradient.
    // Cached on the chart — `ensureUpDownColors` is a no-op when the
    // gradient-stops reference matches the previous call. `restyle()`
    // clears the cache via `invalidateTheme`, and the data-load path
    // refreshes it before rebuilding glyph buffers.
    ensureUpDownColors(chart);

    const numericCat = chart._categoryAxisMode === "numeric";
    const xDomainMin = numericCat ? chart._numericCategoryDomain!.min : -0.5;
    const xDomainMax = numericCat
        ? chart._numericCategoryDomain!.max
        : chart._numCategories - 0.5;
    if (chart._zoomController) {
        chart._zoomController.setBaseDomain(
            xDomainMin,
            xDomainMax,
            chart._yDomain.min,
            chart._yDomain.max,
        );
    }

    const vis = chart._zoomController
        ? chart._zoomController.getVisibleDomain()
        : {
              xMin: xDomainMin,
              xMax: xDomainMax,
              yMin: chart._yDomain.min,
              yMax: chart._yDomain.max,
          };

    // Auto-fit the price axis to the visible X window. Skipped when X
    // is at default zoom (the refit equals `_yDomain` there and would
    // only churn baselines) — Y-axis pan/zoom alone shouldn't trigger
    // an X-window refit.
    if (
        chart._autoFitValue &&
        chart._zoomController &&
        !chart._zoomController.isXDefault()
    ) {
        const fit = computeVisibleCandleExtent(chart, vis.xMin, vis.xMax);
        if (fit.hasFit) {
            vis.yMin = fit.min;
            vis.yMax = fit.max;
        }
    }

    const hasXLabel = chart._groupBy.length > 0;

    const provisionalDomain: CategoricalDomain = {
        levels: chart._rowPaths,
        numRows: chart._numCategories,
        levelLabels: chart._groupBy.slice(),
    };

    let layout: PlotLayout;
    if (numericCat) {
        layout = new PlotLayout(cssWidth, cssHeight, {
            hasXLabel,
            hasYLabel: true,
            hasLegend: false,
            bottomExtra: 24,
        });
    } else {
        const estLeft = 55 + 16;
        const estRight = 16;
        const estPlotWidth = Math.max(1, cssWidth - estLeft - estRight);
        const bottomExtra = measureCategoricalAxisHeight(
            provisionalDomain,
            estPlotWidth,
        );
        layout = new PlotLayout(cssWidth, cssHeight, {
            hasXLabel,
            hasYLabel: true,
            hasLegend: false,
            bottomExtra,
        });
    }

    chart._lastLayout = layout;
    if (chart._zoomController) {
        chart._zoomController.updateLayout(layout);
    }

    const projection = layout.buildProjectionMatrix(
        vis.xMin,
        vis.xMax,
        vis.yMin,
        vis.yMax,
        "y",
        undefined,
        undefined,
        chart._categoryOrigin,
        0,
    );

    const yTicks = computeNiceTicks(vis.yMin, vis.yMax, 6);
    const yLabel = chart._columnSlots[0] || "";

    const xDomain: CategoricalDomain = provisionalDomain;
    const yDomain: AxisDomain = {
        min: vis.yMin,
        max: vis.yMax,
        label: yLabel,
    };

    if (chart._gridlineCanvas) {
        renderBarGridlines(
            chart._gridlineCanvas,
            layout,
            yTicks,
            theme,
            glManager.dpr,
        );
    }

    renderInPlotFrame(gl, layout, glManager.dpr, () => {
        if (chart._defaultChartType === "ohlc") {
            chart._glyphs.ohlc.draw(chart, gl, glManager, projection);
        } else {
            chart._glyphs.bodyWick.draw(chart, gl, glManager, projection);
        }
    });

    chart._lastXDomain = xDomain;
    chart._lastYDomain = yDomain;
    chart._lastYTicks = yTicks;
    chart._lastCatTicks = numericCat
        ? computeNiceTicks(vis.xMin, vis.xMax, 6)
        : null;
    renderCandlestickChromeOverlay(chart);
}

export function renderCandlestickChromeOverlay(chart: CandlestickChart): void {
    if (
        !chart._chromeCanvas ||
        !chart._lastLayout ||
        !chart._lastYDomain ||
        !chart._lastYTicks
    ) {
        return;
    }

    const theme = chart._resolveTheme();
    let catAxis: BarCategoryAxis;
    if (
        chart._categoryAxisMode === "numeric" &&
        chart._numericCategoryDomain &&
        chart._lastCatTicks
    ) {
        catAxis = {
            mode: "numeric",
            domain: {
                min: chart._numericCategoryDomain.min,
                max: chart._numericCategoryDomain.max,
                isDate: chart._numericCategoryDomain.isDate,
                label: chart._numericCategoryDomain.label,
            },
            ticks: chart._lastCatTicks,
        };
    } else if (chart._lastXDomain) {
        catAxis = { mode: "category", domain: chart._lastXDomain };
    } else {
        return;
    }

    // OHLC value axis: all four price columns share the value axis;
    // pick the first available (Open is always present, the rest can
    // be null per `candlestick-build.ts`).
    const valueColumn =
        chart._columnSlots[0] ??
        chart._columnSlots[1] ??
        chart._columnSlots[2] ??
        chart._columnSlots[3];
    const xColumn = chart._groupBy[0];
    renderBarAxesChrome(
        chart._chromeCanvas,
        catAxis,
        {
            mode: "numeric",
            domain: chart._lastYDomain,
            ticks: chart._lastYTicks,
        },
        chart._lastLayout,
        theme,
        chart._glManager?.dpr ?? 1,
        undefined,
        false,
        {
            value: chart.getColumnFormatter(valueColumn, "tick"),
            category: chart.getColumnFormatter(xColumn, "tick"),
        },
    );

    if (chart._hoveredIdx >= 0 && chart._hoveredIdx < chart._candles.count) {
        renderCandlestickTooltip(chart);
    }
}

function renderCandlestickTooltip(chart: CandlestickChart): void {
    if (!chart._chromeCanvas || !chart._lastLayout) {
        return;
    }

    const i = chart._hoveredIdx;
    const candles = chart._candles;
    if (i < 0 || i >= candles.count) {
        return;
    }

    const layout = chart._lastLayout;
    const xCenter = candles.xCenter[i];
    const yMid = (candles.high[i] + candles.low[i]) / 2;
    const pos = layout.dataToPixel(xCenter, yMid);
    const lines = buildCandlestickTooltipLines(chart, i);
    const theme = chart._resolveTheme();
    renderCanvasTooltip(
        chart._chromeCanvas,
        pos,
        lines,
        layout,
        theme,
        chart._glManager?.dpr ?? 1,
        {
            crosshair: false,
            highlightRadius: 0,
        },
    );
}

/**
 * Price extent over candles whose `xCenter` falls inside
 * `[visXMin, visXMax]`. Uses `low`/`high` (not `open`/`close`) so the
 * wick stays inside the plot at any zoom. Cached on
 * `chart._autoFitCache`; hover-only redraws hit the cache.
 *
 * Cache lifetime: reset on data upload ([candlestick.ts]
 * `uploadAndRender`).
 */
function computeVisibleCandleExtent(
    chart: CandlestickChart,
    visXMin: number,
    visXMax: number,
): VisibleExtent {
    const cache = chart._autoFitCache;
    if (cache && cache.xMin === visXMin && cache.xMax === visXMax) {
        return cache;
    }

    const next = cache ?? newCandlestickAutoFitCache();
    next.xMin = visXMin;
    next.xMax = visXMax;

    // Walk the columnar storage directly; the legacy form built a
    // closure adapter per call, defeating monomorphism in
    // `computeVisibleExtent`.
    const candles = chart._candles;
    let lo = Infinity;
    let hi = -Infinity;
    let hasFit = false;
    const xC = candles.xCenter;
    const lows = candles.low;
    const highs = candles.high;
    for (let j = 0; j < candles.count; j++) {
        const cx = xC[j];
        if (cx < visXMin || cx > visXMax) {
            continue;
        }

        if (lows[j] < lo) {
            lo = lows[j];
        }

        if (highs[j] > hi) {
            hi = highs[j];
        }

        hasFit = true;
    }

    next.min = hasFit ? lo : 0;
    next.max = hasFit ? hi : 1;
    next.hasFit = hasFit;
    chart._autoFitCache = next;

    // Reference suppression — `computeVisibleExtent` retained for the
    // shared common helper but no longer used in this fast path.
    void computeVisibleExtent;
    return next;
}

function newCandlestickAutoFitCache(): CandlestickAutoFitCache {
    return { xMin: 0, xMax: 0, min: 0, max: 1, hasFit: false };
}
