/*
 * A lexer for inline-markdown -- that is, the markdown within a
 * paragraph, rather than the block structure.
 *
 * This is made intricate by the constructs `*...*` and `_..._` have
 * the same character starting and ending the span of text (so that we
 * have to keep track of the state) and by the fact that `_` is
 * non-magic inside stars, and stars non-magic inside underscores.
 * Also `__...__` and `**...**` must nest appropriately.
 *
 * Finally, we don't want to have errors when the nesting is ‘wrong’
 * (with ‘wrong’ in scare-quotes, because arguably nothing the user
 * types in Markdown is ‘wrong’), so we carefully pop the stack,
 * generating appropriate end-tokens, at end-of-string and at the
 * closing brace of a `[text](link)` construct.
 *
 * In true Markdown spirit, I'm not really following any standard
 * here.  The Markdown here is probably as primitive as Gruber's
 * original, and certainly not as (insanely) intricate as Commonmark.
 *
 * This file is part of Beastie <https://purl.org/nxg/dist/beastie>
 * SPDX-FileCopyrightText: 2023 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 "beastie.h"
#include "util.h"

#include "parse-mdinline.h"
#include "parse-mdinline.tab.h"

#ifndef WITH_MAIN
#define WITH_MAIN 0
#endif

#define ST_PUSH(x) do {                         \
        if (yyextra->stack_depth == MDINLINE_LEXER_STACK_SIZE) {        \
            fprintf(stderr, "mdinline: state_stack overflow\n");        \
        } else {                                                        \
            yyextra->state_stack[yyextra->stack_depth++] = x;           \
        }                                                               \
    } while(0)
#define ST_POP ((yyextra->stack_depth == 0) \
                ? (fprintf(stderr, "state_stack_underflow!\n"), st_none) \
                : yyextra->state_stack[--(yyextra->stack_depth)])
#define ST_TOP (yyextra->stack_depth > 0 \
                ? yyextra->state_stack[yyextra->stack_depth-1] \
                : st_none)

int in_state_p(mdinline_extra_t, state_stack_t);

/* defined in parse-mdinline.y */
extern int mdinlinedebug;
%}

%option prefix="mdinline" reentrant bison-bridge bison-locations
%option noyywrap noinput debug
%option extra-type="mdinline_extra_t"

%x IN_CITATION
%x IN_ENDLINK

/* We don't particularly expect newlines in the input to this
 * parser, but we should regard them as whitespace if they do
 * turn up
 */
