/* Lex JSON files.
 * See RFC 8259 (which obsoletes RFC 7159)
 *
 * The goal here is to parse valid JSON correctly;
 * we don't try terribly hard to deal with pathologically invalid input,
 * and support no extensions.
 *
 * We assume the input is UTF-8: this is required by RFC 8259 (but wasn't before).
 *
 * This is a very simple lexer.  The only intricate bit is the parsing of \uXXXX escapes.
 *
 * All of the lvalues are s7 objects, not protected from garbage-collection.
 *
 * See also the notes at the top of parse-json.y
 *
 * This file is part of Beastie <https://purl.org/nxg/dist/beastie>
 * SPDX-FileCopyrightText: 2025 Norman Gray <https://nxg.me.uk>
 * SPDX-License-Identifier: BSD-2-Clause
 */

%top{
#if __GNUC__
// for fileno
#define _XOPEN_SOURCE 600
#endif
}

%{
#include <ctype.h>
#include <assert.h>

#include "beastie.h"
#include "util.h"
#include "parse-json.h"
#include "parse-json.tab.h"
#include "unicode.h"
#include "unicode-scm.h"

#ifndef WITH_MAIN
#define WITH_MAIN 0
#endif

    /* We can also define this as #define ERROR "sprintf"
     * to get the error returned as the bad_lexeme lvalue,
     * but not printed */
#define ERROR "print-warning"

%}

%option prefix="json" reentrant noyywrap nounput noinput yylineno bison-bridge bison-locations
%option extra-type="json_extra_t"

 /* RFC 8259, Sect.6:

    number = [ minus ] int [ frac ] [ exp ]
    decimal-point = %x2E       ; .
    digit1-9 = %x31-39         ; 1-9
    e = %x65 / %x45            ; e E
    exp = e [ minus / plus ] 1*DIGIT
    frac = decimal-point 1*DIGIT
    int = zero / ( digit1-9 *DIGIT )
    minus = %x2D               ; -
    plus = %x2B                ; +
    zero = %x30                ; 0
 */
NUMBER	[-]?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)?

 /* OWS isn't [[:space:]]* because the RFC (Sect.2) says that
    whitespace is _only_ these four characters */
OWS	[ \t\n\r]*

 /* this pattern matches and includes \" escapes inside the string */
STRINGCONTENT	[^"]*(\\\"[^"]*)*

