// 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
// widgetDataCtrl.c -- VB3-style Data control for database binding
//
// A visible navigation bar that connects to a SQLite database via
// dvxSql* functions. Reads all rows from the RecordSource query
// into an in-memory cache for bidirectional navigation. Fires
// Reposition events when the cursor moves so bound controls can
// update.
//
// Depends on the dvxsql library DXE (via datactrl.dep) so all
// dvxSql* functions are resolved at load time.

#include "dvxWgtP.h"
#include "../../../libs/kpunch/sql/dvxSql.h"
#include "thirdparty/stb_ds_wrap.h"

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

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

#define DATA_HEIGHT       24
#define DATA_BTN_W        20
#define DATA_BORDER       2
#define DATA_MAX_FIELD    256

// ============================================================
// Per-row cache entry
// ============================================================

typedef struct {
    char **fields;     // array of strdup'd strings, one per column
    int32_t fieldCount;
} DataRowT;

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

typedef struct {
    char      databaseName[DATA_MAX_FIELD];
    char      recordSource[DATA_MAX_FIELD];
    char      caption[DATA_MAX_FIELD];
    char      keyColumn[DATA_MAX_FIELD];

    // Master-detail linking
    char      masterSource[DATA_MAX_FIELD];   // name of master Data control
    char      masterField[DATA_MAX_FIELD];    // column in master to read
    char      detailField[DATA_MAX_FIELD];    // column in this table to filter
    char      masterValue[DATA_MAX_FIELD];    // current filter value (set by runtime)

    // Cached result set
    DataRowT *rows;          // stb_ds dynamic array
    int32_t   rowCount;
    char    **colNames;      // stb_ds dynamic array of strdup'd names
    int32_t   colCount;
    int32_t   currentRow;    // 0-based, -1 = no rows

    bool      bof;
    bool      eof;
    bool      dirty;              // current row has unsaved changes
    bool      isNewRow;           // current row was created by AddNew
    bool      refreshing;         // guards against re-entrant Refresh recursion
} DataCtrlDataT;

static int32_t sTypeId = -1;


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

static bool        autoSave(WidgetT *w);
static bool        canWriteBack(const DataCtrlDataT *d);
void               dataCtrlAddNew(WidgetT *w);
static void        dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font);
static WidgetT    *dataCtrlCreate(WidgetT *parent);
void               dataCtrlDelete(WidgetT *w);
static void        dataCtrlDestroy(WidgetT *w);
static bool        dataCtrlGetBof(const WidgetT *w);
static const char *dataCtrlGetCaption(const WidgetT *w);
const char        *dataCtrlGetCellText(WidgetT *w, int32_t row, int32_t col);
int32_t            dataCtrlGetColCount(WidgetT *w);
const char        *dataCtrlGetColName(WidgetT *w, int32_t col);
static const char *dataCtrlGetDatabaseName(const WidgetT *w);
static const char *dataCtrlGetDetailField(const WidgetT *w);
static bool        dataCtrlGetEof(const WidgetT *w);
const char        *dataCtrlGetField(WidgetT *w, const char *colName);
static const char *dataCtrlGetKeyColumn(const WidgetT *w);
static const char *dataCtrlGetMasterField(const WidgetT *w);
static const char *dataCtrlGetMasterSource(const WidgetT *w);
static const char *dataCtrlGetRecordSource(const WidgetT *w);
int32_t            dataCtrlGetRowCount(WidgetT *w);
static void        dataCtrlMoveFirst(WidgetT *w);
static void        dataCtrlMoveLast(WidgetT *w);
static void        dataCtrlMoveNext(WidgetT *w);
static void        dataCtrlMovePrev(WidgetT *w);
static void        dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy);
static void        dataCtrlPaint(WidgetT *w, DisplayT *disp, const BlitOpsT *ops, const BitmapFontT *font, const ColorSchemeT *colors);
void               dataCtrlRefresh(WidgetT *w);
static void        dataCtrlSetCaption(WidgetT *w, const char *val);
void               dataCtrlSetCurrentRow(WidgetT *w, int32_t row);
static void        dataCtrlSetDatabaseName(WidgetT *w, const char *val);
static void        dataCtrlSetDetailField(WidgetT *w, const char *val);
void               dataCtrlSetField(WidgetT *w, const char *colName, const char *value);
static void        dataCtrlSetKeyColumn(WidgetT *w, const char *val);
static void        dataCtrlSetMasterField(WidgetT *w, const char *val);
static void        dataCtrlSetMasterSource(WidgetT *w, const char *val);
void               dataCtrlSetMasterValue(WidgetT *w, const char *val);
static void        dataCtrlSetRecordSource(WidgetT *w, const char *val);
void               dataCtrlUpdate(WidgetT *w);
void               dataCtrlUpdateRow(WidgetT *w);
static int32_t     findKeyCol(const DataCtrlDataT *d);
static void        fireReposition(WidgetT *w);
static void        freeCache(DataCtrlDataT *d);
void               wgtRegister(void);


