/// @file
/// @ingroup common_utils
/*************************************************************************
 * Copyright (c) 2011 AT&T Intellectual Property
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/org/documents/epl-2.0/EPL-2.0.html
 *
 * Contributors: Details at https://graphviz.org
 *************************************************************************/

%require "3.0"

  /* By default, Bison emits a parser using symbols prefixed with "yy". Graphviz
   * contains multiple Bison-generated parsers, so we alter this prefix to avoid
   * symbol clashes.
   */
%define api.prefix {html}

  /* Generate a reentrant parser with no global state */
%define api.pure full
%param { htmlscan_t *scanner }


%code requires {
#include <common/htmllex.h>
#include <common/htmltable.h>
#include <common/textspan.h>
#include <gvc/gvcext.h>
#include <util/agxbuf.h>
#include <util/list.h>
#include <util/strview.h>
}

%code provides {

static inline void free_ti(textspan_t item) {
  free(item.str);
}

static inline void free_hi(htextspan_t item) {
  for (size_t i = 0; i < item.nitems; i++) {
    free(item.items[i].str);
  }
  free(item.items);
}

struct htmlparserstate_s {
  htmllabel_t* lbl;       /* Generated label */
  htmltbl_t*   tblstack;  /* Stack of tables maintained during parsing */
  LIST(textspan_t)  fitemList;
  LIST(htextspan_t) fspanList;
  agxbuf*      str;       /* Buffer for text */
  LIST(textfont_t *)      fontstack;
  GVC_t*       gvc;
};

typedef struct {
#ifdef HAVE_EXPAT
    struct XML_ParserStruct *parser;
#endif
    char* ptr;         // input source
    int tok;           // token type
    agxbuf* xb;        // buffer to gather T_string data
    agxbuf lb;         // buffer for translating lexical data
    int warn;          // set if warning given
    int error;         // set if error given
    char inCell;       // set if in TD to allow T_string
    char mode;         // for handling artificial <HTML>..</HTML>
    strview_t currtok; // for error reporting
    strview_t prevtok; // for error reporting
    GVC_t *gvc;        // current GraphViz context
    HTMLSTYPE *htmllval; // generated by htmlparse.y
} htmllexstate_t;


struct htmlscan_s {
  htmllexstate_t lexer;
  htmlparserstate_t parser;
};
}

%{

#include <common/render.h>
#include <common/htmltable.h>
#include <common/htmllex.h>
#include <stdbool.h>
#include <util/alloc.h>

/// Clean up cell if error in parsing.
static void cleanCell(htmlcell_t *cp);

/// Clean up table if error in parsing.
static void cleanTbl(htmltbl_t *tp) {
  rows_t *rows = &tp->rows;
  for (size_t r = 0; r < LIST_SIZE(rows); ++r) {
    row_t *rp = LIST_GET(rows, r);
    for (size_t c = 0; c < LIST_SIZE(&rp->rp); ++c) {
      cleanCell(LIST_GET(&rp->rp, c));
    }
  }
  LIST_FREE(rows);
  free_html_data(&tp->data);
  free(tp);
}

/// Clean up cell if error in parsing.
static void
cleanCell (htmlcell_t* cp)
{
  if (cp->child.kind == HTML_TBL) cleanTbl (cp->child.u.tbl);
  else if (cp->child.kind == HTML_TEXT) free_html_text (cp->child.u.txt);
  free_html_data (&cp->data);
  free (cp);
}

/// Append a new text span to the list.
static void
appendFItemList (htmlparserstate_t *html_state, agxbuf *ag);

static void
appendFLineList (htmlparserstate_t *html_state, int v);

static htmltxt_t*
mkText(htmlparserstate_t *html_state);

static row_t *lastRow(htmlparserstate_t *html_state);

/// Add new cell row to current table.
static void addRow(htmlparserstate_t *html_state);

/// Set cell body and type and attach to row
static void setCell(htmlparserstate_t *html_state, htmlcell_t *cp, void *obj, label_type_t kind);

/// Create label, given body and type.
static htmllabel_t *mkLabel(void *obj, label_type_t kind) {
  htmllabel_t* lp = gv_alloc(sizeof(htmllabel_t));

  lp->kind = kind;
  if (kind == HTML_TEXT)
    lp->u.txt = obj;
  else
    lp->u.tbl = obj;
  return lp;
}

/* Called on error. Frees resources allocated during parsing.
 * This includes a label, plus a walk down the stack of
 * tables. Note that `cleanTbl` frees the contained cells.
 */
static void cleanup (htmlparserstate_t *html_state);

/// Return true if s contains a non-space character.
static bool nonSpace(const char *s) {
  char   c;

  while ((c = *s++)) {
    if (c != ' ') return true;
  }
  return false;
}

/// Fonts are allocated in the lexer.
static void
pushFont (htmlparserstate_t *html_state, textfont_t *fp);

static void
popFont (htmlparserstate_t *html_state);

%}

