// -*- C++ -*-
/* Copyright (C) 2000-2020 Free Software Foundation, Inc.
 *
 *  Gaius Mulley (gaius@glam.ac.uk) wrote html-text.cpp
 *
 *  html-text.cpp
 *
 *  provide a troff like state machine interface which
 *  generates html text.
 */

/*
This file is part of groff.

groff is free software; you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or
(at your option) any later version.

groff is distributed in the hope that it will be useful, but WITHOUT ANY
WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>. */

#include "driver.h"
#include "stringclass.h"
#include "cset.h"

#if !defined(TRUE)
#   define TRUE  (1==1)
#endif
#if !defined(FALSE)
#   define FALSE (1==0)
#endif


#include "html-text.h"

html_text::html_text (simple_output *op, html_dialect d) :
  stackptr(NULL), lastptr(NULL), out(op), dialect(d),
  space_emitted(TRUE), current_indentation(-1),
  pageoffset(-1), linelength(-1), blank_para(TRUE),
  start_space(FALSE)
{
}

html_text::~html_text ()
{
  flush_text();
}


#if defined(DEBUGGING)
static int debugStack = FALSE;


/*
 *  turnDebug - flip the debugStack boolean and return the new value.
 */

static int turnDebug (void)
{
  debugStack = 1-debugStack;
  return debugStack;
}

/*
 *  dump_stack_element - display an element of the html stack, p.
 */

void html_text::dump_stack_element (tag_definition *p)
{
  fprintf(stderr, " | ");
  switch (p->type) {

  case P_TAG:      if (p->indent == NULL) {
                      fprintf(stderr, "<P %s>", (char *)p->arg1); break;
                   } else {
                      fprintf(stderr, "<P %s [TABLE]>", (char *)p->arg1); break;
		   }
  case I_TAG:      fprintf(stderr, "<I>"); break;
  case B_TAG:      fprintf(stderr, "<B>"); break;
  case SUB_TAG:    fprintf(stderr, "<SUB>"); break;
  case SUP_TAG:    fprintf(stderr, "<SUP>"); break;
  case TT_TAG:     fprintf(stderr, "<TT>"); break;
  case PRE_TAG:    if (p->indent == NULL) {
                      fprintf(stderr, "<PRE>"); break;
                   } else {
                      fprintf(stderr, "<PRE [TABLE]>"); break;
		   }
  case SMALL_TAG:  fprintf(stderr, "<SMALL>"); break;
  case BIG_TAG:    fprintf(stderr, "<BIG>"); break;
  case BREAK_TAG:  fprintf(stderr, "<BREAK>"); break;
  case COLOR_TAG:  {
    if (p->col.is_default())
      fprintf(stderr, "<COLOR (default)>");
    else {
      unsigned int r, g, b;
      
      p->col.get_rgb(&r, &g, &b);
      fprintf(stderr, "<COLOR %x %x %x>", r/0x101, g/0x101, b/0x101);
    }
    break;
  }
  default: fprintf(stderr, "unknown tag");
  }
  if (p->text_emitted)
    fprintf(stderr, "[t] ");
}

/*
 *  dump_stack - debugging function only.
 */

void html_text::dump_stack (void)
{
  if (debugStack) {
    tag_definition *p = stackptr;

    while (p != NULL) {
      dump_stack_element(p);
      p = p->next;
    }
  }
  fprintf(stderr, "\n");
  fflush(stderr);
}
#else
void html_text::dump_stack (void) {}
#endif


/*
 *  end_tag - shuts down the tag.
 */

