/*
 * A lexer for the 'block' (ie, vertical) parts of Markdown.
 *
 * This file is part of Beastie <https://purl.org/nxg/dist/beastie>
 * SPDX-FileCopyrightText: 2024 Norman Gray <https://nxg.me.uk>
 * SPDX-License-Identifier: BSD-2-Clause
 */

%top{
#include "config.h"
#if __GNUC__ && !defined(__clang__)
// for fileno
#define _XOPEN_SOURCE 600
#endif
}

%{


#include <ctype.h>
#include <stdarg.h>
#include <assert.h>
#if HAVE_ALLOCA_H
#include <alloca.h>
#endif

#include "s7.h"
#include "parse-mdblock.h"
#include "util.h"

#ifndef WITH_MAIN
#define WITH_MAIN 0
#endif

#define X(field) (yyextra->field)

#define RETURN(...) do {                     \
        *yylval = mdblock_lexeme(__VA_ARGS__);           \
        return 1;                               \
    } while (0)

    /* lineno is the line number after the end-of-line */
#define LINENUMBER "line-number", s7_make_integer(S7, yyget_lineno(yyscanner)-1)

#define MAYBEBLANK do {                                                 \
        if (X(vspace_p)) {                                              \
            X(vspace_p) = 0;                                            \
            yyless(0);                                                  \
            RETURN("blank",                                             \
                   s7_f(S7),                                            \
                   LINENUMBER, \
                   NULL);                                               \
        }                                                               \
 } while (0)

static s7_pointer mdblock_lexeme(const char*, s7_pointer, ...);
static s7_pointer string_from_buf(const char* buf_start, const char* p, const size_t blen);

%}

%option prefix="mdblock" reentrant extra-type="mdblock_extra_t" nounput noinput
     // we're not using bison, but we do want the corresponding API
%option bison-bridge bison-locations noyywrap yylineno


