package org.infinispan.query.dsl.impl;

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

import org.infinispan.query.dsl.Expression;
import org.infinispan.query.dsl.FilterConditionContext;
import org.infinispan.query.dsl.FilterConditionEndContext;
import org.infinispan.query.dsl.QueryBuilder;
import org.infinispan.query.dsl.QueryFactory;
import org.infinispan.query.dsl.SortOrder;
import org.infinispan.query.dsl.impl.logging.Log;
import org.jboss.logging.Logger;

/**
 * @author anistor@redhat.com
 * @since 6.0
 */
public abstract class BaseQueryBuilder implements QueryBuilder, Visitable {

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

   protected final QueryFactory queryFactory;

   /**
    * The fully qualified name of the entity being queried. It can be a Java Class name or a Protobuf message type
    * name.
    */
   protected final String rootTypeName;

   /**
    * The attribute paths for the projection.
    */
   protected Expression[] projection;

   protected String[] groupBy;

   protected BaseCondition filterCondition;

   protected BaseCondition whereFilterCondition;

   protected BaseCondition havingFilterCondition;

   protected List<SortCriteria> sortCriteria;

   protected long startOffset = -1;

   protected int maxResults = -1;

   protected BaseQueryBuilder(QueryFactory queryFactory, String rootTypeName) {
      if (rootTypeName == null) {
         throw log.argumentCannotBeNull("rootTypeName");
      }
      this.queryFactory = queryFactory;
      this.rootTypeName = rootTypeName;
   }

   protected String getRootTypeName() {
      return rootTypeName;
   }

   @Override
   public QueryBuilder orderBy(Expression pathExpression) {
      return orderBy(pathExpression, SortOrder.ASC);
   }

   @Override
   public QueryBuilder orderBy(Expression pathExpression, SortOrder sortOrder) {
      if (sortCriteria == null) {
         sortCriteria = new ArrayList<>();
      }
      sortCriteria.add(new SortCriteria(pathExpression, sortOrder));
      return this;
   }

   @Override
   public QueryBuilder orderBy(String attributePath) {
      return orderBy(attributePath, SortOrder.ASC);
   }

   @Override
   public QueryBuilder orderBy(String attributePath, SortOrder sortOrder) {
      return orderBy(Expression.property(attributePath), sortOrder);
   }

   protected List<SortCriteria> getSortCriteria() {
      return sortCriteria;
   }

   @Override
   public QueryBuilder select(String... attributePath) {
      if (attributePath == null || attributePath.length == 0) {
         throw log.projectionCannotBeNullOrEmpty();
      }
      Expression[] projection = new Expression[attributePath.length];
      for (int i = 0; i < attributePath.length; i++) {
         projection[i] = Expression.property(attributePath[i]);
      }
      return select(projection);
   }

   @Override
   public QueryBuilder select(Expression... projection) {
      if (projection == null || projection.length == 0) {
         throw log.projectionCannotBeNullOrEmpty();
      }
      if (this.projection != null) {
         throw log.projectionCanBeSpecifiedOnlyOnce();
      }
      this.projection = projection;
      return this;
   }

   protected Expression[] getProjection() {
      return projection;
   }

   protected String[] getProjectionPaths() {
      if (projection == null) {
         return null;
      }
      String[] _projection = new String[projection.length];
      for (int i = 0; i < projection.length; i++) {
         _projection[i] = projection[i].toString();
      }
      return _projection;
   }

   @Override
   public QueryBuilder groupBy(String... groupBy) {
      if (groupBy == null || groupBy.length == 0) {
         throw log.groupingCannotBeNullOrEmpty();
      }
      if (this.groupBy != null) {
         throw log.groupingCanBeSpecifiedOnlyOnce();
      }
      this.groupBy = groupBy;
      // reset this so we can start a new filter for havingFilterCondition
      filterCondition = null;
      return this;
   }

   protected String[] getGroupBy() {
      return groupBy;
   }

   @Override
   public QueryBuilder startOffset(long startOffset) {
      if (startOffset < 0) {
         throw log.startOffsetCannotBeLessThanZero();
      }
      this.startOffset = startOffset;
      return this;
   }

   @Override
   public QueryBuilder maxResults(int maxResults) {
      if (maxResults <= 0) {
         throw log.maxResultMustBeGreaterThanZero();
      }
      this.maxResults = maxResults;
      return this;
   }

   protected BaseCondition getWhereFilterCondition() {
      return whereFilterCondition;
   }

   protected BaseCondition getHavingFilterCondition() {
      return havingFilterCondition;
   }

   @Override
   public FilterConditionEndContext having(Expression expression) {
      if (filterCondition != null) {
         throw log.cannotUseOperatorAgain("having(..)");
      }
      AttributeCondition attributeCondition = new AttributeCondition(queryFactory, expression);
      attributeCondition.setQueryBuilder(this);
      setFilterCondition(attributeCondition);
      return attributeCondition;
   }

   @Override
   public FilterConditionEndContext having(String attributePath) {
      return having(Expression.property(attributePath));
   }

   private void setFilterCondition(BaseCondition filterCondition) {
      this.filterCondition = filterCondition;
      if (groupBy == null) {
         whereFilterCondition = filterCondition;
      } else {
         havingFilterCondition = filterCondition;
      }
   }

   @Override
   public BaseCondition not() {
      if (filterCondition != null) {
         throw log.cannotUseOperatorAgain("not()");
      }
      IncompleteCondition incompleteCondition = new IncompleteCondition(queryFactory);
      incompleteCondition.setQueryBuilder(this);
      setFilterCondition(incompleteCondition);
      return incompleteCondition.not();
   }

   @Override
   public BaseCondition not(FilterConditionContext fcc) {
      if (fcc == null) {
         throw log.argumentCannotBeNull();
      }

      if (filterCondition != null) {
         throw log.cannotUseOperatorAgain("not(..)");
      }

      BaseCondition baseCondition = ((BaseCondition) fcc).getRoot();
      if (baseCondition.queryFactory != queryFactory) {
         throw log.conditionWasCreatedByAnotherFactory();
      }
      if (baseCondition.queryBuilder != null) {
         throw log.conditionIsAlreadyInUseByAnotherBuilder();
      }

      NotCondition notCondition = new NotCondition(queryFactory, baseCondition);
      notCondition.setQueryBuilder(this);
      setFilterCondition(notCondition);
      return filterCondition;
   }

   @Override
   public <ReturnType> ReturnType accept(Visitor<ReturnType> visitor) {
      return visitor.visit(this);
   }
}
