package org.basex.build;

import static org.basex.build.BuildText.*;
import static org.basex.core.Text.*;
import static org.basex.util.Token.*;

import java.io.*;

import org.basex.core.*;
import org.basex.data.*;
import org.basex.index.name.*;
import org.basex.index.path.*;
import org.basex.io.*;
import org.basex.util.*;
import org.basex.util.list.*;

/**
 * This class provides an interface for building database instances.
 * The specified {@link Parser} sends events to this class whenever nodes
 * are to be added or closed. The builder implementation decides whether
 * the nodes are stored on disk or kept in memory.
 *
 * @author BaseX Team 2005-15, BSD License
 * @author Christian Gruen
 */
public abstract class Builder extends Proc {
  /** Tree structure. */
  final PathSummary path = new PathSummary();
  /** Namespace index. */
  final Namespaces nspaces = new Namespaces();
  /** Parser instance. */
  final Parser parser;
  /** Database name. */
  final String dbname;

  /** Number of cached size values. */
  int ssize;
  /** Currently stored size value. */
  int spos;

  /** Meta data on built database. */
  MetaData meta;
  /** Element name index. */
  Names elemNames;
  /** Attribute name index. */
  Names attrNames;

  /** Parent stack. */
  private final IntList parStack = new IntList();
  /** Stack with element names. */
  private final IntList elemStack = new IntList();
  /** Current tree height. */
  private int level;

  /**
   * Constructor.
   * @param dbname name of database
   * @param parser parser
   */
  Builder(final String dbname, final Parser parser) {
    this.dbname = dbname;
    this.parser = parser;
  }

  // PUBLIC METHODS ===========================================================

  /**
   * Parses the given input source and builds the database.
   * @throws IOException I/O exception
   */
  final void parse() throws IOException {
    // add document node and parse document
    parser.parse(this);
    meta.lastid = meta.size - 1;
  }

  /**
   * Opens a document node.
   * @param value document name
   * @throws IOException I/O exception
   */
  public final void openDoc(final byte[] value) throws IOException {
    path.put(0, Data.DOC, level);
    parStack.set(level++, meta.size);
    addDoc(value);
    nspaces.open();
  }

  /**
   * Closes a document node.
   * @throws IOException I/O exception
   */
  public final void closeDoc() throws IOException {
    final int pre = parStack.get(--level);
    setSize(pre, meta.size - pre);
    ++meta.ndocs;
    nspaces.close(meta.size);
  }

  /**
   * Opens a new element node.
   * @param name name of element
   * @param att attributes
   * @param nsp namespaces
   * @throws IOException I/O exception
   */
  public final void openElem(final byte[] name, final Atts att, final Atts nsp) throws IOException {
    addElem(name, att, nsp);
    ++level;
  }

  /**
   * Stores an empty element.
   * @param name name of element
   * @param att attributes
   * @param nsp namespaces
   * @throws IOException I/O exception
   */
  public final void emptyElem(final byte[] name, final Atts att, final Atts nsp)
      throws IOException {
    addElem(name, att, nsp);
    final int pre = parStack.get(level);
    nspaces.close(pre);
    if(att.size() > IO.MAXATTS) setSize(pre, meta.size - pre);
  }

  /**
   * Closes an element.
   * @throws IOException I/O exception
   */
  public final void closeElem() throws IOException {
    checkStop();
    --level;
    final int pre = parStack.get(level);
    setSize(pre, meta.size - pre);
    nspaces.close(pre);
  }

  /**
   * Stores a text node.
   * @param value text value
   * @throws IOException I/O exception
   */
  public final void text(final byte[] value) throws IOException {
    if(value.length != 0) addText(value, Data.TEXT);
  }

  /**
   * Stores a comment.
   * @param value comment text
   * @throws IOException I/O exception
   */
  public final void comment(final byte[] value) throws IOException {
    addText(value, Data.COMM);
  }

  /**
   * Stores a processing instruction.
   * @param pi processing instruction name and value
   * @throws IOException I/O exception
   */
  public final void pi(final byte[] pi) throws IOException {
    addText(pi, Data.PI);
  }

  /**
   * Sets the document encoding.
   * @param encoding encoding
   */
  public final void encoding(final String encoding) {
    meta.encoding = Strings.normEncoding(encoding);
  }

  // PROGRESS INFORMATION =====================================================

  @Override
  protected final String tit() {
    return CREATING_DB;
  }

  @Override
  public final String det() {
    return spos == 0 ? parser.detail() : FINISHING_D;
  }

  @Override
  public final double prog() {
    return spos == 0 ? parser.progress() : (double) spos / ssize;
  }

  // ABSTRACT METHODS =========================================================

  /**
   * Builds the database.
   * @return data database instance
   * @throws IOException I/O exception
   */
  public abstract Data build() throws IOException;

