/*
 * 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.List;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.Operation;
import org.apache.geode.cache.TransactionId;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.EnumListenerEvent;
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.TXEntryState;
import org.apache.geode.internal.cache.TXRmtEvent;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.internal.offheap.annotations.Released;
import org.apache.geode.internal.sequencelog.EntryLogger;
import org.apache.geode.logging.internal.log4j.api.LogService;

/**
 * Does a put for a transaction that is being committed.
 */
public class RegionMapCommitPut extends AbstractRegionMapPut {
  private static final Logger logger = LogService.getLogger();

  private final boolean onlyExisting;
  private final boolean didDestroy;
  private final TXRmtEvent txEvent;
  private final List<EntryEventImpl> pendingCallbacks;
  private final TXEntryState txEntryState;
  private final boolean remoteOrigin;
  private final boolean invokeCallbacks;

  private boolean callbackEventInPending;
  private Operation putOp;

  public RegionMapCommitPut(FocusedRegionMap focusedRegionMap, InternalRegion owner,
      @Released EntryEventImpl callbackEvent, Operation putOp, boolean didDestroy,
      TransactionId txId, TXRmtEvent txEvent, List<EntryEventImpl> pendingCallbacks,
      TXEntryState txEntryState) {
    super(focusedRegionMap, owner, callbackEvent);
    this.putOp = putOp;
    this.didDestroy = didDestroy;
    this.txEvent = txEvent;
    this.pendingCallbacks = pendingCallbacks;
    this.txEntryState = txEntryState;
    remoteOrigin = !txId.getMemberId().equals(owner.getMyId());
    invokeCallbacks = shouldInvokeCallbacks();
    final boolean isTXHost = txEntryState != null;
    // If the transaction originated on another member and we do not host the transaction entry
    // and are not a replicate or partitioned (i.e. !isAllEvents)
    // then only apply the update to existing entries.
    // If the transaction originated on another member and we do not host the transaction entry
    // and we are a replicate or partitioned
    // then only apply the update to existing entries when the operation is an update and we
    // are initialized.
    // Otherwise use the standard create/update logic.
    onlyExisting = remoteOrigin && !isTXHost
        && (!owner.isAllEvents() || (!putOp.isCreate() && isOwnerInitialized()));
  }

  boolean isRemoteOrigin() {
    return remoteOrigin;
  }

  boolean isInvokeCallbacks() {
    return invokeCallbacks;
  }

  private Operation getPutOp() {
    return putOp;
  }

  private boolean isPutOpCreate() {
    return getPutOp().isCreate();
  }

  private boolean shouldInvokeCallbacks() {
    InternalRegion owner = getOwner();
    boolean isPartitioned = owner.isUsedForPartitionedRegionBucket();
    if (isPartitioned) {
      owner = owner.getPartitionedRegion();
    }
    return (isPartitioned || isOwnerInitialized()) && (owner.shouldDispatchListenerEvent()
        || owner.shouldNotifyBridgeClients() || owner.getConcurrencyChecksEnabled());
  }

  private void setCallbackEventInPending(boolean v) {
    callbackEventInPending = v;
  }

  boolean isCallbackEventInPending() {
    return callbackEventInPending;
  }

  private void makePutOpCreate() {
    putOp = putOp.getCorrespondingCreateOp();
  }

  private void makePutOpUpdate() {
    putOp = putOp.getCorrespondingUpdateOp();
  }

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

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

  @Override
  protected void serializeNewValueIfNeeded() {
    // nothing needed
  }

  @Override
  protected void setOldValueForDelta() {
    // nothing needed
  }

  @Override
  protected void setOldValueInEvent() {
    if (isCreate()) {
      makePutOpCreate();
    } else {
      if (!getRegionEntry().isDestroyedOrRemoved()) {
        makePutOpUpdate();
      } else {
        makePutOpCreate();
      }
    }
    if (isPutOpCreate()) {
      getEvent().makeCreate();
      getEvent().setOldValue(null);
    } else {
      getEvent().makeUpdate();
      Object oldValue = getRegionEntry().getValueInVM(getOwner());
      getEvent().setOldValue(oldValue);
    }
  }

  @Override
  protected void unsetOldValueForDelta() {
    // nothing needed
  }

  @Override
  protected boolean checkPreconditions() {
    return !isOnlyExisting() || !isPutOpCreate();
  }

  @Override
  protected void invokeCacheWriter() {
    // nothing needed
  }

