package org.basex.query.func;

import org.basex.data.Data;
import org.basex.query.QueryContext;
import org.basex.query.QueryException;
import org.basex.query.expr.CmpV;
import org.basex.query.expr.Expr;
import org.basex.query.item.ANode;
import org.basex.query.item.DBNode;
import org.basex.query.item.Empty;
import org.basex.query.item.Item;
import org.basex.query.item.Itr;
import org.basex.query.item.Seq;
import org.basex.query.item.SeqType;
import org.basex.query.item.Type;
import org.basex.query.item.Value;
import org.basex.query.iter.AxisIter;
import org.basex.query.iter.Iter;
import org.basex.query.iter.NodeCache;
import org.basex.query.iter.ValueIter;
import org.basex.query.util.ItemSet;
import org.basex.util.Array;
import org.basex.util.InputInfo;

/**
 * Sequence functions.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 */
public final class FNSeq extends FuncCall {
  /**
   * Constructor.
   * @param ii input info
   * @param f function definition
   * @param e arguments
   */
  public FNSeq(final InputInfo ii, final Function f, final Expr... e) {
    super(ii, f, e);
  }

  @Override
  public Item item(final QueryContext ctx, final InputInfo ii)
      throws QueryException {
    switch(def) {
      case HEAD: return head(ctx);
      default:   return super.item(ctx, ii);
    }
  }

  @Override
  public Iter iter(final QueryContext ctx) throws QueryException {
    switch(def) {
      case INDEXOF:   return indexOf(ctx);
      case DISTINCT:  return distinctValues(ctx);
      case INSBEF:    return insertBefore(ctx);
      case REVERSE:   return reverse(ctx);
      case REMOVE:    return remove(ctx);
      case SUBSEQ:    return subsequence(ctx);
      case TAIL:      return tail(ctx);
      case OUTERMOST: return most(ctx, true);
      case INNERMOST: return most(ctx, false);
      default:        return super.iter(ctx);
    }
  }

  /**
   * Returns the outermost/innermost nodes of a node sequence, i.e. a node is
   * only contained, if none of its ancestors/descendants are.
   * @param ctx query context
   * @param outer outermost flag
   * @return outermost/innermost nodes
   * @throws QueryException exception
   */
  private Iter most(final QueryContext ctx, final boolean outer)
      throws QueryException {
    final Iter iter = expr[0].iter(ctx);
    final NodeCache nc = new NodeCache().random();
    for(Item it; (it = iter.next()) != null;) nc.add(checkNode(it));
    final int len = (int) nc.size();

    // only go further if there are at least two nodes
    if(len < 2) return nc;

    // after this, the iterator is sorted and duplicate free
    if(nc.dbnodes()) {
      // nodes are sorted, so ancestors always come before their descendants
      // the first/last node is thus always included in the output
      final DBNode fst = (DBNode) nc.get(outer ? 0 : len - 1);
      final Data data = fst.data;
      final ANode[] nodes = nc.item.clone();

      if(outer) {
        // skip the subtree of the last added node
        nc.size(0);
        final DBNode dummy = new DBNode(fst.data, 0);
        final NodeCache src = new NodeCache(nodes, len);
        for(int next = 0, p; next < len; next = p < 0 ? -p - 1 : p) {
          final DBNode nd = (DBNode) nodes[next];
          dummy.pre = nd.pre + data.size(nd.pre, data.kind(nd.pre));
          p = src.binarySearch(dummy, next + 1, len - next - 1);
          nc.add(nd);
        }
      } else {
        // skip ancestors of the last added node
        nc.item[0] = fst;
        nc.size(1);
        int before = fst.pre;
        for(int i = len - 1; i-- != 0;) {
          final DBNode nd = (DBNode) nodes[i];
          if(nd.pre + data.size(nd.pre, data.kind(nd.pre)) <= before) {
            nc.add(nd);
            before = nd.pre;
          }
        }

        // nodes were added in reverse order, correct that
        Array.reverse(nc.item, 0, (int) nc.size());
      }

      return nc;
    }

    // multiple documents and/or constructed fragments
    final NodeCache out = new NodeCache(new ANode[len], 0);
    outer: for(int i = 0; i < len; i++) {
      final ANode nd = nc.item[i];
      final AxisIter ax = outer ? nd.anc() : nd.descendant();
      for(ANode a; (a = ax.next()) != null;)
        if(nc.indexOf(a, false) != -1) continue outer;
      out.add(nc.item[i]);
    }

    return out;
  }

  @Override
  public Expr cmp(final QueryContext ctx) {
    // static typing:
    // index-of will create integers, insert-before might add new types
    if(def == Function.INDEXOF || def == Function.INSBEF) return this;

    // all other types will return existing types
    final Type t = expr[0].type().type;
    SeqType.Occ o = SeqType.Occ.ZM;

    // head will return at most one item
    if(def == Function.HEAD) o = SeqType.Occ.ZO;

    // at most one returned item
    if(def == Function.SUBSEQ && expr[0].type().one()) o = SeqType.Occ.ZO;

    type = SeqType.get(t, o);
    return this;
  }

  /**
   * Returns the first item in a sequence.
   * @param ctx query context
   * @return first item
   * @throws QueryException query exception
   */
  private Item head(final QueryContext ctx) throws QueryException {
    final Expr e = expr[0];
    return e.type().zeroOrOne() ? e.item(ctx, input) : e.iter(ctx).next();
  }

