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

// dvxPrefs.c  -- INI-based preferences system (read/write)
//
// Handle-based: each PrefsHandleT holds its own entry array and file
// path. Multiple INI files can be open simultaneously.

#include "dvxPrefs.h"
#include "platform/dvxPlat.h"

#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "dvxMem.h"

#include "thirdparty/stb_ds_wrap.h"


// ============================================================
// Internal types
// ============================================================

typedef struct {
    char *section;
    char *key;
    char *value;
} PrefsEntryT;

// Comment lines are stored to preserve them on save. A comment has
// key=NULL and value=the full line text (including the ; prefix).
// Section headers have key=NULL and value=NULL.

struct PrefsHandleT {
    PrefsEntryT *entries;  // stb_ds dynamic array
    char        *filePath; // path used by prefsSave
};

// Maximum physical INI line length, including the NUL. Lines longer than
// this are read in fragments by fgets; the remainder is discarded so an
// over-long value cannot become bogus entries.
#define PREFS_LINE_MAX 512


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

static char    *dupStr(const char *s);
static int32_t  findEntry(PrefsHandleT *h, const char *section, const char *key);
static int32_t  findSection(PrefsHandleT *h, const char *section);
static void     freeEntry(PrefsEntryT *e);
static int      strcmpci(const char *a, const char *b);
static char    *trimInPlace(char *buf);


// ============================================================
// Helpers
// ============================================================

static char *dupStr(const char *s) {
    if (!s) {
        return NULL;
    }

    size_t len = strlen(s);
    char  *d   = (char *)malloc(len + 1);

    if (d) {
        memcpy(d, s, len + 1);
    }

    return d;
}


static int32_t findEntry(PrefsHandleT *h, const char *section, const char *key) {
    for (int32_t i = 0; i < arrlen(h->entries); i++) {
        PrefsEntryT *e = &h->entries[i];

        if (e->key && e->section &&
            strcmpci(e->section, section) == 0 &&
            strcmpci(e->key, key) == 0) {
            return i;
        }
    }

    return -1;
}


static int32_t findSection(PrefsHandleT *h, const char *section) {
    for (int32_t i = 0; i < arrlen(h->entries); i++) {
        PrefsEntryT *e = &h->entries[i];

        if (!e->key && !e->value && e->section &&
            strcmpci(e->section, section) == 0) {
            return i;
        }
    }

    return -1;
}


static void freeEntry(PrefsEntryT *e) {
    free(e->section);
    free(e->key);
    free(e->value);
    e->section = NULL;
    e->key     = NULL;
    e->value   = NULL;
}


static int strcmpci(const char *a, const char *b) {
    for (;;) {
        int d = tolower((unsigned char)*a) - tolower((unsigned char)*b);

        if (d != 0 || !*a) {
            return d;
        }

        a++;
        b++;
    }
}


static char *trimInPlace(char *buf) {
    char *p = (char *)dvxSkipWs(buf);
    dvxTrimRight(p);
    return p;
}


// ============================================================
// Public API
// ============================================================

void prefsClose(PrefsHandleT *h) {
    if (!h) {
        return;
    }

    for (int32_t i = 0; i < arrlen(h->entries); i++) {
        freeEntry(&h->entries[i]);
    }

    arrfree(h->entries);
    free(h->filePath);
    free(h);
}


PrefsHandleT *prefsCreate(void) {
    PrefsHandleT *h = (PrefsHandleT *)calloc(1, sizeof(PrefsHandleT));
    return h;
}


bool prefsGetBool(PrefsHandleT *h, const char *section, const char *key, bool defaultVal) {
    const char *val = prefsGetString(h, section, key, NULL);

    if (!val) {
        return defaultVal;
    }

    char c = (char)tolower((unsigned char)val[0]);

    if (c == 't' || c == 'y' || c == '1') {
        return true;
    }

    if (c == 'f' || c == 'n' || c == '0') {
        return false;
    }

    return defaultVal;
}


int32_t prefsGetInt(PrefsHandleT *h, const char *section, const char *key, int32_t defaultVal) {
    const char *val = prefsGetString(h, section, key, NULL);

    if (!val) {
        return defaultVal;
    }

    char *end = NULL;
    long  n   = strtol(val, &end, 10);

    if (end == val) {
        return defaultVal;
    }

    return (int32_t)n;
}


const char *prefsGetString(PrefsHandleT *h, const char *section, const char *key, const char *defaultVal) {
    if (!h) {
        return defaultVal;
    }

    int32_t idx = findEntry(h, section, key);

    if (idx < 0) {
        return defaultVal;
    }

    return h->entries[idx].value;
}