void html_text::end_tag (tag_definition *t)
{
  switch (t->type) {

  case I_TAG:      out->put_string("</i>"); break;
  case B_TAG:      out->put_string("</b>"); break;
  case P_TAG:      if (t->indent == NULL) {
                     out->put_string("</p>");
                   } else {
		     delete t->indent;
		     t->indent = NULL;
                     out->put_string("</p>");
		   }
		   out->enable_newlines(FALSE);
                   blank_para = TRUE; break;
  case SUB_TAG:    out->put_string("</sub>"); break;
  case SUP_TAG:    out->put_string("</sup>"); break;
  case TT_TAG:     out->put_string("</tt>"); break;
  case PRE_TAG:    out->put_string("</pre>"); out->enable_newlines(TRUE);
                   blank_para = TRUE;
                   if (t->indent != NULL)
		     delete t->indent;
		   t->indent = NULL;
                   break;
  case SMALL_TAG:  if (! is_in_pre ())
                     out->put_string("</small>");
                   break;
  case BIG_TAG:    if (! is_in_pre ())
                     out->put_string("</big>");
                   break;
  case COLOR_TAG:  if (! is_in_pre ())
                     out->put_string("</font>");
                   break;

  default:
    error("unrecognised tag");
  }
}

/*
 *  issue_tag - writes out an html tag with argument.
 *              space == 0 if no space is requested
 *              space == 1 if a space is requested
 *              space == 2 if tag should not have a space style
 */

void html_text::issue_tag (const char *tagname, const char *arg,
			   int space)
{
  if ((arg == 0) || (strlen(arg) == 0))
    out->put_string(tagname);
  else {
    out->put_string(tagname);
    out->put_string(" ");
    out->put_string(arg);
  }
  if (space == TRUE) {
    out->put_string(" style=\"margin-top: ");
    out->put_string(STYLE_VERTICAL_SPACE);
    out->put_string("\"");
  }
#if 0
  if (space == TRUE || space == FALSE)
    out->put_string(" valign=\"top\"");
#endif
  out->put_string(">");
}

/*
 *  issue_color_begin - writes out an html color tag.
 */

void html_text::issue_color_begin (color *c)
{
  char buf[(INT_HEXDIGITS * 3) + 1];
  unsigned int r, g, b;

  out->put_string("<font color=\"#");
  if (c->is_default())
    sprintf(buf, "000000");
  else {
    c->get_rgb(&r, &g, &b);
    // we have to scale 0..0xFFFF to 0..0xFF
    sprintf(buf, "%.2X%.2X%.2X", r/0x101, g/0x101, b/0x101);
  }
  out->put_string(buf);
  out->put_string("\">");
}

/*
 *  start_tag - starts a tag.
 */

void html_text::start_tag (tag_definition *t)
{
  switch (t->type) {

  case I_TAG:      issue_tag("<i", (char *)t->arg1); break;
  case B_TAG:      issue_tag("<b", (char *)t->arg1); break;
  case P_TAG:      if (t->indent != NULL) {
                     out->nl();
#if defined(DEBUGGING)
		     out->simple_comment("INDENTATION");
#endif
		     out->put_string("\n<p");
		     t->indent->begin(start_space);
                     issue_tag("", (char *)t->arg1);
                   } else {
                     out->nl();
                     issue_tag("\n<p", (char *)t->arg1, start_space);
		   }

                   out->enable_newlines(TRUE); break;
  case SUB_TAG:    issue_tag("<sub", (char *)t->arg1); break;
  case SUP_TAG:    issue_tag("<sup", (char *)t->arg1); break;
  case TT_TAG:     issue_tag("<tt", (char *)t->arg1); break;
  case PRE_TAG:    out->enable_newlines(TRUE);
                   out->nl(); out->put_string("<pre");
		   if (t->indent == NULL)
		     issue_tag("", (char *)t->arg1, start_space);
		   else {
		     t->indent->begin(start_space);
		     issue_tag("", (char *)t->arg1);
		   }
                   out->enable_newlines(FALSE); break;
  case SMALL_TAG:  if (! is_in_pre ())
                     issue_tag("<small", (char *)t->arg1);
                   break;
  case BIG_TAG:    if (! is_in_pre ())
                     issue_tag("<big", (char *)t->arg1);
                   break;
  case BREAK_TAG:  break;
  case COLOR_TAG:  if (! is_in_pre ())
                     issue_color_begin(&t->col);
                   break;

  default:
    error("unrecognised tag");
  }
}

/*
 *  flush_text - flushes html tags which are outstanding on the html stack.
 */

