// KEKS -- C99 KEKS encoder implementation
// Copyright (C) 2024-2025 Sergey Matveev <stargrave@stargrave.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as
// published by the Free Software Foundation, version 3 of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this program.  If not, see <http://www.gnu.org/licenses/>.

#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include <time.h>

#include "atom.h"
#include "dectai.h"
#include "err.h"
#include "frombe.h"
#include "items.h"
#include "schema.h"

static const char CmdEach[] = "*";
static const char CmdEq[] = "=";
static const char CmdExists[] = "E";
static const char CmdGT[] = ">";
static const char CmdLT[] = "<";
static const char CmdNotExists[] = "!E";
static const char CmdSchema[] = "S";
static const char CmdTake[] = ".";
static const char CmdTimePrec[] = "TP";
static const char CmdType[] = "T";
static const char CmdUTC[] = "UTC";

static const char TypeBin[] = "B";
static const char TypeBlob[] = "O";
static const char TypeBool[] = "b";
static const char TypeHexlet[] = "H";
static const char TypeInt[] = "I";
static const char TypeList[] = "L";
static const char TypeMagic[] = "K";
static const char TypeMap[] = "M";
static const char TypeNIL[] = "N";
static const char TypeStr[] = "S";
static const char TypeTAI[] = "T";

static struct KEKSSchemaErr
keksSchemaCmd(
    size_t *taken,
    bool *eachInList,
    bool *eachInMap,
    const struct KEKSItems *schema,
    struct KEKSItems *data,
    size_t idxSchema,
    size_t idxData);

static struct KEKSSchemaErr
keksSchemaLens(
    int64_t *our,
    int64_t *their,
    const struct KEKSItems *schema,
    struct KEKSItems *data,
    size_t idxSchema,
    size_t idxData)
{
    assert(our != NULL);
    assert(their != NULL);
    assert(schema != NULL);
    assert(data != NULL);
    switch (schema->list[idxSchema].atom.typ) {
    case KEKSItemPint:
        (*our) = (int64_t)(schema->list[idxSchema].atom.v.pint);
        break;
    case KEKSItemNint:
        (*our) = schema->list[idxSchema].atom.v.nint;
        break;
    case KEKSItemList:
    case KEKSItemMap:
        (*our) = (int64_t)(schema->list[idxSchema].atom.v.list.len);
        break;
    case KEKSItemStr:
    case KEKSItemBin:
        (*our) = (int64_t)(schema->list[idxSchema].atom.v.str.len);
        break;
    case KEKSItemInvalid:
    case KEKSItemEOC:
    case KEKSItemNIL:
    case KEKSItemFalse:
    case KEKSItemTrue:
    case KEKSItemHexlet:
    case KEKSItemBlob:
    case KEKSItemFloat:
    case KEKSItemTAI64:
    case KEKSItemMagic:
    case KEKSItemRaw:
    default:
        return (struct KEKSSchemaErr){
            .offSchema = schema->offsets[idxSchema],
            .offData = data->offsets[idxData],
            .code = KEKSSchemaErrUnexpectedState,
            .msg = "unsupported len type",
        };
    }
    switch (data->list[idxData].atom.typ) {
    case KEKSItemPint:
        (*their) = (int64_t)(data->list[idxData].atom.v.pint);
        break;
    case KEKSItemNint:
        (*their) = data->list[idxData].atom.v.nint;
        break;
    case KEKSItemList:
    case KEKSItemMap:
        (*their) = (int64_t)(data->list[idxData].atom.v.list.len);
        break;
    case KEKSItemStr:
    case KEKSItemBin:
        (*their) = (int64_t)(data->list[idxData].atom.v.str.len);
        break;
    case KEKSItemInvalid:
    case KEKSItemEOC:
    case KEKSItemNIL:
    case KEKSItemFalse:
    case KEKSItemTrue:
    case KEKSItemHexlet:
    case KEKSItemBlob:
    case KEKSItemFloat:
    case KEKSItemTAI64:
    case KEKSItemMagic:
    case KEKSItemRaw:
    default:
        return (struct KEKSSchemaErr){
            .offSchema = schema->offsets[idxSchema],
            .offData = data->offsets[idxData],
            .code = KEKSSchemaErrUnexpectedState,
            .msg = "unsupported len type",
        };
    }
    return (struct KEKSSchemaErr){.code = KEKSSchemaErrNo};
}