%%
{OWS}"\""{STRINGCONTENT}"\""? {

    ustring_t us = make_ustring(NULL);
    const unsigned char* p = (const unsigned char*)yytext;

    // skip leading whitespace
    while (*p != '"') p++;
    p++;                        // step over opening quote

    uint16_t surrogate1 = 0;    // holds the first of a pair of (escaped) surrogates
    int good_scan_p = 1;
    int scanning_p = 1;
    while (scanning_p) {

        if (*p == '\0') {
            // unterminated string
            *yylval = scheme_eval(ERROR,
                                  s7_make_string(S7, "unterminated string in input: ~s"),
                                  s7_make_string(S7, yytext),
                                  NULL);
            good_scan_p = scanning_p = 0;

        } else if (*p == '"') {

            scanning_p = 0;

        } else if (*p < 0x20) {
            // "control characters" aren't allowed in JSON strings unescaped
            *yylval = scheme_eval(ERROR,
                                  s7_make_string(S7, "control character 0x~x in string at ~a:~a"),
                                  s7_make_integer(S7, *p),
                                  s7_make_string(S7, (yyextra->path == NULL
                                                      ? "<unknown>.json"
                                                      : yyextra->path)),
                                  s7_make_integer(S7, yyget_lineno(yyscanner)),
                                  NULL);
            good_scan_p = scanning_p = 0;

        } else if ((unsigned char)*p >= 0x80) {
            // We're assuming this is UTF-8
            unsigned char nused;
            const char* errmsg;
            int ofs = p -(const unsigned char*)yytext;
            codepoint_t cp = decode_utf8(p, yyleng - ofs, &nused, &errmsg);
            if (cp == UNICODE_BAD_DECODE) {
                *yylval = scheme_eval(ERROR,
                                      s7_make_string(S7, "invalid UTF-8 at ~a:~a: ~a"),
                                      s7_make_string(S7, (yyextra->path == NULL
                                                          ? "<unknown>.json"
                                                          : yyextra->path)),
                                      s7_make_integer(S7, yyget_lineno(yyscanner)),
                                      s7_make_string(S7, errmsg),
                                      NULL);
                good_scan_p = scanning_p = 0;

            } else {
                ustring_append_cp(us, cp, NULL);
                p += nused-1;   // -1 because we p++ below
            }

        } else if (*p == '\\') {
            p++;
            switch (*p) {
              case '"':  ustring_append_cp(us, '"',  NULL); break;
              case '\\': ustring_append_cp(us, '\\', NULL); break;
              case '/':  ustring_append_cp(us, '/',  NULL); break;
              case 'b':  ustring_append_cp(us, '\b', NULL); break;
              case 'f':  ustring_append_cp(us, '\f', NULL); break;
              case 'n':  ustring_append_cp(us, '\n', NULL); break;
              case 'r':  ustring_append_cp(us, '\r', NULL); break;
              case 't':  ustring_append_cp(us, '\t', NULL); break;

              case 'u':
                {
                    // Unicode escape: \uXXXX, containing exactly four hex digits.
                    // We do not tolerate fewer,
                    // and we're careful to examine at most four.
                    // The hex-digits can be in either case (which
                    // matches the behaviour of isxdigit(3)).
                    char buf[5];
                    int i;
                    for (i=0; i<4; i++) {
                        int c0 = *++p;
                        if (c0 == 0) {
                            // unexpected end of string,
                            // but let this be reported as a short escape
                            scanning_p = 0;
                            break;
                        } else if (isxdigit(c0)) {
                            buf[i] = c0;
                        } else {
                            break;
                        }
                    }
                    buf[i] = '\0';
                    if (i < 4) {
                        // it would not unreasonable to make this a warning,
                        // rather than an error
                        *yylval = scheme_eval(ERROR,
                                              s7_make_string(S7, "short Unicode escape \\u~a in ~a:~a"),
                                              s7_make_string(S7, buf),
                                              s7_make_string(S7, (yyextra->path == NULL
                                                                  ? "<unknown>.json"
                                                                  : yyextra->path)),
                                              s7_make_integer(S7, yyget_lineno(yyscanner)),
                                              NULL);
                        good_scan_p = scanning_p = 0;
                        break;  // out of switch, and so soon out of while(scanning_p)
                    }

                    // this number can't be bigger than 0xffff,
                    // since it's formed from four hex digits,
                    // so it's safe to assign it to codepoint_t
                    codepoint_t cp = (codepoint_t)strtol(buf, NULL, 16);

                    if (is_surrogate(cp)) {
                        // RFC 8259: a non-BMP character is
                        // represented by two escapes forming a surrogate pair.
                        // Example:
                        //
                        //     So, for example, a string containing only the G clef
                        //     character (U+1D11E) may be represented as "\uD834\uDD1E".
                        if (surrogate1 != 0) {
                            // this should be the second surrogate pair -- good
                            uint16_t surrogates[2] = {surrogate1, cp};
                            cp = from_surrogate(&surrogates[0], NULL);
                            if (cp == UNICODE_BAD_DECODE) {
                                *yylval = scheme_eval(ERROR,
                                                      s7_make_string(S7, "bad surrogate pair in escape \\u~a, at ~a:~a"),
                                                      s7_make_string(S7, buf),
                                                      s7_make_string(S7, (yyextra->path == NULL
                                                                          ? "<unknown>.json"
                                                                          : yyextra->path)),
                                                      s7_make_integer(S7, yyget_lineno(yyscanner)),
                                                      NULL);
                                good_scan_p = scanning_p = 0;
                            } else {
                                ustring_append_cp(us, cp, NULL);
                            }
                            surrogate1 = 0;
                        } else {
                            // this is the first surrogate pair -- another is to follow
                            surrogate1 = cp;
                        }

                    } else if (surrogate1) {
                        *yylval = scheme_eval(ERROR,
                                              s7_make_string(S7, "bad surrogates in \\u~a: second missing, at ~a:~a"),
                                              s7_make_string(S7, buf),
                                              s7_make_string(S7, (yyextra->path == NULL
                                                                  ? "<unknown>.json"
                                                                  : yyextra->path)),
                                              s7_make_integer(S7, yyget_lineno(yyscanner)),
                                              NULL);
                        good_scan_p = scanning_p = 0;
                        surrogate1 = 0;

                    } else {
                        ustring_append_cp(us, cp, NULL);
                    }
                }
                break;

              default:
                *yylval = scheme_eval(ERROR,
                                      s7_make_string(S7, "unexpected escape \\~a at ~a:~a"),
                                      s7_make_character(S7, *p),
                                      s7_make_string(S7, (yyextra->path == NULL
                                                          ? "<unknown>.json"
                                                          : yyextra->path)),
                                      s7_make_integer(S7, yyget_lineno(yyscanner)),
                                      NULL);
                good_scan_p = scanning_p = 0;
                break;
            }

        } else {
            // an ordinary character
            if (surrogate1 != 0) {
                // this is a non-surrogate following a surrogate
                *yylval = scheme_eval(ERROR,
                                      s7_make_string(S7, "bad surrogate pair in ~a:~a: surrogate U+~x followed by non-surrogate '~a'"),
                                      s7_make_string(S7, (yyextra->path == NULL
                                                          ? "<unknown>.json"
                                                          : yyextra->path)),
                                      s7_make_integer(S7, yyget_lineno(yyscanner)),
                                      s7_make_integer(S7, surrogate1),
                                      s7_make_character(S7, *p),
                                      NULL);
                good_scan_p = scanning_p = 0;
                surrogate1 = 0;
                // discard the most recent character,
                // rather than accepting only one of a putative pair
            } else {
                ustring_append_cp(us, *p, NULL);
            }
        }

        p++;
    }
    if (good_scan_p) {
        *yylval = make_ustring_obj(S7, us);
        return STRING;
    } else {
        // yylval has been assigned
        return BAD_LEXEME;
    }
 }

