// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
// ┃ 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 { PlotLayout, type PlotRect } from "./plot-layout";

/**
 * Tri-state axis mode.
 *
 *   - `"outer"` — one shared axis band reserved at the grid edge;
 *     `outerXAxisRect` / `outerYAxisRect` populated, per-cell gutter
 *     collapsed to 0 on that side. Caller paints the shared axis
 *     once per frame using the grid's outer rect.
 *   - `"cell"` — every cell reserves its own gutter on that side;
 *     caller paints one axis per cell. Outer rect is undefined.
 *   - `"none"` — no gutter anywhere on that side: neither an outer
 *     band nor a per-cell reservation. Intended for chart types with
 *     no numeric axis at all (treemap, sunburst). When BOTH axes are
 *     `"none"` cells are also made flush on the right so adjacent
 *     plot rects share a boundary.
 *
 * Defaults to `"cell"` when undefined.
 */
export type AxisMode = "outer" | "cell" | "none";

export interface FacetGridOptions {
    cssWidth: number;
    cssHeight: number;

    /**
     * See {@link AxisMode}. Default `"cell"`.
     */
    xAxis?: AxisMode;

    /**
     * See {@link AxisMode}. Default `"cell"`.
     */
    yAxis?: AxisMode;

    /**
     * Reserve a right gutter for a single shared legend.
     */
    hasLegend?: boolean;

    /** Axis-label allowance (consumed only when the corresponding axis
     *  mode produces a gutter — outer band or per-cell). */
    hasXLabel?: boolean;
    hasYLabel?: boolean;

    /**
     * Per-facet title strip height (px). 0 disables.
     */
    titleBand?: number;

    /**
     * Pixel gap between adjacent cells. Carved out of the grid
     * interior before cell sizing; outer edges of the leftmost /
     * rightmost columns and top / bottom rows are unaffected. Default
     * 0 (flush cells).
     */
    gap?: number;
}

export interface FacetCell {
    index: number;
    label: string;

    /**
     * Sub-plot layout. Every cell in a grid has *identical*
     * `plotRect.width` and `plotRect.height` — cell internal margins
     * do not vary by edge position. Shared-axis gutters live in
     * `FacetGrid.outerXAxisRect` / `outerYAxisRect` instead, painted
     * once per frame by the caller.
     */
    layout: PlotLayout;

    /**
     * Title strip above the facet's plot rect, if `titleBand > 0`.
     */
    titleRect?: PlotRect;
    isLeftEdge: boolean;
    isBottomEdge: boolean;
}

export interface FacetGrid {
    cells: FacetCell[];

    /**
     * Right-gutter rect for the shared legend.
     */
    legendRect?: PlotRect;

    /**
     * Outer band reserved for the shared X axis (ticks + label). Only
     * set when `xAxis === "outer"`. Spans the grid interior's
     * horizontal extent and sits immediately below the bottom row of
     * cells.
     */
    outerXAxisRect?: PlotRect;

    /**
     * Outer band reserved for the shared Y axis (ticks + label). Only
     * set when `yAxis === "outer"`. Spans the grid interior's
     * vertical extent and sits immediately left of the leftmost
     * column of cells.
     */
    outerYAxisRect?: PlotRect;
}

/**
 * Collect the bottom-row cells' `PlotLayout`s — i.e. the cells that
 * sit on the grid's bottom edge. Shared-X axis renderers paint X
 * ticks aligned to each of these. Empty when the grid has zero cells.
 */
export function bottomRowLayouts(grid: FacetGrid): PlotLayout[] {
    return grid.cells.filter((c) => c.isBottomEdge).map((c) => c.layout);
}

/**
 * Collect the left-column cells' `PlotLayout`s — symmetric to
 * {@link bottomRowLayouts} for the shared-Y axis path.
 */
export function leftColumnLayouts(grid: FacetGrid): PlotLayout[] {
    return grid.cells.filter((c) => c.isLeftEdge).map((c) => c.layout);
}

