// The MIT License (MIT)
//
// Copyright (C) 2026 Scott Duensing
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.

#define DVX_WIDGET_IMPL
// widgetDbGrid.c -- Database grid widget for tabular record display
//
// A read-only grid that displays all records from a Data control in a
// scrollable, sortable table. Columns auto-populate from the Data
// control's column names and can be hidden, resized, and reordered
// by clicking headers. Selecting a row syncs the Data control's
// cursor position.
//
// The grid reads directly from the Data control's cached rows via
// the row-level accessor API (getRowCount, getCellText, etc.) so
// there is no separate copy of the data. The grid is refreshed by
// calling dbGridRefresh() which re-reads the Data control's state.
//
// Depends on the datactrl widget DXE for the DataCtrlApiT.

#include "dvxWgtP.h"
#include "../dataCtrl/dataCtrl.h"

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <strings.h>

// ============================================================
// Constants
// ============================================================

#define DBGRID_BORDER        2
#define DBGRID_MIN_COL_W     20
#define DBGRID_COL_PAD       6
#define DBGRID_PAD           3
#define DBGRID_SORT_W        10
#define DBGRID_MIN_ROWS      3
#define DBGRID_MIN_COLS      20   // minimum visible width in characters
#define DBGRID_MAX_NAME      64
#define DBGRID_RESIZE_ZONE   3
#define DBGRID_HSTEP         20   // horizontal scroll step (px) for arrow clicks

// Vertical padding added to font->charHeight for header and rows.
#define DBGRID_HEADER_VPAD   4
#define DBGRID_ROW_VPAD      2

// Alternating-row background shade: darken the content bg by this per-
// channel amount so every other row is subtly distinct.
#define DBGRID_ALT_ROW_SHADE 8

// Horizontal cull margin: columns whose rect is within this many pixels
// of the visible area are still rendered.  Gives scroll movement a bit
// of pre-draw buffer so columns fade in instead of popping.
#define DBGRID_HORIZ_CULL_MARGIN 20

// ============================================================
// Sort direction (matches ListView for consistency)
// ============================================================

#define SORT_NONE  0
#define SORT_ASC   1
#define SORT_DESC  2

// ============================================================
// Column definition
// ============================================================

typedef struct {
    char      fieldName[DBGRID_MAX_NAME];
    char      header[DBGRID_MAX_NAME];
    int32_t   width;           // tagged size (0 = auto)
    int32_t   align;           // 0=left, 1=center, 2=right
    bool      visible;
    int32_t   dataCol;         // index into Data control's columns (-1 = unresolved)
} DbGridColT;

// ============================================================
// Per-instance data
// ============================================================

typedef struct {
    WidgetT      *dataWidget;   // the Data control (set by form runtime)

    // Column configuration -- heap-allocated, sized to colCount
    DbGridColT   *columns;
    int32_t       colCount;
    int32_t       colCap;        // allocated capacity

    // Display state
    int32_t       selectedRow;  // data-row index (-1 = none)
    int32_t       scrollPos;    // vertical scroll (row units)
    int32_t       scrollPosH;   // horizontal scroll (pixel units)
    int32_t      *resolvedW;     // heap-allocated, sized to colCap
    int32_t       totalColW;    // 0 = needs recalc

    // Sort state
    int32_t       sortCol;      // visible-column index (-1 = none)
    int32_t       sortDir;      // SORT_NONE/ASC/DESC
    int32_t      *sortIndex;    // display-row -> data-row mapping (NULL = natural)
    int32_t       sortCount;    // length of sortIndex

    // Column resize drag state
    int32_t       resizeCol;
    int32_t       resizeStartX;
    int32_t       resizeOrigW;

    // Scrollbar drag state
    int32_t       sbDragOrient; // 0=vert, 1=horiz, -1=col resize
    int32_t       sbDragOff;

    // Flags (grouped last to minimize struct padding)
    bool          gridLines;
    bool          resizeDragging;
} DbGridDataT;

static int32_t sTypeId = -1;


// ============================================================
// Prototypes
// ============================================================

static int32_t     colBorderHit(WidgetT *w, int32_t vx, int32_t vy);
static void        computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH);
static int32_t     dataRowToDisplay(const DbGridDataT *d, int32_t dataRow);
static void        dbGridBuildSortIndex(WidgetT *w);
static void        dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font);
static WidgetT    *dbGridCreate(WidgetT *parent);
static uint32_t    dbGridDarkenColor(const DisplayT *disp, uint32_t packed, uint8_t amount);
static void        dbGridDestroy(WidgetT *w);
static int32_t     dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my);
static bool        dbGridGetGridLines(const WidgetT *w);
int32_t            dbGridGetSelectedRow(const WidgetT *w);
void               dbGridSetSelectedRow(WidgetT *w, int32_t row);
static void        dbGridOnDragEnd(WidgetT *w);
static void        dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y);
static void        dbGridOnKey(WidgetT *w, int32_t key, int32_t mod);
static void        dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
static void        dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void               dbGridRefresh(WidgetT *w);
void               dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header);
void               dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible);
void               dbGridSetColumnWidth(WidgetT *w, const char *fieldName, int32_t width);
void               dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget);
static void        dbGridSetGridLines(WidgetT *w, bool val);
static int32_t     displayToDataRow(const DbGridDataT *d, int32_t dispRow);
static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol);
static int32_t     getDataRowCount(const DbGridDataT *d);
static void        resolveColumns(WidgetT *w, const BitmapFontT *font);
void               wgtRegister(void);


