package org.basex.query.util;

import static org.basex.query.QueryText.*;
import static org.basex.query.util.Err.*;
import java.io.IOException;

import org.basex.io.serial.Serializer;
import org.basex.query.QueryContext;
import org.basex.query.QueryException;
import org.basex.query.expr.Expr;
import org.basex.query.expr.ParseExpr;
import org.basex.query.item.Item;
import org.basex.query.item.QNm;
import org.basex.query.item.SeqType;
import org.basex.query.item.Value;
import org.basex.query.iter.Iter;
import org.basex.util.InputInfo;
import org.basex.util.Token;
import org.basex.util.TokenBuilder;

/**
 * Variable expression.
 *
 * @author BaseX Team 2005-11, BSD License
 * @author Christian Gruen
 */
public final class Var extends ParseExpr {
  /** Variable name. */
  public final QNm name;
  /** Variable ID. */
  public final int id;

  /** Expected return type. */
  public SeqType ret;
  /** Global flag. */
  public boolean global;
  /** Declaration flag. */
  public boolean declared;

  /** Bound value. */
  private Value value;
  /** Bound expression. */
  private Expr expr;

  /**
   * Constructor.
   * @param ii input info
   * @param n variable name
   * @param t data type
   * @param i variable ID
   */
  private Var(final InputInfo ii, final QNm n, final SeqType t, final int i) {
    super(ii);
    name = n;
    type = t;
    id = i;
  }

  /**
   * Creates a new variable.
   * @param ctx query context
   * @param ii input info
   * @param n variable name
   * @param t type
   * @return variable
   */
  public static Var create(final QueryContext ctx, final InputInfo ii,
      final QNm n, final SeqType t) {
    return new Var(ii, n, t, ctx.varIDs++);
  }

  /**
   * Creates a new variable.
   * @param ctx query context
   * @param ii input info
   * @param n variable name
   * @return variable
   */
  public static Var create(final QueryContext ctx, final InputInfo ii,
      final QNm n) {
    return create(ctx, ii, n, (SeqType) null);
  }

  /**
   * Creates a new variable.
   * @param ctx query context
   * @param ii input info
   * @param n variable name
   * @param v variable to be bound
   * @return variable
   */
  public static Var create(final QueryContext ctx, final InputInfo ii,
      final QNm n, final Value v) {
    final Var var = create(ctx, ii, n, v.type());
    var.expr = v;
    var.value = v;
    return var;
  }

  /**
   * Checks if the variable has been correctly declared.
   * @throws QueryException query exception
   */
  public void check() throws QueryException {
    if(expr != null && expr.uses(Use.UPD)) UPNOT.thrw(input, desc());
  }

  @Override
  public Var comp(final QueryContext ctx) throws QueryException {
    if(expr != null) bind(checkUp(expr, ctx).comp(ctx), ctx);
    return this;
  }

  /**
   * Sets the specified variable type.
   * @param t type
   * @param ctx query context
   * @throws QueryException query exception
   */
  public void reset(final SeqType t, final QueryContext ctx)
      throws QueryException {

    type = t;
    if(value != null && !value.type.instance(t.type) && value instanceof Item) {
      value = type.type.e((Item) value, ctx, input);
    }
  }

  /**
   * Binds the specified expression to the variable.
   * @param e expression to be set
   * @param ctx query context
   * @return self reference
   * @throws QueryException query exception
   */
  public Var bind(final Expr e, final QueryContext ctx) throws QueryException {
    expr = e;
    return e.value() ? bind((Value) e, ctx) : this;
  }

  /**
   * Returns the bound expression.
   * @return expression
   */
  public Expr expr() {
    return expr;
  }

  /**
   * Binds the specified value to the variable.
   * @param v value to be set
   * @param ctx query context
   * @return self reference
   * @throws QueryException query exception
   */
  public Var bind(final Value v, final QueryContext ctx) throws QueryException {
    expr = v;
    value = cast(v, ctx);
    return this;
  }

  @Override
  public Item item(final QueryContext ctx, final InputInfo ii)
      throws QueryException {
    return value(ctx).item(ctx, ii);
  }

  @Override
  public Iter iter(final QueryContext ctx) throws QueryException {
    return value(ctx).iter();
  }

  @Override
  public Value value(final QueryContext ctx) throws QueryException {
    if(value == null) {
      if(expr == null) VAREMPTY.thrw(input, this);
      final Value v = ctx.value;
      ctx.value = null;
      value = cast(expr.comp(ctx).value(ctx), ctx);
      ctx.value = v;
    }
    return value;
  }

  /**
   * Checks whether the given variable is identical to this one, i.e. has the
   * same ID.
   * @param v variable to check
   * @return {@code true}, if the IDs are equal, {@code false} otherwise
   */
  public boolean is(final Var v) {
    return id == v.id;
  }

  /**
   * If necessary, casts the specified value if a type is specified.
   * @param v input value
   * @param ctx query context
   * @return cast value
   * @throws QueryException query exception
   */
  private Value cast(final Value v, final QueryContext ctx)
      throws QueryException {
    return type == null ? v : type.promote(v, ctx, input);
  }

  /**
   * Returns a copy of the variable.
   * @return copy
   */
  public Var copy() {
    final Var v = new Var(input, name, type, id);
    v.global = global;
    v.value = value;
    v.expr = expr;
    v.type = type;
    v.ret = ret;
    return v;
  }

  @Override
  public boolean uses(final Use u) {
    return u == Use.VAR;
  }

  @Override
  public int count(final Var v) {
    return is(v) ? 1 : 0;
  }

  @Override
  public boolean removable(final Var v) {
    // only VarRefs can be removed
    return false;
  }

  @Override
  public Var remove(final Var v) {
    return this;
  }

  @Override
  public SeqType type() {
    return ret != null ? ret : type != null ? type :
      expr != null ? expr.type() : SeqType.ITEM_ZM;
  }

  @Override
  public boolean sameAs(final Expr cmp) {
    if(!(cmp instanceof Var)) return false;
    final Var v = (Var) cmp;
    return name.eq(v.name) && type().eq(v.type());
  }

  @Override
  public void plan(final Serializer ser) throws IOException {
    ser.openElement(this, NAM, Token.token(toString()));
    if(expr != null) expr.plan(ser);
    ser.closeElement();
  }

  @Override
  public String toString() {
    final TokenBuilder tb = new TokenBuilder();
    if(name != null) {
      tb.add(DOLLAR).add(name.atom());
      if(type != null) tb.add(" " + AS);
    }
    if(type != null) tb.add(" " + type);
    return tb.toString();
  }
}