// Per-cell internal gutter defaults mirror `PlotLayout`'s constants so
// that a cell with `leftExtra: undefined` reserves the same space the
// outer band would reserve when the axis is shared. Keep these in sync
// with `plot-layout.ts`.
const CELL_LEFT_GUTTER = 55;
const CELL_BOTTOM_GUTTER = 24;
const AXIS_LABEL_W = 16;
const AXIS_LABEL_H = 18;

const TITLE_BAND_DEFAULT = 18;
const LEGEND_GUTTER = 96;

/**
 * Pick `(cols, rows)` so that each resulting cell's aspect ratio is as
 * close to 1 as possible given the grid interior. Sweeps `cols ∈ [1,
 * count]` with `rows = ceil(count / cols)` and minimizes
 * `max(cellW/cellH, cellH/cellW)`. Ties break toward fewer total cells
 * (less unused grid area).
 */
function pickGridShape(
    count: number,
    gridW: number,
    gridH: number,
    gap: number,
): { cols: number; rows: number } {
    if (count <= 1) {
        return { cols: 1, rows: 1 };
    }

    let bestCols = 1;
    let bestRows = count;
    let bestCost = Infinity;
    let bestTotal = count;
    for (let cols = 1; cols <= count; cols++) {
        const rows = Math.ceil(count / cols);
        const cellW = Math.max(1, (gridW - (cols - 1) * gap) / cols);
        const cellH = Math.max(1, (gridH - (rows - 1) * gap) / rows);
        const aspect = cellW / cellH;
        const cost = Math.max(aspect, 1 / aspect);
        const total = cols * rows;
        if (cost < bestCost || (cost === bestCost && total < bestTotal)) {
            bestCols = cols;
            bestRows = rows;
            bestCost = cost;
            bestTotal = total;
        }
    }

    return { cols: bestCols, rows: bestRows };
}

/**
 * Arrange `labels.length` sub-plots in a row-major grid sized to fit
 * `(cssWidth, cssHeight)`.
 *
 * Grid shape is chosen to minimize cell aspect distance from square
 * given the container's grid interior: `cols ∈ [1, count]`,
 * `rows = ceil(count / cols)`, tie-broken toward fewer total cells.
 *
 * **Invariant:** every `cells[i].layout.plotRect` has the same
 * `width` and `height`. Shared-axis gutters are carved out of the
 * outer canvas BEFORE cell sizing, so a cell's edge position never
 * affects its internal margins. This lets per-facet draws reuse the
 * same projection scale and lets shared ticks line up with the
 * interior cell boundaries exactly.
 *
 * Axis modes — see {@link AxisMode}:
 *   - `"outer"` → outer band rect is populated; per-cell gutter is 0.
 *   - `"cell"` → outer band is undefined; each cell owns its own gutter.
 *   - `"none"` → no gutter anywhere on that side; used by axis-less
 *     chart types.
 *
 * Because all cells are identical in size, callers can sample *any*
 * cell's layout (e.g. `cells[0].layout`) for tick / scale
 * computations.
 */