// colBorderHit -- check if mouse X is on a column border
static int32_t colBorderHit(WidgetT *w, int32_t vx, int32_t vy) {
    DbGridDataT *d = (DbGridDataT *)w->data;
    (void)vy;

    int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;

    for (int32_t c = 0; c < d->colCount; c++) {
        if (!d->columns[c].visible) {
            continue;
        }

        x += d->resolvedW[c];

        if (vx >= x - DBGRID_RESIZE_ZONE && vx <= x + DBGRID_RESIZE_ZONE) {
            return c;
        }
    }

    return -1;
}


// Scrollbar geometry helpers (matches ListView pattern)
static void computeLayout(WidgetT *w, const BitmapFontT *font, int32_t *outInnerW, int32_t *outInnerH, int32_t *outHeaderH, int32_t *outVisRows, bool *outNeedV, bool *outNeedH) {
    DbGridDataT *d = (DbGridDataT *)w->data;

    int32_t headerH    = font->charHeight + DBGRID_HEADER_VPAD;
    int32_t innerW     = w->w - DBGRID_BORDER * 2;
    int32_t innerH     = w->h - DBGRID_BORDER * 2 - headerH;
    int32_t rowH       = font->charHeight + DBGRID_ROW_VPAD;
    int32_t rowCount   = getDataRowCount(d);
    int32_t visRows    = innerH / rowH;

    resolveColumns(w, font);

    bool needV = (rowCount > visRows);

    if (needV) {
        innerW -= WGT_SB_W;
    }

    bool needH = (d->totalColW > innerW);

    if (needH) {
        innerH -= WGT_SB_W;
        visRows = innerH / rowH;

        if (!needV && rowCount > visRows) {
            needV = true;
            innerW -= WGT_SB_W;
        }
    }

    if (visRows < 0) {
        visRows = 0;
    }

    *outInnerW  = innerW;
    *outInnerH  = innerH;
    *outHeaderH = headerH;
    *outVisRows = visRows;
    *outNeedV   = needV;
    *outNeedH   = needH;
}


// Map a data-row to display-row (linear search in sortIndex)
static int32_t dataRowToDisplay(const DbGridDataT *d, int32_t dataRow) {
    if (!d->sortIndex) {
        return dataRow;
    }

    for (int32_t i = 0; i < d->sortCount; i++) {
        if (d->sortIndex[i] == dataRow) {
            return i;
        }
    }

    return dataRow;
}


// dbGridBuildSortIndex -- sort rows by the current sort column
static void dbGridBuildSortIndex(WidgetT *w) {
    DbGridDataT *d = (DbGridDataT *)w->data;

    free(d->sortIndex);
    d->sortIndex = NULL;
    d->sortCount = 0;

    if (d->sortDir == SORT_NONE || d->sortCol < 0 || !d->dataWidget) {
        return;
    }

    int32_t rowCount = getDataRowCount(d);

    if (rowCount <= 0) {
        return;
    }

    int32_t dataCol = d->columns[d->sortCol].dataCol;

    if (dataCol < 0) {
        return;
    }

    d->sortIndex = (int32_t *)malloc((size_t)rowCount * sizeof(int32_t));

    if (!d->sortIndex) {
        d->sortCount = 0;
        return;
    }

    d->sortCount = rowCount;

    for (int32_t i = 0; i < rowCount; i++) {
        d->sortIndex[i] = i;
    }

    // Insertion sort (stable, good for small N)
    for (int32_t i = 1; i < rowCount; i++) {
        int32_t key = d->sortIndex[i];
        const char *keyStr = wgtDataCtrlGetCellText(d->dataWidget, key, dataCol);
        int32_t j = i - 1;

        while (j >= 0) {
            int32_t cmpRow = d->sortIndex[j];
            const char *cmpStr = wgtDataCtrlGetCellText(d->dataWidget, cmpRow, dataCol);
            int32_t cmp = strcasecmp(keyStr, cmpStr);

            if (d->sortDir == SORT_DESC) {
                cmp = -cmp;
            }

            if (cmp >= 0) {
                break;
            }

            d->sortIndex[j + 1] = d->sortIndex[j];
            j--;
        }

        d->sortIndex[j + 1] = key;
    }
}


static void dbGridCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    w->calcMinW = font->charWidth * DBGRID_MIN_COLS + DBGRID_BORDER * 2;
    w->calcMinH = (font->charHeight + DBGRID_ROW_VPAD) * (DBGRID_MIN_ROWS + 1) + DBGRID_BORDER * 2;
}


