// pp -- C KEKS pretty-printer
// 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 <ctype.h>
#include <errno.h>
#include <getopt.h>
#include <math.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#ifdef __FreeBSD__
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
#define __BSD_VISIBLE 1
#pragma clang diagnostic pop
#endif
#include <time.h>

#include <keks/atom.h>
#include <keks/err.h>
#include <keks/items.h>

#include "../lib/hex.h"
#include "../lib/printai.h"
#include "../lib/stdinread.h"
#include "../lib/uuid.h"

static char *ColourRed = "\x1b[31m";
static char *ColourGreen = "\x1b[32m";
static char *ColourYellow = "\x1b[33m";
static char *ColourBlue = "\x1b[34m";
static char *ColourMagenta = "\x1b[35m";
static char *ColourCyan = "\x1b[36m";
static char *ColourWhite = "\x1b[37m";
static char *ColourReset = "\x1b[0m";

static size_t MaxStrLen = 64;
static bool NoOffsets = false;
static int OffDigits = 0;
static char OffFmt[16] = {0};

static void
printIndent(const size_t indent)
{
    for (size_t i = 0; i < indent; i++) {
        fputs("  ", stdout);
    }
}

static void
usage(void)
{
    fputs(
        "Usage: pp [OPTIONS] <FILE\n"
        "  --max-str-len X -- limit output of strings to X (40 by default)\n"
        "  --do-encode     -- encode the data after decoding, just to test\n"
        "  --no-offsets    -- do not print offsets\n"
        "  --no-totals     -- do not print memory consumption totals\n"
        "",
        stderr);
    exit(EXIT_FAILURE);
}

