/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.geode.internal.cache.map;

import java.util.Set;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.CacheWriter;
import org.apache.geode.cache.DiskAccessException;
import org.apache.geode.cache.Operation;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.EntryEventSerialization;
import org.apache.geode.internal.cache.InternalRegion;
import org.apache.geode.internal.cache.RegionClearedException;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.cache.ValueComparisonHelper;
import org.apache.geode.internal.cache.entries.AbstractRegionEntry;
import org.apache.geode.internal.cache.versions.ConcurrentCacheModificationException;
import org.apache.geode.internal.cache.versions.VersionTag;
import org.apache.geode.internal.cache.wan.GatewaySenderEventImpl;
import org.apache.geode.internal.offheap.OffHeapHelper;
import org.apache.geode.internal.offheap.ReferenceCountHelper;
import org.apache.geode.internal.offheap.annotations.Released;
import org.apache.geode.internal.offheap.annotations.Unretained;
import org.apache.geode.internal.sequencelog.EntryLogger;
import org.apache.geode.logging.internal.log4j.api.LogService;

public class RegionMapPut extends AbstractRegionMapPut {
  protected static final Logger logger = LogService.getLogger();

  private final CacheModificationLock cacheModificationLock;
  private final EntryEventSerialization entryEventSerialization;
  private final boolean ifNew;
  private final boolean ifOld;
  private final boolean overwriteDestroyed;
  private boolean overwritePutIfAbsent;
  private final boolean requireOldValue;
  private final boolean retrieveOldValueForDelta;
  private final boolean replaceOnClient;
  private final boolean onlyExisting;
  private final boolean cacheWrite;
  private final CacheWriter cacheWriter;
  private final Set netWriteRecipients;
  private final Object expectedOldValue;

  @Released
  private Object oldValueForDelta;

  public RegionMapPut(FocusedRegionMap focusedRegionMap, InternalRegion owner,
      CacheModificationLock cacheModificationLock, EntryEventSerialization entryEventSerialization,
      EntryEventImpl event, boolean ifNew, boolean ifOld, boolean overwriteDestroyed,
      boolean requireOldValue, Object expectedOldValue) {
    super(focusedRegionMap, owner, event);
    this.cacheModificationLock = cacheModificationLock;
    this.entryEventSerialization = entryEventSerialization;
    this.ifNew = ifNew;
    this.ifOld = ifOld;
    this.overwriteDestroyed = overwriteDestroyed;
    this.requireOldValue = requireOldValue;
    retrieveOldValueForDelta = event.getDeltaBytes() != null && event.getRawNewValue() == null;
    replaceOnClient = event.getOperation() == Operation.REPLACE && owner.hasServerProxy();
    onlyExisting = ifOld && !isReplaceOnClient();
    cacheWriter = owner.basicGetWriter();
    cacheWrite = !event.isOriginRemote() && !event.isNetSearch() && event.isGenerateCallbacks()
        && (getCacheWriter() != null || owner.hasServerProxy() || owner.getScope().isDistributed());
    this.expectedOldValue = expectedOldValue;
    if (isCacheWrite() && getCacheWriter() == null) {
      netWriteRecipients = owner.adviseNetWrite();
    } else {
      netWriteRecipients = null;
    }
  }

  private boolean isIfNew() {
    return ifNew;
  }

  private boolean isIfOld() {
    return ifOld;
  }

  private boolean isOverwriteDestroyed() {
    return overwriteDestroyed;
  }

  private boolean isRequireOldValue() {
    return requireOldValue;
  }

  boolean isRetrieveOldValueForDelta() {
    return retrieveOldValueForDelta;
  }

  boolean isReplaceOnClient() {
    return replaceOnClient;
  }

  boolean isOverwritePutIfAbsent() {
    return overwritePutIfAbsent;
  }

  boolean isCacheWrite() {
    return cacheWrite;
  }

  private CacheWriter getCacheWriter() {
    return cacheWriter;
  }

  private Set getNetWriteRecipients() {
    return netWriteRecipients;
  }

  private Object getExpectedOldValue() {
    return expectedOldValue;
  }

  private Object getOldValueForDelta() {
    return oldValueForDelta;
  }