ORDINARY	[^>#=[:space:]*+-]
     //ORDINARY	[^>#=[:space:][:digit:]*+-]
NUMBER		[0-9]
HWS		[ \t]
BULLET		[*+-]
 /* Markdown spec:
  *   Link definition names may consist of letters, numbers, spaces, and punctuation
  *   --  but they are not case sensitive.
  * Vague as usual: I've chosen the 'punctuation' here to be ispunct(3) minus ["'<>\].
  */
LINKREF		[ a-zA-Z0-9!#$%&()*+,./:;=?@^_{|}~-]+

%%

{HWS}*"\n"	X(vspace_p) = 1;
 /*
 {HWS}*"\n"	{
     //X(vspace_p) = 1;
     RETURN("blank",
            s7_f(S7),
            LINENUMBER,
            NULL);
            } */

[=]+"\n"	{
    MAYBEBLANK;
    RETURN("h1underline",
           s7_f(S7),
           LINENUMBER,
           NULL);
}

 /*
[ ]{0,3}{ORDINARY}.*"\n"		 {
    MAYBEBLANK;
    char* p = yytext;
    while (isspace(*p)) p++;
    RETURN("text",
           string_from_buf(yytext, p, yyleng),
           LINENUMBER,
           NULL);
           }*/
 /*  merge with above?
{ORDINARY}.*"\n" {
    RETURN("text",
           string_from_buf(yytext, yytext, yyleng),
           LINENUMBER,
           NULL);
           } */
 /*
[ ]{4}.*"\n" {
    MAYBEBLANK;
    // handle tabs?
    char* p = yytext;
    while (isspace(*p)) {
        assert(*p != '\n');     // should have been caught above
        p++;
    }
    //size_t indent = (p-yytext)/4;
    RETURN("text",
           string_from_buf(yytext, p, yyleng),
           LINENUMBER,
           "indent", s7_make_integer(S7, (p-yytext)/4),
           "indent+", s7_make_integer(S7, (p-yytext)%4),
           NULL);
}
*/

 /* Markdown supports '- - -' or '* * *' as rules, but not a mixture. */
"-"[ -]{3,}"\n"		{
    MAYBEBLANK;
    RETURN("hr",
           s7_f(S7),
           LINENUMBER,
           NULL);
}
"*"[ *]{3,}"\n"		{
    MAYBEBLANK;
    RETURN("hr",
           s7_f(S7),
           LINENUMBER,
           NULL);
}

 /* ### headings */
[#]+{HWS}*.+"\n"	 {
    MAYBEBLANK;
    const char* p = yytext;
    while (*p == '#') p++;
    const char* pfx_end = p;
    while (isspace(*p) && *p != '\0') p++;

    if (*p == '\0') {
        // this looked like a heading, but it's actually a line "##   "
        RETURN("text",
               s7_make_string_with_length(S7, yytext, yyleng-1), // -1 to remove newline
               LINENUMBER,
               NULL);
    }

    // we also want to strip off trailing '#', so can't just use string_from_buf
    const char* endp = &yytext[yyleng-1];
    while ((*endp == '#' || isspace(*endp)) && endp > yytext) endp--;
    if (endp == yytext) {
        // ooops: this 'heading' has no actual text in it, so bail out
        RETURN("text", string_from_buf(yytext, yytext, yyleng));
    }
    // endp now points at the last non-discarded character
    size_t len = endp-p+1;
    char* buf = alloca(len+1);
    memcpy(buf, p, len);
    buf[len] = '\0';

    int prefix_len = pfx_end - yytext;
    RETURN("hn",
           s7_make_string_with_length(S7, buf, len),
           "level", s7_make_integer(S7, (prefix_len > 6 ? 6 : prefix_len)),
           LINENUMBER,
           NULL);
}

{HWS}{0,3}({BULLET}|{NUMBER}+[.]){HWS}+.*"\n" {
    //MAYBEBLANK;
    //fprintf(stderr, "BULLET: ni=%d  nc=%d\n", X(ni), X(nc));
    const char* p = yytext;
    while (isspace(*p)) p++;
    int is_ol_p = isdigit(*p);
    while (!isspace(*p)) p++;
    while (isspace(*p)) p++;
    // now pointing at the LI line text
    int is_after_blank_p = X(vspace_p);
    X(vspace_p) = 0;

    RETURN("li",
           string_from_buf(yytext, p, yyleng),
           LINENUMBER,
           "is-ol?", s7_make_boolean(S7, is_ol_p),
           "blank-before?", s7_make_boolean(S7, is_after_blank_p),
           NULL);
}

.*"\n" {
    MAYBEBLANK;
    // handle tabs?
    char* p = yytext;
    while (isspace(*p)) {
        assert(*p != '\n');     // should have been caught above
        p++;
    }
    RETURN("text",
           string_from_buf(yytext, p, yyleng),
           LINENUMBER,
           "indent", s7_make_integer(S7, (p-yytext)/4),
           "indent+", s7_make_integer(S7, (p-yytext)%4),
           NULL);
}

%%

/* The buffer bstart is of length blen, and ends with '\n'.
 * Create an s7 string from p, which points within the string, to the end,
 * omitting the trailing newline and other trailing whitespace.
 */
static s7_pointer string_from_buf(const char* bstart,
                                  const char* p,
                                  const size_t blen)
{
    s7_pointer result;

    if (p < bstart || p > &bstart[blen]) {
        fprintf(stderr, "string_to_buf: pointer p not in correct range\n");
        return s7_make_string(S7, "");
    }

    const char* endp = bstart + blen - 1;
    while (isspace(*endp) && endp >= p) endp--;

    if (endp < p) {
        // unusual, but don't fail
        result = s7_make_string(S7, "");
    } else {
        // endp now points at the last non-whitespace character
        size_t len = endp - p + 1;
        char* buf = alloca(len+1);
        memcpy(buf, p, len);
        buf[len] = '\0';
        result = s7_make_string_with_length(S7, buf, len);
    }
    return result;
}

// Turn a va_list, pointing to an even number of arguments, terminated
// by NULL, into a symbol-keyed alist.  The first argument of each
// pair is a C string, and the second a scheme object.
// Call initially with the ap about to read the first argument, and a
// 'key' argument of NULL.
static s7_pointer ap2assq(va_list ap, s7_pointer key)
{
    if (key) {
        // second of pair
        s7_pointer obj = va_arg(ap, s7_pointer);
        if (obj == NULL) {
            // unexpected end of argument list
            return s7_make_list(S7, 1, s7_cons(S7, key, s7_f(S7)));
        } else {
            return s7_cons(S7, s7_cons(S7, key, obj), ap2assq(ap, NULL));
        }
    } else {
        // first of pair
        const char* key_string = va_arg(ap, const char*);
        if (key_string == NULL) {
            // end of argument list
            return s7_nil(S7);
        } else {
            return ap2assq(ap, s7_make_symbol(S7, key_string));
        }
    }
}

// Construct a lexeme: the arguments are the tag, the value, and an
// even number of trailing arguments, which are alternately keyword
// (as a C string) and value (a scheme object).  These are packed into
// a three-element vector, with the trailing arguments being an
// symbol-keyed alist.
static s7_pointer mdblock_lexeme(const char* tag, s7_pointer lval, ...)
{
    va_list ap;
    va_start(ap, lval);
    s7_pointer assq = ap2assq(ap, NULL);
    va_end(ap);

    s7_pointer result = s7_make_vector(S7, 3);
    s7_vector_set(S7, result, 0, s7_make_symbol(S7, tag));
    s7_vector_set(S7, result, 1, lval);
    s7_vector_set(S7, result, 2, assq);

    //scheme_printf("lexeme: ~s~%", result, NULL);

    return result;
}


static void init_lexer_state(mdblock_extra_t extra, yyscan_t* scanner_ptr)
{
    yylex_init_extra(extra, scanner_ptr);
    extra->vspace_p = 0;
    // extra->bq_level = 0;
    // extra->ni = 0;
    // extra->nc = 0;
    // extra->found_annotation_p = 0;
    // extra->string_buf = NULL;
    // extra->filename = NULL;
    // extra->infile = NULL;
}

static yyscan_t parse_mdblock_setup_file(mdblock_extra_t extra, const char* fn)
{
    yyscan_t scanner;
    init_lexer_state(extra, &scanner);

    if (fn != NULL) {
        FILE* infile = fopen(fn, "r");
        if (infile == NULL) {
            // warning or error?  #f or '()?
            scheme_eval("print-warning",
                        s7_make_string(S7, "parse-mdblock-file/metadata: can't open file ~a to read"),
                        s7_make_string(S7, fn), 0);
            return NULL;        // JUMP OUT
        }
        //extra->infile = infile;

        yyset_in(infile, scanner);
    }

    // if (mdblockdebug) {
    //     yyset_debug(1, scanner);
    // }

    return scanner;
}

static yyscan_t parse_mdblock_setup_string(mdblock_extra_t extra, const char* s, size_t slen)
{
    yyscan_t scanner;
    init_lexer_state(extra, &scanner);

    // Set up the buffer for yy_scan_buffer.  This involves
    // ensuring the string ends with a newline, and adding the
    // two zero bytes that yy_scan_buffer requires.  We could use
    // yy_scan_string (which involves a further copy of the string)
    // but if we're adding the newline, we might as well do both jobs.
    //
    // We must add the newline, or else the mdblock block parser
    // gets unhappy.  I have tried adding this extra newline with a
    // yywrap() function which sets up a suitable buffer containing
    // only "\n", but that ends up becoming quite complicated,
    // for little real benefit (as well as apparently requiring me to
    // go a bit off-piste regarding the signature of yywrap, by using
    // yyguts* instead of yyscan_t).  Since I do this copy here anyway
    // (at which point it's easy to add the newline), there's no
    // benefit to using yywrap.
    char* t = malloc(slen+3);
    if (t == NULL) {
        fprintf(stderr, "Unable to allocate %ld bytes for input string!\n", slen+3);
        exit(1);
    }
    memcpy(t, s, slen);
    memcpy(&t[slen], "\n\0\0", 3);
    //extra->string_buf = t;

    yy_scan_buffer(t, slen+3, scanner);
    yyset_lineno(1, scanner);

    return scanner;
}

static void parse_mdblock_finish(mdblock_extra_t extra, yyscan_t scanner)
{
    // if (extra->string_buf) {
    //     free(extra->string_buf);
    //     extra->string_buf = NULL;
    // }
    // if (extra->infile) {
    //     fclose(extra->infile);
    //     extra->infile = NULL;
    // }

    if (scanner != NULL) yylex_destroy(scanner);
}

struct mdblocklex_s {
    yyscan_t scanner;
    s7_pointer source;
    unsigned char source_is_file_p;
    struct mdblock_extra_s S;
};
typedef struct mdblocklex_s* mdblocklex_t;

static int mdblocklex_type_tag = 0;

static int is_mdblocklex(s7_pointer obj)
{
    return s7_is_c_object(obj)
        && s7_c_object_type(obj) == mdblocklex_type_tag;
}

static void destroy_mdblock_object(s7_pointer obj)
{
    mdblocklex_t p = s7_c_object_value(obj);
    parse_mdblock_finish(&p->S, p->scanner);
    p->scanner = NULL;
}

static s7_pointer free_mdblocklex(s7_scheme *sc, s7_pointer obj)
{
    destroy_mdblock_object(obj);
    return NULL;
}

static s7_pointer mark_mdblocklex(s7_scheme *sc, s7_pointer obj)
{
    mdblocklex_t p = (mdblocklex_t)s7_c_object_value(obj);
    s7_mark(p->source);
    return NULL;
}

static s7_pointer mdblocklex_to_string(s7_scheme *sc, s7_pointer args)
{
    mdblocklex_t p = (mdblocklex_t)s7_c_object_value(s7_car(args));
    const char* fmt = "<mdblocklex %s:%s>";
    const size_t slen = s7_string_length(p->source) + strlen("string") + strlen(fmt);
    char *s = alloca(slen);
    snprintf(s, slen, fmt,
             (p->source_is_file_p ? "file" : "string"),
             s7_string(p->source));
    return s7_make_string(sc, s);
}

static s7_pointer mdblocklex_is_equal(s7_scheme *sc, s7_pointer args)
{
    s7_pointer o1 = s7_car(args);
    s7_pointer o2 = s7_cadr(args);

    if (o1 == o2) return s7_t(sc);

    if (!s7_is_c_object(o2)
        || (s7_c_object_type(o2) != mdblocklex_type_tag)) return s7_f(sc);

    mdblocklex_t p1 = (mdblocklex_t)s7_c_object_value(o1);
    mdblocklex_t p2 = (mdblocklex_t)s7_c_object_value(o2);
    return s7_make_boolean(sc, s7_is_equal(sc, p1->source, p2->source));
}

static s7_pointer make_mdblocklex_file_func(s7_scheme *sc, s7_pointer args)
{
    s7_pointer filename = s7_car(args);

    if (! s7_is_string(filename)) {
        return s7_wrong_type_arg_error(sc, "mdblock-make-lexer/file", 1, filename, "a string (filename)");
    }

    mdblocklex_t p = malloc(sizeof(struct mdblocklex_s));
    p->scanner = parse_mdblock_setup_file(&p->S, s7_string(filename));
    p->source = filename;
    p->source_is_file_p = 1;

    return s7_make_c_object(sc, mdblocklex_type_tag, (void*)p);
}

static s7_pointer make_mdblocklex_string_func(s7_scheme *sc, s7_pointer args)
{
    s7_pointer input_string = s7_car(args);
    if (! s7_is_string(input_string)) {
        return s7_wrong_type_arg_error(sc, "mdblock-make-lexer/string", 1, input_string, "a string");
    }

    mdblocklex_t p = malloc(sizeof(struct mdblocklex_s));
    p->scanner = parse_mdblock_setup_string(&p->S,
                                            s7_string(input_string),
                                            s7_string_length(input_string));
    p->source = input_string;
    p->source_is_file_p = 0;

    return s7_make_c_object(sc, mdblocklex_type_tag, (void*)p);
}

static s7_pointer mdblocklex_destroy_func(s7_scheme *sc, s7_pointer args)
{
    if (! is_mdblocklex(s7_car(args))) {
        return s7_wrong_type_arg_error(sc, "mdblock-destroy-lexer", 1, s7_car(args), "a mdblock lexer");
    }
    destroy_mdblock_object(s7_car(args));

    return s7_unspecified(sc);
}

static s7_pointer is_mdblocklex_func(s7_scheme *sc, s7_pointer args)
{
    return s7_make_boolean(sc, is_mdblocklex(s7_car(args)));
}

static s7_pointer mdblocklex_get_func(s7_scheme *sc, s7_pointer args)
{
    if (! is_mdblocklex(s7_car(args))) {
        return s7_wrong_type_arg_error(sc, "mdblock-get-lexeme", 1, s7_car(args), "a mdblock lexer");
    }

    mdblocklex_t p = s7_c_object_value(s7_car(args));
    YYSTYPE one_value;
    YYLTYPE locp;

    int status = mdblocklex(&one_value, &locp, p->scanner);
    if (status == 0) {
        return s7_eof_object(sc);
    } else {
        return one_value;
    }
}

s7_pointer mdblock_load_hook(s7_scheme* sc, s7_pointer args)
{
    if (mdblocklex_type_tag != 0) {
        // been here before
        return args;
    }

    s7_pointer module_name = s7_car(args);
    s7_pointer provides = s7_cadr(args);
    s7_pointer env = s7_caddr(args);

    mdblocklex_type_tag = s7_make_c_type(sc, "mdblocklex");
    s7_c_type_set_gc_free(sc, mdblocklex_type_tag, free_mdblocklex);
    s7_c_type_set_gc_mark(sc, mdblocklex_type_tag, mark_mdblocklex);
    s7_c_type_set_is_equal(sc, mdblocklex_type_tag, mdblocklex_is_equal);
    // the following doesn't seem to have any effect
    s7_c_type_set_to_string(sc, mdblocklex_type_tag, mdblocklex_to_string);

    struct {
        const char* name;
        s7_function fnc;
        s7_int required_args;
        s7_int optional_args;
        bool rest_arg;
        const char *doc;
    } local_functions[] = {
    { "mdblock-lexer?",
      is_mdblocklex_func,
      1, 0, false,
      "(mdblock-lexer? obj) : true if the object is an mdblock lexer" },

    { "mdblock-make-lexer/file",
      make_mdblocklex_file_func,
      1, 0, false,
      "(mdblock-make-lexer/file filename) : open the file to read lexemes from it" },

    { "mdblock-make-lexer/string",
      make_mdblocklex_string_func,
      1, 0, false,
      "(mdblock-make-lexer/string str) : read lexemes from a string" },

    { "mdblock-destroy-lexer",
      mdblocklex_destroy_func,
      1, 0, false,
      "(mdblock-destroy-lexer l) : destroy the lexer created by mdblock-make-lexer" },

    { "mdblock-get-lexeme",
      mdblocklex_get_func,
      1, 0, false,
      "(mdblock-get-lexeme l) : retrieve the next lexeme from the lexer" },

    { "mdblock-equal?",
      mdblocklex_is_equal,
      2, 0, false,
      "(mdblock-equal? l1 l2) : true if the lexers refer to the same file" }
    };
    const int n_local = sizeof(local_functions)/sizeof(local_functions[0]);

    for (int i=0; i<n_local; i++) {
        s7_pointer name = s7_make_symbol(sc, local_functions[i].name);
        s7_pointer f = s7_make_function(sc,
                                        local_functions[i].name,
                                        local_functions[i].fnc,
                                        local_functions[i].required_args,
                                        local_functions[i].optional_args,
                                        local_functions[i].rest_arg,
                                        local_functions[i].doc);
        s7_varlet(sc, env, name, f);
        s7_varlet(sc, provides, name, f);
    }

    return s7_list(sc, 3, module_name, provides, env);
}



#if WITH_MAIN

s7_scheme* S7;

static void display_lexemes(yyscan_t scanner)
{
    int l;
    YYSTYPE one_value;
    YYLTYPE locp;

    while ((l = mdblocklex(&one_value, &locp, scanner)) != 0) {
        s7w(NULL, one_value, "\n");
    }
}

static const char* progname;
void Usage(void)
{
    fprintf(stderr, "Usage: %s [-g] [foo.md]\n", progname);
    exit(1);
}

int main(int argc, char** argv)
{
    const char* infilename;
    progname = argv[0];

    int do_debug = 0;

    int ch;
    while ((ch = getopt(argc, argv, "g")) != -1) {
        switch (ch) {
          case 'g':
            do_debug = 1;
            break;
          default:
            Usage();
        }
    }
    argc -= optind;
    argv += optind;

    switch (argc) {
      case 0:
        infilename = NULL;
        break;
      case 1:
        infilename = argv[0];
        break;
      default:
        Usage();
    }

    S7 = s7_init();

    struct mdblock_extra_s S;

    yyscan_t scanner = parse_mdblock_setup_file(&S, infilename);
    yyset_debug(do_debug, scanner);
    display_lexemes(scanner);
    parse_mdblock_finish(&S, scanner);

}

#endif
