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

// obfuscate.c -- Release build name obfuscation
//
// See obfuscate.h for the high-level description.

#include "obfuscate.h"
#include "basEvents.h"
#include "lexer.h"
#include "../basRes.h"
#include "../runtime/values.h"

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

#define BAS_OBF_TOKEN_LEN     BAS_MAX_IDENT  // longest form/control/type token kept from a .frm line
#define BAS_OBF_MAPPED_LEN    16                 // "C" + decimal index + NUL
#define BAS_OBF_MAP_INIT_CAP  16                 // initial name-map capacity (doubles on growth)
#define BAS_REM_KEYWORD_LEN   3                  // "REM"

// ============================================================
// Name map
// ============================================================

typedef struct {
    char    *orig;   // original name (strdup'd, case-preserved)
    char    *mapped; // new name (strdup'd, "C1" .. "Cn")
} NameEntryT;

typedef struct {
    NameEntryT *entries;
    int32_t     count;
    int32_t     cap;
} NameMapT;


// Function prototypes (alphabetical)
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms);
int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap);
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved);
static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len);
static const char *nameMapAdd(NameMapT *m, const char *name);
static void nameMapFree(NameMapT *m);
static void nameMapInit(NameMapT *m);
static const char *nameMapLookup(const NameMapT *m, const char *name);
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize);
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText);
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap);
static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map);
static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map);
static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map);
static const char *skipWhitespace(const char *p, const char *end);

// ============================================================
// Top-level entry point
// ============================================================
void basObfuscateNames(BasModuleT *mod, const char **frmTexts, const int32_t *frmLens, int32_t frmCount, BasObfFrmT *outFrms) {
    if (!mod || frmCount < 0) {
        return;
    }

    // Pass 1: collect all names from all .frm texts.  A control whose name
    // is also a control type or a property key (a TextBox named "Text", a
    // Timer named "Timer") is left alone: the constant pool cannot tell a
    // name reference apart from the identically spelled property/type name
    // used by OP_LOAD_PROP and friends.
    NameMapT names;
    NameMapT reserved;
    NameMapT map;

    nameMapInit(&names);
    nameMapInit(&reserved);
    nameMapInit(&map);

    for (int32_t i = 0; i < frmCount; i++) {
        if (frmTexts[i] && frmLens[i] > 0) {
            collectNamesFromFrm(frmTexts[i], frmLens[i], &names, &reserved);
        }
    }

    for (int32_t i = 0; i < names.count; i++) {
        if (!nameMapLookup(&reserved, names.entries[i].orig)) {
            nameMapAdd(&map, names.entries[i].orig);
        }
    }

    nameMapFree(&names);
    nameMapFree(&reserved);

    // Pass 2: rewrite each .frm, sized by a measuring pass so a form with
    // many short names that grow ("a" -> "C12") can never be truncated.
    for (int32_t i = 0; i < frmCount; i++) {
        outFrms[i].data = NULL;
        outFrms[i].len  = 0;

        if (!frmTexts[i] || frmLens[i] <= 0) {
            continue;
        }

        int32_t  strippedLen = basFindFormEndPos(frmTexts[i], frmLens[i]);
        int32_t  outCap      = rewriteFrmText(frmTexts[i], strippedLen, &map, NULL, 0) + 1;
        uint8_t *outBuf      = malloc(outCap);

        if (!outBuf) {
            continue;
        }

        int32_t outLen = rewriteFrmText(frmTexts[i], strippedLen, &map, outBuf, outCap);

        // Ensure trailing newline
        if (outLen > 0 && outBuf[outLen - 1] != '\n') {
            outBuf[outLen++] = '\n';
        }

        outFrms[i].data = outBuf;
        outFrms[i].len  = outLen;
    }

    // Pass 3: rewrite module
    rewriteModuleConstants(mod, &map);
    rewriteModuleProcs(mod, &map);
    rewriteModuleFormVars(mod, &map);

    nameMapFree(&map);
}