  private void setOldValueForDelta(Object value) {
    oldValueForDelta = value;
  }

  @Override
  protected boolean isOnlyExisting() {
    return onlyExisting;
  }

  @Override
  protected boolean entryExists(RegionEntry regionEntry) {
    return regionEntry != null && !regionEntry.isTombstone();
  }

  @Override
  protected void serializeNewValueIfNeeded() {
    entryEventSerialization.serializeNewValueIfNeeded(getOwner(), getEvent());
  }

  @Override
  protected void runWhileLockedForCacheModification(Runnable r) {
    cacheModificationLock.lockForCacheModification(getOwner(), getEvent());
    try {
      super.runWhileLockedForCacheModification(r);
    } finally {
      cacheModificationLock.releaseCacheModificationLock(getOwner(), getEvent());
    }
  }

  @Override
  protected void setOldValueForDelta() {
    if (isRetrieveOldValueForDelta()) {
      getRegionMap().runWhileEvictionDisabled(() -> {
        setOldValueForDelta(getRegionEntry().getValue(getOwner()));
      });
    }
  }

  @Override
  protected void setOldValueInEvent() {
    final EntryEventImpl event = getEvent();
    final RegionEntry re = getRegionEntry();
    event.setRegionEntry(re);
    if (event.getOperation().guaranteesOldValue()) {
      setOldValueEvenIfFaultedOut();
    } else if (isCacheWrite() || isRequireOldValue()) {
      setOldValueIfNotFaultedOut();
    } else {
      @Unretained
      Object existingValue = re.getValue();
      if (existingValue instanceof GatewaySenderEventImpl) {
        event.setOldValue(existingValue, true);
      }
    }
  }

  private void setOldValueIfNotFaultedOut() {
    final EntryEventImpl event = getEvent();
    ReferenceCountHelper.skipRefCountTracking();
    @Released
    Object oldValueInVM = getRegionEntry().getValueRetain(event.getRegion(), true);
    if (oldValueInVM == null) {
      oldValueInVM = Token.NOT_AVAILABLE;
    }
    ReferenceCountHelper.unskipRefCountTracking();
    try {
      event.setOldValue(oldValueInVM);
    } finally {
      OffHeapHelper.releaseWithNoTracking(oldValueInVM);
    }
  }

  private void setOldValueEvenIfFaultedOut() {
    final EntryEventImpl event = getEvent();
    ReferenceCountHelper.skipRefCountTracking();
    @Released
    Object oldValueInVMOrDisk =
        getRegionEntry().getValueOffHeapOrDiskWithoutFaultIn(event.getRegion());
    ReferenceCountHelper.unskipRefCountTracking();
    try {
      event.setOldValue(oldValueInVMOrDisk, true);
    } finally {
      OffHeapHelper.releaseWithNoTracking(oldValueInVMOrDisk);
    }
  }

  @Override
  protected void unsetOldValueForDelta() {
    OffHeapHelper.release(getOldValueForDelta());
    setOldValueForDelta(null);
    if (isOverwritePutIfAbsent()) {
      getEvent().setOldValue(null);
    }
  }

  @Override
  protected void invokeCacheWriter() {
    final EntryEventImpl event = getEvent();
    if (getOwner().isInitialized() && isCacheWrite()) {
      if (!isReplaceOnClient()) {
        if (getRegionEntry().isDestroyedOrRemoved()
            || (getRegionEntry().isInvalid() && event.getOperation() == Operation.PUT_IF_ABSENT)) {
          event.makeCreate();
        } else {
          event.makeUpdate();
        }
      }
      getOwner().cacheWriteBeforePut(event, getNetWriteRecipients(), getCacheWriter(),
          isRequireOldValue(), getExpectedOldValue());
    }
    if (!getOwner().isInitialized() && !isCacheWrite()) {
      event.oldValueNotAvailable();
    }
  }

  @Override
  protected void createOrUpdateEntry() {
    try {
      if (isUpdate()) {
        updateEntry();
      } else {
        createEntry();
      }
    } catch (RegionClearedException rce) {
      setClearOccurred(true);
    } catch (ConcurrentCacheModificationException ccme) {
      final EntryEventImpl event = getEvent();
      VersionTag tag = event.getVersionTag();
      if (tag != null && tag.isTimeStampUpdated()) {
        getOwner().notifyTimestampsToGateways(event);
      }
      throw ccme;
    }
  }