{OWS}{NUMBER}{OWS} {
    s7_double n = strtod(yytext, NULL);
    *yylval = s7_make_real(S7, n);
    return NUMBER;
}

{OWS}"{"{OWS} return '{';
{OWS}"}"{OWS} return '}';
{OWS}"["{OWS} return '[';
{OWS}"]"{OWS} return ']';
{OWS}","{OWS} return ',';
{OWS}":"{OWS} return ':';

 /* RFC, Sect.3: ‘The literal names [false/true/null] MUST be lowercase.
    No other literal names are allowed.’ */

{OWS}"false"{OWS} *yylval = s7_f(S7);   return JSON_FALSE;
{OWS}"true"{OWS}  *yylval = s7_t(S7);   return JSON_TRUE;
{OWS}"null"{OWS}  *yylval = s7_nil(S7); return JSON_NULL;

<<EOF>> {
    yyterminate();
}


. {
#define OUTBUFLEN 256
    char buf[OUTBUFLEN];
    size_t stringsize = snprintf(buf, OUTBUFLEN,
                                 "Unexpected character '%c' at line %d of %s",
                                 yytext[0], yyget_lineno(yyscanner),
                                 (yyextra->path == NULL ? "<stdin>" : yyextra->path));
    if (stringsize > OUTBUFLEN) {
        // unlikely, but...
        memcpy(&buf[OUTBUFLEN-4], "...", 3);
    }
    *yylval = scheme_eval(ERROR,
                          s7_make_string(S7, "error: ~a~%"),
                          s7_make_string(S7, buf),
                          NULL);
    return BAD_LEXEME;
#undef OUTBUFLEN
}

%%

/* For debugging/test purposes,
 * we also want to be able to parse .json files read from a string.
 * PATH==NULL means stdin; return NULL on error.
 */
