// 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 <stdlib.h>
#include <string.h>

#include "dec.h"
#include "enc.h"
#include "err.h"
#include "items.h"

static const size_t parseMaxRecursionDepth = 1024;

enum KEKSErr
KEKSItemsInit(struct KEKSItems *items, const ptrdiff_t initialLen)
{
    assert(items != NULL);
    items->len = 0;
    items->cap = initialLen;
    items->reallocs = 0;
    items->list = calloc((size_t)(items->cap), sizeof(struct KEKSItem));
    if (items->list == NULL) {
        return KEKSErrNoMem;
    }
    items->offsets = calloc((size_t)(items->cap), sizeof(size_t));
    if (items->offsets == NULL) {
        return KEKSErrNoMem;
    }
    return KEKSErrNo;
}

void
KEKSItemsNoOffsets(struct KEKSItems *items)
{
    assert(items != NULL);
    if (items->offsets == NULL) {
        return;
    }
    free(items->offsets);
    items->offsets = NULL;
}

void
KEKSItemsFree(struct KEKSItems *items)
{
    assert(items != NULL);
    items->len = 0;
    items->cap = 0;
    if (items->list != NULL) {
        free(items->list);
        items->list = NULL;
    }
    KEKSItemsNoOffsets(items);
}

enum KEKSErr
KEKSItemsGrow(struct KEKSItems *items)
{
    assert(items != NULL);
    if (items->cap == -1) {
        return KEKSErrNoMem;
    }
    if ((size_t)(items->cap) >= (SIZE_MAX / 2)) {
        return KEKSErrNoMem;
    }
    items->cap *= 2;
    {
        const ptrdiff_t possibleN = SIZE_MAX / sizeof(struct KEKSItem);
        if (items->cap > possibleN) {
            return KEKSErrNoMem;
        }
    }

    size_t size = (size_t)(items->cap) * sizeof(struct KEKSItem);
    items->list = realloc(items->list, size);
    if (items->list == NULL) {
        return KEKSErrNoMem;
    }
    size_t begin = items->len * sizeof(struct KEKSItem);
    memset((unsigned char *)(items->list) + begin, 0, size - begin);
    items->reallocs++;

    if (items->offsets != NULL) {
        size = (size_t)(items->cap) * sizeof(size_t);
        items->offsets = realloc(items->offsets, size);
        if (items->offsets == NULL) {
            return KEKSErrNoMem;
        }
        begin = items->len * sizeof(size_t);
        memset((unsigned char *)(items->offsets) + begin, 0, size - begin);
    }
    return KEKSErrNo;
}

static enum KEKSErr
keksItemsAdd(
    struct KEKSItems *items,
    size_t *off,
    const unsigned char *buf,
    const size_t len)
{
    assert(items != NULL);
    assert(off != NULL);
    assert(buf != NULL);
    enum KEKSErr err = KEKSErrInvalid;
    if (items->len == (size_t)(items->cap)) {
        err = KEKSItemsGrow(items);
        if (err != KEKSErrNo) {
            return err;
        }
    }
    struct KEKSItem *item = &(items->list[items->len]);
    item->next = 0;
    if (items->offsets != NULL) {
        items->offsets[items->len] = *off;
    }
    size_t got = 0;
    assert(len >= (*off));
    err = KEKSAtomDecode(&got, &(item->atom), buf + *off, len - (*off));
    if (err != KEKSErrNo) {
        return err;
    }
    if ((SIZE_MAX - got) < (*off)) {
        return KEKSErrLenTooBig;
    }
    (*off) += got;
    items->len++;
    return KEKSErrNo;
}