// Save the current row before a cursor move.  Returns false when a save
// was attempted but the row is still dirty (open/escape/truncation/exec
// failure or a Validate cancel): the caller must not move the cursor,
// because the dirty/isNewRow flags describe THIS row and applying them
// to a different row would re-INSERT an existing row as a duplicate.
static bool autoSave(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d->dirty && canWriteBack(d)) {
        dataCtrlUpdate(w);
        return !d->dirty;
    }

    return true;
}


static bool canWriteBack(const DataCtrlDataT *d) {
    if (!d->databaseName[0] || !d->recordSource[0]) {
        return false;
    }

    // Write-back only for simple table names, not SELECT queries
    if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) {
        return false;
    }

    return true;
}


// dataCtrlAddNew -- append a blank row and move cursor to it
void dataCtrlAddNew(WidgetT *w) {
    if (!w || !w->data) {
        return;
    }

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

    // Auto-save current row if dirty; a failed save must block AddNew or
    // the pending row's dirty/isNewRow flags would be clobbered below.
    if (!autoSave(w)) {
        return;
    }

    if (d->colCount == 0) {
        return;
    }

    // Append a blank row
    DataRowT row;
    row.fieldCount = d->colCount;
    row.fields     = (char **)malloc(d->colCount * sizeof(char *));

    for (int32_t i = 0; i < d->colCount; i++) {
        row.fields[i] = strdup("");
    }

    arrput(d->rows, row);
    d->rowCount   = (int32_t)arrlen(d->rows);
    d->currentRow = d->rowCount - 1;
    d->bof        = false;
    d->eof        = false;
    d->dirty      = true;
    d->isNewRow   = true;

    fireReposition(w);
    // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput
}


static void dataCtrlCalcMinSize(WidgetT *w, const BitmapFontT *font) {
    (void)font;
    w->calcMinW = DATA_BTN_W * 4 + 60; // 4 buttons + some caption space
    w->calcMinH = DATA_HEIGHT;
}


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

    WidgetT *w = widgetAlloc(parent, sTypeId);

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

        if (d) {
            d->currentRow = -1;
            d->bof        = true;
            d->eof        = true;
        }

        w->data = d;
        w->minH = wgtPixels(DATA_HEIGHT);
    }

    return w;
}


// dataCtrlDelete -- delete the current row from DB and cache
void dataCtrlDelete(WidgetT *w) {
    if (!w || !w->data) {
        return;
    }

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

    if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) {
        return;
    }

    // Delete from database (unless it's an unsaved new row).  The cached
    // row may only be removed below when the database row is actually
    // gone; dropping it while the DELETE was skipped or failed would
    // leave the UI out of sync until the row reappears on Refresh.
    if (!d->isNewRow && canWriteBack(d)) {
        int32_t db = dvxSqlOpen(d->databaseName);

        if (db <= 0) {
            return;
        }

        int32_t keyCol     = findKeyCol(d);
        const char *keyVal = d->rows[d->currentRow].fields[keyCol];

        // Escape and quote the key value (injection / malformed-SQL fix).
        // Abort if the key is too long to escape (the buffer would be
        // left unterminated) rather than run garbage SQL.
        char keyEsc[DATA_MAX_FIELD * 2];

        if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) < 0) {
            dvxSqlClose(db);
            return;
        }

        char sql[512];
        snprintf(sql, sizeof(sql), "DELETE FROM %s WHERE %s='%s'",
                 d->recordSource, d->colNames[keyCol], keyEsc);

        bool ok = dvxSqlExec(db, sql);

        dvxSqlClose(db);

        if (!ok) {
            return;
        }
    }

    // Free the cached row
    for (int32_t j = 0; j < d->rows[d->currentRow].fieldCount; j++) {
        free(d->rows[d->currentRow].fields[j]);
    }

    free(d->rows[d->currentRow].fields);
    arrdel(d->rows, d->currentRow);
    d->rowCount = (int32_t)arrlen(d->rows);

    // Adjust cursor
    if (d->rowCount == 0) {
        d->currentRow = -1;
        d->bof        = true;
        d->eof        = true;
    } else {
        if (d->currentRow >= d->rowCount) {
            d->currentRow = d->rowCount - 1;
        }

        d->bof = (d->currentRow == 0);
        d->eof = (d->currentRow >= d->rowCount - 1);
    }

    d->dirty    = false;
    d->isNewRow = false;

    fireReposition(w);
}