  /**
   * Returns all but the first item in a sequence.
   * @param ctx query context
   * @return iterator
   * @throws QueryException query exception
   */
  private Iter tail(final QueryContext ctx) throws QueryException {
    final Expr e = expr[0];
    if(e.type().zeroOrOne()) return Empty.ITER;

    final Iter ir = e.iter(ctx);
    if(ir.next() == null) return Empty.ITER;

    return new Iter() {
      @Override
      public Item next() throws QueryException {
        return ir.next();
      }
    };
  }

  /**
   * Returns the indexes of an item in a sequence.
   * @param ctx query context
   * @return position(s) of item
   * @throws QueryException query exception
   */
  private Iter indexOf(final QueryContext ctx) throws QueryException {
    final Item it = checkItem(expr[1], ctx);
    if(expr.length == 3) checkColl(expr[2], ctx);

    return new Iter() {
      final Iter ir = expr[0].iter(ctx);
      int c;

      @Override
      public Item next() throws QueryException {
        while(true) {
          final Item i = ir.next();
          if(i == null) return null;
          ++c;
          if(i.comparable(it) && CmpV.Op.EQ.e(input, i, it)) return Itr.get(c);
        }
      }
    };
  }

  /**
   * Returns all distinct values of a sequence.
   * @param ctx query context
   * @return distinct iterator
   * @throws QueryException query exception
   */
  private Iter distinctValues(final QueryContext ctx) throws QueryException {
    if(expr.length == 2) checkColl(expr[1], ctx);

    return new Iter() {
      final ItemSet map = new ItemSet();
      final Iter ir = expr[0].iter(ctx);

      @Override
      public Item next() throws QueryException {
        while(true) {
          Item i = ir.next();
          if(i == null) return null;
          ctx.checkStop();
          i = atom(i);
          if(map.index(input, i)) return i;
        }
      }
    };
  }

  /**
   * Inserts items before the specified position.
   * @param ctx query context
   * @return iterator
   * @throws QueryException query exception
   */
  private Iter insertBefore(final QueryContext ctx) throws QueryException {
    return new Iter() {
      final long pos = Math.max(1, checkItr(expr[1], ctx));
      final Iter iter = expr[0].iter(ctx);
      final Iter ins = expr[2].iter(ctx);
      long p = pos;
      boolean last;

      @Override
      public Item next() throws QueryException {
        if(last) return p > 0 ? ins.next() : null;
        final boolean sub = p == 0 || --p == 0;
        final Item i = (sub ? ins : iter).next();
        if(i != null) return i;
        if(sub) --p;
        else last = true;
        return next();
      }
    };
  }

  /**
   * Removes an item at a specified position in a sequence.
   * @param ctx query context
   * @return iterator without item
   * @throws QueryException query exception
   */
  private Iter remove(final QueryContext ctx) throws QueryException {
    return new Iter() {
      final long pos = checkItr(expr[1], ctx);
      final Iter iter = expr[0].iter(ctx);
      long c;

      @Override
      public Item next() throws QueryException {
        return ++c != pos || iter.next() != null ? iter.next() : null;
      }
    };
  }

  /**
   * Creates a subsequence out of a sequence, starting with start and
   * ending with end.
   * @param ctx query context
   * @return subsequence
   * @throws QueryException query exception
   */
  private Iter subsequence(final QueryContext ctx) throws QueryException {
    final double ds = checkDbl(expr[1], ctx);
    if(Double.isNaN(ds)) return Empty.ITER;
    final long s = StrictMath.round(ds);

    long l = Long.MAX_VALUE;
    if(expr.length > 2) {
      final double dl = checkDbl(expr[2], ctx);
      if(Double.isNaN(dl)) return Empty.ITER;
      l = s + StrictMath.round(dl);
    }
    final long e = l;

    final Iter iter = ctx.iter(expr[0]);
    final long max = iter.size();
    return max != -1 ? new Iter() {
      // directly access specified items
      long c = Math.max(1, s);
      long m = Math.min(e, max + 1);

      @Override
      public Item next() throws QueryException {
        return c < m ? iter.get(c++ - 1) : null;
      }
      @Override
      public Item get(final long i) throws QueryException {
        return iter.get(c + i - 1);
      }
      @Override
      public long size() {
        return Math.max(0, m - c);
      }
      @Override
      public boolean reset() {
        c = Math.max(1, s);
        return true;
      }
    } : new Iter() {
      // run through all items
      long c;

      @Override
      public Item next() throws QueryException {
        while(true) {
          final Item i = iter.next();
          if(i == null || ++c >= e) return null;
          if(c >= s) return i;
        }
      }
    };
  }

  /**
   * Reverses a sequence.
   * @param ctx query context
   * @return iterator
   * @throws QueryException query exception
   */
  private ValueIter reverse(final QueryContext ctx) throws QueryException {
    // has to be strictly evaluated
    final Value val = ctx.value(expr[0]);
    final ValueIter iter = val.iter();
    // if only one item found: no reversion necessary
    return val.size() == 1 ? iter : new ValueIter() {
      final long s = iter.size();
      long c = s;

      @Override
      public Item next() {
        return --c >= 0 ? iter.get(c) : null;
      }
      @Override
      public Item get(final long i) {
        return iter.get(s - i - 1);
      }
      @Override
      public long size() {
        return s;
      }
      @Override
      public boolean reset() {
        c = s;
        return true;
      }
      @Override
      public Value value() {
        final Item[] arr = new Item[(int) val.size()];
        final int written = val.writeTo(arr, 0);
        Array.reverse(arr, 0, written);
        return Seq.get(arr, written);
      }
    };
  }

  @Override
  public boolean uses(final Use u) {
    return u == Use.X30 && (def == Function.HEAD || def == Function.TAIL) ||
      super.uses(u);
  }
}