static enum KEKSErr
keksItemsParse( // NOLINT(misc-no-recursion)
    struct KEKSItems *items,
    size_t *off,
    const unsigned char *buf,
    const size_t len,
    const bool allowContainers,
    const size_t recursionDepth)
{
    assert(items != NULL);
    assert(off != NULL);
    assert(buf != NULL);
    if (recursionDepth >= parseMaxRecursionDepth) {
        return KEKSErrDeepRecursion;
    }
    size_t item = items->len;
    enum KEKSErr err = keksItemsAdd(items, off, buf, len);
    if (err != KEKSErrNo) {
        return err;
    }
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wswitch-enum"
    switch (items->list[item].atom.typ) {
#pragma clang diagnostic pop
    case KEKSItemList: {
        if (!allowContainers) {
            return KEKSErrUnknownType;
        }
        items->list[item].atom.v.list.head = item + 1;
        items->list[item].atom.v.list.len = 0;
        size_t prev = 0;
        size_t cur = 0;
        size_t idx = item;
        for (;;) {
            err = keksItemsParse(items, off, buf, len, true, recursionDepth + 1);
            if (err != KEKSErrNo) {
                return err;
            }
            cur = idx + 1;
            if (items->list[cur].atom.typ == KEKSItemEOC) {
                if (items->list[item].atom.v.list.len == 0) {
                    items->list[item].atom.v.list.head = 0;
                }
                return KEKSErrNo;
            }
            if (prev != 0) {
                items->list[prev].next = cur;
            }
            prev = cur;
            idx = (items->len) - 1;
            items->list[item].atom.v.list.len++;
        }
    }
    case KEKSItemMap: {
        if (!allowContainers) {
            return KEKSErrUnknownType;
        }
        items->list[item].atom.v.list.head = item + 1;
        items->list[item].atom.v.list.len = 0;
        size_t idx = item;
        size_t prev = 0;
        size_t cur = 0;
        size_t prevKeyLen = 0;
        const unsigned char *prevKey = NULL;
        for (;;) {
            err = keksItemsParse(items, off, buf, len, false, recursionDepth + 1);
            if (err != KEKSErrNo) {
                return err;
            }
            cur = idx + 1;
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wswitch-enum"
            switch (items->list[cur].atom.typ) {
#pragma clang diagnostic pop
            case KEKSItemEOC:
                if (items->list[item].atom.v.list.len == 0) {
                    items->list[item].atom.v.list.head = 0;
                }
                return KEKSErrNo;
            case KEKSItemStr:
                break;
            default:
                return KEKSErrMapBadKey;
            }
            if (items->list[cur].atom.v.str.len == 0) {
                return KEKSErrMapBadKey;
            }
            if (items->list[cur].atom.v.str.len < prevKeyLen) {
                return KEKSErrMapUnordered;
            }
            if ((items->list[cur].atom.v.str.len == prevKeyLen) &&
                (memcmp(prevKey, items->list[cur].atom.v.str.ptr, prevKeyLen) >= 0)) {
                return KEKSErrMapUnordered;
            }
            prevKeyLen = items->list[cur].atom.v.str.len;
            prevKey = items->list[cur].atom.v.str.ptr;
            if (prev != 0) {
                items->list[prev].next = cur;
            }
            prev = cur;
            idx = (items->len) - 1;
            err = keksItemsParse(items, off, buf, len, true, recursionDepth + 1);
            if (err != KEKSErrNo) {
                return err;
            }
            cur = idx + 1;
            if (items->list[cur].atom.typ == KEKSItemEOC) {
                return KEKSErrUnexpectedEOC;
            }
            items->list[prev].next = cur;
            prev = cur;
            idx = (items->len) - 1;
            items->list[item].atom.v.list.len++;
        }
    }
    case KEKSItemBlob: {
        if (!allowContainers) {
            return KEKSErrUnknownType;
        }
        items->list[item].atom.v.blob.chunks = 0;
        const size_t chunkLen = items->list[item].atom.v.blob.chunkLen;
        size_t idx = item;
        size_t prev = 0;
        size_t cur = 0;
        bool eoc = false;
        while (!eoc) {
            err = keksItemsParse(items, off, buf, len, false, recursionDepth + 1);
            if (err != KEKSErrNo) {
                return err;
            }
            cur = idx + 1;
            struct KEKSAtom *atom = &(items->list[cur].atom);
            if (atom->typ != KEKSItemBin) {
                return KEKSErrBlobBadAtom;
            }
            if (atom->v.str.len == chunkLen) {
            } else if (atom->v.str.len < chunkLen) {
                eoc = true;
            } else {
                return KEKSErrBlobBadChunkLen;
            }
            if (prev != 0) {
                items->list[prev].next = cur;
            }
            prev = cur;
            idx = (items->len) - 1;
            items->list[item].atom.v.blob.chunks++;
        }
        break;
    }
    default:
        break;
    }
    return KEKSErrNo;
}

enum KEKSErr
KEKSItemsParse( // NOLINT(misc-no-recursion)
    struct KEKSItems *items,
    size_t *off,
    const unsigned char *buf,
    const size_t len)
{
    enum KEKSErr err = keksItemsParse(items, off, buf, len, true, 0);
    if (items->list[0].atom.typ == KEKSItemEOC) {
        err = KEKSErrUnexpectedEOC;
    }
    return err;
}

