package org.odata4j.producer.inmemory;

import java.math.BigDecimal;
import java.util.Set;

import org.core4j.Enumerable;
import org.odata4j.edm.EdmSimpleType;
import org.odata4j.edm.EdmType;
import org.odata4j.expression.AddExpression;
import org.odata4j.expression.AndExpression;
import org.odata4j.expression.BinaryCommonExpression;
import org.odata4j.expression.BoolCommonExpression;
import org.odata4j.expression.BoolMethodExpression;
import org.odata4j.expression.BoolParenExpression;
import org.odata4j.expression.BooleanLiteral;
import org.odata4j.expression.CastExpression;
import org.odata4j.expression.CommonExpression;
import org.odata4j.expression.ConcatMethodCallExpression;
import org.odata4j.expression.DivExpression;
import org.odata4j.expression.EndsWithMethodCallExpression;
import org.odata4j.expression.EntitySimpleProperty;
import org.odata4j.expression.EqExpression;
import org.odata4j.expression.Expression;
import org.odata4j.expression.GeExpression;
import org.odata4j.expression.GtExpression;
import org.odata4j.expression.IndexOfMethodCallExpression;
import org.odata4j.expression.LeExpression;
import org.odata4j.expression.LengthMethodCallExpression;
import org.odata4j.expression.LiteralExpression;
import org.odata4j.expression.LtExpression;
import org.odata4j.expression.ModExpression;
import org.odata4j.expression.MulExpression;
import org.odata4j.expression.NeExpression;
import org.odata4j.expression.NotExpression;
import org.odata4j.expression.OrExpression;
import org.odata4j.expression.ParenExpression;
import org.odata4j.expression.ReplaceMethodCallExpression;
import org.odata4j.expression.StartsWithMethodCallExpression;
import org.odata4j.expression.SubExpression;
import org.odata4j.expression.SubstringMethodCallExpression;
import org.odata4j.expression.SubstringOfMethodCallExpression;
import org.odata4j.expression.ToLowerMethodCallExpression;
import org.odata4j.expression.ToUpperMethodCallExpression;
import org.odata4j.expression.TrimMethodCallExpression;
import org.odata4j.internal.TypeConverter;

public class InMemoryEvaluation {