void html_text::flush_text (void)
{
  int notext=TRUE;
  tag_definition *p=stackptr;

  while (stackptr != 0) {
    notext = (notext && (! stackptr->text_emitted));
    if (! notext) {
      end_tag(stackptr);
    }
    p = stackptr;
    stackptr = stackptr->next;
    delete p;
  }
  lastptr = NULL;
}

/*
 *  is_present - returns TRUE if tag is already present on the stack.
 */

int html_text::is_present (HTML_TAG t)
{
  tag_definition *p=stackptr;

  while (p != NULL) {
    if (t == p->type)
      return TRUE;
    p = p->next;
  }
  return FALSE;
}

/*
 *  uses_indent - returns TRUE if the current paragraph is using a
 *                html table to effect an indent.
 */

int html_text::uses_indent (void)
{
  tag_definition *p = stackptr;

  while (p != NULL) {
    if (p->indent != NULL)
      return TRUE;
    p = p->next;
  }
  return FALSE;
}

extern void stop();

/*
 *  do_push - places, tag_definition, p, onto the stack
 */

void html_text::do_push (tag_definition *p)
{
  HTML_TAG t = p->type;

#if defined(DEBUGGING)
  if (t == PRE_TAG)
    stop();
  debugStack = TRUE;
  fprintf(stderr, "\nentering do_push (");
  dump_stack_element(p);
  fprintf(stderr, ")\n");
  dump_stack();
  fprintf(stderr, ")\n");
  fflush(stderr);
#endif

  /*
   *  if t is a P_TAG or PRE_TAG make sure it goes on the end of the stack.
   */

  if (((t == P_TAG) || (t == PRE_TAG)) && (lastptr != NULL)) {
    /*
     *  store, p, at the end
     */
    lastptr->next = p;
    lastptr       = p;
    p->next       = NULL;
  } else {
    p->next       = stackptr;
    if (stackptr == NULL)
      lastptr = p;
    stackptr      = p;
  }

#if defined(DEBUGGING)
  dump_stack();
  fprintf(stderr, "exiting do_push\n");
#endif
}

/*
 *  push_para - adds a new entry onto the html paragraph stack.
 */

void html_text::push_para (HTML_TAG t, void *arg, html_indent *in)
{
  tag_definition *p= new tag_definition;

  p->type         = t;
  p->arg1         = arg;
  p->text_emitted = FALSE;
  p->indent       = in;

  if (t == PRE_TAG && is_present(PRE_TAG))
    fatal("cannot have multiple PRE_TAGs");

  do_push(p);
}

void html_text::push_para (HTML_TAG t)
{
  push_para(t, (void *)"", NULL);
}

void html_text::push_para (color *c)
{
  tag_definition *p = new tag_definition;

  p->type         = COLOR_TAG;
  p->arg1         = NULL;
  p->col          = *c;
  p->text_emitted = FALSE;
  p->indent       = NULL;

  do_push(p);
}

/*
 *  do_italic - changes to italic
 */

void html_text::do_italic (void)
{
  if (! is_present(I_TAG))
    push_para(I_TAG);
}

/*
 *  do_bold - changes to bold.
 */

void html_text::do_bold (void)
{
  if (! is_present(B_TAG))
    push_para(B_TAG);
}

/*
 *  do_tt - changes to teletype.
 */

void html_text::do_tt (void)
{
  if ((! is_present(TT_TAG)) && (! is_present(PRE_TAG)))
    push_para(TT_TAG);
}

/*
 *  do_pre - changes to preformated text.
 */

void html_text::do_pre (void)
{
  done_tt();
  if (is_present(P_TAG)) {
    html_indent *i = remove_indent(P_TAG);
    int space = retrieve_para_space();
    (void)done_para();
    if (! is_present(PRE_TAG))
      push_para(PRE_TAG, NULL, i);
    start_space = space;
  } else if (! is_present(PRE_TAG))
    push_para(PRE_TAG, NULL, NULL);
  dump_stack();
}

/*
 *  is_in_pre - returns TRUE if we are currently within a preformatted
 *              <pre> block.
 */

int html_text::is_in_pre (void)
{
  return is_present(PRE_TAG);
}

