package org.infinispan.container;

import static org.infinispan.commons.util.Util.toStr;

import org.infinispan.atomic.Delta;
import org.infinispan.atomic.DeltaAware;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.container.entries.ClusteredRepeatableReadEntry;
import org.infinispan.container.entries.DeltaAwareCacheEntry;
import org.infinispan.container.entries.InternalCacheEntry;
import org.infinispan.container.entries.MVCCEntry;
import org.infinispan.container.entries.NullCacheEntry;
import org.infinispan.container.entries.ReadCommittedEntry;
import org.infinispan.container.entries.RepeatableReadEntry;
import org.infinispan.container.versioning.VersionGenerator;
import org.infinispan.context.InvocationContext;
import org.infinispan.factories.annotations.Inject;
import org.infinispan.factories.annotations.Start;
import org.infinispan.metadata.EmbeddedMetadata;
import org.infinispan.metadata.Metadata;
import org.infinispan.persistence.manager.PersistenceManager;
import org.infinispan.util.TimeService;
import org.infinispan.util.concurrent.IsolationLevel;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

/**
 * {@link EntryFactory} implementation to be used for optimistic locking scheme.
 *
 * @author Mircea Markus
 * @private
 */
@Deprecated
public class EntryFactoryImpl implements EntryFactory {

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

   private boolean useRepeatableRead;
   private DataContainer container;
   private boolean isL1Enabled;
   private Configuration configuration;
   private PersistenceManager persistenceManager; // hack for DeltaAware
   private TimeService timeService;
   private VersionGenerator versionGenerator;
   private boolean useVersioning;

   @Inject
   public void injectDependencies(DataContainer dataContainer, Configuration configuration,
                                  TimeService timeService, PersistenceManager persistenceManager,
                                  VersionGenerator versionGenerator) {
      this.container = dataContainer;
      this.configuration = configuration;
      this.timeService = timeService;
      this.persistenceManager = persistenceManager;
      this.versionGenerator = versionGenerator;
   }

   @Start (priority = 8)
   public void init() {
      useRepeatableRead = configuration.transaction().transactionMode().isTransactional()
            && configuration.locking().isolationLevel() == IsolationLevel.REPEATABLE_READ;
      isL1Enabled = configuration.clustering().l1().enabled();
      // Write-skew check implies isolation level = REPEATABLE_READ && locking mode = OPTIMISTIC
      useVersioning = configuration.clustering().cacheMode().isClustered() &&
            configuration.transaction().transactionMode().isTransactional() &&
            configuration.locking().writeSkewCheck();
   }

   @Override
   public final void wrapEntryForReading(InvocationContext ctx, Object key, boolean isOwner) {
      if (!isOwner && !isL1Enabled) {
         return;
      }
      CacheEntry cacheEntry = getFromContext(ctx, key);
      if (cacheEntry == null) {
         cacheEntry = getFromContainerForRead(key, isOwner);

         if (cacheEntry != null) {
            // With repeatable read, we need to create a RepeatableReadEntry as internal cache entries are mutable
            // Otherwise we can store the InternalCacheEntry directly in the context
            if (useRepeatableRead) {
               cacheEntry = createWrappedEntry(key, cacheEntry);
            }
            ctx.putLookedUpEntry(key, cacheEntry);
         }
      }

      if (trace) {
         log.tracef("Wrap %s for read. Entry=%s", toStr(key), cacheEntry);
      }
   }

   @Override
   public void wrapEntryForWriting(InvocationContext ctx, Object key, boolean isOwner) {
      CacheEntry contextEntry = getFromContext(ctx, key);
      if (contextEntry instanceof MVCCEntry) {
         // Nothing to do, already wrapped.
      } else if (contextEntry != null) {
         // Already in the context as an InternalCacheEntry or DeltaAwareCacheEntry.
         // Need to wrap it in a MVCCEntry.
         MVCCEntry mvccEntry = createWrappedEntry(key, contextEntry);
         ctx.putLookedUpEntry(key, mvccEntry);
         if (trace)
            log.tracef("Updated context entry %s -> %s", contextEntry, mvccEntry);
      } else {
         // Not in the context yet.
         CacheEntry cacheEntry = getFromContainer(key, isOwner, true);
         if (cacheEntry == null) {
            return;
         }
         MVCCEntry mvccEntry = createWrappedEntry(key, cacheEntry);
         if (cacheEntry.isNull()) {
            mvccEntry.setCreated(true);
         }
         ctx.putLookedUpEntry(key, mvccEntry);
         if (trace)
            log.tracef("Updated context entry %s -> %s", contextEntry, mvccEntry);
      }
   }

   @Override
   public void wrapExternalEntry(InvocationContext ctx, Object key, CacheEntry externalEntry, boolean isWrite) {
      // For a write operation, the entry is always already wrapped. For a read operation, the entry may be
      // in the context as an InternalCacheEntry, as null, or missing altogether.
      CacheEntry contextEntry = getFromContext(ctx, key);
      if (contextEntry instanceof MVCCEntry) {
         // Already wrapped for a write. Update the value and the metadata.
         if (contextEntry.skipLookup()) {
            // This can happen during getGroup() invocations, which request the whole group from remote nodes
            // even if some keys are already in the context.
            if (trace)
               log.tracef("Ignored update for context entry %s", contextEntry);
            return;
         }
         contextEntry.setValue(externalEntry.getValue());
         contextEntry.setCreated(externalEntry.getCreated());
         contextEntry.setLastUsed(externalEntry.getLastUsed());
         contextEntry.setMetadata(externalEntry.getMetadata());
         ((MVCCEntry) contextEntry).updatePreviousValue();
         if (trace) log.tracef("Updated context entry %s", contextEntry);
      } else if (contextEntry == null || contextEntry.isNull()) {
         if (isWrite || useRepeatableRead) {
            MVCCEntry mvccEntry = createWrappedEntry(key, externalEntry);
            ctx.putLookedUpEntry(key, mvccEntry);
            if (trace)
               log.tracef("Updated context entry %s -> %s", contextEntry, mvccEntry);
         } else {
            // This is a read operation, store the external entry in the context directly.
            ctx.putLookedUpEntry(key, externalEntry);
            if (trace)
               log.tracef("Updated context entry %s -> %s", contextEntry, externalEntry);
         }
      } else {
         if (useRepeatableRead) {
            if (trace) log.tracef("Ignored update %s -> %s as we do repeatable reads", contextEntry, externalEntry);
         } else {
            ctx.putLookedUpEntry(key, externalEntry);
            if (trace) log.tracef("Updated context entry %s -> %s", contextEntry, externalEntry);
         }
      }
   }