static struct KEKSSchemaErr
keksSchemaCmd( // NOLINT(misc-no-recursion)
    size_t *taken,
    bool *eachInList,
    bool *eachInMap,
    const struct KEKSItems *schema,
    struct KEKSItems *data,
    size_t idxSchema,
    size_t idxData)
{
    assert(taken != NULL);
    assert(eachInList != NULL);
    assert(eachInMap != NULL);
    assert(schema != NULL);
    assert(data != NULL);
    size_t origIdxSchema = idxSchema;
    struct KEKSSchemaErr err;
    size_t v = *taken;
Eached:
    idxSchema = origIdxSchema;
    err.offSchema = schema->offsets[idxSchema];
    err.offData = data->offsets[idxData];
    err.code = KEKSSchemaErrInvalid;
    if (schema->list[idxSchema].atom.v.list.len <= 0) {
        err.code = KEKSSchemaErrInvalidSchema;
        err.msg = "empty cmd list";
        return err;
    }
    idxSchema = schema->list[idxSchema].atom.v.list.head;
    if (schema->list[idxSchema].atom.typ != KEKSItemStr) {
        err.code = KEKSSchemaErrInvalidSchema;
        err.msg = "non-str cmd";
        return err;
    }
    if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdExists)) {
        err.msg = "EXISTS";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrInvalidData;
            return err;
        }
        err.code = KEKSSchemaErrNo;
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdNotExists)) {
        err.msg = "!EXISTS";
        if (v != SIZE_MAX) {
            err.code = KEKSSchemaErrInvalidData;
            return err;
        }
        err.code = KEKSSchemaErrNo;
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdTake)) {
        idxSchema = schema->list[idxSchema].next;
        if (idxSchema == 0) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "wrong number of args";
            return err;
        }
        err.offSchema = schema->offsets[idxSchema];
        switch (schema->list[idxSchema].atom.typ) {
        case KEKSItemNIL:
            (*taken) = idxData;
            break;
        case KEKSItemStr:
            if (data->list[idxData].atom.typ != KEKSItemMap) {
                err.code = KEKSSchemaErrUnexpectedState;
                err.msg = "non-map TAKE target";
                return err;
            }
            (*taken) = KEKSItemsGetByKeyLen(
                data,
                idxData,
                (const char *)schema->list[idxSchema].atom.v.str.ptr,
                schema->list[idxSchema].atom.v.str.len);
            if ((*taken) == 0) {
                (*taken) = SIZE_MAX;
            }
            break;
        case KEKSItemPint:
            if (data->list[idxData].atom.typ != KEKSItemList) {
                err.code = KEKSSchemaErrUnexpectedState;
                err.msg = "non-list TAKE target";
                return err;
            }
            (*taken) = data->list[idxData].atom.v.list.head;
            for (uint64_t i = 0; i < schema->list[idxSchema].atom.v.pint; i++) {
                if ((*taken) == 0) {
                    break;
                }
                (*taken) = data->list[*taken].next;
            }
            if ((*taken) == 0) {
                (*taken) = SIZE_MAX;
            }
            break;
        case KEKSItemInvalid:
        case KEKSItemEOC:
        case KEKSItemFalse:
        case KEKSItemTrue:
        case KEKSItemHexlet:
        case KEKSItemNint:
        case KEKSItemList:
        case KEKSItemMap:
        case KEKSItemBlob:
        case KEKSItemFloat:
        case KEKSItemTAI64:
        case KEKSItemMagic:
        case KEKSItemBin:
        case KEKSItemRaw:
        default:
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "bad TAKE target";
            return err;
        }
        v = *taken;
        err.msg = "TAKE";
        err.code = KEKSSchemaErrNo;
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdEq)) {
        err.msg = "EQ";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            idxSchema = schema->list[idxSchema].next;
            if (idxSchema == 0) {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "wrong number of args";
                return err;
            }
            err.offSchema = schema->offsets[idxSchema];
            if (schema->list[idxSchema].atom.typ != KEKSItemBin) {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "non-bin EQ";
                return err;
            }
            err.code = ((schema->list[idxSchema].atom.v.str.len ==
                         data->list[v].atom.v.str.len) &&
                        (memcmp(
                             schema->list[idxSchema].atom.v.str.ptr,
                             data->list[v].atom.v.str.ptr,
                             schema->list[idxSchema].atom.v.str.len) == 0)) ?
                           KEKSSchemaErrNo :
                           KEKSSchemaErrInvalidData;
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdEach)) {
        err.msg = "EACH";
        (*eachInList) = false;
        (*eachInMap) = false;
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
            return err;
        }
        switch (data->list[v].atom.typ) {
        case KEKSItemList:
            (*eachInList) = true;
            break;
        case KEKSItemMap:
            (*eachInMap) = true;
            break;
        case KEKSItemInvalid:
        case KEKSItemEOC:
        case KEKSItemNIL:
        case KEKSItemFalse:
        case KEKSItemTrue:
        case KEKSItemHexlet:
        case KEKSItemPint:
        case KEKSItemNint:
        case KEKSItemBlob:
        case KEKSItemFloat:
        case KEKSItemTAI64:
        case KEKSItemMagic:
        case KEKSItemBin:
        case KEKSItemStr:
        case KEKSItemRaw:
        default:
            err.code = KEKSSchemaErrUnexpectedState;
            err.msg = "non-iterable EACH";
            return err;
        }
        if (data->list[v].atom.v.list.len == 0) {
            (*taken) = SIZE_MAX;
        } else {
            (*taken) = data->list[v].atom.v.list.head;
            if (*eachInMap) {
                (*taken) = data->list[(*taken)].next;
            }
        }
        err.code = KEKSSchemaErrNo;
        return err;
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdType)) {
        const size_t expectedLen = 16;
        enum KEKSItemType expected[16] = {KEKSItemInvalid};
        size_t idxExpected = 0;
        idxSchema = schema->list[idxSchema].next;
        if (idxSchema == 0) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "wrong number of args";
            return err;
        }
        while (idxSchema != 0) {
            err.offSchema = schema->offsets[idxSchema];
            if (schema->list[idxSchema].atom.typ != KEKSItemStr) {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "non-str TYPE";
                return err;
            }
            if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeNIL)) {
                expected[idxExpected] = KEKSItemNIL;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeBool)) {
                expected[idxExpected] = KEKSItemFalse;
                idxExpected++;
                expected[idxExpected] = KEKSItemTrue;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeHexlet)) {
                expected[idxExpected] = KEKSItemHexlet;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeInt)) {
                expected[idxExpected] = KEKSItemPint;
                idxExpected++;
                expected[idxExpected] = KEKSItemNint;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeList)) {
                expected[idxExpected] = KEKSItemList;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeMap)) {
                expected[idxExpected] = KEKSItemMap;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeBlob)) {
                expected[idxExpected] = KEKSItemBlob;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeTAI)) {
                expected[idxExpected] = KEKSItemTAI64;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeMagic)) {
                expected[idxExpected] = KEKSItemMagic;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeBin)) {
                expected[idxExpected] = KEKSItemBin;
                idxExpected++;
            } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), TypeStr)) {
                expected[idxExpected] = KEKSItemStr;
                idxExpected++;
            } else {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "unknown TYPE";
                return err;
            }
            idxSchema = schema->list[idxSchema].next;
        }
        err.msg = "TYPE";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            bool found = false;
            for (size_t i = 0; i < expectedLen; i++) {
                if (expected[i] == data->list[v].atom.typ) {
                    found = true;
                    break;
                }
            }
            err.code = found ? KEKSSchemaErrNo : KEKSSchemaErrInvalidData;
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdGT)) {
        err.msg = "GT";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            idxSchema = schema->list[idxSchema].next;
            if (idxSchema == 0) {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "wrong number of args";
                return err;
            }
            err.offSchema = schema->offsets[idxSchema];
            err.offData = data->offsets[v];
            int64_t our = 0;
            int64_t their = 0;
            struct KEKSSchemaErr errLens =
                keksSchemaLens(&our, &their, schema, data, idxSchema, v);
            if (errLens.code != KEKSSchemaErrNo) {
                return errLens;
            }
            err.code = (their <= our) ? KEKSSchemaErrInvalidData : KEKSSchemaErrNo;
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdLT)) {
        err.msg = "LT";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            idxSchema = schema->list[idxSchema].next;
            if (idxSchema == 0) {
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "wrong number of args";
                return err;
            }
            err.offSchema = schema->offsets[idxSchema];
            err.offData = data->offsets[v];
            int64_t our = 0;
            int64_t their = 0;
            struct KEKSSchemaErr errLens =
                keksSchemaLens(&our, &their, schema, data, idxSchema, v);
            if (errLens.code != KEKSSchemaErrNo) {
                return errLens;
            }
            err.code = (their >= our) ? KEKSSchemaErrInvalidData : KEKSSchemaErrNo;
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdSchema)) {
        idxSchema = schema->list[idxSchema].next;
        if (idxSchema == 0) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "wrong number of args";
            return err;
        }
        err.offSchema = schema->offsets[idxSchema];
        if (schema->list[idxSchema].atom.typ != KEKSItemStr) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "non-str SCHEMA";
            return err;
        }
        idxSchema = KEKSItemsGetByKeyLen(
            schema,
            0,
            (const char *)schema->list[idxSchema].atom.v.str.ptr,
            schema->list[idxSchema].atom.v.str.len);
        if (idxSchema == 0) {
            err.code = KEKSSchemaErrUnexpectedState;
            err.msg = "unknown SCHEMA";
            return err;
        }
        err.offSchema = schema->offsets[idxSchema];
        err.msg = "SCHEMA";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            struct KEKSSchemaErr errSchema =
                KEKSSchemaValidate(schema, data, idxSchema, v);
            if (errSchema.code != KEKSSchemaErrNo) {
                return errSchema;
            }
            err.code = KEKSSchemaErrNo;
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdTimePrec)) {
        idxSchema = schema->list[idxSchema].next;
        if (idxSchema == 0) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "wrong number of args";
            return err;
        }
        err.offSchema = schema->offsets[idxSchema];
        if (schema->list[idxSchema].atom.typ != KEKSItemPint) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "non-int TIMEPREC";
            return err;
        }
        err.msg = "TIMEPREC";
        err.code = KEKSSchemaErrNo;
        if (v != SIZE_MAX) {
            if (data->list[v].atom.typ != KEKSItemTAI64) {
                err.code = KEKSSchemaErrUnexpectedState;
                err.msg = "non-TAI64 taken";
                return err;
            }
            uint32_t val = 0;
            switch (schema->list[idxSchema].atom.v.pint) {
            case 0: // s
                if (data->list[v].atom.v.str.len > 8) {
                    err.code = KEKSSchemaErrInvalidData;
                    err.msg = ">TAI64";
                    return err;
                }
                break;
            case 3: // ms
                if (data->list[v].atom.v.str.len > 12) {
                    err.code = KEKSSchemaErrInvalidData;
                    err.msg = ">TAI64N";
                    return err;
                }
                if (data->list[v].atom.v.str.len > 8) {
                    val = (uint32_t)keksFromBE(data->list[v].atom.v.str.ptr + 8, 4);
                    if ((val % 1000000) != 0) {
                        err.code = KEKSSchemaErrInvalidData;
                        err.msg = ">ms";
                        return err;
                    }
                }
                break;
            case 6: // µs
                if (data->list[v].atom.v.str.len > 12) {
                    err.code = KEKSSchemaErrInvalidData;
                    err.msg = ">TAI64N";
                    return err;
                }
                if (data->list[v].atom.v.str.len > 8) {
                    val = (uint32_t)keksFromBE(data->list[v].atom.v.str.ptr + 8, 4);
                    if ((val % 1000) != 0) {
                        err.code = KEKSSchemaErrInvalidData;
                        err.msg = ">µs";
                        return err;
                    }
                }
                break;
            case 9: // ns
                if (data->list[v].atom.v.str.len > 12) {
                    err.code = KEKSSchemaErrInvalidData;
                    err.msg = ">TAI64N";
                    return err;
                }
                break;
            case 12: // ps
                if (data->list[v].atom.v.str.len > 12) {
                    val = (uint32_t)keksFromBE(data->list[v].atom.v.str.ptr + 12, 4);
                    if ((val % 1000000) != 0) {
                        err.code = KEKSSchemaErrInvalidData;
                        err.msg = ">ps";
                        return err;
                    }
                }
                break;
            case 15: // fs
                if (data->list[v].atom.v.str.len > 12) {
                    val = (uint32_t)keksFromBE(data->list[v].atom.v.str.ptr + 12, 4);
                    if ((val % 1000) != 0) {
                        err.code = KEKSSchemaErrInvalidData;
                        err.msg = ">fs";
                        return err;
                    }
                }
                break;
            default:
                err.code = KEKSSchemaErrInvalidSchema;
                err.msg = "unknown TIMEPREC value";
                return err;
            }
        }
    } else if (KEKSStrEqual(&(schema->list[idxSchema].atom), CmdUTC)) {
        err.msg = "UTC";
        if (v == SIZE_MAX) {
            err.code = KEKSSchemaErrNo;
        } else {
            if (data->list[v].atom.typ != KEKSItemTAI64) {
                err.code = KEKSSchemaErrUnexpectedState;
                err.msg = "non-TAI64 taken";
                return err;
            }
            struct timespec tv;
            enum KEKSErr errConvert = KEKSTAI64ToTimespec(
                &tv, data->list[v].atom.v.str.ptr, data->list[v].atom.v.str.len);
            if (errConvert != KEKSErrNo) {
                err.code = KEKSSchemaErrInvalidData;
                err.msg = "can not convert to timespec";
                return err;
            }
            errConvert = KEKSTimespecToUTC(&tv);
            if (errConvert != KEKSErrNo) {
                err.code = KEKSSchemaErrInvalidData;
                err.msg = "can not convert to UTC";
                return err;
            }
            err.code = KEKSSchemaErrNo;
        }
    } else {
        err.msg = "unknown cmd";
        err.code = KEKSSchemaErrInvalidSchema;
    }
    if (err.code != KEKSSchemaErrNo) {
        return err;
    }
    if (*eachInList) {
        v = data->list[v].next;
        if (v != 0) {
            goto Eached;
        }
    }
    if (*eachInMap) {
        v = data->list[v].next; // key
        if (v != 0) {
            v = data->list[v].next; // value
            if (v != 0) {
                goto Eached;
            }
        }
    }
    (*eachInList) = false;
    (*eachInMap) = false;
    return err;
}