static enum KEKSErr
printer( // NOLINT(misc-no-recursion)
    const struct KEKSItems *items,
    size_t idx,
    size_t indent,
    const bool inList,
    size_t listIdx,
    const char *mapKey)
{
    if (NoOffsets) {
        fputs("  ", stdout);
    } else {
        fprintf(stdout, "%s%zd%s ", ColourBlue, indent, ColourReset);
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wformat-nonliteral"
        fprintf(stdout, OffFmt, ColourRed, items->offsets[idx], ColourReset);
#pragma clang diagnostic pop
    }
    struct KEKSItem *item = &(items->list[idx]);
    printIndent(indent);
    if (inList) {
        fprintf(stdout, "%s%zu:%s ", ColourYellow, listIdx, ColourReset);
    }
    if (mapKey != NULL) {
        fprintf(stdout, "%s%s:%s ", ColourGreen, mapKey, ColourReset);
    }
    char *str = NULL;
    enum KEKSErr err = KEKSErrInvalid;
    switch (item->atom.typ) {
    case KEKSItemInvalid:
        fputs("INVALID\n", stdout);
        break;
    case KEKSItemNIL:
        fputs("NIL\n", stdout);
        break;
    case KEKSItemFalse:
        fputs("FALSE\n", stdout);
        break;
    case KEKSItemTrue:
        fputs("TRUE\n", stdout);
        break;
    case KEKSItemHexlet:
        UUIDPrint(item->atom.v.str.ptr);
        fputs("\n", stdout);
        break;
    case KEKSItemMagic:
        fputs("Magic(", stdout);
        for (size_t i = 0; i < item->atom.v.str.len; i++) {
            if (isprint(item->atom.v.str.ptr[i]) == 0) {
                printf("\\x%02x", item->atom.v.str.ptr[i]);
            } else {
                fputc(item->atom.v.str.ptr[i], stdout);
            }
        }
        fputs(")\n", stdout);
        break;
    case KEKSItemPint:
        fprintf(stdout, "%zu\n", item->atom.v.pint);
        break;
    case KEKSItemNint:
        fprintf(stdout, "%zd\n", item->atom.v.nint);
        break;
    case KEKSItemList: {
        fprintf(stdout, "[ %s%zd%s\n", ColourCyan, item->atom.v.list.len, ColourReset);
        indent++;
        idx = item->atom.v.list.head;
        listIdx = 0;
        while (idx != 0) {
            err = printer(items, idx, indent, true, listIdx, NULL);
            if (err != KEKSErrNo) {
                return err;
            }
            idx = items->list[idx].next;
            listIdx++;
        }
        for (int i = 0; i < OffDigits + 3; i++) {
            fputs(" ", stdout);
        }
        indent--;
        printIndent(indent);
        fputs("]\n", stdout);
        break;
    }
    case KEKSItemMap: {
        fprintf(stdout, "{ %s%zd%s\n", ColourCyan, item->atom.v.list.len, ColourReset);
        indent++;
        idx = item->atom.v.list.head;
        while (idx != 0) {
            str = strndup(
                (const char *)(items->list[idx].atom.v.str.ptr),
                items->list[idx].atom.v.str.len);
            idx = items->list[idx].next;
            err = printer(items, idx, indent, false, 0, str);
            free(str);
            if (err != KEKSErrNo) {
                return err;
            }
            idx = items->list[idx].next;
        }
        for (int i = 0; i < OffDigits + 3; i++) {
            fputs(" ", stdout);
        }
        indent--;
        printIndent(indent);
        fputs("}\n", stdout);
        break;
    }
    case KEKSItemBlob:
        fprintf(
            stdout,
            "BLOB[ %s%zu l=%zu%s\n",
            ColourCyan,
            item->atom.v.blob.chunks,
            item->atom.v.blob.chunkLen,
            ColourReset);
        indent++;
        idx++;
        listIdx = 0;
        while (idx != 0) {
            err = printer(items, idx, indent, true, listIdx, NULL);
            if (err != KEKSErrNo) {
                return err;
            }
            idx = items->list[idx].next;
            listIdx++;
        }
        for (int i = 0; i < OffDigits + 3; i++) {
            fputs(" ", stdout);
        }
        indent--;
        printIndent(indent);
        fputs("]\n", stdout);
        break;
    case KEKSItemFloat:
        fprintf(stdout, "FLOAT(%f)\n", item->atom.v.fp);
        break;
    case KEKSItemTAI64: {
        err = PrintTAI64(item->atom.v.str.ptr, item->atom.v.str.len);
        if (err != KEKSErrNo) {
            return err;
        }
        break;
    }
    case KEKSItemBin: {
        const size_t l =
            (item->atom.v.str.len > MaxStrLen) ? MaxStrLen : item->atom.v.str.len;
        str = HexEnc(item->atom.v.str.ptr, l);
        fprintf(
            stdout,
            "%s%zu:%s%s%s\n",
            ColourMagenta,
            item->atom.v.str.len,
            ColourReset,
            str,
            (item->atom.v.str.len > MaxStrLen) ? "..." : "");
        free(str);
        break;
    }
    case KEKSItemStr: {
        const size_t l =
            (item->atom.v.str.len > MaxStrLen) ? MaxStrLen : item->atom.v.str.len;
        str = strndup((const char *)item->atom.v.str.ptr, l);
        fprintf(
            stdout, "\"%s%s\"\n", str, (item->atom.v.str.len > MaxStrLen) ? "..." : "");
        free(str);
        break;
    }
    case KEKSItemRaw:
        str = HexEnc(item->atom.v.str.ptr, item->atom.v.str.len);
        fprintf(stdout, "(l=%zu v=%s)\n", item->atom.v.str.len, str);
        free(str);
        break;
    case KEKSItemEOC:
    default:
        fprintf(stderr, "unknown atom\n");
        return EXIT_FAILURE;
    }
    return KEKSErrNo;
}

