/*
 * 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 org.apache.logging.log4j.Logger;

import org.apache.geode.cache.DiskAccessException;
import org.apache.geode.cache.query.internal.index.IndexManager;
import org.apache.geode.internal.cache.EntryEventImpl;
import org.apache.geode.internal.cache.InternalRegion;
import org.apache.geode.internal.cache.RegionEntry;
import org.apache.geode.internal.cache.Token;
import org.apache.geode.logging.internal.log4j.api.LogService;

public abstract class AbstractRegionMapPut {
  private static final Logger logger = LogService.getLogger();

  private final InternalRegion owner;
  private final FocusedRegionMap focusedRegionMap;
  private final EntryEventImpl event;
  private final boolean ownerInitialized;

  private boolean clearOccurred;
  private long lastModifiedTime;
  private RegionEntry regionEntry;
  private boolean create;
  private boolean completed;

  public AbstractRegionMapPut(FocusedRegionMap focusedRegionMap, InternalRegion owner,
      EntryEventImpl event) {
    this.focusedRegionMap = focusedRegionMap;
    this.owner = owner;
    this.event = event;
    ownerInitialized = owner.isInitialized();
  }

  protected boolean isOwnerInitialized() {
    return ownerInitialized;
  }

  protected boolean isClearOccurred() {
    return clearOccurred;
  }

  protected void setClearOccurred(boolean v) {
    clearOccurred = v;
  }

  protected long getLastModifiedTime() {
    return lastModifiedTime;
  }

  protected void setLastModifiedTime(long v) {
    lastModifiedTime = v;
  }

  protected RegionEntry getRegionEntry() {
    return regionEntry;
  }

  private void setRegionEntry(RegionEntry v) {
    regionEntry = v;
  }

  /**
   * @return true if put created a new entry; false if it updated an existing one.
   */
  protected boolean isCreate() {
    return create;
  }

  private void setCreate(boolean v) {
    create = v;
  }

  protected EntryEventImpl getEvent() {
    return event;
  }

  protected boolean isCompleted() {
    return completed;
  }

  private void setCompleted(boolean b) {
    completed = b;
  }

  protected InternalRegion getOwner() {
    return owner;
  }

  protected FocusedRegionMap getRegionMap() {
    return focusedRegionMap;
  }

  protected abstract boolean isOnlyExisting();

  protected abstract boolean entryExists(RegionEntry regionEntry);

  protected abstract void serializeNewValueIfNeeded();

  protected void runWhileLockedForCacheModification(Runnable r) {
    final boolean locked = getOwner().lockWhenRegionIsInitializing();
    try {
      r.run();
    } finally {
      if (locked) {
        getOwner().unlockWhenRegionIsInitializing();
      }
    }
  }

  protected abstract void setOldValueForDelta();

  protected abstract void setOldValueInEvent();

  protected abstract void unsetOldValueForDelta();

  protected abstract boolean checkPreconditions();

  protected abstract void invokeCacheWriter();

  protected abstract void createOrUpdateEntry();

  /**
   * Returns true if getRegionEntry should be removed from the map
   * because the put did not complete.
   * Precondition: isCreate()
   */
  protected abstract boolean shouldCreatedEntryBeRemoved();

  /**
   * Called after the put is done but before setCompleted(true)
   * is called.
   * Note that the RegionEntry that was modified by the put
   * is still synchronized when this is called.
   */
  protected abstract void doBeforeCompletionActions();

  /**
   * Called after the put is done.
   * Always called, even if the put failed.
   * Note that the RegionEntry that was modified by the put
   * is no longer synchronized when this is called.
   *
   * @param disabledEviction true if caller previously disabled eviction
   */
  protected abstract void doAfterCompletionActions(boolean disabledEviction);

  /**
   * @return regionEntry if put completed, otherwise null.
   */
  public RegionEntry put() {
    serializeNewValueIfNeeded();
    runWhileLockedForCacheModification(this::doPut);
    if (isCompleted()) {
      return getRegionEntry();
    } else {
      return null;
    }
  }

  private void doPut() {
    final boolean disabledEviction = getRegionMap().disableLruUpdateCallback();
    try {
      doWithIndexInUpdateMode(this::doPutRetryingIfNeeded);
    } catch (DiskAccessException dae) {
      getOwner().handleDiskAccessException(dae);
      throw dae;
    } finally {
      doAfterCompletionActions(disabledEviction);
    }
  }

  private void doWithIndexInUpdateMode(Runnable r) {
    final IndexManager oqlIndexManager = getInitializedIndexManager();
    if (oqlIndexManager != null) {
      try {
        r.run();
      } finally {
        oqlIndexManager.countDownIndexUpdaters();
      }
    } else {
      r.run();
    }
  }

  private IndexManager getInitializedIndexManager() {
    final IndexManager oqlIndexManager = getOwner().getIndexManager();
    if (oqlIndexManager != null) {
      oqlIndexManager.waitForIndexInit();
    }
    return oqlIndexManager;
  }

  private void doPutRetryingIfNeeded() {
    do {
      if (!findAndSaveExistingEntry()) {
        return;
      }
      createNewEntryIfNeeded();
    } while (!addRegionEntryToMapAndDoPut());
  }

  /**
   * If an existing one is found, save it by calling setRegionEntry.
   *
   * @return false if an existing entry was not found and this put requires
   *         an existing one; otherwise returns true.
   */
  private boolean findAndSaveExistingEntry() {
    RegionEntry re = getRegionMap().getEntry(getEvent());
    if (isOnlyExisting() && !entryExists(re)) {
      setRegionEntry(null);
      return false;
    }
    setRegionEntry(re);
    return true;
  }

  private void createNewEntryIfNeeded() {
    setCreate(getRegionEntry() == null);
    if (isCreate()) {
      final Object key = getEvent().getKey();
      RegionEntry newEntry =
          getRegionMap().getEntryFactory().createEntry(getOwner(), key, Token.REMOVED_PHASE1);
      setRegionEntry(newEntry);
    }
  }

  /**
   * @return false if caller should retry
   */
  private boolean addRegionEntryToMapAndDoPut() {
    synchronized (getRegionEntry()) {
      putIfAbsentNewEntry();
      return doPutOnRegionEntryInMap();
    }
  }

  private void putIfAbsentNewEntry() {
    if (isCreate()) {
      RegionEntry oldRe = getRegionMap().putEntryIfAbsent(getEvent().getKey(), getRegionEntry());
      if (oldRe != null) {
        setCreate(false);
        setRegionEntry(oldRe);
      }
    }
  }

  /**
   * @return false if caller should retry because entry is no longer in the map
   */
  private boolean doPutOnRegionEntryInMap() {
    synchronized (getRegionEntry()) {
      if (isRegionEntryRemoved()) {
        return false;
      }
      doPutOnSynchronizedRegionEntry();
      return true;
    }
  }

  private void doPutOnSynchronizedRegionEntry() {
    setOldValueForDelta();
    try {
      setOldValueInEvent();
      doPutIfPreconditionsSatisified();
    } finally {
      unsetOldValueForDelta();
      if (isCreate() && shouldCreatedEntryBeRemoved()) {
        getRegionMap().removeEntry(getEvent().getKey(), getRegionEntry(), false);
      }
    }
  }

  private void doPutIfPreconditionsSatisified() {
    if (!checkPreconditions()) {
      return;
    }
    invokeCacheWriter();
    runWithIndexUpdatingInProgress(this::doPutAndDeliverEvent);
  }

  private void doPutAndDeliverEvent() {
    createOrUpdateEntry();
    doBeforeCompletionActions();
    setCompleted(true);
  }

  private void runWithIndexUpdatingInProgress(Runnable r) {
    notifyIndex(true);
    try {
      r.run();
    } finally {
      notifyIndex(false);
    }
  }

  private void notifyIndex(boolean isUpdating) {
    if (getOwner().getIndexMaintenanceSynchronous()) {
      getRegionEntry().setUpdateInProgress(isUpdating);
    }
  }

  /**
   * @return true if the entry is in the final stage of removal
   */
  private boolean isRegionEntryRemoved() {
    final RegionEntry re = getRegionEntry();
    if (re.isRemovedPhase2()) {
      getOwner().getCachePerfStats().incRetries();
      getRegionMap().getEntryMap().remove(getEvent().getKey(), re);
      return true;
    } else {
      return false;
    }
  }
}