  public static Object evaluate(CommonExpression expression, Object target, PropertyModel properties) {
    if (expression instanceof LiteralExpression)
      return Expression.literalValue((LiteralExpression) expression);

    if (expression instanceof EntitySimpleProperty)
      return properties.getPropertyValue(target,
          ((EntitySimpleProperty) expression).getPropertyName());

    if (expression instanceof BoolCommonExpression)
      return evaluate((BoolCommonExpression) expression, target,
          properties);

    if (expression instanceof AddExpression)
      return binaryFunction((BinaryCommonExpression) expression, target,
          properties, BinaryFunction.ADD);

    if (expression instanceof SubExpression)
      return binaryFunction((BinaryCommonExpression) expression, target,
          properties, BinaryFunction.SUB);

    if (expression instanceof MulExpression)
      return binaryFunction((BinaryCommonExpression) expression, target,
          properties, BinaryFunction.MUL);

    if (expression instanceof DivExpression)
      return binaryFunction((BinaryCommonExpression) expression, target,
          properties, BinaryFunction.DIV);

    if (expression instanceof ModExpression)
      return binaryFunction((BinaryCommonExpression) expression, target,
          properties, BinaryFunction.MOD);

    if (expression instanceof ParenExpression)
      return evaluate(((ParenExpression) expression).getExpression(),
          target, properties);

    if (expression instanceof BoolParenExpression)
      return evaluate(((BoolParenExpression) expression).getExpression(),
          target, properties);

    if (expression instanceof CastExpression) {
      CastExpression castExpression = (CastExpression) expression;
      EdmSimpleType<?> t = EdmType.getSimple(castExpression.getType());
      if (t == null)
        throw new UnsupportedOperationException("Only simple types supported");
      Class<?> javaType = t.getJavaTypes().iterator().next();
      return TypeConverter.convert(evaluate(castExpression.getExpression(), target, properties), javaType);
    }

    if (expression instanceof ToLowerMethodCallExpression) {
      ToLowerMethodCallExpression e = (ToLowerMethodCallExpression) expression;
      String value = evaluateToString(e.getTarget(), target, properties);
      return value == null ? null : value.toLowerCase();
    }

    if (expression instanceof ToUpperMethodCallExpression) {
      ToUpperMethodCallExpression e = (ToUpperMethodCallExpression) expression;
      String value = evaluateToString(e.getTarget(), target, properties);
      return value == null ? null : value.toUpperCase();
    }

    if (expression instanceof SubstringMethodCallExpression) {
      SubstringMethodCallExpression e = (SubstringMethodCallExpression) expression;
      String value = evaluateToString(e.getTarget(), target, properties);
      if (value == null || e.getStart() == null)
        return value;

      int start = (Integer) evaluate(e.getStart(), target, properties);

      if (e.getLength() == null) {
        return value.substring(start);
      }

      int length = (Integer) evaluate(e.getLength(), target, properties);
      return length == 0 ? "" : value.substring(start, start + length);
    }

    if (expression instanceof IndexOfMethodCallExpression) {
      IndexOfMethodCallExpression e = (IndexOfMethodCallExpression) expression;
      String text = evaluateToString(e.getTarget(), target, properties);
      String search = evaluateToString(e.getValue(), target, properties);
      return text.indexOf(search);
    }

    if (expression instanceof ReplaceMethodCallExpression) {
      ReplaceMethodCallExpression e = (ReplaceMethodCallExpression) expression;
      String text = evaluateToString(e.getTarget(), target, properties);
      String find = evaluateToString(e.getFind(), target, properties);
      String replace = evaluateToString(e.getReplace(), target, properties);
      return text.replace(find, replace);
    }

    if (expression instanceof ConcatMethodCallExpression) {
      ConcatMethodCallExpression e = (ConcatMethodCallExpression) expression;
      String left = evaluateToString(e.getLHS(), target, properties);
      String right = evaluateToString(e.getRHS(), target, properties);
      return left + right;
    }

    if (expression instanceof TrimMethodCallExpression) {
      TrimMethodCallExpression e = (TrimMethodCallExpression) expression;
      String left = evaluateToString(e.getTarget(), target, properties);
      return left == null ? null : left.trim();
    }

    if (expression instanceof LengthMethodCallExpression) {
      LengthMethodCallExpression e = (LengthMethodCallExpression) expression;
      String left = evaluateToString(e.getTarget(), target, properties);
      return left == null ? 0 : left.length();
    }

    throw new UnsupportedOperationException("unsupported expression " + expression);
  }

  private static String evaluateToString(CommonExpression expression, Object target, PropertyModel properties) {
    Object value = evaluate(expression, target, properties);
    if (value == null)
      return null;
    if (value instanceof String)
      return ((String) value);
    return String.valueOf(value);
  }