%union  {
  int    i;
  htmltxt_t*  txt;
  htmlcell_t*  cell;
  htmltbl_t*   tbl;
  textfont_t*  font;
  htmlimg_t*   img;
  row_t *p;
}

%token T_end_br T_end_img T_row T_end_row T_html T_end_html
%token T_end_table T_end_cell T_end_font T_string T_error
%token T_n_italic T_n_bold T_n_underline  T_n_overline T_n_sup T_n_sub T_n_s
%token T_HR T_hr T_end_hr
%token T_VR T_vr T_end_vr
%token <i> T_BR T_br
%token <img> T_IMG T_img
%token <tbl> T_table
%token <cell> T_cell
%token <font> T_font T_italic T_bold T_underline T_overline T_sup T_sub T_s

%type <txt> fonttext
%type <cell> cell cells
%type <i> br
%type <tbl> table fonttable
%type <img> image
%type <p> row rows

%start html

%%

html  : T_html fonttext T_end_html { scanner->parser.lbl = mkLabel($2,HTML_TEXT); }
      | T_html fonttable T_end_html { scanner->parser.lbl = mkLabel($2,HTML_TBL); }
      | error { cleanup(&scanner->parser); YYABORT; }
      ;

fonttext : text { $$ = mkText(&scanner->parser); }
      ;

text : text textitem
     | textitem
     ;

textitem : string { appendFItemList(&scanner->parser,scanner->parser.str);}
         | br {appendFLineList(&scanner->parser,$1);}
         | font text n_font
         | italic text n_italic
         | underline text n_underline
         | overline text n_overline
         | bold text n_bold
         | sup text n_sup
         | sub text n_sub
         | strike text n_strike
         ;

font : T_font { pushFont (&scanner->parser,$1); }
      ;

n_font : T_end_font { popFont (&scanner->parser); }
      ;

italic : T_italic {pushFont(&scanner->parser,$1);}
          ;

n_italic : T_n_italic {popFont(&scanner->parser);}
            ;

bold : T_bold {pushFont(&scanner->parser,$1);}
          ;

n_bold : T_n_bold {popFont(&scanner->parser);}
            ;

strike : T_s {pushFont(&scanner->parser,$1);}
          ;

n_strike : T_n_s {popFont(&scanner->parser);}
            ;

underline : T_underline {pushFont(&scanner->parser,$1);}
          ;

n_underline : T_n_underline {popFont(&scanner->parser);}
            ;

overline : T_overline {pushFont(&scanner->parser,$1);}
          ;

n_overline : T_n_overline {popFont(&scanner->parser);}
            ;

sup : T_sup {pushFont(&scanner->parser,$1);}
          ;

n_sup : T_n_sup {popFont(&scanner->parser);}
            ;

sub : T_sub {pushFont(&scanner->parser,$1);}
          ;

n_sub : T_n_sub {popFont(&scanner->parser);}
            ;

br     : T_br T_end_br { $$ = $1; }
       | T_BR { $$ = $1; }
       ;

string : T_string
       | string T_string
       ;