  @Override
  protected void doBeforeCompletionActions() {
    final EntryEventImpl event = getEvent();
    getOwner().recordEvent(event);
    if (!isOwnerInitialized()) {
      event.inhibitCacheListenerNotification(true);
    }
    updateLru();

    final RegionEntry re = getRegionEntry();
    long lastModTime = getOwner().basicPutPart2(event, re, isOwnerInitialized(),
        getLastModifiedTime(), isClearOccurred());
    setLastModifiedTime(lastModTime);
  }

  private void updateLru() {
    if (!isClearOccurred()) {
      if (getEvent().getOperation().isCreate()) {
        getRegionMap().lruEntryCreate(getRegionEntry());
      } else {
        getRegionMap().lruEntryUpdate(getRegionEntry());
      }
    }
  }

  @Override
  protected boolean shouldCreatedEntryBeRemoved() {
    return getRegionEntry().getValueAsToken() == Token.REMOVED_PHASE1;
  }

  @Override
  protected void doAfterCompletionActions(boolean disabledEviction) {
    try {
      if (isCompleted()) {
        final boolean invokeListeners = getEvent().basicGetNewValue() != Token.TOMBSTONE;
        getOwner().basicPutPart3(getEvent(), getRegionEntry(), isOwnerInitialized(),
            getLastModifiedTime(), invokeListeners, isIfNew(), isIfOld(), getExpectedOldValue(),
            isRequireOldValue());
      }
    } finally {
      finishEviction(disabledEviction);
    }
  }

  private void finishEviction(boolean disabledEviction) {
    if (disabledEviction) {
      getRegionMap().enableLruUpdateCallback();
      if (isCompleted()) {
        lruUpdateCallbackIfNotCleared();
      } else {
        getRegionMap().resetThreadLocals();
      }
    }
  }

  private void lruUpdateCallbackIfNotCleared() {
    if (!isClearOccurred()) {
      try {
        getRegionMap().lruUpdateCallback();
      } catch (DiskAccessException dae) {
        getOwner().handleDiskAccessException(dae);
        throw dae;
      }
    }
  }

  private boolean isUpdate() {
    if (isCacheWrite() && getEvent().getOperation().isUpdate()) {
      // if there is a cacheWriter, type of event has already been set
      return true;
    }
    if (isReplaceOnClient()) {
      return true;
    }
    if (getRegionEntry().isInvalid() && getEvent().getOperation() == Operation.PUT_IF_ABSENT) {
      return false;
    }
    return !getRegionEntry().isRemoved();
  }

  /**
   * @return false if preconditions indicate that
   *         the put should not be done.
   */
  @Override
  protected boolean checkPreconditions() {
    if (!checkUpdatePreconditions()) {
      return false;
    }
    if (!checkUninitializedRegionPreconditions()) {
      return false;
    }
    if (!checkCreatePreconditions()) {
      return false;
    }
    return checkExpectedOldValuePrecondition();
  }

  private boolean checkUpdatePreconditions() {
    if (isIfOld()) {
      final EntryEventImpl event = getEvent();
      final RegionEntry re = getRegionEntry();
      // only update, so just do tombstone maintainence and exit
      if (re.isTombstone() && event.getVersionTag() != null) {
        // refresh the tombstone so it doesn't time out too soon
        getRegionMap().processVersionTag(re, event);
        try {
          re.setValue(getOwner(), Token.TOMBSTONE);
        } catch (RegionClearedException e) {
          // that's okay - when writing a tombstone into a disk, the
          // region has been cleared (including this tombstone)
        }
        getOwner().rescheduleTombstone(re, re.getVersionStamp().asVersionTag());
        return false;
      }
      return !re.isRemoved() || isReplaceOnClient();
    }
    return true;
  }

  private boolean checkUninitializedRegionPreconditions() {
    if (!getOwner().isInitialized()) {
      if (!isOverwriteDestroyed()) {
        Token oldValueInVM = getRegionEntry().getValueAsToken();
        if (oldValueInVM == Token.DESTROYED || oldValueInVM == Token.TOMBSTONE) {
          getEvent().setOldValueDestroyedToken();
          return false;
        }
      }
    }
    return true;
  }