  public static boolean evaluate(BoolCommonExpression expression, Object target, PropertyModel properties) {
    if (expression instanceof EqExpression) {
      return equals((EqExpression) expression, target, properties);
    }
    if (expression instanceof NeExpression) {
      return !equals((NeExpression) expression, target, properties);
    }
    if (expression instanceof AndExpression) {
      AndExpression e = (AndExpression) expression;
      return evaluate(e.getLHS(), target, properties)
          && evaluate(e.getRHS(), target, properties);
    }
    if (expression instanceof OrExpression) {
      OrExpression e = (OrExpression) expression;
      return evaluate(e.getLHS(), target, properties)
          || evaluate(e.getRHS(), target, properties);
    }
    if (expression instanceof BooleanLiteral) {
      return ((BooleanLiteral) expression).getValue();
    }

    if (expression instanceof GtExpression) {
      return compareTo((GtExpression) expression, target, properties) > 0;
    }
    if (expression instanceof LtExpression) {
      return compareTo((LtExpression) expression, target, properties) < 0;
    }
    if (expression instanceof GeExpression) {
      return compareTo((GeExpression) expression, target, properties) >= 0;
    }
    if (expression instanceof LeExpression) {
      return compareTo((LeExpression) expression, target, properties) <= 0;
    }

    if (expression instanceof NotExpression) {
      NotExpression e = (NotExpression) expression;
      Boolean rt = (Boolean) evaluate(e.getExpression(), target,
          properties);
      return !rt;
    }
    if (expression instanceof BoolMethodExpression) {
      return evaluate((BoolMethodExpression) expression, target, properties);
    }
    if (expression instanceof ParenExpression) {
      @SuppressWarnings("unused")
      Object obj = null;
    }
    if (expression instanceof BoolParenExpression) {
      BoolParenExpression e = (BoolParenExpression) expression;
      return evaluate((BoolCommonExpression) e.getExpression(), target,
          properties);
    }

    //Let's try to evaluate generic expressions in hope it evaluates to bool
    Object o = evaluate((CommonExpression) expression, target, properties);
    if (o instanceof Boolean) {
      return (Boolean) o;
    }

    throw new UnsupportedOperationException("unsupported expression "
        + expression);
  }

  private static boolean evaluate(BoolMethodExpression expression, Object target, PropertyModel properties) {
    String targetValue = (String) evaluate(expression.getTarget(), target, properties);
    String searchValue = (String) evaluate(expression.getValue(), target, properties);

    if (targetValue == null || searchValue == null) {
      return false;
    }
    if (expression instanceof SubstringOfMethodCallExpression) {
      return targetValue.contains(searchValue);
    }
    if (expression instanceof StartsWithMethodCallExpression) {
      return targetValue.startsWith(searchValue);
    }
    if (expression instanceof EndsWithMethodCallExpression) {
      return targetValue.endsWith(searchValue);
    }

    throw new UnsupportedOperationException("unsupported expression "
        + expression);
  }

  private static interface BinaryFunction {
    BigDecimal apply(BigDecimal lhs, BigDecimal rhs);

    Double apply(Double lhs, Double rhs);

    Float apply(Float lhs, Float rhs);

    Integer apply(Integer lhs, Integer rhs);

    Long apply(Long lhs, Long rhs);

    public static final BinaryFunction ADD = new BinaryFunction() {
      public BigDecimal apply(BigDecimal lhs, BigDecimal rhs) {
        return lhs.add(rhs);
      }

      public Double apply(Double lhs, Double rhs) {
        return lhs + rhs;
      }

      public Float apply(Float lhs, Float rhs) {
        return lhs + rhs;
      }

      public Integer apply(Integer lhs, Integer rhs) {
        return lhs + rhs;
      }

      public Long apply(Long lhs, Long rhs) {
        return lhs + rhs;
      }
    };

    public static final BinaryFunction SUB = new BinaryFunction() {
      public BigDecimal apply(BigDecimal lhs, BigDecimal rhs) {
        return lhs.subtract(rhs);
      }

      public Double apply(Double lhs, Double rhs) {
        return lhs - rhs;
      }

      public Float apply(Float lhs, Float rhs) {
        return lhs - rhs;
      }

      public Integer apply(Integer lhs, Integer rhs) {
        return lhs - rhs;
      }

      public Long apply(Long lhs, Long rhs) {
        return lhs - rhs;
      }
    };

    public static final BinaryFunction MUL = new BinaryFunction() {
      public BigDecimal apply(BigDecimal lhs, BigDecimal rhs) {
        return lhs.multiply(rhs);
      }

      public Double apply(Double lhs, Double rhs) {
        return lhs * rhs;
      }

      public Float apply(Float lhs, Float rhs) {
        return lhs * rhs;
      }

      public Integer apply(Integer lhs, Integer rhs) {
        return lhs * rhs;
      }

      public Long apply(Long lhs, Long rhs) {
        return lhs * rhs;
      }
    };

