// 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.

// dvxSql.c -- DVX SQL database implementation
//
// Wraps SQLite3 with integer handle management. Databases and
// cursors are stored in dynamically-grown tables indexed by handle.
// Handle 0 is reserved (invalid); handles are 1-based indices.

#include "dvxSql.h"
#include "thirdparty/sqlite/examples/sqlite3.h"

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

// ============================================================
// Handle tables
// ============================================================

typedef struct {
    sqlite3 *db;
    char     lastError[256];
    int32_t  affectedRows;
} DbEntryT;

typedef struct {
    sqlite3_stmt *stmt;
    int32_t       dbHandle; // which database owns this cursor
    bool          eof;      // true after sqlite3_step returns SQLITE_DONE
} CursorEntryT;

// Dynamically grown handle tables. sDbsCount/sCursorsCount track allocated slots.
static DbEntryT     *sDbs         = NULL;
static int32_t       sDbsCount    = 0;
static CursorEntryT *sCursors     = NULL;
static int32_t       sCursorsCount = 0;

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

int32_t              dvxSqlAffectedRows(int32_t db);
void                 dvxSqlClose(int32_t db);
bool                 dvxSqlEof(int32_t rs);
const char          *dvxSqlError(int32_t db);
int32_t              dvxSqlEscape(const char *src, char *dst, int32_t dstSize);
bool                 dvxSqlExec(int32_t db, const char *sql);
const char          *dvxSqlFieldByName(int32_t rs, const char *name);
int32_t              dvxSqlFieldCount(int32_t rs);
double               dvxSqlFieldDbl(int32_t rs, int32_t col);
int32_t              dvxSqlFieldInt(int32_t rs, int32_t col);
const char          *dvxSqlFieldName(int32_t rs, int32_t col);
const char          *dvxSqlFieldText(int32_t rs, int32_t col);
void                 dvxSqlFreeResult(int32_t rs);
bool                 dvxSqlNext(int32_t rs);
int32_t              dvxSqlOpen(const char *path);
int32_t              dvxSqlQuery(int32_t db, const char *sql);
static CursorEntryT *getCursor(int32_t handle);
static DbEntryT     *getDb(int32_t handle);


int32_t dvxSqlAffectedRows(int32_t db) {
    DbEntryT *entry = getDb(db);

    if (!entry) {
        return 0;
    }

    return entry->affectedRows;
}


void dvxSqlClose(int32_t db) {
    DbEntryT *entry = getDb(db);

    if (!entry) {
        return;
    }

    // Close any open cursors belonging to this database
    for (int32_t i = 0; i < sCursorsCount; i++) {
        if (sCursors[i].stmt && sCursors[i].dbHandle == db) {
            sqlite3_finalize(sCursors[i].stmt);
            memset(&sCursors[i], 0, sizeof(CursorEntryT));
        }
    }

    sqlite3_close(entry->db);
    memset(entry, 0, sizeof(DbEntryT));
}


bool dvxSqlEof(int32_t rs) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return true;
    }

    return cur->eof;
}


const char *dvxSqlError(int32_t db) {
    DbEntryT *entry = getDb(db);

    if (!entry) {
        return "Invalid database handle";
    }

    return entry->lastError;
}


int32_t dvxSqlEscape(const char *src, char *dst, int32_t dstSize) {
    if (!src || !dst || dstSize <= 0) {
        return -1;
    }

    int32_t di = 0;

    for (int32_t si = 0; src[si]; si++) {
        if (src[si] == '\'') {
            if (di + 2 >= dstSize) {
                return -1;
            }

            dst[di++] = '\'';
            dst[di++] = '\'';
        } else {
            if (di + 1 >= dstSize) {
                return -1;
            }

            dst[di++] = src[si];
        }
    }

    dst[di] = '\0';
    return di;
}


bool dvxSqlExec(int32_t db, const char *sql) {
    DbEntryT *entry = getDb(db);

    if (!entry || !sql) {
        return false;
    }

    char *errMsg = NULL;
    int rc = sqlite3_exec(entry->db, sql, NULL, NULL, &errMsg);

    if (rc != SQLITE_OK) {
        if (errMsg) {
            snprintf(entry->lastError, sizeof(entry->lastError), "%s", errMsg);
            sqlite3_free(errMsg);
        } else {
            snprintf(entry->lastError, sizeof(entry->lastError), "%s", sqlite3_errmsg(entry->db));
        }

        return false;
    }

    entry->lastError[0] = '\0';
    entry->affectedRows = sqlite3_changes(entry->db);
    return true;
}


const char *dvxSqlFieldByName(int32_t rs, const char *name) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur || !name) {
        return "";
    }

    int32_t colCount = sqlite3_column_count(cur->stmt);

    for (int32_t i = 0; i < colCount; i++) {
        const char *colName = sqlite3_column_name(cur->stmt, i);

        if (colName && strcasecmp(colName, name) == 0) {
            const char *text = (const char *)sqlite3_column_text(cur->stmt, i);
            return text ? text : "";
        }
    }

    return "";
}


int32_t dvxSqlFieldCount(int32_t rs) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return 0;
    }

    return sqlite3_column_count(cur->stmt);
}