table : opt_space T_table {
          if (nonSpace(agxbuse(scanner->parser.str))) {
            htmlerror (scanner,"Syntax error: non-space string used before <TABLE>");
            cleanup(&scanner->parser); YYABORT;
          }
          $2->prev = scanner->parser.tblstack;
          $2->rows = (rows_t){.dtor = free_ritem};
          scanner->parser.tblstack = $2;
          $2->font = *LIST_BACK(&scanner->parser.fontstack);
          $<tbl>$ = $2;
        }
        rows T_end_table opt_space {
          if (nonSpace(agxbuse(scanner->parser.str))) {
            htmlerror (scanner,"Syntax error: non-space string used after </TABLE>");
            cleanup(&scanner->parser); YYABORT;
          }
          $$ = scanner->parser.tblstack;
          scanner->parser.tblstack = scanner->parser.tblstack->prev;
        }
      ;

fonttable : table { $$ = $1; }
          | font table n_font { $$=$2; }
          | italic table n_italic { $$=$2; }
          | underline table n_underline { $$=$2; }
          | overline table n_overline { $$=$2; }
          | bold table n_bold { $$=$2; }
          ;

opt_space : string
          | /* empty*/
          ;

rows : row { $$ = $1; }
     | rows row { $$ = $2; }
     | rows HR row { $1->ruled = true; $$ = $3; }
     ;

row : T_row { addRow (&scanner->parser); } cells T_end_row { $$ = lastRow(&scanner->parser); }
      ;

cells : cell { $$ = $1; }
      | cells cell { $$ = $2; }
      | cells VR cell { $1->vruled = true; $$ = $3; }
      ;

cell : T_cell fonttable { setCell(&scanner->parser,$1,$2,HTML_TBL); } T_end_cell { $$ = $1; }
     | T_cell fonttext { setCell(&scanner->parser,$1,$2,HTML_TEXT); } T_end_cell { $$ = $1; }
     | T_cell image { setCell(&scanner->parser,$1,$2,HTML_IMAGE); } T_end_cell { $$ = $1; }
     | T_cell { setCell(&scanner->parser,$1,mkText(&scanner->parser),HTML_TEXT); } T_end_cell { $$ = $1; }
     ;

image  : T_img T_end_img { $$ = $1; }
       | T_IMG { $$ = $1; }
       ;

HR  : T_hr T_end_hr
    | T_HR
    ;

VR  : T_vr T_end_vr
    | T_VR
    ;


%%

static void
appendFItemList (htmlparserstate_t *html_state, agxbuf *ag)
{
    const textspan_t ti = {.str = agxbdisown(ag),
                           .font = *LIST_BACK(&html_state->fontstack)};
    LIST_APPEND(&html_state->fitemList, ti);
}

static void
appendFLineList (htmlparserstate_t *html_state, int v)
{
    htextspan_t lp = {0};

    size_t cnt = LIST_SIZE(&html_state->fitemList);
    lp.just = v;
    if (cnt) {
	lp.nitems = cnt;
	lp.items = gv_calloc(cnt, sizeof(textspan_t));

	for (size_t i = 0; i < LIST_SIZE(&html_state->fitemList); ++i) {
	    // move this text span into the new list
	    textspan_t *ti = LIST_AT(&html_state->fitemList, i);
	    lp.items[i] = *ti;
	    *ti = (textspan_t){0};
	}
    }
    else {
	lp.items = gv_alloc(sizeof(textspan_t));
	lp.nitems = 1;
	lp.items[0].str = gv_strdup("");
	lp.items[0].font = *LIST_BACK(&html_state->fontstack);
    }

    LIST_CLEAR(&html_state->fitemList);

    LIST_APPEND(&html_state->fspanList, lp);
}

static htmltxt_t*
mkText(htmlparserstate_t *html_state)
{
    htmltxt_t *hft = gv_alloc(sizeof(htmltxt_t));

    if (!LIST_IS_EMPTY(&html_state->fitemList))
	appendFLineList (html_state, UNSET_ALIGN);

    size_t cnt = LIST_SIZE(&html_state->fspanList);
    hft->nspans = cnt;

    hft->spans = gv_calloc(cnt, sizeof(htextspan_t));
    for (size_t i = 0; i < LIST_SIZE(&html_state->fspanList); ++i) {
    	// move this HTML text span into the new list
    	htextspan_t *hi = LIST_AT(&html_state->fspanList, i);
    	hft->spans[i] = *hi;
    	*hi = (htextspan_t){0};
    }

    LIST_CLEAR(&html_state->fspanList);

    return hft;
}