static void dataCtrlDestroy(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d) {
        freeCache(d);
        free(d);
    }
}


static bool dataCtrlGetBof(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->bof;
}


static const char *dataCtrlGetCaption(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->caption;
}


const char *dataCtrlGetCellText(WidgetT *w, int32_t row, int32_t col) {
    if (!w || !w->data) {
        return "";
    }

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

    if (row < 0 || row >= d->rowCount || col < 0 || col >= d->colCount) {
        return "";
    }

    return d->rows[row].fields[col];
}


int32_t dataCtrlGetColCount(WidgetT *w) {
    if (!w || !w->data) {
        return 0;
    }

    return ((DataCtrlDataT *)w->data)->colCount;
}


const char *dataCtrlGetColName(WidgetT *w, int32_t col) {
    if (!w || !w->data) {
        return "";
    }

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

    if (col < 0 || col >= d->colCount) {
        return "";
    }

    return d->colNames[col];
}


static const char *dataCtrlGetDatabaseName(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->databaseName;
}


static const char *dataCtrlGetDetailField(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->detailField;
}


static bool dataCtrlGetEof(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->eof;
}


// Exported for form runtime data binding (resolved via dlsym)
const char *dataCtrlGetField(WidgetT *w, const char *colName) {
    if (!w || !w->data || !colName) {
        return "";
    }

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

    if (d->currentRow < 0 || d->currentRow >= d->rowCount) {
        return "";
    }

    for (int32_t i = 0; i < d->colCount; i++) {
        if (strcasecmp(d->colNames[i], colName) == 0) {
            return d->rows[d->currentRow].fields[i];
        }
    }

    return "";
}


static const char *dataCtrlGetKeyColumn(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->keyColumn;
}


static const char *dataCtrlGetMasterField(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->masterField;
}


static const char *dataCtrlGetMasterSource(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->masterSource;
}


static const char *dataCtrlGetRecordSource(const WidgetT *w) {
    return ((DataCtrlDataT *)w->data)->recordSource;
}


int32_t dataCtrlGetRowCount(WidgetT *w) {
    if (!w || !w->data) {
        return 0;
    }

    return ((DataCtrlDataT *)w->data)->rowCount;
}


static void dataCtrlMoveFirst(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d->rowCount <= 0) {
        return;
    }

    if (!autoSave(w)) {
        return;
    }

    d->currentRow = 0;
    d->bof        = false;
    d->eof        = false;
    fireReposition(w);
}


static void dataCtrlMoveLast(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d->rowCount <= 0) {
        return;
    }

    if (!autoSave(w)) {
        return;
    }

    d->currentRow = d->rowCount - 1;
    d->bof        = false;
    d->eof        = false;
    fireReposition(w);
}


static void dataCtrlMoveNext(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d->rowCount <= 0 || d->currentRow >= d->rowCount - 1) {
        d->eof = true;
        return;
    }

    if (!autoSave(w)) {
        return;
    }

    d->currentRow++;
    d->bof = false;
    d->eof = (d->currentRow >= d->rowCount - 1);
    fireReposition(w);
}