bool
KEKSItemsEncode( // NOLINT(misc-no-recursion)
    const struct KEKSItems *items,
    size_t idx,
    size_t *off,
    unsigned char *buf,
    const size_t cap)
{
    assert(items != NULL);
    assert(off != NULL);
    assert(buf != NULL);
    const struct KEKSItem *item = &(items->list[idx]);
    size_t got = 0;
    bool ok = false;
    assert(cap >= (*off));
    switch (item->atom.typ) {
    case KEKSItemInvalid:
    case KEKSItemEOC:
        return false;
    case KEKSItemNIL:
        ok = KEKSAtomNILEncode(&got, buf + *off, cap - (*off));
        break;
    case KEKSItemFalse:
        ok = KEKSAtomBoolEncode(&got, buf + *off, cap - (*off), false);
        break;
    case KEKSItemTrue:
        ok = KEKSAtomBoolEncode(&got, buf + *off, cap - (*off), true);
        break;
    case KEKSItemHexlet:
        ok = KEKSAtomHexletEncode(&got, buf + *off, cap - (*off), item->atom.v.str.ptr);
        break;
    case KEKSItemMagic:
        ok = KEKSAtomMagicEncode(
            &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
        break;
    case KEKSItemPint:
        ok = KEKSAtomUintEncode(&got, buf + *off, cap - (*off), item->atom.v.pint);
        break;
    case KEKSItemNint:
        ok = KEKSAtomSintEncode(&got, buf + *off, cap - (*off), item->atom.v.nint);
        break;
    case KEKSItemFloat:
        ok = KEKSAtomFloatEncode(&got, buf + *off, cap - (*off), item->atom.v.fp);
        break;
    case KEKSItemList:
        ok = KEKSAtomListEncode(&got, buf + *off, cap - (*off));
        if (!ok) {
            return false;
        }
        if ((SIZE_MAX - got) < (*off)) {
            return false;
        }
        (*off) += got;
        idx = item->atom.v.list.head;
        while (idx != 0) {
            ok = KEKSItemsEncode(items, idx, off, buf, cap);
            if (!ok) {
                return false;
            }
            idx = items->list[idx].next;
        }
        assert(cap >= (*off));
        ok = KEKSAtomEOCEncode(&got, buf + *off, cap - (*off));
        break;
    case KEKSItemMap:
        ok = KEKSAtomMapEncode(&got, buf + *off, cap - (*off));
        if (!ok) {
            return false;
        }
        if ((SIZE_MAX - got) < (*off)) {
            return false;
        }
        (*off) += got;
        idx = item->atom.v.list.head;
        while (idx != 0) {
            ok = KEKSItemsEncode(items, idx, off, buf, cap);
            if (!ok) {
                return false;
            }
            idx = items->list[idx].next;
        }
        ok = KEKSAtomEOCEncode(&got, buf + *off, cap - (*off));
        break;
    case KEKSItemBlob: {
        const size_t chunkLen = (item->atom.v.blob.chunkLen);
        ok = KEKSAtomBlobEncode(&got, buf + *off, cap - (*off), chunkLen);
        if (!ok) {
            return 0;
        }
        if ((SIZE_MAX - got) < (*off)) {
            return false;
        }
        (*off) += got;
        for (;;) {
            idx++;
            item = &(items->list[idx]);
            if (item->atom.v.str.len != chunkLen) {
                break;
            }
            assert(cap >= (*off));
            ok = KEKSAtomBinEncode(
                &got,
                buf + *off,
                cap - (*off),
                item->atom.v.str.ptr,
                item->atom.v.str.len);
            if (!ok) {
                return false;
            }
            if ((SIZE_MAX - got) < (*off)) {
                return false;
            }
            (*off) += got;
            continue;
        }
        assert(cap >= (*off));
        ok = KEKSAtomBinEncode(
            &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
        break;
    }
    case KEKSItemTAI64:
        ok = KEKSAtomTAI64Encode(
            &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
        break;
    case KEKSItemBin:
        ok = KEKSAtomBinEncode(
            &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
        break;
    case KEKSItemStr:
        ok = KEKSAtomStrEncode(
            &got, buf + *off, cap - (*off), item->atom.v.str.ptr, item->atom.v.str.len);
        break;
    case KEKSItemRaw:
        if ((cap - (*off)) < item->atom.v.str.len) {
            return false;
        }
        memcpy(buf + *off, item->atom.v.str.ptr, item->atom.v.str.len);
        ok = true;
        (*off) += item->atom.v.str.len;
        break;
    default:
        return false;
    }
    if (!ok) {
        return false;
    }
    if ((SIZE_MAX - got) < (*off)) {
        return false;
    }
    (*off) += got;
    return true;
}

size_t
KEKSItemsGetByKeyLen(
    const struct KEKSItems *items,
    const size_t itemIdx,
    const char *key,
    const size_t keyLen)
{
    assert(items != NULL);
    const struct KEKSItem *item = &(items->list[itemIdx]);
    if (item->atom.typ != KEKSItemMap) {
        return 0;
    }
    size_t idx = item->atom.v.list.head;
    while (idx != 0) {
        if ((items->list[idx].atom.v.str.len == keyLen) &&
            (memcmp(items->list[idx].atom.v.str.ptr, key, keyLen) == 0)) {
            return idx + 1;
        }
        idx = items->list[idx].next; // the value
        idx = items->list[idx].next; // next key
    }
    return 0;
}

size_t
KEKSItemsGetByKey(const struct KEKSItems *items, const size_t itemIdx, const char *key)
{
    return KEKSItemsGetByKeyLen(items, itemIdx, key, strlen(key));
}

bool
KEKSStrEqual(const struct KEKSAtom *atom, const char *s)
{
    assert(atom != NULL);
    return (atom->v.str.len == strlen(s)) &&
           (memcmp(atom->v.str.ptr, s, atom->v.str.len) == 0);
}