/*
 *  do_color - initiates a new color tag.
 */

void html_text::do_color (color *c)
{
  shutdown(COLOR_TAG);   // shutdown a previous color tag, if present
  push_para(c);
}

/*
 *  done_color - shutdown an outstanding color tag, if it exists.
 */

void html_text::done_color (void)
{
  shutdown(COLOR_TAG);
}

/*
 *  shutdown - shuts down an html tag.
 */

char *html_text::shutdown (HTML_TAG t)
{
  char *arg=NULL;

  if (is_present(t)) {
    tag_definition *p    =stackptr;
    tag_definition *temp =NULL;
    int notext           =TRUE;
    
    dump_stack();
    while ((stackptr != NULL) && (stackptr->type != t)) {
      notext = (notext && (! stackptr->text_emitted));
      if (! notext) {
	end_tag(stackptr);
      }

      /*
       *  pop tag
       */
      p        = stackptr;
      stackptr = stackptr->next;
      if (stackptr == NULL)
	lastptr = NULL;
    
      /*
       *  push tag onto temp stack
       */
      p->next = temp;
      temp    = p;
    }

    /*
     *  and examine stackptr
     */
    if ((stackptr != NULL) && (stackptr->type == t)) {
      if (stackptr->text_emitted) {
	end_tag(stackptr);
      }
      if (t == P_TAG) {
	arg = (char *)stackptr->arg1;
      }
      p        = stackptr;
      stackptr = stackptr->next;
      if (stackptr == NULL)
	lastptr = NULL;
      if (p->indent != NULL)
	delete p->indent;
      delete p;
    }

    /*
     *  and restore unaffected tags
     */
    while (temp != NULL) {
      if (temp->type == COLOR_TAG)
	push_para(&temp->col);
      else
	push_para(temp->type, temp->arg1, temp->indent);
      p    = temp;
      temp = temp->next;
      delete p;
    }
  }
  return arg;
}

/*
 *  done_bold - shuts downs a bold tag.
 */

void html_text::done_bold (void)
{
  shutdown(B_TAG);
}

/*
 *  done_italic - shuts downs an italic tag.
 */

void html_text::done_italic (void)
{
  shutdown(I_TAG);
}

/*
 *  done_sup - shuts downs a sup tag.
 */

void html_text::done_sup (void)
{
  shutdown(SUP_TAG);
}

/*
 *  done_sub - shuts downs a sub tag.
 */

void html_text::done_sub (void)
{
  shutdown(SUB_TAG);
}

/*
 *  done_tt - shuts downs a tt tag.
 */

void html_text::done_tt (void)
{
  shutdown(TT_TAG);
}

/*
 *  done_pre - shuts downs a pre tag.
 */

void html_text::done_pre (void)
{
  shutdown(PRE_TAG);
}

/*
 *  done_small - shuts downs a small tag.
 */

void html_text::done_small (void)
{
  shutdown(SMALL_TAG);
}

/*
 *  done_big - shuts downs a big tag.
 */

void html_text::done_big (void)
{
  shutdown(BIG_TAG);
}

/*
 *  check_emit_text - ensures that all previous tags have been emitted (in order)
 *                    before the text is written.
 */

void html_text::check_emit_text (tag_definition *t)
{
  if ((t != NULL) && (! t->text_emitted)) {
    check_emit_text(t->next);
    t->text_emitted = TRUE;
    start_tag(t);
  }
}

/*
 *  do_emittext - tells the class that text was written during the current tag.
 */

void html_text::do_emittext (const char *s, int length)
{
  if ((! is_present(P_TAG)) && (! is_present(PRE_TAG)))
    do_para("", FALSE);

  if (is_present(BREAK_TAG)) {
    int text = remove_break();
    check_emit_text(stackptr);
    if (text) {
      if (is_present(PRE_TAG))
	out->nl();
      else if (dialect == xhtml)
	out->put_string("<br/>").nl();
      else
	out->put_string("<br>").nl();
    }
  } else
    check_emit_text(stackptr);

  out->put_string(s, length);
  space_emitted = FALSE;
  blank_para = FALSE;
}