static void dataCtrlMovePrev(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    if (d->rowCount <= 0 || d->currentRow <= 0) {
        d->bof = true;
        return;
    }

    if (!autoSave(w)) {
        return;
    }

    d->currentRow--;
    d->bof = (d->currentRow == 0);
    d->eof = false;
    fireReposition(w);
}


static void dataCtrlOnMouse(WidgetT *w, WidgetT *root, int32_t vx, int32_t vy) {
    (void)root;
    (void)vy;

    sFocusedWidget = w;

    int32_t innerX = w->x + DATA_BORDER;
    int32_t innerW = w->w - DATA_BORDER * 2;
    int32_t relX   = vx - innerX;

    if (relX < DATA_BTN_W) {
        dataCtrlMoveFirst(w);
    } else if (relX < DATA_BTN_W * 2) {
        dataCtrlMovePrev(w);
    } else if (relX >= innerW - DATA_BTN_W) {
        dataCtrlMoveLast(w);
    } else if (relX >= innerW - DATA_BTN_W * 2) {
        dataCtrlMoveNext(w);
    }
}


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

    uint32_t face   = colors->buttonFace;
    uint32_t fg     = colors->contentFg;
    uint32_t hi     = colors->windowHighlight;
    uint32_t sh     = colors->windowShadow;

    // Background
    rectFill(disp, ops, w->x, w->y, w->w, w->h, face);

    // Outer bevel
    BevelStyleT bevel;
    bevel.highlight = hi;
    bevel.shadow    = sh;
    bevel.face      = face;
    bevel.width     = DATA_BORDER;
    drawBevel(disp, ops, w->x, w->y, w->w, w->h, &bevel);

    int32_t innerX = w->x + DATA_BORDER;
    int32_t innerY = w->y + DATA_BORDER;
    int32_t innerW = w->w - DATA_BORDER * 2;
    int32_t innerH = w->h - DATA_BORDER * 2;

    // Button zones
    int32_t btnH = innerH;
    int32_t btnY = innerY;

    // |< button (MoveFirst)
    BevelStyleT btnBevel = { hi, sh, face, 1 };
    drawBevel(disp, ops, innerX, btnY, DATA_BTN_W, btnH, &btnBevel);
    // Draw |< glyph
    int32_t cx = innerX + DATA_BTN_W / 2;
    int32_t cy = btnY + btnH / 2;
    drawVLine(disp, ops, cx - 3, cy - 4, 9, fg);

    for (int32_t i = 0; i < 4; i++) {
        drawHLine(disp, ops, cx - 1 + i, cy - i, 1, fg);
        drawHLine(disp, ops, cx - 1 + i, cy + i, 1, fg);
    }

    // < button (MovePrev)
    drawBevel(disp, ops, innerX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel);
    cx = innerX + DATA_BTN_W + DATA_BTN_W / 2;

    for (int32_t i = 0; i < 4; i++) {
        drawHLine(disp, ops, cx + i, cy - i, 1, fg);
        drawHLine(disp, ops, cx + i, cy + i, 1, fg);
    }

    // > button (MoveNext)
    int32_t rightX = innerX + innerW - DATA_BTN_W * 2;
    drawBevel(disp, ops, rightX, btnY, DATA_BTN_W, btnH, &btnBevel);
    cx = rightX + DATA_BTN_W / 2;

    for (int32_t i = 0; i < 4; i++) {
        drawHLine(disp, ops, cx - i, cy - i, 1, fg);
        drawHLine(disp, ops, cx - i, cy + i, 1, fg);
    }

    // >| button (MoveLast)
    drawBevel(disp, ops, rightX + DATA_BTN_W, btnY, DATA_BTN_W, btnH, &btnBevel);
    cx = rightX + DATA_BTN_W + DATA_BTN_W / 2;
    drawVLine(disp, ops, cx + 3, cy - 4, 9, fg);

    for (int32_t i = 0; i < 4; i++) {
        drawHLine(disp, ops, cx - i, cy - i, 1, fg);
        drawHLine(disp, ops, cx - i, cy + i, 1, fg);
    }

    // Caption in center
    int32_t captionX = innerX + DATA_BTN_W * 2 + 4;
    int32_t captionW = innerW - DATA_BTN_W * 4 - 8;
    const char *text = d->caption[0] ? d->caption : d->recordSource;

    if (text[0] && captionW > 0) {
        int32_t tw = textWidth(font, text);
        int32_t tx = captionX + (captionW - tw) / 2;
        int32_t ty = innerY + (innerH - font->charHeight) / 2;
        drawText(disp, ops, font, tx, ty, text, fg, face, false);
    }
}