    public static final BinaryFunction DIV = new BinaryFunction() {
      public BigDecimal apply(BigDecimal lhs, BigDecimal rhs) {
        return lhs.divide(rhs);
      }

      public Double apply(Double lhs, Double rhs) {
        return lhs / rhs;
      }

      public Float apply(Float lhs, Float rhs) {
        return lhs / rhs;
      }

      public Integer apply(Integer lhs, Integer rhs) {
        return lhs / rhs;
      }

      public Long apply(Long lhs, Long rhs) {
        return lhs / rhs;
      }
    };
    public static final BinaryFunction MOD = new BinaryFunction() {
      public BigDecimal apply(BigDecimal lhs, BigDecimal rhs) {
        return lhs.remainder(rhs);
      }

      public Double apply(Double lhs, Double rhs) {
        return lhs % rhs;
      }

      public Float apply(Float lhs, Float rhs) {
        return lhs % rhs;
      }

      public Integer apply(Integer lhs, Integer rhs) {
        return lhs % rhs;
      }

      public Long apply(Long lhs, Long rhs) {
        return lhs % rhs;
      }
    };
  }

  private static class ObjectPair {
    public Object lhs;
    public Object rhs;

    public ObjectPair(CommonExpression lhs, CommonExpression rhs,
        Object target, PropertyModel properties) {
      this(evaluate(lhs, target, properties), evaluate(rhs, target,
          properties));
    }

    public ObjectPair(Object lhs, Object rhs) {
      this.lhs = lhs;
      this.rhs = rhs;
    }
  }

  private static Object binaryFunction(BinaryCommonExpression be,
      Object target, PropertyModel properties, BinaryFunction function) {
    ObjectPair pair = new ObjectPair(be.getLHS(), be.getRHS(), target,
        properties);
    binaryNumericPromotion(pair);

    // * Edm.Decimal
    // * Edm.Double
    // * Edm.Single
    // * Edm.Int32
    // * Edm.Int64

    if (pair.lhs instanceof BigDecimal)
      return function.apply((BigDecimal) pair.lhs, (BigDecimal) pair.rhs);
    if (pair.lhs instanceof Double)
      return function.apply((Double) pair.lhs, (Double) pair.rhs);
    if (pair.lhs instanceof Float)
      return function.apply((Float) pair.lhs, (Float) pair.rhs);
    if (pair.lhs instanceof Integer)
      return function.apply((Integer) pair.lhs, (Integer) pair.rhs);
    if (pair.lhs instanceof Long)
      return function.apply((Long) pair.lhs, (Long) pair.rhs);

    throw new UnsupportedOperationException("unsupported add type "
        + pair.lhs);
  }