double dvxSqlFieldDbl(int32_t rs, int32_t col) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return 0.0;
    }

    return sqlite3_column_double(cur->stmt, col);
}


int32_t dvxSqlFieldInt(int32_t rs, int32_t col) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return 0;
    }

    return sqlite3_column_int(cur->stmt, col);
}


const char *dvxSqlFieldName(int32_t rs, int32_t col) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return "";
    }

    const char *name = sqlite3_column_name(cur->stmt, col);
    return name ? name : "";
}


const char *dvxSqlFieldText(int32_t rs, int32_t col) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return "";
    }

    const char *text = (const char *)sqlite3_column_text(cur->stmt, col);
    return text ? text : "";
}


void dvxSqlFreeResult(int32_t rs) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur) {
        return;
    }

    sqlite3_finalize(cur->stmt);
    memset(cur, 0, sizeof(CursorEntryT));
}


bool dvxSqlNext(int32_t rs) {
    CursorEntryT *cur = getCursor(rs);

    if (!cur || cur->eof) {
        return false;
    }

    int rc = sqlite3_step(cur->stmt);

    if (rc == SQLITE_ROW) {
        return true;
    }

    // Anything other than SQLITE_DONE is a real step error, not clean EOF;
    // record it so callers don't mistake a failure for end-of-results.
    if (rc != SQLITE_DONE) {
        DbEntryT *db = getDb(cur->dbHandle);

        if (db) {
            snprintf(db->lastError, sizeof(db->lastError), "%s", sqlite3_errmsg(db->db));
        }
    }

    cur->eof = true;
    return false;
}


int32_t dvxSqlOpen(const char *path) {
    if (!path) {
        return 0;
    }

    // Find a free slot (reuse after close)
    int32_t slot = -1;

    for (int32_t i = 0; i < sDbsCount; i++) {
        if (!sDbs[i].db) {
            slot = i;
            break;
        }
    }

    // No free slot -- grow the table
    if (slot < 0) {
        int32_t newCount = sDbsCount == 0 ? 8 : sDbsCount * 2;
        DbEntryT *grown  = (DbEntryT *)realloc(sDbs, sizeof(DbEntryT) * newCount);

        if (!grown) {
            return 0;
        }

        memset(grown + sDbsCount, 0, sizeof(DbEntryT) * (newCount - sDbsCount));
        sDbs       = grown;
        slot       = sDbsCount;
        sDbsCount  = newCount;
    }

    int rc = sqlite3_open(path, &sDbs[slot].db);

    if (rc != SQLITE_OK) {
        if (sDbs[slot].db) {
            snprintf(sDbs[slot].lastError, sizeof(sDbs[slot].lastError), "%s", sqlite3_errmsg(sDbs[slot].db));
            sqlite3_close(sDbs[slot].db);
            sDbs[slot].db = NULL;
        }

        return 0;
    }

    sDbs[slot].lastError[0] = '\0';
    sDbs[slot].affectedRows = 0;
    return slot + 1; // 1-based handle
}


int32_t dvxSqlQuery(int32_t db, const char *sql) {
    DbEntryT *entry = getDb(db);

    if (!entry || !sql) {
        return 0;
    }

    // Find a free cursor slot (reuse after free)
    int32_t slot = -1;

    for (int32_t i = 0; i < sCursorsCount; i++) {
        if (!sCursors[i].stmt) {
            slot = i;
            break;
        }
    }

    // No free slot -- grow the table
    if (slot < 0) {
        int32_t newCount    = sCursorsCount == 0 ? 16 : sCursorsCount * 2;
        CursorEntryT *grown = (CursorEntryT *)realloc(sCursors, sizeof(CursorEntryT) * newCount);

        if (!grown) {
            snprintf(entry->lastError, sizeof(entry->lastError), "Out of memory growing cursor table");
            return 0;
        }

        memset(grown + sCursorsCount, 0, sizeof(CursorEntryT) * (newCount - sCursorsCount));
        sCursors      = grown;
        slot          = sCursorsCount;
        sCursorsCount = newCount;
    }

    sqlite3_stmt *stmt = NULL;
    int rc = sqlite3_prepare_v2(entry->db, sql, -1, &stmt, NULL);

    if (rc != SQLITE_OK) {
        snprintf(entry->lastError, sizeof(entry->lastError), "%s", sqlite3_errmsg(entry->db));
        return 0;
    }

    entry->lastError[0]     = '\0';
    sCursors[slot].stmt     = stmt;
    sCursors[slot].dbHandle = db;
    sCursors[slot].eof      = false;
    return slot + 1; // 1-based handle
}


static CursorEntryT *getCursor(int32_t handle) {
    if (handle <= 0 || handle > sCursorsCount) {
        return NULL;
    }

    if (!sCursors[handle - 1].stmt) {
        return NULL;
    }

    return &sCursors[handle - 1];
}


static DbEntryT *getDb(int32_t handle) {
    if (handle <= 0 || handle > sDbsCount) {
        return NULL;
    }

    if (!sDbs[handle - 1].db) {
        return NULL;
    }

    return &sDbs[handle - 1];
}