int32_t basStripFrmComments(const char *src, int32_t srcLen, uint8_t *outBuf, int32_t outCap) {
    if (!src || srcLen <= 0 || !outBuf || outCap <= 0) {
        return 0;
    }

    int32_t outLen = 0;
    int32_t i      = 0;

    while (i < srcLen) {
        int32_t lineStart = i;

        while (i < srcLen && src[i] != '\n' && src[i] != '\r') {
            i++;
        }

        int32_t lineEnd = i;

        if (i < srcLen && src[i] == '\r') {
            i++;
        }

        if (i < srcLen && src[i] == '\n') {
            i++;
        }

        // Scan for first unquoted ' (comment start).
        bool    inStr        = false;
        int32_t commentStart = -1;

        for (int32_t j = lineStart; j < lineEnd; j++) {
            char c = src[j];

            if (c == '"') {
                inStr = !inStr;
            } else if (c == '\'' && !inStr) {
                commentStart = j;
                break;
            }
        }

        int32_t contentEnd = (commentStart >= 0) ? commentStart : lineEnd;

        // Check for whole-line REM. Find first non-whitespace position.
        int32_t firstNonWs = lineStart;

        while (firstNonWs < contentEnd && (src[firstNonWs] == ' ' || src[firstNonWs] == '\t')) {
            firstNonWs++;
        }

        if (contentEnd - firstNonWs >= BAS_REM_KEYWORD_LEN &&
            strncasecmp(src + firstNonWs, "REM", BAS_REM_KEYWORD_LEN) == 0 &&
            (contentEnd - firstNonWs == BAS_REM_KEYWORD_LEN ||
             src[firstNonWs + BAS_REM_KEYWORD_LEN] == ' ' ||
             src[firstNonWs + BAS_REM_KEYWORD_LEN] == '\t')) {
            contentEnd = firstNonWs;
        }

        // Trim trailing whitespace.
        while (contentEnd > lineStart && (src[contentEnd - 1] == ' ' || src[contentEnd - 1] == '\t')) {
            contentEnd--;
        }

        // Drop lines that have no non-whitespace content.
        if (contentEnd <= firstNonWs) {
            continue;
        }

        // Strip leading whitespace -- the form parser trims per line
        // anyway, so shipping indentation just bloats the embedded resource.
        int32_t writeLen = contentEnd - firstNonWs;

        if (outLen + writeLen + 1 >= outCap) {
            break;
        }

        memcpy(outBuf + outLen, src + firstNonWs, writeLen);
        outLen += writeLen;
        outBuf[outLen++] = '\n';
    }

    return outLen;
}


// ============================================================
// Pass 1: collect form/control names and the identifiers they must not
// collide with
// ============================================================

// Scan a .frm text: every "Begin <Type> <Name>" adds Name to names, and
// Type plus every "<Key> = ..." property key goes into reserved.
static void collectNamesFromFrm(const char *text, int32_t len, NameMapT *names, NameMapT *reserved) {
    const char *p   = text;
    const char *end = text + len;

    while (p < end) {
        // Read one line
        const char *lineStart = p;

        while (p < end && *p != '\n' && *p != '\r') {
            p++;
        }

        const char *lineEnd = p;

        if (p < end && *p == '\r') {
            p++;
        }

        if (p < end && *p == '\n') {
            p++;
        }

        // Trim leading whitespace
        const char *l = skipWhitespace(lineStart, lineEnd);
        char        token[BAS_OBF_TOKEN_LEN];

        if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
            l = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
            l = readToken(l, lineEnd, token, sizeof(token));

            if (token[0] == '\0') {
                continue;
            }

            nameMapAdd(reserved, token);

            l = skipWhitespace(l, lineEnd);
            readToken(l, lineEnd, token, sizeof(token));

            if (token[0] && basIsValidIdent(token)) {
                nameMapAdd(names, token);
            }

            continue;
        }

        // "<Key> = value": the key is a property name.
        l = readToken(l, lineEnd, token, sizeof(token));
        l = skipWhitespace(l, lineEnd);

        if (l < lineEnd && *l == '=' && token[0] && basIsValidIdent(token)) {
            nameMapAdd(reserved, token);
        }
    }
}


// ============================================================
// Pass 2: rewrite .frm text with mapped names
// ============================================================

// Appends len bytes of src to out (bounded by outCap) and always advances
// *outLen, so a NULL out measures the exact output size.
static void emitBytes(uint8_t *out, int32_t outCap, int32_t *outLen, const char *src, int32_t len) {
    if (out && *outLen + len <= outCap) {
        memcpy(out + *outLen, src, len);
    }

    *outLen += len;
}


// Returns true if c is a valid identifier character.
static const char *nameMapAdd(NameMapT *m, const char *name) {
    const char *existing = nameMapLookup(m, name);

    if (existing) {
        return existing;
    }

    if (m->count >= m->cap) {
        int32_t     newCap     = m->cap == 0 ? BAS_OBF_MAP_INIT_CAP : m->cap * 2;
        NameEntryT *newEntries = realloc(m->entries, newCap * sizeof(NameEntryT));

        if (!newEntries) {
            return NULL;
        }

        m->entries = newEntries;
        m->cap     = newCap;
    }

    char mapped[BAS_OBF_MAPPED_LEN];
    snprintf(mapped, sizeof(mapped), "C%ld", (long)(m->count + 1));

    m->entries[m->count].orig   = strdup(name);
    m->entries[m->count].mapped = strdup(mapped);
    m->count++;

    return m->entries[m->count - 1].mapped;
}