yyscan_t parse_json_setup_file(json_extra_t extra, const char* path)
{
    FILE* f = NULL;
    if (path != NULL) {
        f = fopen(path, "r");
        if (f == NULL) return NULL; // JUMP OUT
    }

    yyscan_t scanner;
    yylex_init_extra(extra, &scanner);

    extra->yyscanbuf = 0;
    if (path == NULL) {
        extra->path = NULL;
    } else {
        extra->path = strdup(path);
    }
    extra->f = f;

    if (f != NULL) {
        yyset_in(f, scanner);
    }

    return scanner;
}
yyscan_t parse_json_setup_string(json_extra_t extra, const char* s)
{
    yyscan_t scanner;
    yylex_init_extra(extra, &scanner);
    extra->yyscanbuf = yy_scan_string(s, scanner);

    const int maxchars = 20;   // maximum number of chars shown from s
    const char* fmt;
    if (strlen(s) < maxchars) {
        fmt = "<string:%.*s>";
    } else {
        fmt = "<string:%.*s...>";
    }
    //const char fmt[] = "<string:%.*s...>";
    const size_t totlen = maxchars + strlen(fmt);
    char* source_legend = malloc(totlen+1);
    snprintf(source_legend, totlen, fmt, maxchars, s);
    extra->path = source_legend;
    extra->f = NULL;

    yyset_debug(1, scanner);

    yyset_lineno(1, scanner);
    return scanner;
}
void parse_json_finish(json_extra_t extra, yyscan_t scanner)
{
    if (extra->yyscanbuf) {
        yy_delete_buffer((YY_BUFFER_STATE)extra->yyscanbuf, scanner);
        extra->yyscanbuf = NULL;
    }
    if (extra->path) free(extra->path);
    if (extra->f) fclose(extra->f);

    yylex_destroy(scanner);
}

#if WITH_MAIN
#include <stdio.h>
#include <unistd.h>
#include "s7.h"
#include "util.h"

YYSTYPE one_value;
YYLTYPE locp;
s7_scheme* S7;

static void display_lexemes(yyscan_t scanner)
{
    int l;
    int eof_p = 0;

    while (!eof_p && (l = jsonlex(&one_value, &locp, scanner)) != 0) {
        printf("l=%d\n", l);
        switch (l) {
          case '{': case '}':
          case '[': case ']':
          case ':': case ',':
            printf("%c\n", l);
            break;

          case JSON_FALSE:
            printf("false\n");
            break;
          case JSON_NULL:
            printf("null\n");
            break;
          case JSON_TRUE:
            printf("true\n");
            break;

          case STRING:
            s7w("string(", one_value, ")\n");
            break;

          case NUMBER:
            s7w("number(", one_value, ")\n");
            break;

          case BAD_LEXEME:
            printf("** bad lexeme\n");
            break;

          default:
            printf("Unexpected lexeme: %d\n", l);
        }
    }
}

const char* progname;
void Usage(int exitstatus)
{
    fprintf(stderr, "Usage: %s [-s] [foo.json]\n", progname);
    exit(exitstatus);
}

int main(int argc, char** argv)
{
    const char* jsonin = NULL;
    char input_is_file_p = 1;
    progname = argv[0];

    char optchar;
    while ((optchar = getopt(argc, argv, "hs")) >= 0) {
        switch (optchar) {
          case 's':
            input_is_file_p = 0;
            break;

          case 'h':
            Usage(0);

          default:
            Usage(1);
        }
    }
    argc -= optind;
    argv += optind;

    if (argc == 0) {
        if (! input_is_file_p) {
            fprintf(stderr, "With -s, an argument must be provided\n");
            exit(1);
        }
    } else {
        jsonin = argv[0];
    }

    S7 = s7_init();
    s7_eval_c_string(S7,
                     "(define (beastie-error fmt . args)\n"
                     "  (apply format\n"
                     "     (cons (current-error-port) (cons (string-append \"error: \" fmt \"~%\") args))))");
    s7_eval_c_string(S7,
                     "(define (print-warning fmt . args)\n"
                     "  (apply format\n"
                     "     (cons (current-error-port) (cons (string-append \"warning: \" fmt \"~%\") args))))");

    unicode_load_hook(S7, s7_nil(S7));

    yyscan_t scanner;
    struct json_extra_s S;
    if (input_is_file_p) {
        scanner = parse_json_setup_file(&S, jsonin);
        if (scanner == NULL) {
            fprintf(stderr,
                    "%s: can't open file %s to read\n", progname,
                    (jsonin == NULL ? "<stdin>" : jsonin));
            exit(1);
        }
    } else {
        scanner = parse_json_setup_string(&S, jsonin);
        if (scanner == NULL) {
            fprintf(stderr,
                    "%s: can't open input string!\n", progname);
            exit(1);
        }
    }

    display_lexemes(scanner);

    parse_json_finish(&S, scanner);
}
#endif