/*
 *  do_para - starts a new paragraph
 */

void html_text::do_para (const char *arg, html_indent *in, int space)
{
  if (! is_present(P_TAG)) {
    if (is_present(PRE_TAG)) {
      html_indent *i = remove_indent(PRE_TAG);
      done_pre();    
      if ((arg == NULL || (strcmp(arg, "") == 0)) &&
	  (i == in || in == NULL))
	in = i;
      else
	delete i;
    }
    remove_sub_sup();
    push_para(P_TAG, (void *)arg, in);
    start_space = space;
  }
}

void html_text::do_para (const char *arg, int space)
{
  do_para(arg, NULL, space);
}

void html_text::do_para (simple_output *op, const char *arg1,
			 int indentation_value, int page_offset,
			 int line_length, int space)
{
  html_indent *ind;

  if (indentation_value == 0)
    ind = NULL;
  else
    ind = new html_indent(op, indentation_value, page_offset, line_length);
  do_para(arg1, ind, space);
}

/*
 *  done_para - shuts down a paragraph tag.
 */

char *html_text::done_para (void)
{
  char *result;
  space_emitted = TRUE;
  result = shutdown(P_TAG);
  start_space = FALSE;
  return result;
}

/*
 *  remove_indent - returns the indent associated with, tag.
 *                  The indent associated with tag is set to NULL.
 */

html_indent *html_text::remove_indent (HTML_TAG tag)
{
  tag_definition *p=stackptr;

  while (p != NULL) {
    if (tag == p->type) {
      html_indent *i = p->indent;
      p->indent = NULL;
      return i;
    }
    p = p->next;
  }
  return NULL;
}

/*
 *  remove_para_space - removes the leading space to a paragraph
 *                      (effectively this trims off a leading '.sp' tag).
 */

void html_text::remove_para_space (void)
{
  start_space = FALSE;
}

/*
 *  do_space - issues an end of paragraph
 */

void html_text::do_space (void)
{
  if (is_in_pre()) {
    do_emittext("", 0);
    out->force_nl();
    space_emitted = TRUE;
  } else {
    html_indent *i = remove_indent(P_TAG);

    do_para(done_para(), i, TRUE);
    space_emitted = TRUE;
  }
}

/*
 *  do_break - issue a break tag.
 */

void html_text::do_break (void)
{
  if (! is_present(PRE_TAG))
    if (emitted_text())
      if (! is_present(BREAK_TAG))
	push_para(BREAK_TAG);

  space_emitted = TRUE;
}

/*
 *  do_newline - issue a newline providing that we are inside a <pre> tag.
 */

void html_text::do_newline (void)
{
  if (is_present(PRE_TAG)) {
    do_emittext("\n", 1);
    space_emitted = TRUE;
  }
}

/*
 *  emitted_text - returns FALSE if white space has just been written.
 */

int html_text::emitted_text (void)
{
  return !space_emitted;
}

/*
 *  ever_emitted_text - returns TRUE if we have ever emitted text in this
 *                      paragraph.
 */

int html_text::ever_emitted_text (void)
{
  return !blank_para;
}

/*
 *  starts_with_space - returns TRUE if we started this paragraph with a .sp
 */

int html_text::starts_with_space (void)
{
  return start_space;
}

/*
 *  retrieve_para_space - returns TRUE, if the paragraph starts with
 *                        a space and text has not yet been emitted.
 *                        If TRUE is returned, then the, start_space,
 *                        variable is set to FALSE.
 */

int html_text::retrieve_para_space (void)
{
  if (start_space && blank_para) {
    start_space = FALSE;
    return TRUE;
  }
  else
    return FALSE;
}

/*
 *  emit_space - writes a space providing that text was written beforehand.
 */

void html_text::emit_space (void)
{
  if (is_present(PRE_TAG))
    do_emittext(" ", 1);
  else
    out->space_or_newline();

  space_emitted = TRUE;
}

/*
 *  remove_def - removes a definition, t, from the stack.
 */

