package org.basex.query.gflwor;

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

import java.util.*;

import org.basex.query.*;
import org.basex.query.expr.*;
import org.basex.query.func.*;
import org.basex.query.gflwor.GFLWOR.Eval;
import org.basex.query.iter.*;
import org.basex.query.util.*;
import org.basex.query.value.*;
import org.basex.query.value.item.*;
import org.basex.query.value.node.*;
import org.basex.query.value.type.*;
import org.basex.query.var.*;
import org.basex.util.*;
import org.basex.util.hash.*;

/**
 * FLWOR {@code let} clause, binding an expression to a variable.
 *
 * @author BaseX Team 2005-14, BSD License
 * @author Leo Woerteler
 */
public final class Let extends GFLWOR.Clause {
  /** Variable. */
  public final Var var;
  /** Bound expression. */
  public Expr expr;
  /** Score flag. */
  private final boolean score;

  /**
   * Constructor.
   * @param v variable
   * @param e expression
   * @param scr score flag
   * @param ii input info
   */
  public Let(final Var v, final Expr e, final boolean scr, final InputInfo ii) {
    super(ii, v);
    var = v;
    expr = e;
    score = scr;
  }

  /**
   * Creates a let expression from a for loop over a single item.
   * @param fr for loop
   * @return let binding
   */
  static Let fromFor(final For fr) {
    final Let lt = new Let(fr.var, fr.expr, false, fr.info);
    lt.type = fr.expr.type();
    return lt;
  }

  /**
   * Creates a let binding for the score variable of a for clause.
   * @param fr for clause
   * @return let binding for the score variable
   */
  static Let fromForScore(final For fr) {
    final Expr varRef = new VarRef(fr.info, fr.var);
    return new Let(fr.score, varRef, true, fr.info);
  }

  @Override
  LetEval eval(final Eval sub) {
    if(!(sub instanceof LetEval)) return new LetEval(this, sub);
    final LetEval eval = (LetEval) sub;
    eval.lets.add(this);
    return eval;
  }

  /**
   * Calculates the score of the given iterator.
   * @param iter iterator
   * @return score
   * @throws QueryException evaluation exception
   */
  private static Dbl score(final Iter iter) throws QueryException {
    double sum = 0;
    int sz = 0;
    for(Item it; (it = iter.next()) != null; sum += it.score(), sz++);
    return Dbl.get(sum / sz);
  }

  @Override
  public void plan(final FElem plan) {
    final FElem e = planElem();
    if(score) e.add(planAttr(Token.token(SCORE), Token.TRUE));
    var.plan(e);
    expr.plan(e);
    plan.add(e);
  }

  @Override
  public String toString() {
    return LET + ' ' + (score ? SCORE + ' ' : "") + var + ' ' + ASSIGN + ' ' + expr;
  }

  @Override
  public boolean has(final Flag flag) {
    return expr.has(flag);
  }

  @Override
  public Let compile(final QueryContext ctx, final VarScope scp) throws QueryException {
    var.refineType(score ? SeqType.DBL : expr.type(), ctx, info);
    expr = expr.compile(ctx, scp);
    return optimize(ctx, scp);
  }

  @Override
  public Let optimize(final QueryContext ctx, final VarScope scp) throws QueryException {
    if(!score && expr instanceof TypeCheck) {
      final TypeCheck tc = (TypeCheck) expr;
      if(tc.isRedundant(var) || var.adoptCheck(tc.type, tc.promote)) {
        ctx.compInfo(OPTCAST, tc.type);
        expr = tc.expr;
      }
    }

    type = score ? SeqType.DBL : expr.type();
    var.refineType(type, ctx, info);
    if(var.checksType() && expr.isValue()) {
      expr = var.checkType((Value) expr, ctx, info, true);
      var.refineType(expr.type(), ctx, info);
    }
    size = score ? 1 : expr.size();
    return this;
  }

  @Override
  public boolean removable(final Var v) {
    return expr.removable(v);
  }

  @Override
  public VarUsage count(final Var v) {
    return expr.count(v);
  }

  @Override
  public GFLWOR.Clause inline(final QueryContext ctx, final VarScope scp,
      final Var v, final Expr e) throws QueryException {
    final Expr sub = expr.inline(ctx, scp, v, e);
    if(sub == null) return null;
    expr = sub;
    return optimize(ctx, scp);
  }

  @Override
  public Let copy(final QueryContext ctx, final VarScope scp, final IntObjMap<Var> vs) {
    final Var v = scp.newCopyOf(ctx, var);
    vs.put(var.id, v);
    return new Let(v, expr.copy(ctx, scp, vs), score, info);
  }

  @Override
  public boolean accept(final ASTVisitor visitor) {
    return expr.accept(visitor) && visitor.declared(var);
  }

  @Override
  public void checkUp() throws QueryException {
    checkNoUp(expr);
  }

  @Override
  long calcSize(final long cnt) {
    return cnt;
  }

  /**
   * Returns an expression that is appropriate for inlining.
   * @param ctx query context
   * @param scp variable scope
   * @return inlineable expression
   * @throws QueryException query exception
   */
  public Expr inlineExpr(final QueryContext ctx, final VarScope scp) throws QueryException {
    return score ? Function._FT_SCORE.get(null, expr).optimize(ctx, scp)
                 : var.checked(expr, ctx, scp, info);
  }

  @Override
  public int exprSize() {
    return expr.exprSize();
  }

  /** Evaluator for a block of {@code let} expressions. */
  private static class LetEval implements Eval {
    /** Let expressions of the current block, in declaration order. */
    private final ArrayList<Let> lets;
    /** Sub-evaluator. */
    private final Eval sub;

    /**
     * Constructor for the first let binding in the block.
     * @param let first let binding
     * @param subEval sub-evaluator
     */
    LetEval(final Let let, final Eval subEval) {
      lets = new ArrayList<Let>();
      lets.add(let);
      sub = subEval;
    }

    @Override
    public boolean next(final QueryContext ctx) throws QueryException {
      if(!sub.next(ctx)) return false;
      for(final Let let : lets)
        ctx.set(let.var, let.score ? score(let.expr.iter(ctx)) : ctx.value(let.expr), let.info);
      return true;
    }
  }
}