static WidgetT *dbGridCreate(WidgetT *parent) {
    if (!parent) {
        return NULL;
    }

    WidgetT *w = widgetAlloc(parent, sTypeId);

    if (w) {
        DbGridDataT *d = (DbGridDataT *)calloc(1, sizeof(DbGridDataT));

        if (d) {
            d->selectedRow  = -1;
            d->sortCol      = -1;
            d->sbDragOrient = -2;
            d->gridLines    = true;
        }

        w->data = d;
    }

    return w;
}


static uint32_t dbGridDarkenColor(const DisplayT *disp, uint32_t packed, uint8_t amount) {
    uint8_t r;
    uint8_t g;
    uint8_t b;

    // contentBg is a packColor() result, not an RGB888 triple, so subtracting
    // a packed constant underflows across channels (and is a palette index in
    // 8bpp).  Unpack, saturate per channel, then repack.
    unpackColor(disp, packed, &r, &g, &b);
    r = (r > amount) ? (uint8_t)(r - amount) : 0;
    g = (g > amount) ? (uint8_t)(g - amount) : 0;
    b = (b > amount) ? (uint8_t)(b - amount) : 0;
    return packColor(disp, r, g, b);
}


static void dbGridDestroy(WidgetT *w) {
    DbGridDataT *d = (DbGridDataT *)w->data;

    if (d) {
        free(d->columns);
        free(d->resolvedW);
        free(d->sortIndex);
        free(d);
    }
}


static int32_t dbGridGetCursorShape(WidgetT *w, int32_t mx, int32_t my) {
    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return 0;
    }

    int32_t headerH = ctx->font.charHeight + DBGRID_HEADER_VPAD;
    int32_t headerTop = w->y + DBGRID_BORDER;

    if (my >= headerTop && my < headerTop + headerH) {
        resolveColumns(w, &ctx->font);

        if (colBorderHit(w, mx, my) >= 0) {
            return CURSOR_RESIZE_H;
        }
    }

    return 0;
}


static bool dbGridGetGridLines(const WidgetT *w) {
    return ((DbGridDataT *)w->data)->gridLines;
}


// Get the selected data-row index (-1 = none)
int32_t dbGridGetSelectedRow(const WidgetT *w) {
    if (!w || !w->data) {
        return -1;
    }

    return ((DbGridDataT *)w->data)->selectedRow;
}


// Programmatically select a data-row (same effect as a user click).
// Clamps to the current row range, syncs the bound Data control's
// cursor (which cascades to any controls bound to that Data via
// the normal Reposition path), and repaints.  -1 deselects.
void dbGridSetSelectedRow(WidgetT *w, int32_t row) {
    if (!w || !w->data) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;
    int32_t rowCount = getDataRowCount(d);

    if (row < -1) {
        row = -1;
    }

    if (row >= rowCount) {
        row = rowCount - 1;
    }

    if (d->selectedRow == row) {
        return;
    }

    d->selectedRow = row;

    if (d->dataWidget && row >= 0) {
        wgtDataCtrlSetCurrentRow(d->dataWidget, row);
    }

    wgtInvalidatePaint(w);
}


static void dbGridOnDragEnd(WidgetT *w) {
    DbGridDataT *d = (DbGridDataT *)w->data;
    d->sbDragOrient   = -2;
    d->resizeDragging = false;
}


static void dbGridOnDragUpdate(WidgetT *w, WidgetT *root, int32_t x, int32_t y) {
    DbGridDataT *d = (DbGridDataT *)w->data;
    (void)root;
    (void)y;

    // Column resize drag
    if (d->sbDragOrient == -1) {
        d->resizeDragging = true;
        int32_t delta = x - d->resizeStartX;
        int32_t newW  = d->resizeOrigW + delta;

        if (newW < DBGRID_MIN_COL_W) {
            newW = DBGRID_MIN_COL_W;
        }

        d->totalColW += (newW - d->resolvedW[d->resizeCol]);
        d->resolvedW[d->resizeCol] = newW;
        wgtInvalidatePaint(w);
        return;
    }

    // Vertical scrollbar thumb drag
    if (d->sbDragOrient == 0) {
        AppContextT *ctx = wgtGetContext(w);

        if (!ctx) {
            return;
        }

        int32_t innerW, innerH, headerH, visRows;
        bool needV, needH;
        computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);

        int32_t rowCount = getDataRowCount(d);
        int32_t maxScroll = rowCount - visRows;

        if (maxScroll < 1) {
            return;
        }

        int32_t track    = innerH - WGT_SB_W * 2;
        int32_t sbY      = w->y + DBGRID_BORDER + headerH;
        int32_t relMouse = y - sbY - WGT_SB_W - d->sbDragOff;

        d->scrollPos = widgetScrollbarThumbDragScroll(track, rowCount, visRows, relMouse, maxScroll);

        wgtInvalidatePaint(w);
        return;
    }

    // Horizontal scrollbar thumb drag
    if (d->sbDragOrient == 1) {
        AppContextT *ctx = wgtGetContext(w);

        if (!ctx) {
            return;
        }

        int32_t innerW, innerH, headerH, visRows;
        bool needV, needH;
        computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);

        int32_t maxScroll = d->totalColW - innerW;

        if (maxScroll < 1) {
            return;
        }

        int32_t track    = innerW - WGT_SB_W * 2;
        int32_t sbX      = w->x + DBGRID_BORDER;
        int32_t relMouse = x - sbX - WGT_SB_W - d->sbDragOff;

        d->scrollPosH = widgetScrollbarThumbDragScroll(track, d->totalColW, innerW, relMouse, maxScroll);

        wgtInvalidatePaint(w);
    }
}


