package org.infinispan.objectfilter.impl.predicateindex.be;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import org.infinispan.objectfilter.impl.MetadataAdapter;
import org.infinispan.objectfilter.impl.predicateindex.EqualsCondition;
import org.infinispan.objectfilter.impl.predicateindex.IntervalPredicate;
import org.infinispan.objectfilter.impl.predicateindex.IsNullCondition;
import org.infinispan.objectfilter.impl.predicateindex.LikeCondition;
import org.infinispan.objectfilter.impl.predicateindex.Predicate;
import org.infinispan.objectfilter.impl.syntax.AndExpr;
import org.infinispan.objectfilter.impl.syntax.BooleanExpr;
import org.infinispan.objectfilter.impl.syntax.BooleanOperatorExpr;
import org.infinispan.objectfilter.impl.syntax.ComparisonExpr;
import org.infinispan.objectfilter.impl.syntax.ConstantBooleanExpr;
import org.infinispan.objectfilter.impl.syntax.ConstantValueExpr;
import org.infinispan.objectfilter.impl.syntax.IsNullExpr;
import org.infinispan.objectfilter.impl.syntax.LikeExpr;
import org.infinispan.objectfilter.impl.syntax.NotExpr;
import org.infinispan.objectfilter.impl.syntax.OrExpr;
import org.infinispan.objectfilter.impl.syntax.PrimaryPredicateExpr;
import org.infinispan.objectfilter.impl.syntax.PropertyValueExpr;
import org.infinispan.objectfilter.impl.util.Interval;

/**
 * Creates a BETree out of a BooleanExpr.
 *
 * @author anistor@redhat.com
 * @since 7.0
 */
public final class BETreeMaker<AttributeId extends Comparable<AttributeId>> {

   private final MetadataAdapter<?, ?, AttributeId> metadataAdapter;

   private final boolean useIntervals;

   public BETreeMaker(MetadataAdapter<?, ?, AttributeId> metadataAdapter, boolean useIntervals) {
      this.metadataAdapter = metadataAdapter;
      this.useIntervals = useIntervals;
   }

   public BETree make(BooleanExpr booleanExpr, Map<String, Object> namedParameters) {
      List<BENode> nodes = new ArrayList<>();
      List<Integer> treeCounters = new ArrayList<>();

      if (booleanExpr == null) {
         treeCounters.add(BETree.EXPR_TRUE);
      } else if (booleanExpr instanceof ConstantBooleanExpr) {
         treeCounters.add(((ConstantBooleanExpr) booleanExpr).getValue() ? BETree.EXPR_TRUE : BETree.EXPR_FALSE);
      } else {
         preorderTraversal(null, booleanExpr, nodes, treeCounters, namedParameters);
      }

      int[] countersArray = new int[treeCounters.size()];
      for (int i = 0; i < countersArray.length; i++) {
         countersArray[i] = treeCounters.get(i);
      }
      return new BETree(nodes.toArray(new BENode[nodes.size()]), countersArray);
   }

   private void preorderTraversal(BENode parent, BooleanExpr child, List<BENode> nodes, List<Integer> treeCounters, Map<String, Object> namedParameters) {
      if (child instanceof NotExpr) {
         PrimaryPredicateExpr condition = (PrimaryPredicateExpr) ((NotExpr) child).getChild();
         makePredicateNode(parent, nodes, treeCounters, condition, true, namedParameters);
      } else if (child instanceof PrimaryPredicateExpr) {
         PrimaryPredicateExpr condition = (PrimaryPredicateExpr) child;
         makePredicateNode(parent, nodes, treeCounters, condition, false, namedParameters);
      } else if (child instanceof OrExpr) {
         makeBooleanOperatorNode((OrExpr) child, nodes, treeCounters, new OrNode(parent), namedParameters);
      } else if (child instanceof AndExpr) {
         makeBooleanOperatorNode((AndExpr) child, nodes, treeCounters, new AndNode(parent), namedParameters);
      } else {
         throw new IllegalStateException("Unexpected *Expr node type: " + child);
      }
   }

