/*
 * This is the lexer part of a parser for .bst files.
 *
 * I would like to be able to preserve comments, but it's unexpectedly
 * difficult to do in a comprehensive list of cases, so the following
 * simply discards comments at lexing time (I would like to preserve
 * comments so that I can round-trip a .bst file to bst-scm
 * (parse-tree) and back to .bst.  Why?  Because I'd like to be able
 * to edit the bst-scm intermediate and re-serialise, because that's a
 * better way of generating the urlbst outputs than hacking away at
 * the .bst source with Perl).  Since it's probably _only_ me that
 * wants to preserve the comments, we can bear to lose them, at least
 * for the moment.
 *
 * To preserve comments, we need to have the `/%/` rule send
 * `scheme_trimmed_string(yytext)` to `*yylval`, and return a token `COMMENT`,
 * and make corresponding changes in parse-bst.y.
 *
 * 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__ && !defined(__clang__)
// for fileno
#define _XOPEN_SOURCE 600
#endif
}

%{
#include "config.h"

#include <string.h>
#include <ctype.h>
#if HAVE_ALLOCA_H
#include <alloca.h>
#endif

#include "beastie.h"
#include "util.h"
#include "unicode.h"
#include "unicode-scm.h"

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

#ifndef WITH_MAIN
#define WITH_MAIN 0
#endif

#define LINENO s7_make_integer(S7, yyget_lineno(yyscanner))
%}

 /* optional white space */
OWS	[[:space:]]*
WS	[[:space:]]+

%option prefix="bst" reentrant bison-bridge bison-locations
%option noyywrap nounput yylineno
%option extra-type="bst_extra_t"

/* btxhak: Variable and function names may not begin with a
 * numeral and may not contain any of the ten restricted
 * characters on page 143 of the LaTeX book, but may otherwise
 * contain any printing characters. Also, BibTEX considers upper-
 * and lower-case equivalents to be the same.
 *
 * The following seems a useful subset; I should probably make a point
 * of expanding this list.
 */
NAME	[A-Za-z][A-Za-z0-9.$_-]*

%%

{OWS}"%".* {
    input(yyscanner);                    // gobble a newline
    // ...but otherwise discard
}

 /* tokens BO and BC are '{' and '}', but with a semantic value which is their line number */
{OWS}"{"{OWS}	{ *yylval = LINENO; return BO; }
{OWS}"}"{OWS}	{ *yylval = LINENO; return BC; }
{OWS}"'"{WS}	{
    fprintf(stderr,
            "Space after quotes is not permitted (line %d)\n",
            yyget_lineno(yyscanner));
    return QUOTE;
 }
{OWS}"'"	return QUOTE;
{OWS}":="{OWS}	return ASSIGNMENT;
{OWS}"="{OWS}	return '=';
{OWS}"<"{OWS}	return '<';
{OWS}">"{OWS}	return '>';
{OWS}"*"{OWS}	return '*';
{OWS}"+"{OWS}	return '+';
{OWS}"-"{OWS}	return '-';


{OWS}"#"-?[0-9]+{OWS} 	{
    const char* p = strchr(yytext, '#');
    *yylval = s7_make_integer(S7, strtol(&p[1], NULL, 10));
    return NUMBER;
}

{OWS}["][^"]*["]{OWS}	{
    const char* startp = strchr(yytext, '"');
    startp++;
    const char* endp = strrchr(yytext, '"');

    const char* errmsg;
    ustring_t us = make_ustring(&errmsg);
    if (us == NULL) {
        return_beastie_error(S7, "bst-lex: can't create ustring: %s", *errmsg);
    }

    us = ustring_append_utf8_with_length(us,
                                         (const uint8_t*)startp,
                                         endp-startp,
                                         &errmsg);
    if (us == NULL) {
        return_beastie_error(S7, "bst-lex: can't append ustring: %s", *errmsg);
    }

    *yylval = make_ustring_obj(S7, us);

    return STRING;
}

 /* spot commands, and return the appropriate token type, with the
  *  'value' of the command being its line number
  */
{OWS}[eE][nN][tT][rR][yY]{OWS}			{ *yylval = LINENO; return CMD_ENTRY; }
{OWS}[eE][xX][eE][cC][uU][tT][eE]{OWS}		{ *yylval = LINENO; return CMD_EXECUTE; }
{OWS}[fF][uU][nN][cC][tT][iI][oO][nN]{OWS}	{ *yylval = LINENO; return CMD_FUNCTION; }
{OWS}[iI][nN][tT][eE][gG][eE][rR][sS]{OWS}	{ *yylval = LINENO; return CMD_INTEGERS; }
{OWS}[iI][tT][eE][rR][aA][tT][eE]{OWS}		{ *yylval = LINENO; return CMD_ITERATE; }
{OWS}[mM][aA][cC][rR][oO]{OWS}			{ *yylval = LINENO; return CMD_MACRO; }
{OWS}[rR][eE][aA][dD]{OWS}			{ *yylval = LINENO; return CMD_READ; }
{OWS}[rR][eE][vV][eE][rR][sS][eE]{OWS}		{ *yylval = LINENO; return CMD_REVERSE; }
{OWS}[sS][oO][rR][tT]{OWS}			{ *yylval = LINENO; return CMD_SORT; }
{OWS}[sS][tT][rR][iI][nN][gG][sS]{OWS}		{ *yylval = LINENO; return CMD_STRINGS; }

 /* Coerce all tokens to lowercase */
{NAME}{OWS}	{
    char* b = alloca(yyleng+1);
    for (int i=0; i<yyleng; i++) b[i] = tolower(yytext[i]);

    // trim from the left
    char* startp = &b[0];
    char* eosp = &b[yyleng];
    // we know there is at least one non-space character in yytext
    while (isspace(*startp)) startp++;
    // trim after end of token
    char* endp = startp + 1;
    while (endp < eosp && !isspace(*endp)) endp++;

    *endp = '\0';
    *yylval = s7_make_symbol(S7, startp);

    return TOKEN;
}

