package edu.washington.escience.myria.expression;

import com.google.common.base.Preconditions;

import edu.washington.escience.myria.Type;
import edu.washington.escience.myria.expression.evaluate.ExpressionOperatorParameter;

/**
 * Cast the output from an expression to another type.
 */
public class CastExpression extends BinaryExpression {

  /***/
  private static final long serialVersionUID = 1L;

  /**
   * Type of casting.
   */
  private enum CastType {
    /**
     * from long to int.
     */
    LONG_TO_INT,
    /**
     * from float or double to int.
     */
    FLOATS_TO_INT,
    /**
     * int or long to float.
     */
    INT_OR_LONG_TO_FLOAT,
    /**
     * from double to float.
     */
    DOUBLE_TO_FLOAT,
    /**
     * from number to double.
     */
    NUM_TO_DOUBLE,
    /**
     * from int to long.
     */
    INT_TO_LONG,
    /**
     * from float or double to long.
     */
    FLOATS_TO_LONG,
    /**
     * from anything to string.
     */
    TO_STR,
    /**
     * from string to int.
     */
    STR_TO_INT,
    /**
     * from string to float.
     */
    STR_TO_FLOAT,
    /**
     * from string to double.
     */
    STR_TO_DOUBLE,
    /**
     * from string to long.
     */
    STR_TO_LONG,
    /**
     * unsupported cast.
     */
    UNSUPPORTED,
  }

  /**
   * This is not really unused, it's used automagically by Jackson deserialization.
   */
  @SuppressWarnings("unused")
  private CastExpression() {
  }

  /**
   * @param left what to cast.
   * @param right the output type of this operand is used to determine what to cast to.
   */
  public CastExpression(final ExpressionOperator left, final ExpressionOperator right) {
    super(left, right);
    Preconditions.checkArgument(right instanceof TypeExpression || right instanceof TypeOfExpression,
        "The right child of a cast operator must be a Type or a TypeOf, not a %s", right.getClass().getSimpleName());
  }

  @Override
  public Type getOutputType(final ExpressionOperatorParameter parameters) {
    final Type castFrom = getLeft().getOutputType(parameters);
    final Type castTo = getRight().getOutputType(parameters);
    Preconditions.checkArgument((castFrom == castTo) || getCastType(castFrom, castTo) != CastType.UNSUPPORTED,
        "Cast from %s to %s is not supported.", castFrom, castTo);
    return castTo;
  }

  /**
   * @param castTo the type that we cast to
   * @param castFrom the type that we cast from
   * @return true if the cast can be done using value of
   */
  private CastType getCastType(final Type castFrom, final Type castTo) {

    switch (castFrom + "|" + castTo) {
    /* any type to string. */
      case "INT_TYPE|STRING_TYPE":
      case "FLOAT_TYPE|STRING_TYPE":
      case "DOUBLE_TYPE|STRING_TYPE":
      case "LONG_TYPE|STRING_TYPE":
      case "DATETIME_TYPE|STRING_TYPE":
        return CastType.TO_STR;
        /* numeric type to int. */
      case "LONG_TYPE|INT_TYPE":
        return CastType.LONG_TO_INT;
      case "FLOAT_TYPE|INT_TYPE":
      case "DOUBLE_TYPE|INT_TYPE":
        return CastType.FLOATS_TO_INT;
        /* numeric type to float. */
      case "INT_TYPE|FLOAT_TYPE":
      case "LONG_TYPE|FLOAT_TYPE":
        return CastType.INT_OR_LONG_TO_FLOAT;
      case "DOUBLE_TYPE|FLOAT_TYPE":
        return CastType.DOUBLE_TO_FLOAT;
        /* numeric type to double. */
      case "INT_TYPE|DOUBLE_TYPE":
      case "LONG_TYPE|DOUBLE_TYPE":
      case "FLOAT_TYPE|DOUBLE_TYPE":
        return CastType.NUM_TO_DOUBLE;
        /* numeric type to long. */
      case "INT_TYPE|LONG_TYPE":
        return CastType.INT_TO_LONG;
      case "FLOAT_TYPE|LONG_TYPE":
      case "DOUBLE_TYPE|LONG_TYPE":
        return CastType.FLOATS_TO_LONG;
        /* String to numeric */
      case "STRING_TYPE|INT_TYPE":
        return CastType.STR_TO_INT;
      case "STRING_TYPE|FLOAT_TYPE":
        return CastType.STR_TO_FLOAT;
      case "STRING_TYPE|DOUBLE_TYPE":
        return CastType.STR_TO_DOUBLE;
      case "STRING_TYPE|LONG_TYPE":
        return CastType.STR_TO_LONG;
      default:
        break;
    }
    return CastType.UNSUPPORTED;
  }

  /**
   * Returns the string for a primitive cast: '((' + targetType + ')' + left + ')'.
   * 
   * @param targetType string of the type to be casted to.
   * @param parameters parameters that are needed to determine the output type.
   * @return string that used for numeric type cast.
   */
  private String getPrimitiveTypeCastString(final String targetType, final ExpressionOperatorParameter parameters) {
    return new StringBuilder().append("((").append(targetType).append(")(").append(getLeft().getJavaString(parameters))
        .append("))").toString();
  }

  @Override
  public String getJavaString(final ExpressionOperatorParameter parameters) {
    final Type castFrom = getLeft().getOutputType(parameters);
    final Type castTo = getRight().getOutputType(parameters);
    switch (getCastType(castFrom, castTo)) {
      case LONG_TO_INT:
        return getLeftFunctionCallString("com.google.common.primitives.Ints.checkedCast", parameters);
      case FLOATS_TO_INT:
        return getLeftFunctionCallWithParameterString("com.google.common.math.DoubleMath.roundToInt", parameters,
            "java.math.RoundingMode.DOWN");
      case INT_OR_LONG_TO_FLOAT:
        return getPrimitiveTypeCastString("float", parameters);
      case DOUBLE_TO_FLOAT:
        return getLeftFunctionCallString("edu.washington.escience.myria.util.MathUtils.castDoubleToFloat", parameters);
      case NUM_TO_DOUBLE:
        return getPrimitiveTypeCastString("double", parameters);
      case INT_TO_LONG:
        return getPrimitiveTypeCastString("long", parameters);
      case FLOATS_TO_LONG:
        return getLeftFunctionCallWithParameterString("com.google.common.math.DoubleMath.roundToLong", parameters,
            "java.math.RoundingMode.DOWN");
      case TO_STR:
        return getLeftFunctionCallString("String.valueOf", parameters);
      case STR_TO_INT:
        return getLeftFunctionCallString("Integer.parseInt", parameters);
      case STR_TO_FLOAT:
        return getLeftFunctionCallString("Float.parseFloat", parameters);
      case STR_TO_DOUBLE:
        return getLeftFunctionCallString("Double.parseDouble", parameters);
      case STR_TO_LONG:
        return getLeftFunctionCallString("Long.parseLong", parameters);
      default:
        throw new IllegalStateException("should not reach here.");
    }
  }
}

@@ -4886,24 +4886,176 @@
 arameters);%0A
+%0A    /* Trivial casts are, of course, allowed. See also #626. */%0A    if (castFrom == castTo) %7B%0A      return getLeft().getJavaString(parameters);%0A    %7D%0A%0A
     switch (

allow self-cast