   @Override
   public void wrapEntryForDelta(InvocationContext ctx, Object deltaKey, Delta delta, boolean isOwner) {
      CacheEntry cacheEntry = getFromContext(ctx, deltaKey);
      DeltaAwareCacheEntry deltaAwareEntry;
      if (cacheEntry instanceof DeltaAwareCacheEntry) {
         // Already delta-aware, nothing to do.
         deltaAwareEntry = (DeltaAwareCacheEntry) cacheEntry;
      } else if (cacheEntry != null) {
         // Wrap the existing context entry inside a DeltaAwareCacheEntry
         deltaAwareEntry = createWrappedDeltaEntry(deltaKey, (DeltaAware) cacheEntry.getValue(), cacheEntry, ctx);
         ctx.putLookedUpEntry(deltaKey, deltaAwareEntry);
      } else {
         // Read the value from the container and wrap it
         cacheEntry = getFromContainer(deltaKey, isOwner, false);
         DeltaAwareCacheEntry deltaEntry =
               createWrappedDeltaEntry(deltaKey, cacheEntry != null ? (DeltaAware) cacheEntry.getValue() : null, null, ctx);

         ctx.putLookedUpEntry(deltaKey, deltaEntry);
         deltaAwareEntry = deltaEntry;
      }
      if (trace) log.tracef("Wrap %s for delta. Entry=%s", deltaKey, deltaAwareEntry);
   }

   private CacheEntry getFromContext(InvocationContext ctx, Object key) {
      final CacheEntry cacheEntry = ctx.lookupEntry(key);
      if (trace) log.tracef("Exists in context? %s ", cacheEntry);
      return cacheEntry;
   }

   private CacheEntry getFromContainer(Object key, boolean isOwner, boolean writeOperation) {
      if (isOwner) {
         final InternalCacheEntry ice = innerGetFromContainer(key, writeOperation);
         if (trace)
            log.tracef("Retrieved from container %s", ice);
         if (ice == null) {
            return NullCacheEntry.getInstance();
         }
         return ice;
      } else if (isL1Enabled) {
         final InternalCacheEntry ice = innerGetFromContainer(key, writeOperation);
         if (trace)
            log.tracef("Retrieved from container %s", ice);
         if (ice == null || !ice.isL1Entry()) return null;
         return ice;
      }
      return null;
   }

   private CacheEntry getFromContainerForRead(Object key, boolean isOwner) {
      InternalCacheEntry ice = container.get(key);
      if (trace) {
         log.tracef("Retrieved from container %s", ice);
      }
      if (isOwner) {
         return ice == null ? NullCacheEntry.getInstance() : ice;
      } else {
         return ice == null || !ice.isL1Entry() ? null : ice;
      }
   }

   private InternalCacheEntry innerGetFromContainer(Object key, boolean writeOperation) {
      InternalCacheEntry ice;
      // Write operations should not cause expiration events to occur, because we will most likely overwrite the
      // value anyways - also required for remove expired to not cause infinite loop
      if (writeOperation) {
         ice = container.peek(key);
         if (ice != null && ice.canExpire()) {
            long wallClockTime = timeService.wallClockTime();
            if (ice.isExpired(wallClockTime)) {
               ice = null;
            } else {
               ice.touch(wallClockTime);
            }
         }
      } else {
         ice = container.get(key);
      }
      return ice;
   }

   protected MVCCEntry createWrappedEntry(Object key, CacheEntry cacheEntry) {
      Object value = null;
      Metadata metadata = null;
      if (cacheEntry != null) {
         value = cacheEntry.getValue();
         metadata = cacheEntry.getMetadata();
      }

      if (trace) log.tracef("Creating new entry for key %s", toStr(key));
      if (useRepeatableRead) {
         MVCCEntry mvccEntry;
         if (useVersioning) {
            if (metadata == null) {
               metadata = new EmbeddedMetadata.Builder().version(versionGenerator.nonExistingVersion()).build();
            }
            mvccEntry = new ClusteredRepeatableReadEntry(key, value, metadata);
         } else {
            mvccEntry = new RepeatableReadEntry(key, value, metadata);
         }
         return mvccEntry;
      } else {
         return new ReadCommittedEntry(key, value, metadata);
      }
   }

   private DeltaAwareCacheEntry createWrappedDeltaEntry(Object key, DeltaAware deltaAware, CacheEntry entry, InvocationContext ctx) {
      DeltaAwareCacheEntry deltaAwareCacheEntry = new DeltaAwareCacheEntry(key, deltaAware, entry, ctx, persistenceManager, timeService);
      // Set the delta aware entry to created so it ignores the previous value and only merges new deltas when it is
      // committed
      if (entry != null && entry.isCreated()) {
         deltaAwareCacheEntry.setCreated(true);
      }
      return deltaAwareCacheEntry;
   }
}