.	{
    fprintf(stderr,
            "Unexpected character '%c' (0x%0x) at line %d\n",
            yytext[0], yytext[0], yyget_lineno(yyscanner));
}

%%

/* For debugging/test purposes,
 * we also want to be able to parse .bst files read from a string.
 * PATH==NULL means stdin; return NULL on error.
 */
yyscan_t parse_bst_setup_file(bst_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_bst_setup_string(bst_extra_t extra, const char* s)
{
    yyscan_t scanner;
    yylex_init_extra(extra, &scanner);
    extra->yyscanbuf = yy_scan_string(s, scanner);

    const int buflen = 20;
    const char fmt[] = "<string:%.*s...>";
    const size_t totlen = buflen + sizeof(fmt);
    char* source_legend = malloc(totlen+1);
    snprintf(source_legend, totlen, fmt, buflen, s);
    extra->path = source_legend;
    extra->f = NULL;

    yyset_lineno(1, scanner);
    return scanner;
}
void parse_bst_finish(bst_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 "s7.h"
#include "util.h"

YYSTYPE one_value;
YYLTYPE locp;
s7_scheme* S7;

static void display_lexemes(yyscan_t scanner)
{
    int l;

    while ((l = bstlex(&one_value, &locp, scanner)) != 0) {
        //printf("l=%d\n", l);
        switch (l) {
          case '=':
          case '<':
          case '>':
          case '*':
          case '+':
          case '-':
            printf("%c\n", l);
            break;

          case NUMBER:
            s7w("number(", one_value, ")\n");
            break;
          case QUOTE:
            printf("quote\n");
            break;
          case TOKEN:
            s7w("token(", one_value, ")\n");
            break;
          case STRING:
            s7w("string(", one_value, ")\n");
            break;
          case ASSIGNMENT:
            printf(":=\n");
            break;
          case BO:
            printf("{\n");
            break;
          case BC:
            printf("}\n");
            break;

          case CMD_ENTRY: printf("ENTRY\n"); break;
          case CMD_EXECUTE: printf("EXECUTE\n"); break;
          case CMD_FUNCTION: printf("FUNCTION\n"); break;
          case CMD_INTEGERS: printf("INTEGERS\n"); break;
          case CMD_ITERATE: printf("ITERATE\n"); break;
          case CMD_MACRO: printf("MACRO\n"); break;
          case CMD_READ: printf("READ\n"); break;
          case CMD_REVERSE: printf("REVERSE\n"); break;
          case CMD_SORT: printf("SORT\n"); break;
          case CMD_STRINGS: printf("STRINGS\n"); break;

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

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

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

    switch (argc) {
      case 1:
        bstin = NULL;
        break;
      case 2:
        if (argv[1][0] == '-') Usage();
        bstin = argv[1];
        break;
      default:
        Usage();
    }

    S7 = s7_init();
    unicode_load_hook(S7, s7_nil(S7));

    struct bst_extra_s S;
    yyscan_t scanner = parse_bst_setup_file(&S, bstin);
    if (scanner == NULL) {
        fprintf(stderr,
                "%s: can't open file %s to read\n", argv[0],
                (bstin == NULL ? "<stdin>" : bstin));
        exit(1);
    }

    display_lexemes(scanner);
    parse_bst_finish(&S, scanner);
}
#endif