static row_t *lastRow(htmlparserstate_t *html_state) {
  htmltbl_t* tbl = html_state->tblstack;
  row_t *sp = *LIST_BACK(&tbl->rows);
  return sp;
}

static void addRow(htmlparserstate_t *html_state) {
  htmltbl_t* tbl = html_state->tblstack;
  row_t *sp = gv_alloc(sizeof(row_t));
  if (tbl->hrule)
    sp->ruled = true;
  LIST_APPEND(&tbl->rows, sp);
}

static void setCell(htmlparserstate_t *html_state, htmlcell_t *cp, void *obj, label_type_t kind) {
  htmltbl_t* tbl = html_state->tblstack;
  row_t *rp = *LIST_BACK(&tbl->rows);
  LIST_APPEND(&rp->rp, cp);
  cp->child.kind = kind;
  if (tbl->vrule) {
    cp->vruled = true;
    cp->hruled = false;
  }

  if(kind == HTML_TEXT)
  	cp->child.u.txt = obj;
  else if (kind == HTML_IMAGE)
    cp->child.u.img = obj;
  else
    cp->child.u.tbl = obj;
}

static void cleanup (htmlparserstate_t *html_state)
{
  htmltbl_t* tp = html_state->tblstack;
  htmltbl_t* next;

  if (html_state->lbl) {
    free_html_label (html_state->lbl,1);
    html_state->lbl = NULL;
  }
  while (tp) {
    next = tp->prev;
    cleanTbl (tp);
    tp = next;
  }

  LIST_CLEAR(&html_state->fitemList);
  LIST_CLEAR(&html_state->fspanList);

  LIST_FREE(&html_state->fontstack);
}

static void
pushFont (htmlparserstate_t *html_state, textfont_t *fp)
{
    textfont_t* curfont = *LIST_BACK(&html_state->fontstack);
    textfont_t  f = *fp;

    if (curfont) {
	if (!f.color && curfont->color)
	    f.color = curfont->color;
	if ((f.size < 0.0) && (curfont->size >= 0.0))
	    f.size = curfont->size;
	if (!f.name && curfont->name)
	    f.name = curfont->name;
	if (curfont->flags)
	    f.flags |= curfont->flags;
    }

    textfont_t *const ft = dtinsert(html_state->gvc->textfont_dt, &f);
    LIST_PUSH_BACK(&html_state->fontstack, ft);
}

static void
popFont (htmlparserstate_t *html_state)
{
    LIST_DROP_BACK(&html_state->fontstack);
}

/* Return parsed label or NULL if failure.
 * Set warn to 0 on success; 1 for warning message; 2 if no expat; 3 for error
 * message.
 */
htmllabel_t*
parseHTML (char* txt, int* warn, htmlenv_t *env)
{
  agxbuf        str = {0};
  htmllabel_t*  l = NULL;
  htmlscan_t    scanner = {0};

  LIST_PUSH_BACK(&scanner.parser.fontstack, NULL);
  scanner.parser.fitemList.dtor = free_ti;
  scanner.parser.fspanList.dtor = free_hi;
  scanner.parser.gvc = GD_gvc(env->g);
  scanner.parser.str = &str;

  if (initHTMLlexer (&scanner, txt, &str, env)) {/* failed: no libexpat - give up */
    *warn = 2;
  }
  else {
    htmlparse(&scanner);
    *warn = clearHTMLlexer (&scanner);
    l = scanner.parser.lbl;
  }

  LIST_FREE(&scanner.parser.fitemList);
  LIST_FREE(&scanner.parser.fspanList);

  LIST_FREE(&scanner.parser.fontstack);

  agxbfree (&str);

  return l;
}