  /**
   * Returns a data clip with the parsed input.
   * @return data data clip
   * @throws IOException I/O exception
   */
  public abstract DataClip dataClip() throws IOException;

  /**
   * Closes open references.
   * @throws IOException I/O exception
   */
  public abstract void close() throws IOException;

  /**
   * Adds a document node to the database.
   * @param value name of the document
   * @throws IOException I/O exception
   */
  protected abstract void addDoc(byte[] value) throws IOException;

  /**
   * Adds an element node to the database. This method stores a preliminary
   * size value; if this node has further descendants, {@link #setSize} must
   * be called to set the final size value.
   * @param dist distance to parent
   * @param nameId id of element name
   * @param asize number of attributes
   * @param uriId id of namespace uri
   * @param ne namespace flag (indicates if this element introduces new namespaces)
   * @throws IOException I/O exception
   */
  protected abstract void addElem(int dist, int nameId, int asize, int uriId, boolean ne)
      throws IOException;

  /**
   * Adds an attribute to the database.
   * @param nameId id of attribute name
   * @param value attribute value
   * @param dist distance to parent
   * @param uriId id of namespace uri
   * @throws IOException I/O exception
   */
  protected abstract void addAttr(int nameId, byte[] value, int dist, int uriId) throws IOException;

  /**
   * Adds a text node to the database.
   * @param value the token to be added
   * @param dist distance to parent
   * @param kind the node kind
   * @throws IOException I/O exception
   */
  protected abstract void addText(byte[] value, int dist, byte kind) throws IOException;

  /**
   * Stores a size value to the specified table position.
   * @param pre pre reference
   * @param size value to be stored
   * @throws IOException I/O exception
   */
  protected abstract void setSize(int pre, int size) throws IOException;

  // PRIVATE METHODS ==========================================================

  /**
   * Adds an element node to the storage.
   * @param name element name
   * @param atts attributes
   * @param nsp namespaces
   * @throws IOException I/O exception
   */
  private void addElem(final byte[] name, final Atts atts, final Atts nsp) throws IOException {
    // get reference of element name
    int nameId = elemNames.index(name, null, true);
    path.put(nameId, Data.ELEM, level);

    // cache pre value
    final int pre = meta.size;
    // remember id of element name and parent reference
    elemStack.set(level, nameId);
    parStack.set(level, pre);

    // parse namespaces
    nspaces.open(pre, nsp);

    // get and store element references
    final int dis = level == 0 ? 1 : pre - parStack.get(level - 1);
    final int as = atts.size();
    final byte[] pref = prefix(name);
    int uriId = nspaces.uriIdForPrefix(pref, true);
    if(uriId == 0 && pref.length != 0 && !eq(pref, XML))
      throw new BuildException(WHICHNS, parser.detail(), prefix(name));
    addElem(dis, nameId, Math.min(IO.MAXATTS, as + 1), uriId, !nsp.isEmpty());

    // get and store attribute references
    for(int a = 0; a < as; ++a) {
      final byte[] an = atts.name(a), av = atts.value(a);
      final byte[] ap = prefix(an);
      nameId = attrNames.index(an, av, true);
      uriId = nspaces.uriIdForPrefix(ap, false);
      if(uriId == 0 && ap.length != 0 && !eq(ap, XML))
        throw new BuildException(WHICHNS, parser.detail(), an);

      path.put(nameId, Data.ATTR, level + 1, av, meta);
      addAttr(nameId, av, Math.min(IO.MAXATTS, a + 1), uriId);
    }

    // set leaf node information in index
    if(level > 1) elemNames.stat(elemStack.get(level - 1)).setLeaf(false);

    // check if data ranges exceed database limits, based on the storage details in {@link Data}
    limit(elemNames.size(), 0x8000, LIMITELEMS);
    limit(attrNames.size(), 0x8000, LIMITATTS);
    limit(nspaces.size(), 0x100, LIMITNS);
    if(meta.size < 0) limit(0, 0, LIMITRANGE);
  }

  /**
   * Checks a value limit and optionally throws an exception.
   * @param value value
   * @param limit limit
   * @param msg message
   * @throws IOException I/O exception
   */
  private void limit(final int value, final int limit, final String msg) throws IOException {
    if(value >= limit) throw new BuildException(msg, parser.detail(), limit);
  }

  /**
   * Adds a simple text, comment or processing instruction to the database.
   * @param value the value to be added
   * @param kind the node type
   * @throws IOException I/O exception
   */
  private void addText(final byte[] value, final byte kind) throws IOException {
    final int l = level;
    if(l > 1) {
      final int i = elemStack.get(l - 1);
      // text node processing for statistics
      if(kind == Data.TEXT) elemNames.index(i, value);
      // set leaf node information in index
      else elemNames.stat(i).setLeaf(false);
    }

    path.put(0, kind, l, value, meta);
    addText(value, l == 0 ? 1 : meta.size - parStack.get(l - 1), kind);
  }
}
