package org.infinispan.objectfilter.impl.syntax.parser;

import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;

import org.infinispan.objectfilter.impl.logging.Log;
import org.infinispan.objectfilter.impl.ql.PropertyPath;
import org.infinispan.objectfilter.impl.ql.QueryRendererDelegate;
import org.infinispan.objectfilter.impl.syntax.AggregationExpr;
import org.infinispan.objectfilter.impl.syntax.AndExpr;
import org.infinispan.objectfilter.impl.syntax.BetweenExpr;
import org.infinispan.objectfilter.impl.syntax.BooleanExpr;
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.FullTextBoostExpr;
import org.infinispan.objectfilter.impl.syntax.FullTextOccurExpr;
import org.infinispan.objectfilter.impl.syntax.FullTextRangeExpr;
import org.infinispan.objectfilter.impl.syntax.FullTextRegexpExpr;
import org.infinispan.objectfilter.impl.syntax.FullTextTermExpr;
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.PropertyValueExpr;
import org.jboss.logging.Logger;

/**
 * Builder for the creation of WHERE/HAVING clause filters targeting a single entity.
 * <p/>
 * Implemented as a stack of {@link LazyBooleanExpr}s which allows to add elements to the constructed query in a
 * uniform manner while traversing through the original query parse tree.
 *
 * @author anistor@redhat.com
 * @since 9.0
 */
final class ExpressionBuilder<TypeMetadata> {

   private static final Log log = Logger.getMessageLogger(Log.class, ExpressionBuilder.class.getName());

   private final ObjectPropertyHelper<TypeMetadata> propertyHelper;

   private TypeMetadata entityType;

   /**
    * Keep track of all the parent expressions ({@code AND}, {@code OR}, {@code NOT}) of the WHERE/HAVING clause of the
    * built query.
    */
   private final Deque<LazyBooleanExpr> stack = new ArrayDeque<>();

   ExpressionBuilder(ObjectPropertyHelper<TypeMetadata> propertyHelper) {
      this.propertyHelper = propertyHelper;
   }

   public void setEntityType(TypeMetadata entityType) {
      this.entityType = entityType;
      stack.push(new LazyRootBooleanExpr());
   }

   public void addFullTextTerm(PropertyPath<?> propertyPath, String value, Integer fuzzySlop) {
      push(new FullTextTermExpr(makePropertyValueExpr(propertyPath), value, fuzzySlop));
   }

   public void addFullTextRegexp(PropertyPath<?> propertyPath, String regexp) {
      push(new FullTextRegexpExpr(makePropertyValueExpr(propertyPath), regexp));
   }

   public void addFullTextRange(PropertyPath<?> propertyPath, boolean includeLower, Object lower, Object upper, boolean includeUpper) {
      push(new FullTextRangeExpr(makePropertyValueExpr(propertyPath), includeLower, lower, upper, includeUpper));
   }

   public void addComparison(PropertyPath<?> propertyPath, ComparisonExpr.Type comparisonType, Object value) {
      Comparable typedValue = (Comparable) propertyHelper.convertToBackendType(entityType, propertyPath.asArrayPath(), value);
      push(new ComparisonExpr(makePropertyValueExpr(propertyPath), new ConstantValueExpr(typedValue), comparisonType));
   }

   public void addRange(PropertyPath<?> propertyPath, Object lower, Object upper) {
      Comparable lowerValue = (Comparable) propertyHelper.convertToBackendType(entityType, propertyPath.asArrayPath(), lower);
      Comparable upperValue = (Comparable) propertyHelper.convertToBackendType(entityType, propertyPath.asArrayPath(), upper);
      push(new BetweenExpr(makePropertyValueExpr(propertyPath), new ConstantValueExpr(lowerValue), new ConstantValueExpr(upperValue)));
   }

