package org.basex.io.serial;

import static org.basex.query.QueryError.*;
import static org.basex.util.Token.*;
import static org.basex.util.XMLToken.*;

import java.io.*;
import java.util.function.*;

import org.basex.query.*;
import org.basex.query.value.item.*;
import org.basex.util.*;
import org.basex.util.hash.*;

/**
 * This class serializes items as HTML.
 *
 * @author BaseX Team, BSD License
 * @author Christian Gruen
 */
final class HTMLSerializer extends XhtmlHtmlSerializer {
  /** (X)HTML: elements with an empty content model. */
  static final TokenSet EMPTIES = new TokenSet("area", "base", "basefont", "br", "col", "embed",
      "frame", "hr", "img", "input", "isindex", "link", "meta", "param");
  /** HTML5: elements with an empty content model. */
  static final TokenSet EMPTIES5 = new TokenSet("area", "base", "br", "col", "command", "embed",
      "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr");
  /** (X)HTML: formatted elements. */
  static final TokenSet FORMATTEDS = new TokenSet("pre", "script", "style", "textarea", "title");
  /** (X)HTML: inline elements. */
  static final TokenSet INLINES = new TokenSet("a", "abbr", "acronym", "b", "bdo", "big", "br",
      "button", "cite", "code", "dfn", "em", "i", "img", "input", "kbd", "label", "map", "object",
      "q", "s", "samp", "script", "select", "small", "span", "strike", "strong", "sub", "sup",
      "textarea", "tt", "u", "var");
  /** HTML5: inline elements. */
  static final TokenSet INLINES5 = new TokenSet("a", "abbr", "area", "audio", "b", "bdi", "bdo",
      "br", "button", "canvas", "cite", "code", "data", "datalist", "dfn", "em", "embed", "i",
      "img", "input", "kbd", "keygen", "label", "map", "mark", "math", "meter", "noscript",
      "object", "output", "picture", "progress", "q", "rp", "rt", "ruby", "s", "samp", "select",
      "small", "span", "strong", "sub", "sup", "svg", "textarea", "time", "u", "var", "video",
      "wbr");
  /** (X)HTML: URI attributes. */
  static final TokenSet URIS = new TokenSet("a@href", "a@name", "applet@codebase", "area@href",
      "base@href", "blockquote@cite", "body@background", "button@datasrc", "del@cite",
      "div@datasrc", "form@action", "frame@longdesc", "frame@src", "head@profile",
      "iframe@longdesc", "iframe@src", "img@longdesc", "img@src", "img@usemap", "input@datasrc",
      "input@src", "input@usemap", "ins@cite", "link@href", "object@archive", "object@classid",
      "object@codebase", "object@data", "object@datasrc", "object@usemap", "q@cite", "script@for",
      "script@src", "select@datasrc", "span@datasrc", "table@datasrc", "textarea@datasrc");
  /** HTML: script elements. */
  private static final TokenSet SCRIPTS = new TokenSet("script", "style");
  /** HTML: boolean attributes. */
  private static final TokenSet BOOLEAN = new TokenSet("area@nohref", "audio@autoplay",
      "audio@controls", "audio@loop", "audio@muted", "button@disabled", "button@autofocus",
      "button@formnovalidate", "details@open", "dialog@open", "dir@compact", "dl@compact",
      "fieldset@disabled", "form@novalidate", "frame@noresize", "hr@noshade", "img@ismap",
      "input@checked", "input@disabled", "input@multiple", "input@readonly", "input@required",
      "input@autofocus", "input@formnovalidate", "iframe@seamless", "keygen@autofocus",
      "keygen@disabled", "menu@compact", "object@declare", "object@typemustmatch", "ol@compact",
      "ol@reversed", "optgroup@disabled", "option@selected", "option@disabled", "script@defer",
      "script@async", "select@multiple", "select@disabled", "select@autofocus", "select@required",
      "style@scoped", "td@nowrap", "textarea@disabled", "textarea@readonly", "textarea@autofocus",
      "textarea@required", "th@nowrap", "track@default", "ul@compact", "video@autoplay",
      "video@controls", "video@loop", "video@muted");

  /**
   * Constructor, specifying serialization options.
   * @param os output stream
   * @param sopts serialization parameters
   * @throws IOException I/O exception
   */
  HTMLSerializer(final OutputStream os, final SerializerOptions sopts) throws IOException {
    super(os, sopts, V50, V401, V40);
  }