static void nameMapFree(NameMapT *m) {
    for (int32_t i = 0; i < m->count; i++) {
        free(m->entries[i].orig);
        free(m->entries[i].mapped);
    }

    free(m->entries);
    m->entries = NULL;
    m->count   = 0;
    m->cap     = 0;
}


static void nameMapInit(NameMapT *m) {
    m->entries = NULL;
    m->count   = 0;
    m->cap     = 0;
}


// Look up an original name (case-insensitive). Returns mapped name or NULL.
static const char *nameMapLookup(const NameMapT *m, const char *name) {
    for (int32_t i = 0; i < m->count; i++) {
        if (strcasecmp(m->entries[i].orig, name) == 0) {
            return m->entries[i].mapped;
        }
    }

    return NULL;
}


// Copy next whitespace-delimited token into buf. Returns pointer after token.
static const char *readToken(const char *p, const char *end, char *buf, int32_t bufSize) {
    int32_t len = 0;

    while (p < end && *p != ' ' && *p != '\t' && *p != '\r' && *p != '\n' && len < bufSize - 1) {
        buf[len++] = *p++;
    }

    buf[len] = '\0';
    return p;
}


// ============================================================
// Module rewriting
// ============================================================

// Replace the contents of a constant pool entry with a new string.
static void replaceConstant(BasModuleT *mod, int32_t idx, const char *newText) {
    BasStringT *newStr = basStringNew(newText, (int32_t)strlen(newText));

    if (!newStr) {
        return;
    }

    basStringUnref(mod->constants[idx]);
    mod->constants[idx] = newStr;
}


// Rewrites the .frm text line by line.  Only positions that hold a
// form/control NAME are remapped: the name token of a "Begin <Type> <Name>"
// line and a property value that is exactly a mapped name (bare, or the
// whole content of a quoted string such as DataSource = "datCat").  Type
// tokens and property keys are copied verbatim.  Identifiers of any length
// are copied from the source span, never through a fixed buffer.  With out
// NULL nothing is written and the return value is the required size.
static int32_t rewriteFrmText(const char *src, int32_t srcLen, const NameMapT *map, uint8_t *out, int32_t outCap) {
    int32_t     outLen = 0;
    const char *p      = src;
    const char *end    = src + srcLen;

    while (p < end) {
        const char *lineStart = p;

        while (p < end && *p != '\n' && *p != '\r') {
            p++;
        }

        const char *lineEnd = p;

        if (p < end && *p == '\r') {
            p++;
        }

        if (p < end && *p == '\n') {
            p++;
        }

        const char *l = skipWhitespace(lineStart, lineEnd);
        const char *replaceStart = NULL;   // span of the original name to swap
        const char *replaceEnd   = NULL;
        const char *mapped       = NULL;
        char        token[BAS_OBF_TOKEN_LEN];

        if ((lineEnd - l) >= BAS_BEGIN_PREFIX_LEN && strncasecmp(l, "Begin ", BAS_BEGIN_PREFIX_LEN) == 0) {
            // Begin <Type> <Name>: only the name token is a candidate.
            const char *t = skipWhitespace(l + BAS_BEGIN_PREFIX_LEN, lineEnd);
            t = readToken(t, lineEnd, token, sizeof(token));
            t = skipWhitespace(t, lineEnd);

            const char *nameStart = t;
            t = readToken(t, lineEnd, token, sizeof(token));

            if (token[0] && t - nameStart == (int32_t)strlen(token)) {
                mapped       = nameMapLookup(map, token);
                replaceStart = nameStart;
                replaceEnd   = t;
            }
        } else {
            // <Key> = value: a value that is exactly a mapped name (bare or
            // quoted) refers to a control and follows the rename.
            const char *t = readToken(l, lineEnd, token, sizeof(token));
            t = skipWhitespace(t, lineEnd);

            if (t < lineEnd && *t == '=') {
                t = skipWhitespace(t + 1, lineEnd);

                const char *valueStart = t;
                const char *valueEnd   = lineEnd;

                while (valueEnd > valueStart && (valueEnd[-1] == ' ' || valueEnd[-1] == '\t')) {
                    valueEnd--;
                }

                if (valueEnd - valueStart >= 2 && *valueStart == '"' && valueEnd[-1] == '"') {
                    valueStart++;
                    valueEnd--;
                }

                int32_t valueLen = (int32_t)(valueEnd - valueStart);
                bool    isIdent  = (valueLen > 0 && valueLen < (int32_t)sizeof(token));

                for (int32_t k = 0; isIdent && k < valueLen; k++) {
                    isIdent = basIsIdentChar(valueStart[k]);
                }

                if (isIdent) {
                    memcpy(token, valueStart, valueLen);
                    token[valueLen] = '\0';
                    mapped          = nameMapLookup(map, token);
                    replaceStart    = valueStart;
                    replaceEnd      = valueEnd;
                }
            }
        }

        if (mapped) {
            emitBytes(out, outCap, &outLen, lineStart, (int32_t)(replaceStart - lineStart));
            emitBytes(out, outCap, &outLen, mapped, (int32_t)strlen(mapped));
            emitBytes(out, outCap, &outLen, replaceEnd, (int32_t)(p - replaceEnd));
        } else {
            emitBytes(out, outCap, &outLen, lineStart, (int32_t)(p - lineStart));
        }
    }

    return outLen;
}


