package org.infinispan.objectfilter.impl;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import org.infinispan.objectfilter.FilterCallback;
import org.infinispan.objectfilter.ObjectFilter;
import org.infinispan.objectfilter.SortField;
import org.infinispan.objectfilter.impl.aggregation.FieldAccumulator;
import org.infinispan.objectfilter.impl.logging.Log;
import org.infinispan.objectfilter.impl.predicateindex.AttributeNode;
import org.infinispan.objectfilter.impl.predicateindex.FilterEvalContext;
import org.infinispan.objectfilter.impl.predicateindex.MatcherEvalContext;
import org.infinispan.objectfilter.impl.predicateindex.PredicateIndex;
import org.infinispan.objectfilter.impl.predicateindex.be.BETree;
import org.infinispan.objectfilter.impl.predicateindex.be.BETreeMaker;
import org.infinispan.objectfilter.impl.syntax.BooleanExpr;
import org.infinispan.objectfilter.impl.syntax.BooleanFilterNormalizer;
import org.infinispan.objectfilter.impl.syntax.parser.IckleParsingResult;
import org.infinispan.objectfilter.impl.util.StringHelper;
import org.jboss.logging.Logger;

/**
 * @author anistor@redhat.com
 * @since 7.0
 */
final class ObjectFilterImpl<TypeMetadata, AttributeMetadata, AttributeId extends Comparable<AttributeId>>
      extends ObjectFilterBase<TypeMetadata> implements ObjectFilter {

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

   private static final FilterCallback emptyCallback = (userContext, eventType, instance, projection, sortProjection) -> {
      // do nothing
   };

   private final BaseMatcher<TypeMetadata, AttributeMetadata, AttributeId> matcher;

   private final MetadataAdapter<TypeMetadata, AttributeMetadata, AttributeId> metadataAdapter;

   private final FieldAccumulator[] acc;

   private final String[] projection;

   private final Class<?>[] projectionTypes;

   private final List<List<AttributeId>> translatedProjections;

   private final SortField[] sortFields;

   private final List<List<AttributeId>> translatedSortFields;

   private final BooleanExpr normalizedQuery;

   private FilterSubscriptionImpl<TypeMetadata, AttributeMetadata, AttributeId> filterSubscription;

   private AttributeNode<AttributeMetadata, AttributeId> root;

   ObjectFilterImpl(BaseMatcher<TypeMetadata, AttributeMetadata, AttributeId> matcher,
                    MetadataAdapter<TypeMetadata, AttributeMetadata, AttributeId> metadataAdapter,
                    IckleParsingResult<TypeMetadata> parsingResult,
                    FieldAccumulator[] acc) {
      super(parsingResult, null);
      this.projection = parsingResult.getProjections();
      this.projectionTypes = parsingResult.getProjectedTypes();
      this.sortFields = parsingResult.getSortFields();

      if (acc != null) {
         if (projectionTypes == null) {
            throw new IllegalArgumentException("Accumulators can only be used with projections");
         }
         if (sortFields != null) {
            throw new IllegalArgumentException("Accumulators cannot be used with sorting");
         }
      }

      this.matcher = matcher;
      this.metadataAdapter = metadataAdapter;
      this.acc = acc;

      if (projection != null && projection.length != 0) {
         translatedProjections = new ArrayList<>(projection.length);
         for (String projectionPath : projection) {
            translatedProjections.add(metadataAdapter.mapPropertyNamePathToFieldIdPath(StringHelper.split(projectionPath)));
         }
      } else {
         translatedProjections = null;
      }

      if (sortFields != null) {
         // deduplicate sort fields
         LinkedHashMap<String, SortField> sortFieldMap = new LinkedHashMap<>();
         for (SortField sf : sortFields) {
            String path = sf.getPath().asStringPath();
            if (!sortFieldMap.containsKey(path)) {
               sortFieldMap.put(path, sf);
            }
         }
         SortField[] sortFields = sortFieldMap.values().toArray(new SortField[sortFieldMap.size()]);
         // translate sort field paths
         translatedSortFields = new ArrayList<>(sortFields.length);
         for (SortField sortField : sortFields) {
            translatedSortFields.add(metadataAdapter.mapPropertyNamePathToFieldIdPath(sortField.getPath().asArrayPath()));
         }
      } else {
         translatedSortFields = null;
      }

      BooleanFilterNormalizer booleanFilterNormalizer = new BooleanFilterNormalizer();
      normalizedQuery = booleanFilterNormalizer.normalize(parsingResult.getWhereClause());

      if (getParameterNames().isEmpty()) {
         subscribe();
      }
   }

   private ObjectFilterImpl(ObjectFilterImpl<TypeMetadata, AttributeMetadata, AttributeId> other, Map<String, Object> namedParameters) {
      super(other.parsingResult, Collections.unmodifiableMap(namedParameters));

      this.matcher = other.matcher;
      this.metadataAdapter = other.metadataAdapter;
      this.acc = other.acc;
      this.projection = other.projection;
      this.projectionTypes = other.projectionTypes;
      this.sortFields = other.sortFields;
      this.translatedProjections = other.translatedProjections;
      this.translatedSortFields = other.translatedSortFields;
      this.normalizedQuery = other.normalizedQuery;

      subscribe();
   }

   private void subscribe() {
      BETreeMaker<AttributeId> treeMaker = new BETreeMaker<>(metadataAdapter, false);
      BETree beTree = treeMaker.make(normalizedQuery, namedParameters);

      PredicateIndex<AttributeMetadata, AttributeId> predicateIndex = new PredicateIndex<>(metadataAdapter);
      root = predicateIndex.getRoot();

      filterSubscription = new FilterSubscriptionImpl<>(parsingResult.getQueryString(), namedParameters, false, metadataAdapter, beTree,
            emptyCallback, false, projection, projectionTypes, translatedProjections, sortFields, translatedSortFields, null);
      filterSubscription.registerProjection(predicateIndex);
      filterSubscription.subscribe(predicateIndex);
      filterSubscription.index = 0;
   }

   @Override
   public String getEntityTypeName() {
      return metadataAdapter.getTypeName();
   }

   @Override
   public String[] getProjection() {
      return projection;
   }

   @Override
   public Class<?>[] getProjectionTypes() {
      return projectionTypes;
   }

   @Override
   public ObjectFilter withParameters(Map<String, Object> namedParameters) {
      if (namedParameters == null) {
         throw log.getNamedParametersCannotBeNull();
      }
      for (String paramName : getParameterNames()) {
         if (namedParameters.get(paramName) == null) {
            throw new IllegalArgumentException("Query parameter '" + paramName + "' was not set");
         }
      }
      return new ObjectFilterImpl<>(this, namedParameters);
   }

   @Override
   public SortField[] getSortFields() {
      return sortFields;
   }

   @Override
   public Comparator<Comparable[]> getComparator() {
      if (filterSubscription == null) {
         throw new IllegalStateException("Parameter values were not bound yet.");
      }

      return filterSubscription.getComparator();
   }

   @Override
   public FilterResult filter(Object instance) {
      if (filterSubscription == null) {
         throw new IllegalStateException("Parameter values were not bound yet.");
      }

      if (instance == null) {
         throw new IllegalArgumentException("instance cannot be null");
      }

      MatcherEvalContext<TypeMetadata, AttributeMetadata, AttributeId> matcherEvalContext = matcher.startSingleTypeContext(null, null, instance, filterSubscription.getMetadataAdapter());
      if (matcherEvalContext != null) {
         FilterEvalContext filterEvalContext = matcherEvalContext.initSingleFilterContext(filterSubscription);
         if (acc != null) {
            filterEvalContext.acc = acc;
            for (FieldAccumulator a : acc) {
               if (a != null) {
                  a.init(filterEvalContext.getProjection());
               }
            }
         }
         matcherEvalContext.process(root);

         if (filterEvalContext.isMatching()) {
            Object o = filterEvalContext.getProjection() == null ? matcher.convert(instance) : null;
            return new FilterResultImpl(o, filterEvalContext.getProjection(), filterEvalContext.getSortProjection());
         }
      }

      return null;
   }
}