  @Override
  protected void attribute(final byte[] name, final byte[] value, final boolean standalone)
      throws IOException {

    if(!standalone) delimitAttribute();
    out.print(name);

    // don't append value for boolean attributes
    byte[] val = value;
    if(!BOOLEAN.isEmpty() || !URIS.isEmpty()) {
      final byte[] key = concat(lc(elem.string()), AT, lc(name));
      if(BOOLEAN.contains(key) && eq(lc(name), lc(val))) return;
      // escape URI attributes
      if(escape && URIS.contains(key)) val = encodeUri(val, UriEncoder.ESCAPE);
    }

    out.print(ATT1);
    final int vl = val.length;
    for(int v = 0; v < vl; v += cl(val, v)) {
      final int ch = cp(val, v);
      if(ch == '<' || ch == '&' && val[Math.min(v + 1, vl - 1)] == '{') {
        out.print(ch);
      } else if(ch == '"') {
        out.print(E_QUOT);
      } else if(ch == 0x9 || ch == 0xA) {
        printHex(ch);
      } else {
        printChar(ch);
      }
    }
    out.print(ATT2);
  }

  @Override
  protected void comment(final byte[] value) throws IOException {
    if(sep) indent();
    out.print(COMM_O);
    out.print(value);
    out.print(COMM_C);
  }

  @Override
  protected void pi(final byte[] name, final byte[] value) throws IOException {
    if(html5) {
      // output PIs as comments; properly escape multiple dashes (----)
      final Function<byte[], String> esc = v -> string(v).replace("--", "- -").replace("--", "- -");
      final TokenBuilder tb = new TokenBuilder().add('?').add(esc.apply(name));
      if(value.length > 0) tb.add(' ').add(esc.apply(value));
      comment(tb.add('?').finish());
    } else {
      if(sep) indent();
      if(contains(value, '>')) throw SERPI.getIO();
      out.print(PI_O);
      out.print(name);
      out.print(' ');
      out.print(value);
      out.print(ELEM_C);
    }
  }

  @Override
  protected void print(final int cp) throws IOException {
    if(script > 0) out.print(cp);
    else if(cp > 0x7F && cp < 0xA0 && !html5) throw SERILL_X.getIO(Integer.toHexString(cp));
    else if(cp == 0xA0) out.print(E_NBSP);
    else super.print(cp);
  }

  @Override
  protected void startOpen(final QNm name) throws IOException {
    if(opened.isEmpty()) checkRoot(name);
    if(sep) indent();
    out.print(ELEM_O);
    out.print(name.string());
    indAttrLength = out.lineLength();
    sep = indent;
    if(content && eq(lc(elem.local()), HEAD)) skip++;
  }

  @Override
  protected void finishOpen() throws IOException {
    super.finishOpen();
    printCT(false, true);
    if(SCRIPTS.contains(lc(elem.local()))) script++;
  }

  @Override
  protected void finishEmpty() throws IOException {
    byte[] uri = elem.uri();
    if(uri.length > 0 && !eq(uri, XHTML_URI)) {
      super.finishEmpty();
    } else {
      if(printCT(true, true)) return;
      out.print(ELEM_C);
      final byte[] lc = lc(elem.local());
      if(EMPTIES.contains(lc)) {
        uri = nsUri(EMPTY);
        if(uri == null || uri.length == 0) return;
      }
      if(html5 && EMPTIES5.contains(lc)) return;
      sep = false;
      super.finishClose();
    }
  }

  @Override
  protected void finishClose() throws IOException {
    super.finishClose();
    if(SCRIPTS.contains(lc(elem.local()))) script--;
  }

  @Override
  protected void doctype(final QNm name) throws IOException {
    if(docpub != null || docsys != null) {
      printDoctype(name.local(), docpub, docsys);
    } else if(html5 && eq(lc(name.local()), HTML)) {
      printDoctype(uc(HTML), null, null);
    }
  }

  @Override
  boolean inline() {
    final TokenSet inlines = html5 ? INLINES5 : INLINES;
    return inlines.contains(lc(closed.local())) ||
        opening && inlines.contains(lc(elem.local())) ||
        super.inline();
  }

  @Override
  boolean suppressIndentation(final QNm qname) throws QueryIOException {
    return FORMATTEDS.contains(lc(qname.local())) || super.suppressIndentation(qname);
  }
}