static void rewriteModuleConstants(BasModuleT *mod, const NameMapT *map) {
    // KNOWN LIMITATION: the constant pool dedupes a plain string literal and a
    // same-text control/form-name reference into ONE entry, so a release build
    // that does e.g. MsgBox "Save" while also having a control named "Save"
    // will see that literal rewritten to the obfuscated name ("C5").  Fully
    // separating name references from literals would give name refs their own
    // pool slots and change emitted bytecode for every release build, so it is
    // deferred; avoid string literals that exactly match a control/form name
    // in release builds.  Names that collide with a property key or control
    // type are never mapped at all (see basObfuscateNames).
    for (int32_t i = 0; i < mod->constCount; i++) {
        const BasStringT *s = mod->constants[i];

        if (!s) {
            continue;
        }

        const char *mapped = nameMapLookup(map, s->data);

        if (mapped) {
            replaceConstant(mod, i, mapped);
            continue;
        }

        // "<Name>_<Event>" handler names used as SetEvent targets follow the
        // procedure rename so basModuleFindProc still resolves them.
        const char *underscore = strrchr(s->data, '_');

        if (!underscore || !basEventSuffixMatch(underscore + 1)) {
            continue;
        }

        int32_t prefixLen = (int32_t)(underscore - s->data);
        char    prefix[BAS_MAX_IDENT];

        if (prefixLen >= (int32_t)sizeof(prefix)) {
            continue;
        }

        memcpy(prefix, s->data, prefixLen);
        prefix[prefixLen] = '\0';
        mapped            = nameMapLookup(map, prefix);

        if (mapped) {
            char newName[BAS_MAX_IDENT];
            snprintf(newName, sizeof(newName), "%s_%s", mapped, underscore + 1);
            replaceConstant(mod, i, newName);
        }
    }
}


static void rewriteModuleFormVars(BasModuleT *mod, const NameMapT *map) {
    for (int32_t i = 0; i < mod->formVarInfoCount; i++) {
        BasFormVarInfoT *fv = &mod->formVarInfo[i];
        const char      *mapped = nameMapLookup(map, fv->formName);

        if (mapped) {
            snprintf(fv->formName, sizeof(fv->formName), "%s", mapped);
        }
    }
}


static void rewriteModuleProcs(BasModuleT *mod, const NameMapT *map) {
    for (int32_t i = 0; i < mod->procCount; i++) {
        BasProcEntryT *proc = &mod->procs[i];

        if (proc->name[0] == '\0') {
            continue;
        }

        // Remap the owning form name (used at runtime to bind form-scope
        // variables).  The form itself gets renamed by the same pass.
        if (proc->formName[0]) {
            const char *mappedForm = nameMapLookup(map, proc->formName);

            if (mappedForm) {
                snprintf(proc->formName, sizeof(proc->formName), "%s", mappedForm);
            }
        }

        // Find last underscore
        char *underscore = strrchr(proc->name, '_');

        if (!underscore) {
            continue;
        }

        const char *suffix = underscore + 1;

        if (!basEventSuffixMatch(suffix)) {
            continue;
        }

        // Split on underscore
        int32_t prefixLen = (int32_t)(underscore - proc->name);
        char    prefix[BAS_MAX_IDENT];

        if (prefixLen >= (int32_t)sizeof(prefix)) {
            prefixLen = (int32_t)sizeof(prefix) - 1;
        }

        memcpy(prefix, proc->name, prefixLen);
        prefix[prefixLen] = '\0';

        const char *mapped = nameMapLookup(map, prefix);

        if (mapped) {
            char newName[BAS_MAX_IDENT];
            snprintf(newName, sizeof(newName), "%s_%s", mapped, suffix);
            snprintf(proc->name, sizeof(proc->name), "%s", newName);
        }
    }
}


// ============================================================
// .frm parsing helpers
// ============================================================

// Skip ASCII whitespace. Returns pointer past whitespace.
static const char *skipWhitespace(const char *p, const char *end) {
    while (p < end && (*p == ' ' || *p == '\t')) {
        p++;
    }

    return p;
}
