package org.basex.query.value.map;

import org.basex.core.jobs.*;
import org.basex.query.*;
import org.basex.query.value.*;
import org.basex.query.value.item.*;
import org.basex.query.value.seq.*;
import org.basex.query.value.type.*;

/**
 * Hash array mapped trie implementation.
 *
 * @author BaseX Team, BSD License
 * @author Leo Woerteler
 */
public final class XQTrieMap extends XQMap {
  /** The empty map. */
  static final XQMap EMPTY = new XQTrieMap(TrieEmpty.VALUE, null, Types.MAP);
  /** Root node. */
  private final TrieNode root;
  /** Map order ({@code null} for empty and singleton maps). */
  private final TrieOrder order;

  /**
   * Constructor.
   * @param root root node
   * @param order map order (can be ({@code null})
   * @param type map type
   */
  XQTrieMap(final TrieNode root, final TrieOrder order, final Type type) {
    super(type);
    this.root = root;
    this.order = order;
  }

  @Override
  public Value getOrNull(final Item key) throws QueryException {
    return root.get(key.hashCode(), key, 0);
  }

  @Override
  public Value keys() {
    return order != null ? order.keys() : structSize() == 1 ? ((TrieLeaf) root).key : Empty.VALUE;
  }

  @Override
  public Item keyAt(final long index) {
    return order != null ? order.keyAt((int) index) : ((TrieLeaf) root).key;
  }

  @Override
  public Value valueAt(final long index) {
    return value(keyAt(index));
  }

  @Override
  public XQTrieMap put(final Item key, final Value value) throws QueryException {
    final long oldSize = structSize();
    if(oldSize == 0) return new XQTrieMap(new TrieLeaf(key.hashCode(), key, value), null,
          MapType.get(key.type, value.seqType()));

    final TrieUpdate update = new TrieUpdate(key, value, order);
    final TrieNode node = root.put(key.hashCode(), 0, update);
    if(node == root) return this;

    final TrieOrder to;
    final Type mt;
    if(node.size == 1) {
      // single entry: no map order, initialize type
      to = null;
      mt = MapType.get(((TrieLeaf) root).key.type, value.seqType());
    } else {
      // initialize map order if a second entry was added
      to = oldSize == 1 ? new TrieOrder(((TrieLeaf) root).key, key) : update.order();
      mt = ((MapType) type).union(key.type, value.seqType());
    }
    return new XQTrieMap(node, to, mt);
  }

  @Override
  public XQTrieMap putAt(final int index, final Value value) throws QueryException {
    return put(keyAt(index), value);
  }

  @Override
  public XQMap remove(final Item key) throws QueryException {
    final TrieUpdate update = new TrieUpdate(key, null, order);
    final TrieNode node = root.remove(key.hashCode(), 0, update);
    if(node == root) return this;
    if(node == null) return EMPTY;

    // drop map order if a single entry is left
    final TrieOrder to = node.size == 1 ? null : update.order();
    return new XQTrieMap(node, to, type);
  }

  @Override
  public long structSize() {
    return root.size;
  }

  @Override
  public void forEach(final QueryBiConsumer<Item, Value> func) throws QueryException {
    for(final Entry entry : entries()) {
      func.accept(entry.key(), entry.value());
    }
  }

  @Override
  public boolean test(final QueryBiPredicate<Item, Value> func) throws QueryException {
    for(final Entry entry : entries()) {
      if(!func.test(entry.key(), entry.value())) return false;
    }
    return true;
  }

  @Override
  public Item shrink(final Job job) throws QueryException {
    return this == EMPTY ? this : rebuild(job);
  }
}