void html_text::remove_def (tag_definition *t)
{
  tag_definition *p = stackptr;
  tag_definition *l = 0;
    
  while ((p != 0) && (p != t)) {
    l = p;
    p = p->next;
  }
  if ((p != 0) && (p == t)) {
    if (p == stackptr) {
      stackptr = stackptr->next;
      if (stackptr == NULL)
	lastptr = NULL;
    } else if (l == 0) {
      error("stack list pointers are wrong");
    } else {
      l->next = p->next;
      if (l->next == NULL)
	lastptr = l;
    }
    delete p;
  }
}

/*
 *  remove_tag - removes a tag from the stack.
 */

void html_text::remove_tag (HTML_TAG tag)
{
  tag_definition *p = stackptr;
    
  while ((p != 0) && (p->type != tag)) {
    p = p->next;
  }
  if ((p != 0) && (p->type == tag))
    remove_def(p);
}

/*
 *  remove_sub_sup - removes a sub or sup tag, should either exist
 *                   on the stack.
 */

void html_text::remove_sub_sup (void)
{
  if (is_present(SUB_TAG)) {
    remove_tag(SUB_TAG);
  }
  if (is_present(SUP_TAG)) {
    remove_tag(SUP_TAG);
  }
  if (is_present(PRE_TAG)) {
    remove_tag(PRE_TAG);
  }
}

/*
 *  remove_break - break tags are not balanced thus remove it once it has been emitted.
 *                 It returns TRUE if text was emitted before the <br> was issued.
 */

int html_text::remove_break (void)
{
  tag_definition *p    = stackptr;
  tag_definition *l    = 0;
  tag_definition *q    = 0;

  while ((p != 0) && (p->type != BREAK_TAG)) {
    l = p;
    p = p->next;
  }
  if ((p != 0) && (p->type == BREAK_TAG)) {
    if (p == stackptr) {
      stackptr = stackptr->next;
      if (stackptr == NULL)
	lastptr = NULL;
      q = stackptr;
    } else if (l == 0)
      error("stack list pointers are wrong");
    else {
      l->next = p->next;
      q = p->next;
      if (l->next == NULL)
	lastptr = l;
    }
    delete p;
  }
  /*
   *  now determine whether text was issued before <br>
   */
  while (q != 0) {
    if (q->text_emitted)
      return TRUE;
    else
      q = q->next;
  }
  return FALSE;
}

/*
 *  remove_para_align - removes a paragraph which has a text
 *                      argument. If the paragraph has no text
 *                      argument then it is left alone.
 */

void html_text::remove_para_align (void)
{
  if (is_present(P_TAG)) {
    tag_definition *p=stackptr;

    while (p != NULL) {
      if (p->type == P_TAG && p->arg1 != NULL) {
	html_indent *i = remove_indent(P_TAG);
	int          space = retrieve_para_space();
	done_para();
	do_para("", i, space);
	return;
      }
      p = p->next;
    }
  }
}

/*
 *  get_alignment - returns the alignment for the paragraph.
 *                  If no alignment was given then we return "".
 */

char *html_text::get_alignment (void)
{
  if (is_present(P_TAG)) {
    tag_definition *p=stackptr;

    while (p != NULL) {
      if (p->type == P_TAG && p->arg1 != NULL)
	return (char *)p->arg1;
      p = p->next;
    }
  }
  return (char *)"";
}

/*
 *  do_small - potentially inserts a <small> tag into the html stream.
 *             However we check for a <big> tag, if present then we terminate it.
 *             Otherwise a <small> tag is inserted.
 */

void html_text::do_small (void)
{
  if (is_present(BIG_TAG))
    done_big();
  else
    push_para(SMALL_TAG);
}

/*
 *  do_big - is the mirror image of do_small.
 */

void html_text::do_big (void)
{
  if (is_present(SMALL_TAG))
    done_small();
  else
    push_para(BIG_TAG);
}

/*
 *  do_sup - save a superscript tag on the stack of tags.
 */

void html_text::do_sup (void)
{
  push_para(SUP_TAG);
}

/*
 *  do_sub - save a subscript tag on the stack of tags.
 */

void html_text::do_sub (void)
{
  push_para(SUB_TAG);
}