export function buildFacetGrid(
    labels: string[],
    opts: FacetGridOptions,
): FacetGrid {
    const count = labels.length;
    const { cssWidth, cssHeight } = opts;

    if (count <= 0 || cssWidth <= 0 || cssHeight <= 0) {
        return { cells: [] };
    }

    const titleBand = opts.titleBand ?? TITLE_BAND_DEFAULT;
    const legendW = opts.hasLegend ? LEGEND_GUTTER : 0;

    const xMode: AxisMode = opts.xAxis ?? "cell";
    const yMode: AxisMode = opts.yAxis ?? "cell";

    // Axis-less chart types (trees) benefit from fully-flush cells —
    // no per-cell breathing on the right either, so adjacent plot
    // rects share a boundary instead of leaving a 16 px seam.
    const cellsFlush = xMode === "none" && yMode === "none";

    // Outer margins: shared-axis gutters + legend gutter live OUTSIDE
    // the per-cell rects.
    const outerLeft =
        yMode === "outer"
            ? CELL_LEFT_GUTTER + (opts.hasYLabel ? AXIS_LABEL_W : 0)
            : 0;
    const outerBottom =
        xMode === "outer"
            ? CELL_BOTTOM_GUTTER + (opts.hasXLabel ? AXIS_LABEL_H : 0)
            : 0;
    const outerTop = 0;
    const outerRight = legendW;

    const gridX = outerLeft;
    const gridY = outerTop;
    const gridW = Math.max(1, cssWidth - outerLeft - outerRight);
    const gridH = Math.max(1, cssHeight - outerTop - outerBottom);

    // Carve the total inter-cell gap out of the grid interior before
    // sizing cells so every cell remains identical in size (the
    // grid-uniform invariant). Gaps only sit BETWEEN neighbors — not
    // against the outer edges.
    const gap = Math.max(0, opts.gap ?? 0);
    const { cols, rows } = pickGridShape(count, gridW, gridH, gap);
    const totalGapX = Math.max(0, cols - 1) * gap;
    const totalGapY = Math.max(0, rows - 1) * gap;
    const cellW = Math.max(1, (gridW - totalGapX) / cols);
    const cellH = Math.max(1, (gridH - totalGapY) / rows);

    const cells: FacetCell[] = [];
    for (let i = 0; i < count; i++) {
        const row = Math.floor(i / cols);
        const col = i - row * cols;
        const isBottomEdge = row === rows - 1 || i + cols >= count;
        const isLeftEdge = col === 0;

        const cellX = gridX + col * (cellW + gap);
        const cellY = gridY + row * (cellH + gap);

        // Carve a title strip from the top of each cell. The remaining
        // rect becomes the per-cell `PlotLayout`.
        const plotTop = cellY + titleBand;
        const plotLeft = cellX;
        const plotWidth = cellW;
        const plotHeight = Math.max(1, cellH - titleBand);

        // Per-cell gutters:
        //   - "cell" → keep `PlotLayout` default (undefined).
        //   - "outer" / "none" → collapse to 0 (no internal gutter).
        // Per-cell labels only paint when the axis is per-cell.
        const layout = new PlotLayout(cssWidth, cssHeight, {
            hasXLabel: xMode === "cell" && opts.hasXLabel === true,
            hasYLabel: yMode === "cell" && opts.hasYLabel === true,
            hasLegend: false,
            leftExtra: yMode === "cell" ? undefined : 0,
            bottomExtra: xMode === "cell" ? undefined : 0,
            rightExtra: cellsFlush ? 0 : undefined,
            originX: plotLeft,
            originY: plotTop,
            cellWidth: plotWidth,
            cellHeight: plotHeight,
        });

        const titleRect: PlotRect | undefined =
            titleBand > 0
                ? {
                      x: plotLeft,
                      y: cellY,
                      width: plotWidth,
                      height: titleBand,
                  }
                : undefined;

        cells.push({
            index: i,
            label: labels[i],
            layout,
            titleRect,
            isLeftEdge,
            isBottomEdge,
        });
    }

    const legendRect: PlotRect | undefined = opts.hasLegend
        ? {
              x: gridX + gridW,
              y: outerTop,
              width: legendW,
              height: gridH,
          }
        : undefined;

    const outerXAxisRect: PlotRect | undefined =
        xMode === "outer"
            ? {
                  x: gridX,
                  y: gridY + gridH,
                  width: gridW,
                  height: outerBottom,
              }
            : undefined;

    const outerYAxisRect: PlotRect | undefined =
        yMode === "outer"
            ? {
                  x: 0,
                  y: gridY,
                  width: outerLeft,
                  height: gridH,
              }
            : undefined;

    return { cells, legendRect, outerXAxisRect, outerYAxisRect };
}
