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

// frmParser.c -- callback-based .frm text parser
//
// See frmParser.h for the public API.

#include "frmParser.h"
#include "dvxPlat.h"

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

#define FRM_MAX_VB_VERSION    2.0  // highest VB form VERSION we can import (VB4+ rejected)


// Prototypes (alphabetical)
bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb);
bool frmParseBool(const char *val);
bool frmParseBoolDefault(const char *val, bool defaultValue);
void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax);
void frmStripQuotes(char *val);
static bool readToken(const char **p, char *buf, int32_t bufMax);


typedef enum {
    BLK_FORM,
    BLK_MENU,
    BLK_CTRL
} BlkTypeE;


bool frmParse(const char *source, int32_t sourceLen, const FrmParserCbsT *cb) {
    if (!source || sourceLen <= 0 || !cb) {
        return false;
    }

    BlkTypeE blockStack[FRM_MAX_NESTING];
    int32_t  blockDepth    = 0;
    int32_t  menuNestDepth = 0;
    bool     inForm        = false;

    const char *pos = source;
    const char *end = source + sourceLen;

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

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

        int32_t lineLen = (int32_t)(pos - lineStart);

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

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

        char line[FRM_MAX_LINE_LEN];

        if (lineLen >= FRM_MAX_LINE_LEN) {
            lineLen = FRM_MAX_LINE_LEN - 1;
        }

        memcpy(line, lineStart, lineLen);
        line[lineLen] = '\0';

        const char *trimmed = dvxSkipWs(line);

        if (*trimmed == '\0' || *trimmed == '\'') {
            continue;
        }

        // "VERSION DVX x.xx" (native) or "VERSION x.xx" (VB import)
        if (strncasecmp(trimmed, "VERSION ", sizeof("VERSION ") - 1) == 0) {
            const char *ver = trimmed + sizeof("VERSION ") - 1;

            if (strncasecmp(ver, "DVX ", sizeof("DVX ") - 1) != 0) {
                double vbVer = atof(ver);

                if (vbVer > FRM_MAX_VB_VERSION) {
                    return false; // VB4+ form, not compatible
                }
            }

            continue;
        }

        // "Begin TypeName CtrlName"
        if (strncasecmp(trimmed, "Begin ", sizeof("Begin ") - 1) == 0) {
            const char *rest = trimmed + sizeof("Begin ") - 1;
            char        typeName[FRM_MAX_TOKEN_LEN];
            char        ctrlName[FRM_MAX_TOKEN_LEN];
            BlkTypeE    blkType;
            bool        doPush = false;

            if (!readToken(&rest, typeName, FRM_MAX_TOKEN_LEN)) {
                continue;
            }

            rest = dvxSkipWs(rest);
            readToken(&rest, ctrlName, FRM_MAX_TOKEN_LEN);

            // Classify the block first so overflow is checked once and all
            // begin side-effects stay in lockstep with the stack push.
            if (strcasecmp(typeName, "Form") == 0) {
                // A second Begin Form inside an open form is malformed: its
                // End would otherwise terminate the whole file early and
                // silently drop every control after it.
                if (inForm) {
                    return false;
                }

                blkType = BLK_FORM;
                doPush  = true;
            } else if (strcasecmp(typeName, "Menu") == 0 && inForm) {
                blkType = BLK_MENU;
                doPush  = true;
            } else if (inForm) {
                blkType = BLK_CTRL;
                doPush  = true;
            }

            if (!doPush) {
                continue;
            }

            // Hard-error on overflow before any callback or state mutation so
            // begins and pushes never desync.
            if (blockDepth >= FRM_MAX_NESTING) {
                return false;
            }

            switch (blkType) {
                case BLK_FORM:
                    if (cb->onFormBegin && !cb->onFormBegin(cb->userData, ctrlName)) {
                        return false;
                    }

                    inForm = true;
                    break;

                case BLK_MENU:
                    if (cb->onMenuBegin) {
                        cb->onMenuBegin(cb->userData, ctrlName, menuNestDepth);
                    }

                    menuNestDepth++;
                    break;

                case BLK_CTRL:
                    if (cb->onCtrlBegin) {
                        cb->onCtrlBegin(cb->userData, typeName, ctrlName);
                    }

                    break;
            }

            blockStack[blockDepth++] = blkType;

            continue;
        }

        // "End"
        if (strcasecmp(trimmed, "End") == 0) {
            if (blockDepth == 0) {
                continue;
            }

            blockDepth--;
            BlkTypeE blk = blockStack[blockDepth];

            if (blk == BLK_MENU) {
                if (cb->onMenuEnd) {
                    cb->onMenuEnd(cb->userData);
                }

                menuNestDepth--;

                if (menuNestDepth < 0) {
                    menuNestDepth = 0;
                }
            } else if (blk == BLK_CTRL) {
                if (cb->onCtrlEnd) {
                    cb->onCtrlEnd(cb->userData);
                }
            } else {
                // Outer Form End -- deliver any trailing text and stop.
                inForm = false;

                if (cb->onFormEnd) {
                    cb->onFormEnd(cb->userData, pos, (int32_t)(end - pos));
                }

                return true;
            }

            continue;
        }

        // Property assignment: Key = Value
        char key[FRM_MAX_TOKEN_LEN];
        char value[FRM_MAX_LINE_LEN];
        frmParseKeyValue(trimmed, key, FRM_MAX_TOKEN_LEN, value, FRM_MAX_LINE_LEN);

        if (key[0] == '\0' || !inForm) {
            continue;
        }

        BlkTypeE curBlock = (blockDepth > 0) ? blockStack[blockDepth - 1] : BLK_FORM;

        if (curBlock == BLK_MENU) {
            if (cb->onMenuProp) {
                cb->onMenuProp(cb->userData, key, value);
            }
        } else if (curBlock == BLK_CTRL) {
            if (cb->onCtrlProp) {
                cb->onCtrlProp(cb->userData, key, value);
            }
        } else {
            if (cb->onFormProp) {
                cb->onFormProp(cb->userData, key, value);
            }
        }
    }

    return true;
}


