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

import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.infinispan.query.objectfilter.impl.FilterRegistry;
import org.infinispan.query.objectfilter.impl.FilterSubscriptionImpl;
import org.infinispan.query.objectfilter.impl.predicateindex.be.BETree;

/**
 * Stores processing state during the matching process of all filters registered with a Matcher.
 *
 * @param <TypeMetadata>      representation of entity type information, ie a Class object or anything that represents a
 *                            type
 * @param <AttributeMetadata> representation of attribute type information
 * @param <AttributeId>       representation of attribute identifiers
 * @author anistor@redhat.com
 * @since 7.0
 */
public abstract class MatcherEvalContext<TypeMetadata, AttributeMetadata, AttributeId extends Comparable<AttributeId>> {

   protected AttributeNode<AttributeMetadata, AttributeId> rootNode;

   /**
    * Current node during traversal of the attribute tree.
    */
   protected AttributeNode<AttributeMetadata, AttributeId> currentNode;

   private final Object userContext;

   protected final Object key;

   private final Object instance;

   protected final Object metadata;

   private final Object eventType;

   private FilterEvalContext singleFilterContext;

   /**
    * Each filter subscription has its own evaluation context, created on demand.
    */
   private FilterEvalContext[] filterContexts;

   private List<FilterSubscriptionImpl> filterSubscriptions;

   private Map<Predicate<?>, Counter> suspendedPredicateSubscriptionCounts;

   protected MatcherEvalContext(Object userContext, Object eventType, Object key, Object instance, Object metadata) {
      if (instance == null) {
         throw new IllegalArgumentException("instance cannot be null");
      }
      this.userContext = userContext;
      this.eventType = eventType;
      this.key = key;
      this.instance = instance;
      this.metadata = metadata;
   }

   public abstract TypeMetadata getEntityType();

   /**
    * The instance being matched with the filters.
    */
   public Object getInstance() {
      return instance;
   }

   public Object getUserContext() {
      return userContext;
   }

   public Object getEventType() {
      return eventType;
   }

   public void initMultiFilterContext(FilterRegistry<TypeMetadata, AttributeMetadata, AttributeId> filterRegistry) {
      rootNode = filterRegistry.getPredicateIndex().getRoot();
      suspendedPredicateSubscriptionCounts = new HashMap<>();
      filterSubscriptions = filterRegistry.getFilterSubscriptions();
      filterContexts = new FilterEvalContext[filterRegistry.getFilterSubscriptions().size()];
   }

   public FilterEvalContext initSingleFilterContext(FilterSubscriptionImpl filterSubscription) {
      singleFilterContext = new FilterEvalContext(this, filterSubscription);
      return singleFilterContext;
   }

   private boolean isSingleFilter() {
      return singleFilterContext != null;
   }

   public FilterEvalContext getFilterEvalContext(FilterSubscriptionImpl filterSubscription) {
      if (isSingleFilter()) {
         return singleFilterContext;
      }

      FilterEvalContext filterEvalContext = filterContexts[filterSubscription.index];
      if (filterEvalContext == null) {
         filterEvalContext = new FilterEvalContext(this, filterSubscription);
         filterContexts[filterSubscription.index] = filterEvalContext;
      }
      return filterEvalContext;
   }

   public void addSuspendedSubscription(Predicate<?> predicate) {
      if (isSingleFilter()) {
         return;
      }
      Counter counter = suspendedPredicateSubscriptionCounts.computeIfAbsent(predicate, k -> new Counter());
      counter.value++;
   }

   public int getSuspendedSubscriptionsCounter(Predicate<AttributeId> predicate) {
      if (isSingleFilter()) {
         return -1;
      }

      Counter counter = suspendedPredicateSubscriptionCounts.get(predicate);
      return counter == null ? 0 : counter.value;
   }

   public AttributeNode<AttributeMetadata, AttributeId> getRootNode() {
      return rootNode;
   }

   public void process(AttributeNode<AttributeMetadata, AttributeId> node) {
      currentNode = node;
      processAttributes(currentNode, instance);
   }

   public void notifySubscribers() {
      if (isSingleFilter()) {
         return;
      }

      for (int i = 0; i < filterContexts.length; i++) {
         FilterSubscriptionImpl s = filterSubscriptions.get(i);
         FilterEvalContext filterEvalContext = filterContexts[i];
         if (filterEvalContext == null) {
            if (s.getBETree().getChildCounters()[0] == BETree.EXPR_TRUE) {
               // this filter is a tautology and since its FilterEvalContext was never activated that means it also does not have projections
               filterEvalContext = new FilterEvalContext(this, s);
               filterContexts[i] = filterEvalContext;
            } else {
               continue;
            }
         }
         if (filterEvalContext.isMatching()) {
            s.getCallback().onFilterResult(userContext, eventType, instance, filterEvalContext.getProjection(), filterEvalContext.getSortProjection());
         }
      }
   }

   public void notifyDeltaSubscribers(MatcherEvalContext other, Object joiningEvent, Object updateEvent, Object leavingEvent) {
      if (isSingleFilter()) {
         throw new AssertionError("Single filters contexts do not support delta matching.");
      }

      for (int i = 0; i < filterContexts.length; i++) {
         FilterSubscriptionImpl s = filterSubscriptions.get(i);

         FilterEvalContext filterEvalContext1 = filterContexts[i];
         if (filterEvalContext1 == null && s.getBETree().getChildCounters()[0] == BETree.EXPR_TRUE) {
            // this filter is a tautology and since its FilterEvalContext was never activated that means it also does not have projections
            filterEvalContext1 = new FilterEvalContext(this, s);
            filterContexts[i] = filterEvalContext1;
         }

         FilterEvalContext filterEvalContext2 = null;
         if (other != null) {
            filterEvalContext2 = other.filterContexts[i];
            if (filterEvalContext2 == null && s.getBETree().getChildCounters()[0] == BETree.EXPR_TRUE) {
               // this filter is a tautology and since its FilterEvalContext was never activated that means it also does not have projections
               filterEvalContext2 = new FilterEvalContext(other, s);
               other.filterContexts[i] = filterEvalContext2;
            }
         }

         boolean before = filterEvalContext1 != null && filterEvalContext1.isMatching();
         boolean after = filterEvalContext2 != null && filterEvalContext2.isMatching();

         if (!before && after) {
            s.getCallback().onFilterResult(userContext, joiningEvent, other.instance, filterEvalContext2.getProjection(), filterEvalContext2.getSortProjection());
         } else if (before && !after) {
            s.getCallback().onFilterResult(userContext, leavingEvent, instance, filterEvalContext1.getProjection(), filterEvalContext1.getSortProjection());
         } else if (before && after) {
            s.getCallback().onFilterResult(userContext, updateEvent, other.instance, filterEvalContext2.getProjection(), filterEvalContext2.getSortProjection());
         }
      }
   }

   protected abstract void processAttributes(AttributeNode<AttributeMetadata, AttributeId> node, Object instance);

   private static final class Counter {
      int value = 0;
   }
}