int
main(int argc, char **argv)
{
    bool doEncode = false;
    bool noTotals = false;
    bool onlyTotals = false;
    ptrdiff_t itemsInitialLen = 2048;
    ptrdiff_t maxParseCycles = PTRDIFF_MAX;
    struct option longopts[] = {
        {"max-str-len", required_argument, NULL, 'a'},
        {"do-encode", no_argument, NULL, 'b'},
        {"no-offsets", no_argument, NULL, 'c'},
        {"no-totals", no_argument, NULL, 'd'},
        {"only-totals", no_argument, NULL, 'e'},
        {"items-initial-len", required_argument, NULL, 'f'},
        {"max-parse-cycles", required_argument, NULL, 'g'},
        {NULL, 0, NULL, 0}};
    int ch = 0;
    for (;;) {
        ch = getopt_long(argc, argv, "", longopts, NULL);
        if (ch == -1) {
            break;
        }
        switch (ch) {
        case 'a': { // max-str-len
            errno = 0;
            long tmp = strtol(optarg, NULL, 10);
            if (errno != 0) {
                fprintf(stderr, "--max-str-len: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
            }
            if (tmp < 0) {
                fputs("--max-str-len: is negative\n", stderr);
                exit(EXIT_FAILURE);
            }
            MaxStrLen = (size_t)tmp;
            break;
        }
        case 'b': // do-encode
            doEncode = true;
            break;
        case 'c': // no-offsets
            NoOffsets = true;
            break;
        case 'd': // no-totals
            noTotals = true;
            break;
        case 'e': // only-totals
            onlyTotals = true;
            break;
        case 'f': { // items-initial-len
            errno = 0;
            long tmp = strtol(optarg, NULL, 10);
            if (errno != 0) {
                fprintf(stderr, "--items-initial-len: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
            }
            if (tmp < 0) {
                fputs("--items-initial-len: is negative\n", stderr);
                exit(EXIT_FAILURE);
            }
            itemsInitialLen = (ptrdiff_t)tmp;
            break;
        }
        case 'g': { // max-parse-cycles
            errno = 0;
            long tmp = strtol(optarg, NULL, 10);
            if (errno != 0) {
                fprintf(stderr, "--max-parse-cycles: %s\n", strerror(errno));
                exit(EXIT_FAILURE);
            }
            if (tmp < 0) {
                fputs("--max-parse-cycles: is negative\n", stderr);
                exit(EXIT_FAILURE);
            }
            maxParseCycles = (ptrdiff_t)tmp;
            break;
        }
        case '?':
        default:
            usage();
        }
    }
    argc -= optind;
    argv += optind;

    if (getenv("NO_COLOR") != NULL) {
        ColourRed = "";
        ColourGreen = "";
        ColourYellow = "";
        ColourBlue = "";
        ColourMagenta = "";
        ColourCyan = "";
        ColourWhite = "";
        ColourReset = "";
    }

    size_t len = 0;
    size_t cap = (size_t)1 << (size_t)10;
    unsigned char *buf = NULL;
    if (!stdinread(&buf, &len, &cap)) {
        fprintf(stderr, "stdinread(): %s\n", strerror(errno));
        exit(EXIT_FAILURE);
    }
    OffDigits = NoOffsets ? -1 : (int)(1 + floor(log10((double)len)));
    snprintf(OffFmt, sizeof OffFmt, "%%s%%0%dzd%%s ", OffDigits);

    struct KEKSItems items;
    enum KEKSErr err = KEKSErrInvalid;
    size_t off = 0;
    struct timespec started;
    struct timespec finished;
    ptrdiff_t parseCycles = 0;
    while (off < len) {
        err = KEKSItemsInit(&items, itemsInitialLen);
        if (err != KEKSErrNo) {
            fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
            return EXIT_FAILURE;
        }
        if (NoOffsets) {
            KEKSItemsNoOffsets(&items);
        }
        errno = 0;
#ifdef __FreeBSD__
        if (clock_gettime(CLOCK_MONOTONIC_PRECISE, &started) != 0) {
#else
        if (clock_gettime(CLOCK_MONOTONIC, &started) != 0) {
#endif
            fprintf(stderr, "clock_gettime(started): %s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
        err = KEKSItemsParse(&items, &off, buf, len);
        errno = 0;
#ifdef __FreeBSD__
        if (clock_gettime(CLOCK_MONOTONIC_PRECISE, &finished) != 0) {
#else
        if (clock_gettime(CLOCK_MONOTONIC, &finished) != 0) {
#endif
            fprintf(stderr, "clock_gettime(finished): %s\n", strerror(errno));
            exit(EXIT_FAILURE);
        }
        if (err != KEKSErrNo) {
            fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
            return EXIT_FAILURE;
        }
        if (!onlyTotals) {
            setenv("TZ", "UTC", 1);
            err = printer(&items, 0, 0, false, 0, NULL);
            if (err != KEKSErrNo) {
                fprintf(stderr, "err: %s\n", KEKSErr2Str(err));
                return EXIT_FAILURE;
            }
        }
        if (!noTotals) {
            printf(
                "items: %zu  size: %zu  time: %zuns\n",
                items.len,
                items.len *
                    (sizeof(struct KEKSItem) + (NoOffsets ? 0 : sizeof(size_t))),
                1000000000 * (finished.tv_sec - started.tv_sec) + finished.tv_nsec -
                    started.tv_nsec);
        }
        parseCycles++;
        if (parseCycles >= maxParseCycles) {
            break;
        }
    }
    if (off < len) {
        const size_t l = len - off;
        char *hex = HexEnc(buf + off, (l > MaxStrLen) ? MaxStrLen : l);
        printf("tail: %s%s\n", hex, (l > MaxStrLen) ? "..." : "");
        free(hex);
    }
    if (doEncode) {
        unsigned char *dst = malloc(len);
        assert(dst != NULL);
        size_t lenExpected = off;
        off = 0;
        assert(KEKSItemsEncode(&items, 0, &off, dst, len));
        assert(off == lenExpected);
        assert(memcmp(dst, buf, lenExpected) == 0);
    }
    free(buf);
    return EXIT_SUCCESS;
}