// dataCtrlRefresh -- execute query and cache all rows
void dataCtrlRefresh(WidgetT *w) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;

    // fireReposition (below) fires onChange, which for a detail control
    // cascades back into Refresh; a cyclic/self master-detail link or a
    // Reposition handler that calls .Refresh on this control would otherwise
    // recurse without bound.  Block re-entry on the same control.
    if (d->refreshing) {
        return;
    }

    d->refreshing = true;

    freeCache(d);

    if (!d->databaseName[0] || !d->recordSource[0]) {
        // Unconfigured: still fire Reposition so bound controls clear instead
        // of keeping the previous record's stale values.
        fireReposition(w);
        d->refreshing = false;
        return;
    }

    int32_t db = dvxSqlOpen(d->databaseName);

    if (db <= 0) {
        fireReposition(w);
        d->refreshing = false;
        return;
    }

    // Build query from RecordSource, with optional master-detail WHERE clause
    char query[1024];

    if (strncasecmp(d->recordSource, "SELECT ", 7) == 0) {
        snprintf(query, sizeof(query), "%s", d->recordSource);
    } else {
        snprintf(query, sizeof(query), "SELECT * FROM %s", d->recordSource);
    }

    // Append WHERE filter for master-detail linking
    if (d->detailField[0] && d->masterValue[0]) {
        char escaped[DATA_MAX_FIELD * 2];
        dvxSqlEscape(d->masterValue, escaped, sizeof(escaped));

        int32_t len = (int32_t)strlen(query);
        snprintf(query + len, sizeof(query) - len, " WHERE %s='%s'", d->detailField, escaped);
    }

    int32_t rs = dvxSqlQuery(db, query);

    if (rs <= 0) {
        dvxSqlClose(db);
        fireReposition(w);
        d->refreshing = false;
        return;
    }

    // Cache column names
    d->colCount = dvxSqlFieldCount(rs);

    for (int32_t i = 0; i < d->colCount; i++) {
        const char *name = dvxSqlFieldName(rs, i);
        arrput(d->colNames, strdup(name ? name : ""));
    }

    // Cache all rows
    while (dvxSqlNext(rs)) {
        DataRowT row;
        row.fieldCount = d->colCount;
        row.fields     = (char **)malloc(d->colCount * sizeof(char *));

        for (int32_t i = 0; i < d->colCount; i++) {
            const char *text = dvxSqlFieldText(rs, i);
            row.fields[i] = strdup(text ? text : "");
        }

        arrput(d->rows, row);
        // cppcheck-suppress memleak ; row.fields is stored into d->rows via arrput
    }

    d->rowCount = (int32_t)arrlen(d->rows);
    dvxSqlFreeResult(rs);
    dvxSqlClose(db);

    if (d->rowCount > 0) {
        d->currentRow = 0;
        d->bof        = false;
        d->eof        = false;
    }

    // Auto-caption
    if (!d->caption[0]) {
        snprintf(d->caption, sizeof(d->caption), "%s", d->recordSource);
    }

    // Refresh moved the cursor to row 0; fire Reposition like every other
    // cursor move so bound controls update and the user handler runs.
    // fireReposition also invalidates the paint, so no separate call is needed.
    fireReposition(w);
    d->refreshing = false;
}


static void dataCtrlSetCaption(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->caption, sizeof(d->caption), "%s", val ? val : "");
    wgtInvalidatePaint(w);
}


void dataCtrlSetCurrentRow(WidgetT *w, int32_t row) {
    if (!w || !w->data) {
        return;
    }

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

    if (row < 0 || row >= d->rowCount) {
        return;
    }

    // Auto-save before moving; a failed save keeps the cursor in place.
    if (!autoSave(w)) {
        return;
    }

    d->currentRow = row;
    d->bof        = (row == 0);
    d->eof        = (row >= d->rowCount - 1);

    wgtInvalidatePaint(w);

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


static void dataCtrlSetDatabaseName(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->databaseName, sizeof(d->databaseName), "%s", val ? val : "");
}