struct KEKSSchemaErr
KEKSSchemaValidate( // NOLINT(misc-no-recursion)
    const struct KEKSItems *schema,
    struct KEKSItems *data,
    size_t idxSchema,
    size_t idxData)
{
    assert(schema != NULL);
    assert(data != NULL);
    struct KEKSSchemaErr err = (struct KEKSSchemaErr){
        .offSchema = schema->offsets[idxSchema],
        .offData = data->offsets[idxData],
        .code = KEKSSchemaErrInvalid,
    };
    if (schema->list[idxSchema].atom.typ != KEKSItemList) {
        err.code = KEKSSchemaErrInvalidSchema;
        err.msg = "non-list schema";
        return err;
    }
    idxSchema = schema->list[idxSchema].atom.v.list.head;
    err.offSchema = schema->offsets[idxSchema];
    size_t taken = idxData;
    bool eachInList = false;
    bool eachInMap = false;
    struct KEKSSchemaErr errCmd;
    while (idxSchema != 0) {
        if (schema->list[idxSchema].atom.typ != KEKSItemList) {
            err.code = KEKSSchemaErrInvalidSchema;
            err.msg = "non-list cmds";
            return err;
        }
        errCmd = keksSchemaCmd(
            &taken, &eachInList, &eachInMap, schema, data, idxSchema, idxData);
        if (errCmd.code != KEKSSchemaErrNo) {
            return errCmd;
        }
        idxSchema = schema->list[idxSchema].next;
        err.offSchema = schema->offsets[idxSchema];
    }
    err.code = KEKSSchemaErrNo;
    return err;
}