bool frmParseBool(const char *val) {
    return frmParseBoolDefault(val, false);
}


bool frmParseBoolDefault(const char *val, bool defaultValue) {
    if (!val) {
        return defaultValue;
    }

    if (strcasecmp(val, "True") == 0 || strcmp(val, "-1") == 0) {
        return true;
    }

    if (strcasecmp(val, "False") == 0 || strcmp(val, "0") == 0) {
        return false;
    }

    return defaultValue;
}


void frmParseKeyValue(const char *line, char *key, int32_t keyMax, char *value, int32_t valueMax) {
    key[0]   = '\0';
    value[0] = '\0';

    line = dvxSkipWs(line);

    int32_t ki = 0;

    while (*line && *line != '=' && *line != ' ' && *line != '\t' && ki < keyMax - 1) {
        key[ki++] = *line++;
    }

    key[ki] = '\0';

    line = dvxSkipWs(line);

    if (*line == '=') {
        line++;
    }

    line = dvxSkipWs(line);

    int32_t vi = 0;

    while (*line && *line != '\r' && *line != '\n' && vi < valueMax - 1) {
        value[vi++] = *line++;
    }

    value[vi] = '\0';

    while (vi > 0 && (value[vi - 1] == ' ' || value[vi - 1] == '\t')) {
        value[--vi] = '\0';
    }
}


void frmStripQuotes(char *val) {
    if (!val || val[0] != '"') {
        return;
    }

    int32_t len = (int32_t)strlen(val);

    if (len >= 2 && val[len - 1] == '"') {
        memmove(val, val + 1, len - 2);
        val[len - 2] = '\0';
    } else if (len >= 1) {
        // Unterminated leading quote -- strip just the leading "
        memmove(val, val + 1, len - 1);
        val[len - 1] = '\0';
    }
}


// Read a whitespace-delimited token starting at *p.  Advances *p past
// the WHOLE token even when it overflows buf (the stored copy is
// truncated), so an over-long type name never bleeds into the control
// name read next.  Returns false if no token was present.
static bool readToken(const char **p, char *buf, int32_t bufMax) {
    const char *cur = *p;
    int32_t     len = 0;

    while (*cur && *cur != ' ' && *cur != '\t' && *cur != '\r' && *cur != '\n') {
        if (len < bufMax - 1) {
            buf[len++] = *cur;
        }

        cur++;
    }

    buf[len] = '\0';
    *p       = cur;
    return (len > 0);
}