   public void addIn(PropertyPath<?> propertyPath, List<Object> values) {
      PropertyValueExpr valueExpr = makePropertyValueExpr(propertyPath);
      List<BooleanExpr> children = new ArrayList<>(values.size());
      for (Object element : values) {  //todo [anistor] need more efficient implementation
         Comparable typedValue = (Comparable) propertyHelper.convertToBackendType(entityType, propertyPath.asArrayPath(), element);
         ComparisonExpr booleanExpr = new ComparisonExpr(valueExpr, new ConstantValueExpr(typedValue), ComparisonExpr.Type.EQUAL);
         children.add(booleanExpr);
      }
      if (children.size() == 1) {
         // simplify INs with just one clause by removing the wrapper boolean expression
         push(children.get(0));
      } else {
         push(new OrExpr(children));
      }
   }

   public void addLike(PropertyPath<?> propertyPath, Object patternValue, Character escapeCharacter) {
      // TODO [anistor] escapeCharacter is ignored for now
      push(new LikeExpr(makePropertyValueExpr(propertyPath), patternValue));
   }

   public void addIsNull(PropertyPath<?> propertyPath) {
      push(new IsNullExpr(makePropertyValueExpr(propertyPath)));
   }

   public void pushAnd() {
      push(new LazyAndExpr());
   }

   public void pushOr() {
      push(new LazyOrExpr());
   }

   public void pushNot() {
      push(new LazyNegationExpr());
   }

   public void pushFullTextBoost(float boost) {
      push(new LazyFTBoostExpr(boost));
   }

   public void pushFullTextOccur(QueryRendererDelegate.Occur occur) {
      push(new LazyFTOccurExpr(occur));
   }

   private void push(BooleanExpr expr) {
      push(new LazyLeafBooleanExpr(expr));
   }

   private void push(LazyBooleanExpr expr) {
      // add as sub-expression to the current top expression
      stack.peek().addChild(expr);

      // push to expression stack if required
      if (expr.isParent()) {
         stack.push(expr);
      }
   }

   public void pop() {
      stack.pop();
   }

   public BooleanExpr build() {
      return stack.getFirst().get();
   }

   private PropertyValueExpr makePropertyValueExpr(PropertyPath<?> propertyPath) {
      //todo [anistor] 2 calls to propertyHelper .. very inefficient
      boolean isRepeated = propertyHelper.isRepeatedProperty(entityType, propertyPath.asArrayPath());
      Class<?> primitiveType = propertyHelper.getPrimitivePropertyType(entityType, propertyPath.asArrayPath());
      if (propertyPath instanceof AggregationPropertyPath) {
         return new AggregationExpr((AggregationPropertyPath) propertyPath, isRepeated, primitiveType);
      } else {
         return new PropertyValueExpr(propertyPath, isRepeated, primitiveType);
      }
   }

   public void addConstantBoolean(boolean booleanConstant) {
      push(ConstantBooleanExpr.forBoolean(booleanConstant));
   }

   /**
    * Delays construction until {@link #get} is called.
    */
   abstract static class LazyBooleanExpr {

      /**
       * Indicates whether this expression can have sub-expressions or not.
       */
      public boolean isParent() {
         return true;
      }

      /**
       * Adds the given lazy expression to this.
       *
       * @param child the child to add
       */
      public abstract void addChild(LazyBooleanExpr child);

      /**
       * Returns the query represented by this lazy expression. Contains the all sub-expressions if this is a parent.
       *
       * @return the expression represented by this lazy expression
       */
      public abstract BooleanExpr get();
   }

   private static final class LazyLeafBooleanExpr extends LazyBooleanExpr {

      private final BooleanExpr booleanExpr;

      LazyLeafBooleanExpr(BooleanExpr booleanExpr) {
         this.booleanExpr = booleanExpr;
      }

      @Override
      public boolean isParent() {
         return false;
      }

      @Override
      public void addChild(LazyBooleanExpr child) {
         throw new UnsupportedOperationException("Adding a sub-expression to a non-parent expression is illegal");
      }