static void dbGridOnKey(WidgetT *w, int32_t key, int32_t mod) {
    (void)mod;
    DbGridDataT *d = (DbGridDataT *)w->data;
    AppContextT *ctx = wgtGetContext(w);

    if (!ctx) {
        return;
    }

    int32_t rowCount = getDataRowCount(d);

    if (rowCount <= 0) {
        return;
    }

    int32_t innerW, innerH, headerH, visRows;
    bool needV, needH;
    computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);

    int32_t dispRow = dataRowToDisplay(d, d->selectedRow);
    int32_t newDisp = dispRow;

    // Up arrow
    if (key == KEY_UP) {
        newDisp = dispRow > 0 ? dispRow - 1 : 0;
    }

    // Down arrow
    if (key == KEY_DOWN) {
        newDisp = dispRow < rowCount - 1 ? dispRow + 1 : rowCount - 1;
    }

    // Page Up
    if (key == KEY_PGUP) {
        newDisp = dispRow - visRows;

        if (newDisp < 0) {
            newDisp = 0;
        }
    }

    // Page Down
    if (key == KEY_PGDN) {
        newDisp = dispRow + visRows;

        if (newDisp >= rowCount) {
            newDisp = rowCount - 1;
        }
    }

    // Home
    if (key == KEY_HOME) {
        newDisp = 0;
    }

    // End
    if (key == KEY_END) {
        newDisp = rowCount - 1;
    }

    // Enter = activate (fire DblClick)
    if (key == '\r' || key == '\n') {
        if (w->onDblClick) {
            w->onDblClick(w);
        }

        return;
    }

    if (newDisp != dispRow) {
        int32_t newDataRow = displayToDataRow(d, newDisp);
        d->selectedRow = newDataRow;

        // Auto-scroll
        if (newDisp < d->scrollPos) {
            d->scrollPos = newDisp;
        } else if (newDisp >= d->scrollPos + visRows) {
            d->scrollPos = newDisp - visRows + 1;
        }

        // Sync Data control cursor
        if (d->dataWidget) {
            wgtDataCtrlSetCurrentRow(d->dataWidget, newDataRow);
        }

        if (w->onClick) {
            w->onClick(w);
        }

        wgtInvalidatePaint(w);
    }
}