  @Override
  protected void createOrUpdateEntry() {
    final RegionEntry regionEntry = getRegionEntry();
    final EntryEventImpl callbackEvent = getEvent();
    final InternalRegion owner = getOwner();
    final FocusedRegionMap regionMap = getRegionMap();
    final Object key = callbackEvent.getKey();
    final Object newValue = computeNewValue(callbackEvent);

    final int oldSize = isPutOpCreate() ? 0 : owner.calculateRegionEntryValueSize(regionEntry);
    callbackEvent.setRegionEntry(regionEntry);
    regionMap.txRemoveOldIndexEntry(getPutOp(), regionEntry);
    setLastModifiedTime(owner.cacheTimeMillis());
    if (didDestroy) {
      regionEntry.txDidDestroy(getLastModifiedTime());
    }
    if (txEvent != null) {
      txEvent.addPut(getPutOp(), owner, regionEntry, key, newValue,
          callbackEvent.getCallbackArgument());
    }
    regionEntry.setValueResultOfSearch(getPutOp().isNetSearch());
    try {
      regionMap.processAndGenerateTXVersionTag(callbackEvent, regionEntry, txEntryState);
      setNewValueOnRegionEntry(newValue);
      int newSize = owner.calculateRegionEntryValueSize(regionEntry);
      if (isPutOpCreate()) {
        owner.updateSizeOnCreate(key, newSize);
      } else {
        owner.updateSizeOnPut(key, oldSize, newSize);
      }
    } catch (RegionClearedException rce) {
      setClearOccurred(true);
    }
    EntryLogger.logTXPut(owner, key, newValue);
  }

  private void setNewValueOnRegionEntry(final Object newValue) throws RegionClearedException {
    final RegionEntry regionEntry = getRegionEntry();
    final InternalRegion owner = getOwner();
    final boolean wasTombstone = regionEntry.isTombstone();
    final Object preparedValue =
        regionEntry.prepareValueForCache(owner, newValue, getEvent(), !isPutOpCreate());
    regionEntry.setValue(owner, preparedValue);
    if (wasTombstone) {
      owner.unscheduleTombstone(regionEntry);
    }
  }

  private static Object computeNewValue(EntryEventImpl callbackEvent) {
    Object newValue = callbackEvent.getRawNewValueAsHeapObject();
    if (newValue == null) {
      if (callbackEvent.isLocalInvalid()) {
        newValue = Token.LOCAL_INVALID;
      } else {
        newValue = Token.INVALID;
      }
    }
    return newValue;
  }

  @Override
  protected boolean shouldCreatedEntryBeRemoved() {
    return !isCompleted();
  }

  @Override
  protected void doBeforeCompletionActions() {
    final RegionEntry regionEntry = getRegionEntry();
    final EntryEventImpl callbackEvent = getEvent();
    final InternalRegion owner = getOwner();
    final FocusedRegionMap regionMap = getRegionMap();
    final Object key = callbackEvent.getKey();

    regionEntry.updateStatsForPut(getLastModifiedTime(), getLastModifiedTime());
    owner.txApplyPutPart2(regionEntry, key, getLastModifiedTime(), isPutOpCreate(), didDestroy,
        isClearOccurred());
    if (isInvokeCallbacks()) {
      callbackEvent.changeRegionToBucketsOwner();
      callbackEvent.setOriginRemote(isRemoteOrigin());
      pendingCallbacks.add(callbackEvent);
      setCallbackEventInPending(true);
    }
    if (!isClearOccurred()) {
      if (isCreate()) {
        regionMap.lruEntryCreate(regionEntry);
        regionMap.incEntryCount(1);
      } else {
        regionMap.lruEntryUpdate(regionEntry);
      }
    }
  }

  @Override
  protected void doAfterCompletionActions(boolean disabledEviction) {
    if (isOnlyExisting() && !isCompleted()) {
      if (didDestroy) {
        getOwner().txApplyPutHandleDidDestroy(getEvent().getKey());
      }
      if (isInvokeCallbacks()) {
        getEvent().makeUpdate();
        getOwner().invokeTXCallbacks(EnumListenerEvent.AFTER_UPDATE, getEvent(), false);
      }
    }
    if (getOwner().getConcurrencyChecksEnabled() && txEntryState != null) {
      txEntryState.setVersionTag(getEvent().getVersionTag());
    }
    if (!isCallbackEventInPending()) {
      getEvent().release();
    }
  }

}