static void dataCtrlSetDetailField(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->detailField, sizeof(d->detailField), "%s", val ? val : "");
}


// dataCtrlSetField -- update a field value in the current cached row
void dataCtrlSetField(WidgetT *w, const char *colName, const char *value) {
    if (!w || !w->data || !colName || !value) {
        return;
    }

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

    if (d->currentRow < 0 || d->currentRow >= d->rowCount) {
        return;
    }

    for (int32_t i = 0; i < d->colCount; i++) {
        if (strcasecmp(d->colNames[i], colName) == 0) {
            free(d->rows[d->currentRow].fields[i]);
            d->rows[d->currentRow].fields[i] = strdup(value);
            d->dirty = true;
            return;
        }
    }
}


static void dataCtrlSetKeyColumn(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->keyColumn, sizeof(d->keyColumn), "%s", val ? val : "");
}


static void dataCtrlSetMasterField(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->masterField, sizeof(d->masterField), "%s", val ? val : "");
}


static void dataCtrlSetMasterSource(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->masterSource, sizeof(d->masterSource), "%s", val ? val : "");
}


void dataCtrlSetMasterValue(WidgetT *w, const char *val) {
    if (!w || !w->data) {
        return;
    }

    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->masterValue, sizeof(d->masterValue), "%s", val ? val : "");
}


static void dataCtrlSetRecordSource(WidgetT *w, const char *val) {
    DataCtrlDataT *d = (DataCtrlDataT *)w->data;
    snprintf(d->recordSource, sizeof(d->recordSource), "%s", val ? val : "");
}


// dataCtrlUpdate -- save the current row to the database
// If the current row was created by AddNew, executes an INSERT.
// Otherwise executes an UPDATE using the KeyColumn to identify the row.
// Clears the dirty and isNewRow flags on success.
void dataCtrlUpdate(WidgetT *w) {
    if (!w || !w->data) {
        return;
    }

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

    if (!d->dirty || !canWriteBack(d)) {
        return;
    }

    if (d->currentRow < 0 || d->currentRow >= d->rowCount || d->colCount == 0) {
        return;
    }

    // Fire Validate event — return false cancels the write
    if (w->onValidate && !w->onValidate(w)) {
        return;
    }

    int32_t db = dvxSqlOpen(d->databaseName);

    if (db <= 0) {
        return;
    }

    char sql[2048];
    int32_t pos = 0;

    if (d->isNewRow) {
        // INSERT INTO table (col1, col2, ...) VALUES ('val1', 'val2', ...)
        pos = snprintf(sql, sizeof(sql), "INSERT INTO %s (", d->recordSource);

        for (int32_t i = 0; i < d->colCount; i++) {
            if (i > 0) {
                pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
            }

            pos = dvxStrAppendf(sql, sizeof(sql), pos,"%s", d->colNames[i]);
        }

        pos = dvxStrAppendf(sql, sizeof(sql), pos, ") VALUES (");

        for (int32_t i = 0; i < d->colCount; i++) {
            if (i > 0) {
                pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
            }

            char escaped[DATA_MAX_FIELD * 2];

            // A field too long to escape would leave 'escaped' without a
            // NUL terminator (dvxSqlEscape returns -1 and bails); abort
            // rather than read an unterminated buffer into the SQL.
            if (dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)) < 0) {
                dvxSqlClose(db);
                return;
            }

            pos = dvxStrAppendf(sql, sizeof(sql), pos,"'%s'", escaped);
        }

        pos = dvxStrAppendf(sql, sizeof(sql), pos, ")");
    } else {
        // UPDATE table SET col1='val1', ... WHERE keyCol=keyVal
        int32_t keyCol       = findKeyCol(d);
        const char *keyVal   = d->rows[d->currentRow].fields[keyCol];

        pos = snprintf(sql, sizeof(sql), "UPDATE %s SET ", d->recordSource);

        bool first = true;

        for (int32_t i = 0; i < d->colCount; i++) {
            if (i == keyCol) {
                continue;
            }

            if (!first) {
                pos = dvxStrAppendf(sql, sizeof(sql), pos,", ");
            }

            char escaped[DATA_MAX_FIELD * 2];

            if (dvxSqlEscape(d->rows[d->currentRow].fields[i], escaped, sizeof(escaped)) < 0) {
                dvxSqlClose(db);
                return;
            }

            pos = dvxStrAppendf(sql, sizeof(sql), pos,"%s='%s'", d->colNames[i], escaped);
            first = false;
        }

        // Escape and quote the key value: an unquoted/unescaped key
        // produces malformed SQL for text keys and is an injection vector.
        char keyEsc[DATA_MAX_FIELD * 2];

        if (dvxSqlEscape(keyVal, keyEsc, sizeof(keyEsc)) < 0) {
            dvxSqlClose(db);
            return;
        }

        pos = dvxStrAppendf(sql, sizeof(sql), pos, " WHERE %s='%s'", d->colNames[keyCol], keyEsc);
    }

    // A negative position means the statement was truncated; executing it
    // would lose values or the WHERE clause.  Keep the row dirty so the
    // edit is not silently discarded.
    if (pos < 0) {
        dvxSqlClose(db);
        return;
    }

    bool ok = dvxSqlExec(db, sql);

    dvxSqlClose(db);

    if (ok) {
        d->dirty    = false;
        d->isNewRow = false;
    }
}


