package org.infinispan.query.dsl.embedded.impl;

import java.util.EnumSet;
import java.util.List;
import java.util.concurrent.TimeUnit;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.eviction.EvictionStrategy;
import org.infinispan.eviction.EvictionType;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.objectfilter.impl.aggregation.FieldAccumulator;
import org.infinispan.query.logging.Log;
import org.infinispan.registry.InternalCacheRegistry;
import org.infinispan.transaction.TransactionMode;
import org.infinispan.util.logging.LogFactory;

import net.jcip.annotations.ThreadSafe;

/**
 * A local cache for 'parsed' queries. Each cache manager has at most one QueryCache which is backed by a lazily
 * created Cache.
 *
 * @author anistor@redhat.com
 * @since 7.0
 */
@ThreadSafe
public class QueryCache {

   @FunctionalInterface
   public interface QueryCreator<Q> {

      /**
       * Create a new query object based on the input args, or just return {@code null}. If {@code null} is returned
       * this will be propagated to the caller of {@link QueryCache#get} and the {@code null} will not be cached.
       */
      Q create(String queryString, List<FieldAccumulator> accumulators);
   }

   private static final Log log = LogFactory.getLog(QueryCache.class, Log.class);

   /**
    * Users can define a cache configuration with this name if they need to fine tune query caching. If they do not do
    * so a default config is used (see {@link QueryCache#getQueryCacheConfig()}).
    */
   public static final String QUERY_CACHE_NAME = "___query_cache";

   /**
    * Max number of cached entries.
    */
   private static final long MAX_ENTRIES = 200;

   /**
    * Cache entry lifespan in seconds.
    */
   private static final long ENTRY_LIFESPAN = 300;  // seconds

   private EmbeddedCacheManager cacheManager;

   private InternalCacheRegistry internalCacheRegistry;

   private volatile Cache<QueryCacheKey, Object> lazyCache;

   @Inject
   public void init(EmbeddedCacheManager cacheManager, InternalCacheRegistry internalCacheRegistry) {
      this.cacheManager = cacheManager;
      this.internalCacheRegistry = internalCacheRegistry;
   }

   /**
    * Gets the cached query object. The key used for lookup is an object pair containing the query string and a
    * discriminator value which is usually the Class of the cached query object and an optional {@link List} of {@link
    * FieldAccumulator}s.
    */
   public <T> T get(String queryString, List<FieldAccumulator> accumulators, Object queryTypeDiscriminator, QueryCreator<T> queryCreator) {
      QueryCacheKey key = new QueryCacheKey(queryString, accumulators, queryTypeDiscriminator);
      return (T) getCache().computeIfAbsent(key, (k) -> queryCreator.create(k.queryString, k.accumulators));
   }

   public void clear() {
      getCache().clear();
   }

   /**
    * Obtain the cache. Start it lazily when needed.
    */
   private Cache<QueryCacheKey, Object> getCache() {
      final Cache<QueryCacheKey, Object> cache = lazyCache;

      //Most likely branch first:
      if (cache != null) {
         return cache;
      }
      synchronized (this) {
         if (lazyCache == null) {
            // define the query cache configuration if it does not already exist (from a previous call or manually defined by the user)
            internalCacheRegistry.registerInternalCache(QUERY_CACHE_NAME, getQueryCacheConfig().build(), EnumSet.noneOf(InternalCacheRegistry.Flag.class));
            lazyCache = cacheManager.getCache(QUERY_CACHE_NAME);
         }
         return lazyCache;
      }
   }

   /**
    * Create the configuration of the internal query cache.
    */
   private ConfigurationBuilder getQueryCacheConfig() {
      ConfigurationBuilder cfgBuilder = new ConfigurationBuilder();
      cfgBuilder
            .clustering().cacheMode(CacheMode.LOCAL)
            .transaction().transactionMode(TransactionMode.NON_TRANSACTIONAL)
            .expiration().maxIdle(ENTRY_LIFESPAN, TimeUnit.SECONDS)
            .eviction().type(EvictionType.COUNT).size(MAX_ENTRIES)
            .strategy(EvictionStrategy.LIRS);
      return cfgBuilder;
   }

   /**
    * The key of the query cache: a tuple with 3 components. Serialization of this object is not expected as
    * the cache is local and there is no store configured.
    */
   private static final class QueryCacheKey {

      final String queryString;

      final List<FieldAccumulator> accumulators;

      final Object queryTypeDiscriminator;

      QueryCacheKey(String queryString, List<FieldAccumulator> accumulators, Object queryTypeDiscriminator) {
         this.queryString = queryString;
         this.accumulators = accumulators;
         this.queryTypeDiscriminator = queryTypeDiscriminator;
      }

      @Override
      public boolean equals(Object obj) {
         if (this == obj) return true;
         if (!(obj instanceof QueryCacheKey)) return false;
         QueryCacheKey other = (QueryCacheKey) obj;
         return queryString.equals(other.queryString)
               && (accumulators != null ? accumulators.equals(other.accumulators) : other.accumulators == null)
               && queryTypeDiscriminator.equals(other.queryTypeDiscriminator);
      }

      @Override
      public int hashCode() {
         int result = queryString.hashCode();
         result = 31 * result + (accumulators != null ? accumulators.hashCode() : 0);
         result = 31 * result + queryTypeDiscriminator.hashCode();
         return result;
      }

      @Override
      public String toString() {
         return "QueryCacheKey{" +
               "queryString='" + queryString + '\'' +
               ", accumulators=" + accumulators +
               ", queryTypeDiscriminator=" + queryTypeDiscriminator +
               '}';
      }
   }
}