PrefsHandleT *prefsLoad(const char *filename) {
    PrefsHandleT *h = prefsCreate();

    if (!h) {
        return NULL;
    }

    h->filePath = dupStr(filename);

    FILE *fp = fopen(filename, "rb");

    if (!fp) {
        return h;
    }

    char  line[PREFS_LINE_MAX];
    char *currentSection = dupStr("");

    while (fgets(line, sizeof(line), fp)) {
        // Detect a physical line longer than the buffer (fgets returns it in
        // fragments) and discard the remainder, so an over-long value can't
        // be re-parsed as bogus key=value / comment entries.  Capture this
        // before dvxTrimRight strips the trailing newline.
        size_t lineLen   = strlen(line);
        bool   truncated = (lineLen == sizeof(line) - 1 && line[lineLen - 1] != '\n');

        if (truncated) {
            int ch;

            while ((ch = fgetc(fp)) != '\n' && ch != EOF) {
                // discard the remainder of the over-long physical line
            }
        }

        dvxTrimRight(line);

        char *p = (char *)dvxSkipWs(line);

        if (*p == '\0') {
            PrefsEntryT e = {0};
            e.section = dupStr(currentSection);
            e.value   = dupStr("");
            arrput(h->entries, e);
            continue;
        }

        if (*p == ';' || *p == '#') {
            PrefsEntryT e = {0};
            e.section = dupStr(currentSection);
            e.value   = dupStr(line);
            arrput(h->entries, e);
            continue;
        }

        if (*p == '[') {
            char *close = strchr(p, ']');

            if (close) {
                *close = '\0';
                free(currentSection);
                currentSection = dupStr(trimInPlace(p + 1));

                PrefsEntryT e = {0};
                e.section = dupStr(currentSection);
                arrput(h->entries, e);
            }

            continue;
        }

        char *eq = strchr(p, '=');

        if (eq) {
            *eq = '\0';

            PrefsEntryT e = {0};
            e.section = dupStr(currentSection);
            e.key     = dupStr(trimInPlace(p));
            e.value   = dupStr(trimInPlace(eq + 1));
            arrput(h->entries, e);
        }
    }

    free(currentSection);
    fclose(fp);
    return h;
}


void prefsRemove(PrefsHandleT *h, const char *section, const char *key) {
    if (!h) {
        return;
    }

    int32_t idx = findEntry(h, section, key);

    if (idx >= 0) {
        freeEntry(&h->entries[idx]);
        arrdel(h->entries, idx);
    }
}


bool prefsSave(PrefsHandleT *h) {
    if (!h || !h->filePath) {
        return false;
    }

    return prefsSaveAs(h, h->filePath);
}


bool prefsSaveAs(PrefsHandleT *h, const char *filename) {
    if (!h) {
        return false;
    }

    FILE *fp = fopen(filename, "wb");

    if (!fp) {
        return false;
    }

    for (int32_t i = 0; i < arrlen(h->entries); i++) {
        PrefsEntryT *e = &h->entries[i];

        if (!e->key && e->value) {
            fprintf(fp, "%s\r\n", e->value);
            continue;
        }

        if (!e->key && !e->value) {
            fprintf(fp, "[%s]\r\n", e->section);
            continue;
        }

        if (e->key && e->value) {
            fprintf(fp, "%s = %s\r\n", e->key, e->value);
        }
    }

    fclose(fp);
    return true;
}


void prefsSetBool(PrefsHandleT *h, const char *section, const char *key, bool value) {
    prefsSetString(h, section, key, value ? "true" : "false");
}


void prefsSetInt(PrefsHandleT *h, const char *section, const char *key, int32_t value) {
    char buf[32];
    snprintf(buf, sizeof(buf), "%ld", (long)value);
    prefsSetString(h, section, key, buf);
}


void prefsSetString(PrefsHandleT *h, const char *section, const char *key, const char *value) {
    if (!h) {
        return;
    }

    // Normalize NULL to empty so every stored value is a valid string:
    // otherwise prefsGetString returns NULL instead of the default and
    // prefsSaveAs silently drops the key.
    if (!value) {
        value = "";
    }

    int32_t idx = findEntry(h, section, key);

    if (idx >= 0) {
        free(h->entries[idx].value);
        h->entries[idx].value = dupStr(value);
        return;
    }

    int32_t secIdx = findSection(h, section);

    if (secIdx < 0) {
        if (arrlen(h->entries) > 0) {
            PrefsEntryT blank = {0};
            blank.section = dupStr(section);
            blank.value   = dupStr("");
            arrput(h->entries, blank);
        }

        PrefsEntryT secEntry = {0};
        secEntry.section = dupStr(section);
        arrput(h->entries, secEntry);
        secIdx = arrlen(h->entries) - 1;
    }

    int32_t insertAt = secIdx + 1;

    while (insertAt < arrlen(h->entries)) {
        PrefsEntryT *e = &h->entries[insertAt];

        if (e->section && strcmpci(e->section, section) != 0) {
            break;
        }

        insertAt++;
    }

    PrefsEntryT newEntry = {0};
    newEntry.section = dupStr(section);
    newEntry.key     = dupStr(key);
    newEntry.value   = dupStr(value);
    arrins(h->entries, insertAt, newEntry);
}
