package org.basex.io.parse.json;

import static org.basex.query.QueryError.*;

import java.util.*;

import org.basex.build.json.*;
import org.basex.query.*;
import org.basex.query.value.node.*;
import org.basex.util.*;
import org.basex.util.hash.*;

/**
 * <p>This class converts a <a href="http://jsonml.org">JsonML</a>
 * document to XML.
 * The specified JSON input is first transformed into a tree representation
 * and then converted to an XML document.
 *
 * @author BaseX Team 2005-22, BSD License
 * @author Christian Gruen
 * @author Leo Woerteler
 */
final class JsonMLConverter extends JsonXmlConverter {
  /** Element stack. */
  private final Stack<FElem> stack = new Stack<>();
  /** Current attributes. */
  private final TokenSet atts = new TokenSet();
  /** Current attribute name. */
  private byte[] attName;

  /**
   * Constructor.
   * @param opts json options
   */
  JsonMLConverter(final JsonParserOptions opts) {
    super(opts);
  }

  @Override
  FDoc finish() {
    return doc.add(stack.pop());
  }

  /**
   * Raises an error with the specified message.
   * @param msg error message
   * @param ext error details
   * @throws QueryIOException query I/O exception
   */
  private static void error(final String msg, final Object... ext) throws QueryIOException {
    throw JSON_PARSE_X.getIO(Util.info(msg, ext) + '.');
  }

  /**
   * Returns the specified name.
   * @param name name
   * @return cached QName
   * @throws QueryIOException query I/O exception
   */
  private static byte[] check(final byte[] name) throws QueryIOException {
    // retrieve name from cache, or create new instance
    if(!XMLToken.isNCName(name)) error("Invalid name: \"%\"", name);
    return name;
  }

  @Override
  public void openObject() throws QueryIOException {
    if(curr == null || attName != null || stack.peek() != null)
      error("No object allowed at this stage");
  }

  @Override
  public void openPair(final byte[] key, final boolean add) throws QueryIOException {
    attName = check(key);
    if(!atts.add(attName)) error("Duplicate attribute found");
  }

  @Override
  public void closePair(final boolean add) { }

  @Override
  public void closeObject() {
    stack.pop();
    stack.push(curr);
    reset();
  }

  @Override
  public void openArray() throws QueryIOException {
    if(!stack.isEmpty()) {
      if(attName == null && curr != null && stack.peek() == null) {
        stack.pop();
        stack.push(curr);
      } else if(attName != null || curr != null || stack.peek() == null) {
        error("No array allowed at this stage");
      }
    }
    stack.push(null);
    reset();
  }

  @Override
  public void openItem() { }

  @Override
  public void closeItem() { }

  @Override
  public void closeArray() throws QueryIOException {
    FElem value = stack.pop();
    if(value == null) {
      value = curr;
      reset();
    }
    if(value == null) error("Missing element name");

    if(stack.isEmpty()) stack.push(value);
    else stack.peek().add(value);
  }

  @Override
  public void stringLit(final byte[] value) throws QueryIOException {
    if(attName == null && curr != null && stack.peek() == null) {
      stack.pop();
      stack.push(curr);
      reset();
    }

    if(curr == null) {
      final FElem elem = stack.isEmpty() ? null : stack.peek();
      if(elem == null) curr = new FElem(check(value));
      else elem.add(new FTxt(value));
    } else if(attName != null) {
      curr.add(attName, value);
      attName = null;
    } else {
      error("No string allowed at this stage");
    }
  }

  @Override
  public void numberLit(final byte[] value) throws QueryIOException {
    error("No numbers allowed");
  }

  @Override
  public void nullLit() throws QueryIOException {
    error("No 'null' allowed");
  }

  @Override
  public void booleanLit(final byte[] b) throws QueryIOException {
    error("No booleans allowed");
  }

  /**
   * Resets the element creation.
   */
  private void reset() {
    curr = null;
    atts.clear();
  }
}