      @Override
      public BooleanExpr get() {
         return booleanExpr;
      }
   }

   private static final class LazyAndExpr extends LazyBooleanExpr {

      private final List<LazyBooleanExpr> children = new ArrayList<>(3);

      @Override
      public void addChild(LazyBooleanExpr child) {
         children.add(child);
      }

      @Override
      public BooleanExpr get() {
         if (children.isEmpty()) {
            throw new IllegalStateException("A conjunction must have at least one child");
         }
         BooleanExpr firstChild = children.get(0).get();
         if (children.size() == 1) {
            return firstChild;
         }

         AndExpr andExpr = new AndExpr(firstChild);

         for (int i = 1; i < children.size(); i++) {
            BooleanExpr child = children.get(i).get();
            andExpr.getChildren().add(child);
         }

         return andExpr;
      }
   }

   private static final class LazyOrExpr extends LazyBooleanExpr {

      private final List<LazyBooleanExpr> children = new ArrayList<>(3);

      @Override
      public void addChild(LazyBooleanExpr child) {
         children.add(child);
      }

      @Override
      public BooleanExpr get() {
         if (children.isEmpty()) {
            throw new IllegalStateException("A disjunction must have at least one child");
         }
         BooleanExpr firstChild = children.get(0).get();
         if (children.size() == 1) {
            return firstChild;
         }

         OrExpr orExpr = new OrExpr(firstChild);

         for (int i = 1; i < children.size(); i++) {
            BooleanExpr child = children.get(i).get();
            orExpr.getChildren().add(child);
         }

         return orExpr;
      }
   }

   private static final class LazyNegationExpr extends LazyBooleanExpr {

      private LazyBooleanExpr child;

      @Override
      public void addChild(LazyBooleanExpr child) {
         if (this.child != null) {
            throw log.getNotMoreThanOnePredicateInNegationAllowedException(child);
         }
         this.child = child;
      }

      public LazyBooleanExpr getChild() {
         return child;
      }

      @Override
      public BooleanExpr get() {
         return new NotExpr(getChild().get());
      }
   }

   private static final class LazyFTBoostExpr extends LazyBooleanExpr {

      private LazyBooleanExpr child;

      private final float boost;

      LazyFTBoostExpr(float boost) {
         this.boost = boost;
      }

      @Override
      public void addChild(LazyBooleanExpr child) {
         if (this.child != null) {
            throw log.getNotMoreThanOnePredicateInNegationAllowedException(child);
         }
         this.child = child;
      }

      public LazyBooleanExpr getChild() {
         return child;
      }

      @Override
      public BooleanExpr get() {
         return new FullTextBoostExpr(getChild().get(), boost);
      }
   }

   private static final class LazyFTOccurExpr extends LazyBooleanExpr {

      private LazyBooleanExpr child;

      private final QueryRendererDelegate.Occur occur;

      LazyFTOccurExpr(QueryRendererDelegate.Occur occur) {
         this.occur = occur;
      }

      @Override
      public void addChild(LazyBooleanExpr child) {
         if (this.child != null) {
            throw log.getNotMoreThanOnePredicateInNegationAllowedException(child);
         }
         this.child = child;
      }

      public LazyBooleanExpr getChild() {
         return child;
      }

      @Override
      public BooleanExpr get() {
         return new FullTextOccurExpr(getChild().get(), occur);
      }
   }

   private static final class LazyRootBooleanExpr extends LazyBooleanExpr {

      private LazyBooleanExpr child;

      @Override
      public void addChild(LazyBooleanExpr child) {
         if (this.child != null) {
            throw log.getNotMoreThanOnePredicateInRootOfWhereClauseAllowedException(child);
         }
         this.child = child;
      }

      @Override
      public BooleanExpr get() {
         return child == null ? null : child.get();
      }
   }
}