// dataCtrlUpdateRow -- legacy wrapper, calls dataCtrlUpdate
void dataCtrlUpdateRow(WidgetT *w) {
    dataCtrlUpdate(w);
}


// findKeyCol -- locate the key column index
static int32_t findKeyCol(const DataCtrlDataT *d) {
    if (d->keyColumn[0]) {
        for (int32_t i = 0; i < d->colCount; i++) {
            if (strcasecmp(d->colNames[i], d->keyColumn) == 0) {
                return i;
            }
        }
    }

    return 0;
}


static void fireReposition(WidgetT *w) {
    wgtInvalidatePaint(w);

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


// freeCache -- release the cached result set
static void freeCache(DataCtrlDataT *d) {
    for (int32_t i = 0; i < d->rowCount; i++) {
        for (int32_t j = 0; j < d->rows[i].fieldCount; j++) {
            free(d->rows[i].fields[j]);
        }

        free(d->rows[i].fields);
    }

    arrfree(d->rows);
    d->rows     = NULL;
    d->rowCount = 0;

    for (int32_t i = 0; i < d->colCount; i++) {
        free(d->colNames[i]);
    }

    arrfree(d->colNames);
    d->colNames   = NULL;
    d->colCount   = 0;
    d->currentRow = -1;
    d->bof        = true;
    d->eof        = true;

    // Reset the pending-write flags too: a Refresh after AddNew left
    // isNewRow set, so the next autoSave would re-INSERT row 0's values
    // as a duplicate.
    d->dirty    = false;
    d->isNewRow = false;
}


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

static const WidgetClassT sClass = {
    .version = WGT_CLASS_VERSION,
    .flags   = WCLASS_FOCUSABLE,
    .handlers = {
        [WGT_METHOD_PAINT]         = (void *)dataCtrlPaint,
        [WGT_METHOD_CALC_MIN_SIZE] = (void *)dataCtrlCalcMinSize,
        [WGT_METHOD_ON_MOUSE]      = (void *)dataCtrlOnMouse,
        [WGT_METHOD_DESTROY]       = (void *)dataCtrlDestroy,
    }
};


static const struct {
    WidgetT *(*create)(WidgetT *parent);
    void     (*refresh)(WidgetT *w);
    void     (*moveFirst)(WidgetT *w);
    void     (*movePrev)(WidgetT *w);
    void     (*moveNext)(WidgetT *w);
    void     (*moveLast)(WidgetT *w);
    const char *(*getField)(WidgetT *w, const char *colName);
    void     (*setField)(WidgetT *w, const char *colName, const char *value);
    void     (*updateRow)(WidgetT *w);
    void     (*update)(WidgetT *w);
    void     (*addNew)(WidgetT *w);
    void     (*delete)(WidgetT *w);
    void     (*setMasterValue)(WidgetT *w, const char *val);
    int32_t  (*getRowCount)(WidgetT *w);
    int32_t  (*getColCount)(WidgetT *w);
    const char *(*getColName)(WidgetT *w, int32_t col);
    const char *(*getCellText)(WidgetT *w, int32_t row, int32_t col);
    void     (*setCurrentRow)(WidgetT *w, int32_t row);
} sApi = {
    .create    = dataCtrlCreate,
    .refresh   = dataCtrlRefresh,
    .moveFirst = dataCtrlMoveFirst,
    .movePrev  = dataCtrlMovePrev,
    .moveNext  = dataCtrlMoveNext,
    .moveLast  = dataCtrlMoveLast,
    .getField  = dataCtrlGetField,
    .setField  = dataCtrlSetField,
    .updateRow = dataCtrlUpdateRow,
    .update    = dataCtrlUpdate,
    .addNew    = dataCtrlAddNew,
    .delete         = dataCtrlDelete,
    .setMasterValue = dataCtrlSetMasterValue,
    .getRowCount    = dataCtrlGetRowCount,
    .getColCount    = dataCtrlGetColCount,
    .getColName     = dataCtrlGetColName,
    .getCellText    = dataCtrlGetCellText,
    .setCurrentRow  = dataCtrlSetCurrentRow,
};

static const WgtPropDescT sProps[] = {
    { "DatabaseName",  WGT_IFACE_STRING, (void *)dataCtrlGetDatabaseName,  (void *)dataCtrlSetDatabaseName,  NULL },
    { "RecordSource",  WGT_IFACE_STRING, (void *)dataCtrlGetRecordSource,  (void *)dataCtrlSetRecordSource,  NULL },
    { "KeyColumn",     WGT_IFACE_STRING, (void *)dataCtrlGetKeyColumn,     (void *)dataCtrlSetKeyColumn,     NULL },
    { "MasterSource",  WGT_IFACE_STRING, (void *)dataCtrlGetMasterSource,  (void *)dataCtrlSetMasterSource,  NULL },
    { "MasterField",   WGT_IFACE_STRING, (void *)dataCtrlGetMasterField,   (void *)dataCtrlSetMasterField,   NULL },
    { "DetailField",   WGT_IFACE_STRING, (void *)dataCtrlGetDetailField,   (void *)dataCtrlSetDetailField,   NULL },
    { "Caption",       WGT_IFACE_STRING, (void *)dataCtrlGetCaption,       (void *)dataCtrlSetCaption,       NULL },
    { "BOF",           WGT_IFACE_BOOL,   (void *)dataCtrlGetBof,           NULL,                             NULL },
    { "EOF",           WGT_IFACE_BOOL,   (void *)dataCtrlGetEof,           NULL,                             NULL },
};

static const WgtMethodDescT sMethods[] = {
    { "AddNew",       WGT_SIG_VOID, (void *)dataCtrlAddNew },
    { "Delete",       WGT_SIG_VOID, (void *)dataCtrlDelete },
    { "MoveFirst",    WGT_SIG_VOID, (void *)dataCtrlMoveFirst },
    { "MoveLast",     WGT_SIG_VOID, (void *)dataCtrlMoveLast },
    { "MoveNext",     WGT_SIG_VOID, (void *)dataCtrlMoveNext },
    { "MovePrevious", WGT_SIG_VOID, (void *)dataCtrlMovePrev },
    { "Refresh",      WGT_SIG_VOID, (void *)dataCtrlRefresh },
    { "Update",       WGT_SIG_VOID, (void *)dataCtrlUpdate },
};

static const WgtEventDescT sEvents[] = {
    { "Reposition" },
    { "Validate" },
};

static const WgtIfaceT sIface = {
    .basName      = "Data",
    .props        = sProps,
    .propCount    = 9,
    .methods      = sMethods,
    .methodCount  = 8,
    .events       = sEvents,
    .eventCount   = 2,
    .createSig    = WGT_CREATE_PARENT,
    .isContainer  = false,
    .defaultEvent = "Reposition",
    .namePrefix   = "Data",
};


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