OWS		[[:space:]\n]*
/* Define ORDINARY by exclusion -- everything that isn't one of the following */
ORDINARY	[^]!\\_*`@<>[]
 /* Here, we permit an empty link, (): this is unexpected, but
  * presumably deliberate on the part of the user (for example,
  * I've used [[ref]]() as a way of describing citations).
  */
ENDLINK		"]"{OWS}"("{OWS}
ENDREFLINK	"]"{OWS}"["{OWS}[^]]*"]"
 /* For the citation syntax, common to pandoc and RMarkdown, see
  * https://pandoc.org/MANUAL.html#citation-syntax
  * or https://bookdown.org/yihui/rmarkdown-cookbook/bibliography.html
  * The following are recognised when in the IN_CITATION state.
  */
CITATION_PLAIN	"@"[a-zA-Z0-9_]+([:.#$%&+?<>~/-][a-zA-Z0-9_]+)*
CITATION_BRACED	"@{"[^}]*"}"

%%

{ORDINARY}+	{
    *yylval = s7_make_string(S7, yytext);
    return TEXT;
}

"\\".		{
    *yylval = s7_make_string_with_length(S7, &yytext[1], 1);
    return TEXT;
}

"`"[^`]+"`"	{
    *yylval = s7_make_string_with_length(S7, &yytext[1], yyleng-2);
    return CODE;
}
"`" 		{ *yylval = s7_make_string_with_length(S7, "`", 1); return TEXT; }

"*" {
    if (in_state_p(yyextra, st_em_star)) {
        ST_POP;
        return EM_C;
    } else if (in_state_p(yyextra, st_em_uscore)) {
        *yylval = s7_make_string(S7, "*");
        return TEXT;
    } else {
        ST_PUSH(st_em_star);
        return EM_O;
    }
}

"<"[^>]+">" {
    *yylval = s7_make_string_with_length(S7, &yytext[1], yyleng-2);
    return IMPLICITLINK;
}
"<" {
    *yylval = s7_make_string(S7, "<"); // we don't (need to) do any escaping here
    return TEXT;
}
">" {
    *yylval = s7_make_string(S7, ">");
    return TEXT;
}

"_" {
    if (in_state_p(yyextra, st_em_uscore)) {
        ST_POP;
        return EM_C;
    } else if (in_state_p(yyextra, st_em_star)) {
        *yylval = s7_make_string(S7, "_");
        return TEXT;
    } else {
        ST_PUSH(st_em_uscore);
        return EM_O;
    }
}

"**" {
    if (in_state_p(yyextra, st_strong_star)) {
        ST_POP;
        return STRONG_C;
    } else if (in_state_p(yyextra, st_strong_uscore)) {
        *yylval = s7_make_string(S7, "**");
        return TEXT;
    } else {
        ST_PUSH(st_strong_star);
        return STRONG_O;
    }
}

"__" {
    if (in_state_p(yyextra, st_strong_uscore)) {
        ST_POP;
        return STRONG_C;
    } else if (in_state_p(yyextra, st_strong_star)) {
        *yylval = s7_make_string(S7, "__");
        return TEXT;
    } else {
        ST_PUSH(st_strong_uscore);
        return STRONG_O;
    }
}

"!["	ST_PUSH(st_link); return IMGLINK_START;
"["	ST_PUSH(st_link); return '[';
{ENDLINK} {
    //printf("endlink: top=%s\n", st(ST_TOP));
    if (in_state_p(yyextra, st_link)) {
        if (ST_TOP == st_link) {
            // yytext is "](" and will be followed by '](url)' or '](url "title")'
            // (with WS* around contents).
            // We must allow "title" to contain parentheses, including unbalanced ones,
            // so we can't just match all of this in the ENDLINK regexp.
            ST_POP;
            BEGIN(IN_ENDLINK);
            return ENDLINK;
        } else {
            state_stack_t exiting = ST_POP;
            //printf("..exiting %s\n", st(exiting));
            yyless(0);
            if (exiting == st_em_star || exiting == st_em_uscore) {
                return EM_C;
            } else {
                return STRONG_C;
            }
        }
    } else {
        *yylval = s7_make_string_with_length(S7, yytext, yyleng);
        return TEXT;
    }
}
{ENDREFLINK} {
    // This may be the link part of a reference-style link: [text][link],
    // depending on what state we're in.
    //printf("endreflink: top=%s\n", st(ST_TOP));
    if (in_state_p(yyextra, st_link)) {
        // It's a link reference.
        // We do _not_ attempt to coerce the link to lowercase,
        // but let parse-markdown.scm do that in a Unicode-sensitive way.
        if (ST_TOP == st_link) {
            char* ref = strrchr(yytext, '['); // <-- difference from the ENDLINK case
            ref++;
            char* ref_end = &yytext[yyleng-1];
            *yylval = scheme_trimmed_string_with_length(ref, ref_end-ref);
            ST_POP;
            return REFLINK;    // <-- difference from the ENDLINK case
        } else {
            state_stack_t exiting = ST_POP;
            //printf("..exiting %s\n", st(exiting));
            yyless(0);
            if (exiting == st_em_star || exiting == st_em_uscore) {
                return EM_C;
            } else {
                return STRONG_C;
            }
        }
    } else {
        *yylval = s7_make_string_with_length(S7, yytext, yyleng);
        return TEXT;
    }

}
"]" {
    if (in_state_p(yyextra, st_link)) {
        if (ST_TOP == st_link) {
            ST_POP;
            return ']';
        } else {
            state_stack_t exiting = ST_POP;
            yyless(0);
            if (exiting == st_em_star || exiting == st_em_uscore) {
                return EM_C;
            } else {
                return STRONG_C;
            }
        }
    } else {
        *yylval = s7_make_string(S7, "]");
        return TEXT;
    }
}
"!" {
    /* An unmatched exclamation mark, by this point, is just an exclamation mark */
    *yylval = s7_make_string(S7, "!");
    return TEXT;
}

 /*
  * OWS is [[:space:]] plus newline,
  * so the following will always include at least one non-whitespace character.
  *
  * Though the Gruber documentation doesn't say one way or the other,
  * any parentheses in the link should be balanced, and we don't end the
  * link prematurely in this case:
  * [Scheme](https://en.wikipedia.org/wiki/Scheme_(programming_language))
  *
  * FIXME: The following isn't quite satisfactory.  The rule below almost works,
  * but can't cope with the balanced parentheses.  It's possible to partially
  * accomodate this, by substantially changing util.c:scan_to_matching_char,
  * but what I came up with fails in the case "not [actually](a", where we get
  * EOF before the braces are balanced, which I _think_ exposes a slightly subtle bug
  * in flex (cf <https://stackoverflow.com/questions/78171821/>).
  *
  * I should resolve this, perhaps, while noting that this isn't supposed to be
  * a Markdown parser, so some omissions and deficiencies are acceptable.
  */
<IN_ENDLINK>{OWS}[^)\"'[:space:]\n]+{OWS}              {
    // Within an endlink, there should be no more than one TEXT element
    // (see the grammar), so for convenience, trim this string before
    // returning it.
    *yylval = scheme_trimmed_string_with_length(yytext, yyleng);
    return TEXT;
}
 /* The following two do match empty quote-strings '' and "" */
<IN_ENDLINK>{OWS}"\""[^"]*"\""{OWS} {
    const char* start = strchr(yytext, '"');
    const char* end = strrchr(yytext, '"');
    *yylval = s7_make_string_with_length(S7, start+1, end-start-1);
    return QUOTED_TEXT;
}
<IN_ENDLINK>{OWS}['][^']*[']{OWS} {
    const char* start = strchr(yytext, '\'');
    const char* end = strrchr(yytext, '\'');
    *yylval = s7_make_string_with_length(S7, start+1, end-start-1);
    return QUOTED_TEXT;
}
<IN_ENDLINK>{OWS}")"	{
    BEGIN(INITIAL);
    return ')';
}
<IN_ENDLINK>{OWS}. 		{
    // this will surely be a syntax error
    // (resist the temptation to make this {OWS}[^)]+, since that ends up being longer
    // than the above patterns, so gobbles more than it should
    *yylval = s7_make_string(S7, yytext);
    BEGIN(INITIAL);
    return TEXT;
}

"[@"				{ unput('@'); BEGIN(IN_CITATION); return CITATION; }
<IN_CITATION>{CITATION_PLAIN}	{
    *yylval = s7_make_string(S7, &yytext[1]);
    return CITATION_KEY;
}
<IN_CITATION>{CITATION_BRACED}	{
    *yylval = s7_make_string_with_length(S7, &yytext[2], yyleng-3);
    return CITATION_KEY;
}
<IN_CITATION>";"[[:space:]]*	return ';';
<IN_CITATION>[^]@;]+		{ *yylval = s7_make_string(S7, yytext); return TEXT; }
<IN_CITATION>"]"		{ BEGIN(INITIAL); return ']'; }
<IN_CITATION>"@" 		{ BEGIN(INITIAL); *yylval = s7_make_string_with_length(S7, "@", 1); return TEXT; }
"@" 				{ *yylval = s7_make_string_with_length(S7, "@", 1); return TEXT; }


<<EOF>> {
    if (yyextra->stack_depth > 0) {
        switch (ST_POP) {
          case st_none:
            // I think this case shouldn't actually happen
            return 0;
          case st_link:
            yyless(0);
            // One option here is to return ']' and thus close the
            // [...] pair.  That's not entirely unreasonable -- it's
            // defensibly what the user wanted, and it keeps the
            // grammar simpler.  Instead, we accommodate this by
            // adding a couple of special cases to the grammar.
            *yylval = s7_make_string(S7, "");
            return TEXT;
          case st_em_star: case st_em_uscore:
            yyless(0);
            return EM_C;
          default:
            yyless(0);
            return STRONG_C;
        }
    } else {
        return 0;
    }
}

 /* . { fprintf(stderr, "Unexpected character '%c' in mdinline\n", yytext[0]); } */
 /* <INITIAL,IN_ENDLINK,IN_CITATION>. {*/
 /* fallback: if we get here, we've missed something */
<*>. {
    fprintf(stderr, "Unexpected character '%c' in mdinline (%s:%d)\n",
            yytext[0],
            yyextra->input_source,
            yyextra->initial_line_number+yyget_lineno(yyscanner));
    *yylval = s7_make_string(S7, yytext);
    return TEXT;
 }


%%

yyscan_t parse_mdinline_setup_string(mdinline_extra_t extra,
                                     const char* input_source,
                                     const int initial_line_number,
                                     const char* mdline)
{
    yyscan_t scanner;
    yylex_init_extra(extra, &scanner);
    extra->stack_depth = 0;
    extra->input_source = input_source;
    extra->initial_line_number = initial_line_number;

    // possibly set this if mdinlinedebug is true?  Or not?
    if (mdinlinedebug) {
        fprintf(stderr, "mdinline: %s\n", mdline);
        yyset_debug(1, scanner);
    }

    extra->yyscanbuf = yy_scan_string(mdline, scanner);
    yyset_lineno(1, scanner);

    return scanner;
}
void parse_mdinline_finish(mdinline_extra_t extra, yyscan_t scanner)
{
    if (extra->yyscanbuf) {
        yy_delete_buffer((YY_BUFFER_STATE)extra->yyscanbuf, scanner);
        extra->yyscanbuf = NULL;
    }
    yylex_destroy(scanner);
}

// Given a state_state_t S, are we 'in' that state?
// We enter the st_link state when we encounter a '[', and we are in
// that state if there is an st_link item on the stack.
// We enter the two 'em' states when we encounter `_` or `*`; these
// two states are distinct from each other, so that in the st_em_star
// state, the character `_` is taken to be an ordinary character.  We
// are in the st_em_star state if we encounter that item on the stack
// _above_ any st_link item.  Similarly for `__` and `**` characters
// and the corresponding 'strong' states.
int in_state_p(mdinline_extra_t x, state_stack_t s)
{
    for (int i = x->stack_depth-1; i>=0; i--) {
        if (x->state_stack[i] == s) return 1;
        if (x->state_stack[i] == st_link) return 0;
    }
    return 0;
}

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

s7_scheme* S7;

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

    while ((l = mdinlinelex(&one_value, &locp, scanner)) != 0) {
        switch (l) {
          case TEXT:
            s7w("text:", one_value, "\n");
            break;
          case QUOTED_TEXT:
            s7w("quoted-text:", one_value, "\n");
            break;
          case CODE:
            s7w("code:", one_value, "\n");
            break;
          case ENDLINK:
            printf("endlink\n");//s7w("endlink:", one_value, "\n");
            break;
          case IMGLINK_START:
            printf("imglink_start\n");
            break;
          case CITATION:
            printf("[@ start\n");
            break;
          case CITATION_KEY:
            s7w("key:", one_value, "\n");
            break;
          case '[':
          case ']':
          case ')':
          case ';':
            printf("%c\n", l);
            break;
          case EM_O:
            printf("<em>\n");
            break;
          case EM_C:
            printf("</em>\n");
            break;
          case STRONG_O:
            printf("<strong>\n");
            break;
          case STRONG_C:
            printf("</strong>\n");
            break;
          default:
            printf("Unexpected lexeme: %d\n", l);
        }
    }
}

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

int main(int argc, char** argv)
{
    char debug_p = 0;
    int ch;

    progname = argv[0];

    while ((ch = getopt(argc, argv, "gh")) != -1) {
        switch (ch) {
          case 'g':
            debug_p = 1;
            break;
          case 'h':             // so it's not an 'illegal option'
            Usage();
          default:
            Usage();
        }
    }
    argc -= optind;
    argv += optind;

    if (argc != 1) Usage();
    const char* input = argv[0];

    S7 = s7_init();

    struct mdinline_extra_s S;
    yyscan_t scanner = parse_mdinline_setup_string(&S, "<stdin>", 1, input);

    if (debug_p) {
        printf("debugging!\n");
        yyset_debug(1, scanner);
    }

    display_lexemes(scanner);

    parse_mdinline_finish(&S, scanner);
}
#endif