  private boolean checkCreatePreconditions() {
    if (isIfNew()) {
      if (!getRegionEntry().isDestroyedOrRemoved()) {
        // retain the version stamp of the existing entry for use in processing failures
        EntryEventImpl event = getEvent();
        if (getOwner().getConcurrencyChecksEnabled() &&
            event.getOperation() == Operation.PUT_IF_ABSENT &&
            event.isPossibleDuplicate()) {
          Object retainedValue = getRegionEntry().getValueRetain(getOwner());
          try {
            if (isSameValueAlreadyInCacheForPutIfAbsent(retainedValue)) {
              if (logger.isDebugEnabled()) {
                logger.debug("retried putIfAbsent found same value already in cache "
                    + "- allowing the operation.  entry={}; event={}", getRegionEntry(),
                    getEvent());
              }
              overwritePutIfAbsent = true;
              return true;
            }
          } finally {
            OffHeapHelper.release(retainedValue);
          }
        }
        return false;
      }
    }
    return true;
  }

  private boolean isSameValueAlreadyInCacheForPutIfAbsent(Object retainedValue) {
    Object rawNewValue = getEvent().getRawNewValue();
    if (Token.isInvalid(retainedValue)) {
      return rawNewValue == null || Token.isInvalid(rawNewValue);
    }

    return ValueComparisonHelper.checkEquals(retainedValue, rawNewValue,
        isCompressedOffHeap(getEvent()), getOwner().getCache());
  }


  private boolean isCompressedOffHeap(EntryEventImpl event) {
    return event.getRegion().getAttributes().getOffHeap()
        && event.getRegion().getAttributes().getCompressor() != null;
  }

  private boolean checkExpectedOldValuePrecondition() {
    // replace is propagated to server, so no need to check
    // satisfiesOldValue on client
    final EntryEventImpl event = getEvent();
    if (getExpectedOldValue() != null && !isReplaceOnClient()) {
      assert event.getOperation().guaranteesOldValue();
      // We already called setOldValueInEvent so the event will have the old value.
      @Unretained
      Object v = event.getRawOldValue();
      // Note that v will be null instead of INVALID because setOldValue`
      // converts INVALID to null.
      // But checkExpectedOldValue handle this and says INVALID equals null.
      return AbstractRegionEntry.checkExpectedOldValue(getExpectedOldValue(), v, event.getRegion());
    }
    return true;
  }

  private void createEntry() throws RegionClearedException {
    final EntryEventImpl event = getEvent();
    final RegionEntry re = getRegionEntry();
    final boolean wasTombstone = re.isTombstone();
    getRegionMap().processVersionTag(re, event);
    event.putNewEntry(getOwner(), re);
    updateSize(0, false, wasTombstone);
    if (!event.getRegion().isInitialized()) {
      getOwner().getImageState().removeDestroyedEntry(event.getKey());
    }
  }

  private void updateEntry() throws RegionClearedException {
    final EntryEventImpl event = getEvent();
    final RegionEntry re = getRegionEntry();
    final boolean wasTombstone = re.isTombstone();
    final int oldSize = event.getRegion().calculateRegionEntryValueSize(re);
    getRegionMap().processVersionTag(re, event);
    event.putExistingEntry(event.getRegion(), re, isRequireOldValue(), getOldValueForDelta());
    EntryLogger.logPut(event);
    updateSize(oldSize, true/* isUpdate */, wasTombstone);
  }

  private void updateSize(int oldSize, boolean isUpdate, boolean wasTombstone) {
    final EntryEventImpl event = getEvent();
    final Object key = event.getKey();
    final int newBucketSize = event.getNewValueBucketSize();
    if (isUpdate && !wasTombstone) {
      getOwner().updateSizeOnPut(key, oldSize, newBucketSize);
    } else {
      getOwner().updateSizeOnCreate(key, newBucketSize);
      if (!wasTombstone) {
        getOwner().getCachePerfStats().incEntryCount(1);
      }
    }
  }

}