static void dbGridOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    DbGridDataT *d = (DbGridDataT *)w->data;
    AppContextT *ctx = (AppContextT *)root->userData;

    if (!ctx) {
        return;
    }

    sFocusedWidget = w;

    int32_t innerW, innerH, headerH, visRows;
    bool needV, needH;
    computeLayout(w, &ctx->font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);

    int32_t rowH      = ctx->font.charHeight + DBGRID_ROW_VPAD;
    int32_t headerTop = w->y + DBGRID_BORDER;
    int32_t dataTop   = headerTop + headerH;
    int32_t rowCount  = getDataRowCount(d);

    // Vertical scrollbar click
    if (needV && vx >= w->x + w->w - DBGRID_BORDER - WGT_SB_W) {
        int32_t sbY = dataTop;
        int32_t sbH = innerH;
        int32_t maxScroll = rowCount - visRows;

        if (maxScroll < 0) {
            maxScroll = 0;
        }

        int32_t relY = vy - sbY;
        ScrollHitE sh = widgetScrollbarHitTest(sbH, relY, rowCount, visRows, d->scrollPos);

        if (sh == ScrollHitArrowDecE) {
            if (d->scrollPos > 0) {
                d->scrollPos--;
            }
        } else if (sh == ScrollHitArrowIncE) {
            if (d->scrollPos < maxScroll) {
                d->scrollPos++;
            }
        } else if (sh == ScrollHitPageDecE) {
            d->scrollPos -= visRows;
            d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
        } else if (sh == ScrollHitPageIncE) {
            d->scrollPos += visRows;
            d->scrollPos = clampInt(d->scrollPos, 0, maxScroll);
        } else if (sh == ScrollHitThumbE) {
            int32_t trackLen = sbH - WGT_SB_W * 2;
            int32_t thumbPos;
            int32_t thumbSize;
            widgetScrollbarThumb(trackLen, rowCount, visRows, d->scrollPos, &thumbPos, &thumbSize);

            d->sbDragOrient = 0;
            d->sbDragOff    = relY - WGT_SB_W - thumbPos;

            // Register the drag with the WM so it dispatches drag-update/end
            // to dbGridOnDragUpdate/dbGridOnDragEnd while the button is held.
            sDragWidget = w;
        }

        wgtInvalidatePaint(w);
        return;
    }

    // Horizontal scrollbar click
    if (needH && vy >= w->y + w->h - DBGRID_BORDER - WGT_SB_W) {
        int32_t sbX = w->x + DBGRID_BORDER;
        int32_t sbW = innerW;
        int32_t maxScroll = d->totalColW - innerW;

        if (maxScroll < 0) {
            maxScroll = 0;
        }

        int32_t relX = vx - sbX;
        ScrollHitE sh = widgetScrollbarHitTest(sbW, relX, d->totalColW, innerW, d->scrollPosH);

        if (sh == ScrollHitArrowDecE) {
            d->scrollPosH -= DBGRID_HSTEP;
        } else if (sh == ScrollHitArrowIncE) {
            d->scrollPosH += DBGRID_HSTEP;
        } else if (sh == ScrollHitPageDecE) {
            d->scrollPosH -= innerW;
        } else if (sh == ScrollHitPageIncE) {
            d->scrollPosH += innerW;
        } else if (sh == ScrollHitThumbE) {
            int32_t trackLen = sbW - WGT_SB_W * 2;
            int32_t thumbPos;
            int32_t thumbSize;
            widgetScrollbarThumb(trackLen, d->totalColW, innerW, d->scrollPosH, &thumbPos, &thumbSize);

            d->sbDragOrient = 1;
            d->sbDragOff    = relX - WGT_SB_W - thumbPos;

            // Register the drag with the WM (see vertical-thumb case above).
            sDragWidget = w;
        }

        d->scrollPosH = clampInt(d->scrollPosH, 0, maxScroll);

        wgtInvalidatePaint(w);
        return;
    }

    // Column header area
    if (vy >= headerTop && vy < dataTop) {
        resolveColumns(w, &ctx->font);

        // Check column border for resize
        int32_t borderCol = colBorderHit(w, vx, vy);

        if (borderCol >= 0) {
            d->resizeCol    = borderCol;
            d->resizeStartX = vx;
            d->resizeOrigW  = d->resolvedW[borderCol];
            d->resizeDragging = false;
            d->sbDragOrient = -1;

            // Register the drag with the WM (see vertical-thumb case above).
            sDragWidget = w;
            return;
        }

        // Header click = sort
        int32_t x = w->x + DBGRID_BORDER - d->scrollPosH;

        for (int32_t c = 0; c < d->colCount; c++) {
            if (!d->columns[c].visible) {
                continue;
            }

            if (vx >= x && vx < x + d->resolvedW[c]) {
                if (d->sortCol == c) {
                    d->sortDir = (d->sortDir == SORT_ASC) ? SORT_DESC : SORT_ASC;
                } else {
                    d->sortCol = c;
                    d->sortDir = SORT_ASC;
                }

                dbGridBuildSortIndex(w);
                wgtInvalidatePaint(w);
                return;
            }

            x += d->resolvedW[c];
        }

        return;
    }

    // Data row click
    if (vy >= dataTop && vy < dataTop + innerH) {
        int32_t relY    = vy - dataTop;
        int32_t dispRow = d->scrollPos + relY / rowH;

        if (dispRow >= 0 && dispRow < rowCount) {
            int32_t dataRow = displayToDataRow(d, dispRow);
            d->selectedRow = dataRow;

            // Sync Data control cursor
            if (d->dataWidget) {
                wgtDataCtrlSetCurrentRow(d->dataWidget, dataRow);
            }

            if (w->onClick) {
                w->onClick(w);
            }

            wgtInvalidatePaint(w);
        }
    }
}