  private static boolean equals(BinaryCommonExpression be, Object target,
      PropertyModel properties) {
    ObjectPair pair = new ObjectPair(be.getLHS(), be.getRHS(), target,
        properties);
    binaryNumericPromotion(pair);
    return (pair.lhs == null ? pair.rhs == null : pair.lhs.equals(pair.rhs));
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  private static int compareTo(BinaryCommonExpression be, Object target,
      PropertyModel properties) {
    ObjectPair pair = new ObjectPair(be.getLHS(), be.getRHS(), target,
        properties);
    binaryNumericPromotion(pair);
    return ((Comparable) pair.lhs).compareTo(((Comparable) pair.rhs));
  }

  @SuppressWarnings({ "unchecked", "rawtypes" })
  private static final Set<Class> SUPPORTED_CLASSES_FOR_BINARY_PROMOTION = Enumerable
      .create(BigDecimal.class, Double.class, Float.class, Byte.class,
          Integer.class, Short.class, Long.class).cast(Class.class)
      .toSet();

  @SuppressWarnings("unchecked")
  private static void binaryNumericPromotion(ObjectPair pair) {

    // * Edm.Decimal
    // * Edm.Double
    // * Edm.Single
    // * Edm.Byte
    // * Edm.Int16
    // * Edm.Int32
    // * Edm.Int64

    if (pair.lhs == null || pair.rhs == null)
      return;
    Class<?> lhsClass = pair.lhs.getClass();
    Class<?> rhsClass = pair.rhs.getClass();
    if (lhsClass.equals(rhsClass))
      return;
    if (!SUPPORTED_CLASSES_FOR_BINARY_PROMOTION.contains(lhsClass)
        || !SUPPORTED_CLASSES_FOR_BINARY_PROMOTION.contains(rhsClass))
      return;

    // If supported, binary numeric promotion SHOULD consist of the
    // application of the following rules in the order specified:

    // * If either operand is of type Edm.Decimal, the other operand is
    // converted to Edm.Decimal unless it is of type Edm.Single or
    // Edm.Double.
    if (lhsClass.equals(BigDecimal.class)
        && Enumerable
            .create(Byte.class, Short.class, Integer.class,
                Long.class).cast(Class.class)
            .contains(rhsClass))
      pair.rhs = BigDecimal.valueOf(((Number) pair.rhs).longValue());
    else if (rhsClass.equals(BigDecimal.class)
        && Enumerable
            .create(Byte.class, Short.class, Integer.class,
                Long.class).cast(Class.class)
            .contains(lhsClass))
      pair.lhs = BigDecimal.valueOf(((Number) pair.lhs).longValue());

    // * Otherwise, if either operand is Edm.Double, the other operand is
    // converted to type Edm.Double.
    else if (lhsClass.equals(Double.class))
      pair.rhs = ((Number) pair.rhs).doubleValue();
    else if (rhsClass.equals(Double.class))
      pair.lhs = ((Number) pair.lhs).doubleValue();

    // * Otherwise, if either operand is Edm.Single, the other operand is
    // converted to type Edm.Single.
    else if (lhsClass.equals(Float.class))
      pair.rhs = ((Number) pair.rhs).floatValue();
    else if (rhsClass.equals(Float.class))
      pair.lhs = ((Number) pair.lhs).floatValue();

    // * Otherwise, if either operand is Edm.Int64, the other operand is
    // converted to type Edm.Int64.
    else if (lhsClass.equals(Long.class))
      pair.rhs = ((Number) pair.rhs).longValue();
    else if (rhsClass.equals(Long.class))
      pair.lhs = ((Number) pair.lhs).longValue();

    // * Otherwise, if either operand is Edm.Int32, the other operand is
    // converted to type Edm.Int32
    else if (lhsClass.equals(Integer.class))
      pair.rhs = ((Number) pair.rhs).intValue();
    else if (rhsClass.equals(Integer.class))
      pair.lhs = ((Number) pair.lhs).intValue();

    // * Otherwise, if either operand is Edm.Int16, the other operand is
    // converted to type Edm.Int16.
    else if (lhsClass.equals(Short.class))
      pair.rhs = ((Number) pair.rhs).shortValue();
    else if (rhsClass.equals(Short.class))
      pair.lhs = ((Number) pair.lhs).shortValue();

    // * If binary numeric promotion is supported, a data service SHOULD use
    // a castExpression to promote an operand to the target type.

  }

  public static Object cast(Object obj, Class<?> targetType) {
    if (obj == null)
      return null;
    Class<?> objClass = obj.getClass();
    if (targetType.isAssignableFrom(objClass))
      return obj;

    if ((obj instanceof Number) && targetType.equals(Double.class))
      return ((Double) obj).doubleValue();
    if ((obj instanceof Number) && targetType.equals(Float.class))
      return ((Number) obj).floatValue();
    if ((obj instanceof Number) && targetType.equals(Long.class))
      return ((Number) obj).longValue();
    if ((obj instanceof Number) && targetType.equals(Integer.class))
      return ((Number) obj).intValue();
    if ((obj instanceof Number) && targetType.equals(Short.class))
      return ((Number) obj).shortValue();
    if ((obj instanceof Number) && targetType.equals(Byte.class))
      return ((Number) obj).byteValue();

    if (objClass.equals(Integer.class) && targetType.equals(Integer.TYPE))
      return obj;

    throw new UnsupportedOperationException("Unable to cast a "
        + objClass.getSimpleName() + " to a "
        + targetType.getSimpleName());
  }

}
