package org.infinispan.objectfilter.impl.predicateindex;

import java.util.List;

import org.infinispan.objectfilter.impl.FilterSubscriptionImpl;
import org.infinispan.objectfilter.impl.MetadataAdapter;
import org.infinispan.objectfilter.impl.predicateindex.be.PredicateNode;

/**
 * Keeps track of all predicates and all projections from all filters of an entity type and determines efficiently which
 * predicates match a given entity instance. There is a single instance at most of this for class per each entity type.
 * The predicates are stored in an index-like structure to allow fast matching and are reference counted in order to
 * allow sharing of predicates between filters rather than duplicating them.
 *
 * @param <AttributeMetadata> is the type of the metadata attached to an AttributeNode
 * @param <AttributeId>       is the type used to represent attribute IDs (usually String or Integer)
 * @author anistor@redhat.com
 * @since 7.0
 */
public final class PredicateIndex<AttributeMetadata, AttributeId extends Comparable<AttributeId>> {

   private final AttributeNode<AttributeMetadata, AttributeId> root;

   public PredicateIndex(MetadataAdapter<?, AttributeMetadata, AttributeId> metadataAdapter) {
      root = new RootNode<>(metadataAdapter);
   }

   public AttributeNode<AttributeMetadata, AttributeId> getRoot() {
      return root;
   }

   public Predicates.Subscription<AttributeId> addSubscriptionForPredicate(PredicateNode<AttributeId> predicateNode, FilterSubscriptionImpl filterSubscription) {
      AttributeNode<AttributeMetadata, AttributeId> attributeNode = addAttributeNodeByPath(predicateNode.getAttributePath());
      predicateNode.getPredicate().attributeNode = attributeNode;
      return attributeNode.addPredicateSubscription(predicateNode, filterSubscription);
   }

   public void removeSubscriptionForPredicate(Predicates.Subscription<AttributeId> subscription) {
      AttributeNode<AttributeMetadata, AttributeId> current = getAttributeNodeByPath(subscription.getPredicateNode().getAttributePath());
      current.removePredicateSubscription(subscription);

      // remove the nodes that no longer have a purpose
      while (current != root) {
         if (current.getNumChildren() > 0 || current.hasPredicates() || current.hasProjections()) {
            break;
         }

         AttributeId childId = current.getAttribute();
         current = current.getParent();
         current.removeChild(childId);
      }
   }

   private AttributeNode<AttributeMetadata, AttributeId> getAttributeNodeByPath(List<AttributeId> attributePath) {
      AttributeNode<AttributeMetadata, AttributeId> node = root;
      for (AttributeId attribute : attributePath) {
         node = node.getChild(attribute);
         if (node == null) {
            throw new IllegalStateException("Child not found : " + attribute);
         }
      }
      return node;
   }

   private AttributeNode<AttributeMetadata, AttributeId> addAttributeNodeByPath(List<AttributeId> attributePath) {
      AttributeNode<AttributeMetadata, AttributeId> node = root;
      for (AttributeId attribute : attributePath) {
         node = node.addChild(attribute);
      }
      return node;
   }

   public int addProjections(FilterSubscriptionImpl<?, AttributeMetadata, AttributeId> filterSubscription, List<List<AttributeId>> projection, int i) {
      for (List<AttributeId> projectionPath : projection) {
         AttributeNode node = addAttributeNodeByPath(projectionPath);
         node.addProjection(filterSubscription, i++);
      }
      return i;
   }

   public void removeProjections(FilterSubscriptionImpl<?, AttributeMetadata, AttributeId> filterSubscription, List<List<AttributeId>> projection) {
      for (List<AttributeId> projectionPath : projection) {
         AttributeNode node = getAttributeNodeByPath(projectionPath);
         node.removeProjections(filterSubscription);
      }
   }
}