static void dbGridPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors) {
    DbGridDataT *d = (DbGridDataT *)w->data;

    int32_t innerW, innerH, headerH, visRows;
    bool needV, needH;
    computeLayout(w, font, &innerW, &innerH, &headerH, &visRows, &needV, &needH);

    int32_t rowH     = font->charHeight + DBGRID_ROW_VPAD;
    int32_t rowCount = getDataRowCount(d);

    // Outer sunken border
    BevelStyleT bevel = { colors->windowShadow, colors->windowHighlight, colors->contentBg, DBGRID_BORDER };
    drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);

    int32_t contentX = w->x + DBGRID_BORDER;
    int32_t contentY = w->y + DBGRID_BORDER;

    // Save clip rect
    int32_t oldClipX = disp->clipX;
    int32_t oldClipY = disp->clipY;
    int32_t oldClipW = disp->clipW;
    int32_t oldClipH = disp->clipH;

    // ---- Column Headers ----
    setClipRect(disp, contentX, contentY, innerW, headerH);

    int32_t hx = contentX - d->scrollPosH;

    for (int32_t c = 0; c < d->colCount; c++) {
        if (!d->columns[c].visible) {
            continue;
        }

        int32_t cw = d->resolvedW[c];

        if (hx + cw > contentX - DBGRID_HORIZ_CULL_MARGIN && hx < contentX + innerW + DBGRID_HORIZ_CULL_MARGIN) {
            BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
            drawBevel(disp, ops, hx, contentY, cw, headerH, &hdrBevel);

            // Header text (centered)
            const char *hdr = d->columns[c].header;
            int32_t tw = textWidth(font, hdr);
            int32_t tx = hx + (cw - tw) / 2;
            int32_t ty = contentY + (headerH - font->charHeight) / 2;
            drawText(disp, ops, font, tx, ty, hdr, colors->contentFg, colors->buttonFace, false);

            // Sort indicator
            if (c == d->sortCol && d->sortDir != SORT_NONE) {
                int32_t sx = hx + cw - DBGRID_SORT_W;
                int32_t sy = contentY + headerH / 2;

                if (d->sortDir == SORT_ASC) {
                    for (int32_t i = 0; i < 3; i++) {
                        drawHLine(disp, ops, sx + 3 - i, sy - 1 + i, 1 + i * 2, colors->contentFg);
                    }
                } else {
                    for (int32_t i = 0; i < 3; i++) {
                        drawHLine(disp, ops, sx + 3 - i, sy + 1 - i, 1 + i * 2, colors->contentFg);
                    }
                }
            }
        }

        hx += cw;
    }

    // Fill remaining header space
    if (hx < contentX + innerW) {
        BevelStyleT hdrBevel = BEVEL_RAISED(colors, 1);
        drawBevel(disp, ops, hx, contentY, contentX + innerW - hx, headerH, &hdrBevel);
    }

    // ---- Data Rows ----
    int32_t dataY = contentY + headerH;
    setClipRect(disp, contentX, dataY, innerW, innerH);

    // Background fill
    rectFill(disp, ops, contentX, dataY, innerW, innerH, colors->contentBg);

    // The alternating-row shade is loop-invariant, so compute it once here
    // instead of recomputing it (up to twice) per odd row.
    uint32_t altBg = dbGridDarkenColor(disp, colors->contentBg, DBGRID_ALT_ROW_SHADE);

    for (int32_t i = 0; i < visRows && d->scrollPos + i < rowCount; i++) {
        int32_t dispRow = d->scrollPos + i;
        int32_t dataRow = displayToDataRow(d, dispRow);
        int32_t ry      = dataY + i * rowH;

        // Alternating row background
        if (i % 2 == 1) {
            rectFill(disp, ops, contentX, ry, innerW, rowH, altBg);
        }

        // Selection highlight
        bool selected = (dataRow == d->selectedRow);

        if (selected) {
            rectFill(disp, ops, contentX, ry, innerW, rowH, colors->menuHighlightBg);
        }

        uint32_t fg = selected ? colors->menuHighlightFg : colors->contentFg;
        uint32_t bg = selected ? colors->menuHighlightBg : (i % 2 == 1 ? altBg : colors->contentBg);

        // Draw cells
        int32_t cx = contentX - d->scrollPosH;

        for (int32_t c = 0; c < d->colCount; c++) {
            if (!d->columns[c].visible) {
                continue;
            }

            int32_t cw = d->resolvedW[c];

            if (cx + cw > contentX && cx < contentX + innerW) {
                const char *text = getCellText(d, dispRow, c);
                int32_t tw = textWidth(font, text);
                int32_t tx;

                if (d->columns[c].align == 1) {
                    tx = cx + (cw - tw) / 2;
                } else if (d->columns[c].align == 2) {
                    tx = cx + cw - tw - DBGRID_PAD;
                } else {
                    tx = cx + DBGRID_PAD;
                }

                int32_t ty = ry + (rowH - font->charHeight) / 2;
                drawText(disp, ops, font, tx, ty, text, fg, bg, false);
            }

            // Grid line
            if (d->gridLines && cx + cw > contentX && cx + cw <= contentX + innerW) {
                drawVLine(disp, ops, cx + cw - 1, ry, rowH, colors->windowShadow);
            }

            cx += cw;
        }

        // Horizontal grid line
        if (d->gridLines) {
            drawHLine(disp, ops, contentX, ry + rowH - 1, innerW, colors->windowShadow);
        }
    }

    setClipRect(disp, oldClipX, oldClipY, oldClipW, oldClipH);

    // ---- Scrollbars ----
    if (needV) {
        int32_t sbX = contentX + innerW;
        int32_t sbY = dataY;
        widgetDrawScrollbarV(disp, ops, colors, sbX, sbY, innerH, rowCount, visRows, d->scrollPos);
    }

    if (needH) {
        int32_t sbX = contentX;
        int32_t sbY = dataY + innerH;
        widgetDrawScrollbarH(disp, ops, colors, sbX, sbY, innerW, d->totalColW, innerW, d->scrollPosH);

        if (needV) {
            rectFill(disp, ops, contentX + innerW, sbY, WGT_SB_W, WGT_SB_W, colors->windowFace);
        }
    }

    // Focus rect
    if (w == sFocusedWidget) {
        drawFocusRect(disp, ops, w->x + 1, w->y + 1, w->w - 2, w->h - 2, colors->contentFg);
    }
}


