package org.infinispan.persistence;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

import org.infinispan.container.DataContainer;
import org.infinispan.container.InternalEntryFactory;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.container.entries.InternalCacheValue;
import org.infinispan.context.InvocationContext;
import org.infinispan.filter.KeyFilter;
import org.infinispan.marshall.core.MarshalledEntry;
import org.infinispan.metadata.InternalMetadata;
import org.infinispan.metadata.Metadata;
import org.infinispan.metadata.impl.InternalMetadataImpl;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.persistence.spi.AdvancedCacheLoader;
import org.infinispan.util.TimeService;
import org.infinispan.util.concurrent.WithinThreadExecutor;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * @author Mircea Markus
 * @since 6.0
 */
public class PersistenceUtil {

   private static Log log = LogFactory.getLog(PersistenceUtil.class);
   private static final boolean trace = log.isTraceEnabled();

   public static KeyFilter notNull(KeyFilter filter) {
      return filter == null ? KeyFilter.ACCEPT_ALL_FILTER : filter;
   }

   public static <K, V> int count(AdvancedCacheLoader<K, V> acl, KeyFilter<? super K> filter) {
      final AtomicInteger result = new AtomicInteger(0);
      acl.process(filter, (marshalledEntry, taskContext) -> result.incrementAndGet(), new WithinThreadExecutor(), false, false);
      return result.get();
   }

   public static <K, V> Set<K> toKeySet(AdvancedCacheLoader<K, V> acl, KeyFilter<? super K> filter) {
      if (acl == null)
         return Collections.emptySet();
      final Set<K> set = new HashSet<K>();
      acl.process(filter, (marshalledEntry, taskContext) -> set.add(marshalledEntry.getKey()), new WithinThreadExecutor(), false, false);
      return set;
   }

   public static <K, V> Set<InternalCacheEntry> toEntrySet(AdvancedCacheLoader<K, V> acl, KeyFilter<? super K> filter, final InternalEntryFactory ief) {
      if (acl == null)
         return Collections.emptySet();
      final Set<InternalCacheEntry> set = new HashSet<InternalCacheEntry>();
      acl.process(filter, (ce, taskContext) -> set.add(ief.create(ce.getKey(), ce.getValue(), ce.getMetadata())), new WithinThreadExecutor(), true, true);
      return set;
   }

   public static long getExpiryTime(InternalMetadata internalMetadata) {
      return internalMetadata == null ? -1 : internalMetadata.expiryTime();
   }

   public static InternalMetadata internalMetadata(InternalCacheEntry ice) {
      return ice.getMetadata() == null ? null : new InternalMetadataImpl(ice);
   }

   public static InternalMetadata internalMetadata(InternalCacheValue icv) {
      return icv.getMetadata() == null ? null : new InternalMetadataImpl(icv.getMetadata(), icv.getCreated(), icv.getLastUsed());
   }

   public static <K, V> InternalCacheEntry<K,V> loadAndStoreInDataContainer(DataContainer<K, V> dataContainer, final PersistenceManager persistenceManager,
                                                         K key, final InvocationContext ctx, final TimeService timeService,
                                                         final AtomicReference<Boolean> isLoaded) {
      return dataContainer.compute(key, (k, oldEntry, factory) -> {
         //under the lock, check if the entry exists in the DataContainer
         if (oldEntry != null && (!oldEntry.canExpire() || !oldEntry.isExpired(timeService.wallClockTime()))) {
            isLoaded.set(null); //not loaded
            return oldEntry; //no changes in container
         }

         MarshalledEntry loaded = loadAndCheckExpiration(persistenceManager, k, ctx, timeService);
         if (loaded == null) {
            isLoaded.set(Boolean.FALSE); //not loaded
            return null; //no changed in container
         }

         InternalCacheEntry<K, V> newEntry = convert(loaded, factory);

         isLoaded.set(Boolean.TRUE); //loaded!
         return newEntry;
      });
   }

   public static <K, V> InternalCacheEntry<K,V> loadAndComputeInDataContainer(DataContainer<K, V> dataContainer, final PersistenceManager persistenceManager,
                                                                              K key, final InvocationContext ctx, final TimeService timeService,
                                                                              DataContainer.ComputeAction<K, V> action) {
      return dataContainer.compute(key, (k, oldEntry, factory) -> {
         //under the lock, check if the entry exists in the DataContainer
         if (oldEntry != null && (!oldEntry.canExpire() || !oldEntry.isExpired(timeService.wallClockTime()))) {
            return action.compute(k, oldEntry, factory);
         }

         MarshalledEntry loaded = loadAndCheckExpiration(persistenceManager, k, ctx, timeService);
         if (loaded == null) {
            return action.compute(k, null, factory);
         }

         InternalCacheEntry<K, V> newEntry = convert(loaded, factory);
         return action.compute(k, newEntry, factory);
      });
   }

   public static MarshalledEntry loadAndCheckExpiration(PersistenceManager persistenceManager, Object key,
                                                        InvocationContext context, TimeService timeService) {
      final MarshalledEntry loaded = persistenceManager.loadFromAllStores(key, context);
      if (trace) {
         log.tracef("Loaded %s for key %s from persistence.", loaded, key);
      }
      if (loaded == null) {
         return null;
      }
      InternalMetadata metadata = loaded.getMetadata();
      if (metadata != null && metadata.isExpired(timeService.wallClockTime())) {
         return null;
      }
      return loaded;
   }

   public static <K, V> InternalCacheEntry<K, V> convert(MarshalledEntry<K, V> loaded, InternalEntryFactory factory) {
      InternalMetadata metadata = loaded.getMetadata();
      if (metadata != null) {
         Metadata actual = metadata instanceof InternalMetadataImpl ? ((InternalMetadataImpl) metadata).actual() :
               metadata;
         //noinspection unchecked
         return factory.create(loaded.getKey(), loaded.getValue(), actual, metadata.created(), metadata.lifespan(),
                               metadata.lastUsed(), metadata.maxIdle());
      } else {
         //metadata is null!
         //noinspection unchecked
         return factory.create(loaded.getKey(), loaded.getValue(), (Metadata) null);
      }
   }
}