   private void makePredicateNode(BENode parent, List<BENode> nodes, List<Integer> treeCounters, PrimaryPredicateExpr condition, boolean isNegated, Map<String, Object> namedParameters) {
      final PropertyValueExpr pve = (PropertyValueExpr) condition.getChild();
      final List<AttributeId> path = metadataAdapter.mapPropertyNamePathToFieldIdPath(pve.getPropertyPath().asArrayPath());
      final boolean isRepeated = pve.isRepeated();

      if (condition instanceof ComparisonExpr) {
         ComparisonExpr expr = (ComparisonExpr) condition;
         ConstantValueExpr right = (ConstantValueExpr) expr.getRightChild();
         Comparable rightConstant = right.getConstantValueAs(pve.getPrimitiveType(), namedParameters);
         switch (expr.getComparisonType()) {
            case NOT_EQUAL:
               if (useIntervals) {
                  if (!(parent instanceof OrNode)) {
                     parent = new OrNode(parent);
                     int size = nodes.size();
                     parent.setLocation(size, size + 4);
                     nodes.add(parent);
                     treeCounters.add(3);
                  }
                  // the special case of non-equality is transformed into two intervals, excluding the compared value, + an IS NULL predicate
                  addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(Interval.getMinusInf(), false, rightConstant, false)));
                  addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(rightConstant, false, Interval.getPlusInf(), false)));
                  addPredicateNode(parent, nodes, treeCounters, isNegated, path, new Predicate<>(isRepeated, IsNullCondition.INSTANCE));
               } else {
                  addPredicateNode(parent, nodes, treeCounters, !isNegated, path, new Predicate<>(isRepeated, new EqualsCondition(rightConstant)));
               }
               break;
            case EQUAL:
               if (useIntervals) {
                  addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(rightConstant, true, rightConstant, true)));
               } else {
                  addPredicateNode(parent, nodes, treeCounters, isNegated, path, new Predicate<>(isRepeated, new EqualsCondition(rightConstant)));
               }
               break;
            case LESS:
               addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(Interval.getMinusInf(), false, rightConstant, false)));
               break;
            case LESS_OR_EQUAL:
               addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(Interval.getMinusInf(), false, rightConstant, true)));
               break;
            case GREATER:
               addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(rightConstant, false, Interval.getPlusInf(), false)));
               break;
            case GREATER_OR_EQUAL:
               addPredicateNode(parent, nodes, treeCounters, isNegated, path, new IntervalPredicate(isRepeated, new Interval(rightConstant, true, Interval.getPlusInf(), false)));
               break;
            default:
               throw new IllegalStateException("Unexpected comparison type: " + expr.getComparisonType());
         }
      } else if (condition instanceof IsNullExpr) {
         addPredicateNode(parent, nodes, treeCounters, isNegated, path, new Predicate<>(isRepeated, IsNullCondition.INSTANCE));
      } else if (condition instanceof LikeExpr) {
         LikeExpr likeExpr = (LikeExpr) condition;
         addPredicateNode(parent, nodes, treeCounters, isNegated, path, new Predicate<>(isRepeated, new LikeCondition(likeExpr.getPattern(), likeExpr.getEscapeChar())));
      } else {
         throw new IllegalStateException("Unexpected condition type (" + condition.getClass().getSimpleName() + "): " + condition);
      }
   }

   private void addPredicateNode(BENode parent, List<BENode> nodes, List<Integer> treeCounters, boolean isNegated, List<AttributeId> path, Predicate predicate) {
      PredicateNode predicateNode = new PredicateNode<>(parent, predicate, isNegated, path);
      int size = nodes.size();
      predicateNode.setLocation(size, size + 1);
      nodes.add(predicateNode);
      treeCounters.add(1);
   }

   private void makeBooleanOperatorNode(BooleanOperatorExpr child, List<BENode> nodes, List<Integer> treeCounters, BENode node, Map<String, Object> namedParameters) {
      int index = nodes.size();
      nodes.add(node);
      List<BooleanExpr> children = child.getChildren();
      treeCounters.add(children.size());
      for (BooleanExpr c : children) {
         preorderTraversal(node, c, nodes, treeCounters, namedParameters);
      }
      node.setLocation(index, nodes.size());
   }
}