// Refresh the grid display (e.g. after Data control refresh)
void dbGridRefresh(WidgetT *w) {
    if (!w || !w->data) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;
    d->totalColW = 0;

    // Rebuild sort index if sorting is active
    if (d->sortDir != SORT_NONE) {
        dbGridBuildSortIndex(w);
    }

    // Clamp scroll and selection
    int32_t rowCount = getDataRowCount(d);

    if (d->scrollPos >= rowCount) {
        d->scrollPos = rowCount > 0 ? rowCount - 1 : 0;
    }

    if (d->selectedRow >= rowCount) {
        d->selectedRow = rowCount > 0 ? rowCount - 1 : -1;
    }

    wgtInvalidate(w);
}


// Set a column's header text
void dbGridSetColumnHeader(WidgetT *w, const char *fieldName, const char *header) {
    if (!w || !w->data || !fieldName || !header) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;

    for (int32_t i = 0; i < d->colCount; i++) {
        if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
            snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", header);
            d->totalColW = 0;
            wgtInvalidate(w);
            return;
        }
    }
}


// Hide or show a column by field name
void dbGridSetColumnVisible(WidgetT *w, const char *fieldName, bool visible) {
    if (!w || !w->data || !fieldName) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;

    for (int32_t i = 0; i < d->colCount; i++) {
        if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
            d->columns[i].visible = visible;
            d->totalColW = 0;
            wgtInvalidate(w);
            return;
        }
    }
}


// Set a column's width (tagged size, 0 = auto)
void dbGridSetColumnWidth(WidgetT *w, const char *fieldName, int32_t width) {
    if (!w || !w->data || !fieldName) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;

    for (int32_t i = 0; i < d->colCount; i++) {
        if (strcasecmp(d->columns[i].fieldName, fieldName) == 0) {
            d->columns[i].width = width;
            d->totalColW = 0;
            wgtInvalidate(w);
            return;
        }
    }
}


// Set the Data control widget that this grid reads from.
// Also auto-populates columns from the Data control's column names.
void dbGridSetDataWidget(WidgetT *w, WidgetT *dataWidget) {
    if (!w || !w->data) {
        return;
    }

    DbGridDataT *d = (DbGridDataT *)w->data;
    d->dataWidget = dataWidget;
    d->totalColW  = 0;

    // Auto-populate columns
    if (dataWidget) {
        int32_t colCount = wgtDataCtrlGetColCount(dataWidget);

        // Grow column arrays as needed
        if (colCount > d->colCap) {
            DbGridColT *gc = (DbGridColT *)realloc(d->columns,  sizeof(DbGridColT) * colCount);
            int32_t    *gw = (int32_t    *)realloc(d->resolvedW, sizeof(int32_t)    * colCount);

            if (gc && gw) {
                d->columns   = gc;
                d->resolvedW = gw;
                d->colCap    = colCount;
            } else {
                // Allocation failed; keep what we have
                if (gc) {
                    d->columns = gc;
                }

                if (gw) {
                    d->resolvedW = gw;
                }

                colCount = d->colCap;
            }
        }

        d->colCount = colCount;

        for (int32_t i = 0; i < colCount; i++) {
            const char *name = wgtDataCtrlGetColName(dataWidget, i);
            snprintf(d->columns[i].fieldName, DBGRID_MAX_NAME, "%s", name);
            snprintf(d->columns[i].header, DBGRID_MAX_NAME, "%s", name);
            d->columns[i].width   = 0;
            d->columns[i].align   = 0;
            d->columns[i].visible = true;
            d->columns[i].dataCol = i;
        }
    }

    // Reset sort
    free(d->sortIndex);
    d->sortIndex = NULL;
    d->sortCount = 0;
    d->sortCol   = -1;
    d->sortDir   = SORT_NONE;

    wgtInvalidate(w);
}


static void dbGridSetGridLines(WidgetT *w, bool val) {
    ((DbGridDataT *)w->data)->gridLines = val;
    wgtInvalidatePaint(w);
}


// Map a display-row to data-row
static int32_t displayToDataRow(const DbGridDataT *d, int32_t dispRow) {
    if (!d->sortIndex || dispRow < 0 || dispRow >= d->sortCount) {
        return dispRow;
    }

    return d->sortIndex[dispRow];
}


// Get cell text for a display-row and visible-column index
static const char *getCellText(const DbGridDataT *d, int32_t dispRow, int32_t visCol) {
    if (!d->dataWidget || visCol < 0 || visCol >= d->colCount || !d->columns[visCol].visible) {
        return "";
    }

    int32_t dataRow = displayToDataRow(d, dispRow);
    int32_t dataCol = d->columns[visCol].dataCol;

    if (dataCol < 0) {
        return "";
    }

    return wgtDataCtrlGetCellText(d->dataWidget, dataRow, dataCol);
}


static int32_t getDataRowCount(const DbGridDataT *d) {
    if (!d->dataWidget) {
        return 0;
    }

    return wgtDataCtrlGetRowCount(d->dataWidget);
}


// resolveColumns -- compute pixel widths for each column
static void resolveColumns(WidgetT *w, const BitmapFontT *font) {
    DbGridDataT *d = (DbGridDataT *)w->data;

    if (d->totalColW > 0) {
        return;
    }

    int32_t rowCount = getDataRowCount(d);

    d->totalColW = 0;

    for (int32_t c = 0; c < d->colCount; c++) {
        if (!d->columns[c].visible) {
            d->resolvedW[c] = 0;
            continue;
        }

        if (d->columns[c].width > 0) {
            d->resolvedW[c] = wgtResolveSize(d->columns[c].width, 0, font->charWidth);
        } else {
            // Auto-size: find widest content
            int32_t maxW = textWidth(font, d->columns[c].header) + DBGRID_COL_PAD;

            for (int32_t r = 0; r < rowCount; r++) {
                int32_t dataCol = d->columns[c].dataCol;

                if (dataCol >= 0) {
                    const char *text = wgtDataCtrlGetCellText(d->dataWidget, r, dataCol);
                    int32_t tw = textWidth(font, text) + DBGRID_COL_PAD;

                    if (tw > maxW) {
                        maxW = tw;
                    }
                }
            }

            // Add space for sort indicator on header
            maxW += DBGRID_SORT_W;
            d->resolvedW[c] = maxW;
        }

        if (d->resolvedW[c] < DBGRID_MIN_COL_W) {
            d->resolvedW[c] = DBGRID_MIN_COL_W;
        }

        d->totalColW += d->resolvedW[c];
    }
}


// ============================================================
// DXE registration
// ============================================================

static const WidgetClassT sClass = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE | WCLASS_NO_HIT_RECURSE | WCLASS_SCROLLABLE,
    .handlers = {
        [WGT_METHOD_PAINT]            = (void *)dbGridPaint,
        [WGT_METHOD_CALC_MIN_SIZE]    = (void *)dbGridCalcMinSize,
        [WGT_METHOD_ON_MOUSE]         = (void *)dbGridOnMouse,
        [WGT_METHOD_ON_KEY]           = (void *)dbGridOnKey,
        [WGT_METHOD_DESTROY]          = (void *)dbGridDestroy,
        [WGT_METHOD_GET_CURSOR_SHAPE] = (void *)dbGridGetCursorShape,
        [WGT_METHOD_ON_DRAG_UPDATE]   = (void *)dbGridOnDragUpdate,
        [WGT_METHOD_ON_DRAG_END]      = (void *)dbGridOnDragEnd,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent);
    void     (*setDataWidget)(WidgetT *w, WidgetT *dataWidget);
    void     (*refresh)(WidgetT *w);
    void     (*setColumnVisible)(WidgetT *w, const char *fieldName, bool visible);
    void     (*setColumnHeader)(WidgetT *w, const char *fieldName, const char *header);
    void     (*setColumnWidth)(WidgetT *w, const char *fieldName, int32_t width);
    int32_t  (*getSelectedRow)(const WidgetT *w);
    void     (*setSelectedRow)(WidgetT *w, int32_t row);
} sApi = {
    .create           = dbGridCreate,
    .setDataWidget    = dbGridSetDataWidget,
    .refresh          = dbGridRefresh,
    .setColumnVisible = dbGridSetColumnVisible,
    .setColumnHeader  = dbGridSetColumnHeader,
    .setColumnWidth   = dbGridSetColumnWidth,
    .getSelectedRow   = dbGridGetSelectedRow,
    .setSelectedRow   = dbGridSetSelectedRow,
};

static const WgtPropDescT sProps[] = {
    { "GridLines",   WGT_IFACE_BOOL, (void *)dbGridGetGridLines,    (void *)dbGridSetGridLines,    NULL },
    { "SelectedRow", WGT_IFACE_INT,  (void *)dbGridGetSelectedRow,  (void *)dbGridSetSelectedRow,  NULL },
};

static const WgtMethodDescT sMethods[] = {
    { "Refresh",           WGT_SIG_VOID,        (void *)dbGridRefresh },
    { "SetColumnVisible",  WGT_SIG_STR_BOOL,    (void *)dbGridSetColumnVisible },
    { "SetColumnHeader",   WGT_SIG_STR_STR,     (void *)dbGridSetColumnHeader },
    { "SetColumnWidth",    WGT_SIG_STR_INT,     (void *)dbGridSetColumnWidth },
};

static const WgtEventDescT sEvents[] = {
    { "Click" },
    { "DblClick" },
};

static const WgtIfaceT sIface = {
    .basName      = "DBGrid",
    .props        = sProps,
    .propCount    = sizeof(sProps) / sizeof(sProps[0]),
    .methods      = sMethods,
    .methodCount  = sizeof(sMethods) / sizeof(sMethods[0]),
    .events       = sEvents,
    .eventCount   = sizeof(sEvents) / sizeof(sEvents[0]),
    .createSig    = WGT_CREATE_PARENT,
    .isContainer  = false,
    .defaultEvent = "DblClick",
    .namePrefix   = "DBGrid",
};


void wgtRegister(void) {
    sTypeId = wgtRegisterClass(&sClass);
    wgtRegisterApi("dbgrid", &sApi);
    wgtRegisterIface("dbgrid", &sIface);
}
