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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;

import org.apache.logging.log4j.Logger;

import org.apache.geode.cache.persistence.PartitionOfflineException;
import org.apache.geode.distributed.DistributedLockService;
import org.apache.geode.distributed.internal.ReplyException;
import org.apache.geode.distributed.internal.membership.InternalDistributedMember;
import org.apache.geode.internal.cache.PartitionedRegion.BucketLock;
import org.apache.geode.internal.cache.partitioned.RedundancyAlreadyMetException;
import org.apache.geode.internal.cache.persistence.PersistenceAdvisorImpl;
import org.apache.geode.internal.cache.persistence.PersistentMemberID;
import org.apache.geode.internal.cache.persistence.PersistentMemberManager;
import org.apache.geode.internal.cache.persistence.PersistentMemberView;
import org.apache.geode.internal.cache.persistence.PersistentStateListener.PersistentStateAdapter;
import org.apache.geode.internal.i18n.LocalizedStrings;
import org.apache.geode.internal.logging.LogService;
import org.apache.geode.internal.logging.log4j.LocalizedMessage;
import org.apache.geode.internal.logging.log4j.LogMarker;
import org.apache.geode.internal.util.TransformUtils;

/**
 *
 */
public class BucketPersistenceAdvisor extends PersistenceAdvisorImpl {

  private static final Logger logger = LogService.getLogger();

  public CountDownLatch someMemberRecoveredLatch = new CountDownLatch(1);
  public boolean recovering = true;
  private boolean atomicCreation;
  private final BucketLock bucketLock;
  /**
   * A listener to watch for removes during the recovery process. After recovery is done, we know
   * longer need to worry about removes.
   */
  private final RecoveryListener recoveryListener;
  private final ProxyBucketRegion proxyBucket;
  private short version;
  private RuntimeException recoveryException;

  public BucketPersistenceAdvisor(CacheDistributionAdvisor advisor, DistributedLockService dl,
      PersistentMemberView storage, String regionPath, DiskRegionStats diskStats,
      PersistentMemberManager memberManager, BucketLock bucketLock,
      ProxyBucketRegion proxyBucketRegion) {
    super(advisor, dl, storage, regionPath, diskStats, memberManager);
    this.bucketLock = bucketLock;

    recoveryListener = new RecoveryListener();
    this.proxyBucket = proxyBucketRegion;
    addListener(recoveryListener);
  }

  public void recoveryDone(RuntimeException e) {
    this.recovering = false;
    if (!getPersistedMembers().isEmpty()) {
      ((BucketAdvisor) advisor).setHadPrimary();
    }
    // Make sure any removes that we saw during recovery are
    // applied.
    removeListener(recoveryListener);
    for (PersistentMemberID id : recoveryListener.getRemovedMembers()) {
      removeMember(id);
    }
    if (someMemberRecoveredLatch.getCount() > 0) {
      this.recoveryException = e;
      this.someMemberRecoveredLatch.countDown();
    } else if (recoveryException != null) {
      logger.fatal(LocalizedMessage.create(
          LocalizedStrings.BucketPersistenceAdvisor_ERROR_RECOVERYING_SECONDARY_BUCKET_0,
          new Object[] {proxyBucket.getPartitionedRegion().getFullPath(),
              proxyBucket.getBucketId()}),
          e);
    }
  }

  protected void checkInterruptedByShutdownAll() {
    // when ShutdownAll is on-going, break all the GII for BR
    if (proxyBucket.getCache().isCacheAtShutdownAll()) {
      throw proxyBucket.getCache().getCacheClosedException("Cache is being closed by ShutdownAll");
    }
    proxyBucket.getPartitionedRegion().checkReadiness();
  }

  public boolean isRecovering() {
    return this.recovering;
  }

  @Override
  protected void beginWaitingForMembershipChange(Set<PersistentMemberID> membersToWaitFor) {
    if (recovering) {
      bucketLock.unlock();
    } else {
      if (membersToWaitFor != null && !membersToWaitFor.isEmpty()) {
        String message = LocalizedStrings.PartitionedRegionDataStore_DATA_OFFLINE_MESSAGE
            .toLocalizedString(proxyBucket.getPartitionedRegion().getFullPath(),
                proxyBucket.getBucketId(), membersToWaitFor);
        throw new PartitionOfflineException((Set) membersToWaitFor, message);
      }
    }
  }

  @Override
  protected void logWaitingForMember(Set<PersistentMemberID> allMembersToWaitFor,
      Set<PersistentMemberID> offlineMembersToWaitFor) {
    // We only log the bucket level information at fine level.
    if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
      Set<String> membersToWaitForPrettyFormat = new HashSet<String>();

      if (offlineMembersToWaitFor != null && !offlineMembersToWaitFor.isEmpty()) {
        TransformUtils.transform(offlineMembersToWaitFor, membersToWaitForPrettyFormat,
            TransformUtils.persistentMemberIdToLogEntryTransformer);
        logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE, LocalizedMessage.create(
            LocalizedStrings.BucketPersistenceAdvisor_WAITING_FOR_LATEST_MEMBER,
            new Object[] {proxyBucket.getPartitionedRegion().getFullPath(),
                proxyBucket.getBucketId(),
                TransformUtils.persistentMemberIdToLogEntryTransformer.transform(getPersistentID()),
                membersToWaitForPrettyFormat}));
      } else {
        TransformUtils.transform(allMembersToWaitFor, membersToWaitForPrettyFormat,
            TransformUtils.persistentMemberIdToLogEntryTransformer);
        if (logger.isDebugEnabled()) {
          logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
              "All persistent members being waited on are online, but they have not yet initialized");
        }
        logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE, LocalizedMessage.create(
            LocalizedStrings.BucketPersistenceAdvisor_WAITING_FOR_LATEST_MEMBER,
            new Object[] {proxyBucket.getPartitionedRegion().getFullPath(),
                proxyBucket.getBucketId(),
                TransformUtils.persistentMemberIdToLogEntryTransformer.transform(getPersistentID()),
                membersToWaitForPrettyFormat}));
      }
    }
  }

  @Override
  protected void endWaitingForMembershipChange() {
    if (recovering) {
      bucketLock.lock();
      // We allow regions with persistent colocated children to exceed redundancy
      // so that we can create the child bucket. Otherwise, we need to check
      // that redundancy has not already been met now that we've got the dlock
      // the delock.
      if (!proxyBucket.hasPersistentChildRegion()
          && !proxyBucket.checkBucketRedundancyBeforeGrab(null, false)) {
        if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
          logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
              "{}-{}: After reacquiring dlock, we detected that redundancy is already satisfied",
              shortDiskStoreId(), regionPath);
        }
        // Remove the persistent data for this bucket, since
        // redundancy is already satisfied.
        proxyBucket.destroyOfflineData();
        throw new RedundancyAlreadyMetException();
      }
    }
  }

  @Override
  public void updateMembershipView(InternalDistributedMember replicate,
      boolean targetReinitializing) {
    if (recovering) {
      super.updateMembershipView(replicate, targetReinitializing);
      someMemberRecoveredLatch.countDown();
    } else {
      // don't update the membership view, we already updated it during recovery.
    }
  }

  /**
   * Wait for there to be initialized copies of this bucket. Get the latest membership view from
   * those copies.
   *
   */
  public void initializeMembershipView() {
    MembershipChangeListener listener = new MembershipChangeListener();
    addListener(listener);
    boolean interrupted = false;
    try {
      while (!isClosed) {
        advisor.getAdvisee().getCancelCriterion().checkCancelInProgress(null);

        // Look for any online copies of the bucket.
        // If there are any, get a membership view from them.
        Map<InternalDistributedMember, PersistentMemberID> onlineMembers =
            advisor.adviseInitializedPersistentMembers();
        if (onlineMembers != null) {
          if (updateMembershipView(onlineMembers.keySet())) {
            break;
          }
        }

        Set<InternalDistributedMember> postRecoveryMembers =
            ((BucketAdvisor) advisor).adviseRecoveredFromDisk();
        if (postRecoveryMembers != null) {
          if (updateMembershipView(postRecoveryMembers)) {
            break;
          }
        }

        Set<PersistentMemberID> membersToWaitFor = getPersistedMembers();

        if (!membersToWaitFor.isEmpty()) {
          try {
            listener.waitForChange(membersToWaitFor, membersToWaitFor);
          } catch (InterruptedException e) {
            interrupted = true;
          }
        } else {
          beginUpdatingPersistentView();
          break;
        }
      }
    } finally {
      removeListener(listener);
      if (interrupted) {
        Thread.currentThread().interrupt();
      }
    }
  }

  private boolean updateMembershipView(Collection<InternalDistributedMember> targets) {
    for (InternalDistributedMember target : targets) {
      try {
        updateMembershipView(target, false);
        return true;
      } catch (ReplyException e) {
        // The member left?
        if (logger.isDebugEnabled()) {
          logger.debug("Received a reply exception trying to update membership view", e);
        }
      }
    }
    return false;
  }

  public void bucketRemoved() {
    this.resetState();
  }

  public boolean acquireTieLock() {
    // We don't actually need to get a dlock here for PRs, we're already
    // holding the bucket lock when we create a bucket region
    return true;
  }

  public void releaseTieLock() {
    // We don't actually need to get a dlock here for PRs, we're already
    // holding the bucket lock when we create a bucket region
  }


  @Override
  protected String getRegionPathForOfflineMembers() {
    return proxyBucket.getPartitionedRegion().getFullPath();
  }

  @Override
  public Set<PersistentMemberID> getMissingMembers() {
    if (recovering) {
      return super.getMissingMembers();
    } else {
      Set<PersistentMemberID> offlineMembers = getPersistedMembers();
      offlineMembers.removeAll(advisor.advisePersistentMembers().values());
      return offlineMembers;
    }
  }

  @Override
  public PersistentMemberID generatePersistentID() {
    PersistentMemberID id = storage.generatePersistentID();
    if (id == null) {
      return id;
    } else {
      id = new PersistentMemberID(id.diskStoreId, id.host, id.directory,
          this.proxyBucket.getPartitionedRegion().getBirthTime(), version++);
      return id;
    }
  }



  private static class RecoveryListener extends PersistentStateAdapter {
    private Set<PersistentMemberID> removedMembers =
        Collections.synchronizedSet(new HashSet<PersistentMemberID>());

    @Override
    public void memberRemoved(PersistentMemberID persistentID, boolean revoked) {
      this.removedMembers.add(persistentID);
    }

    public HashSet<PersistentMemberID> getRemovedMembers() {
      synchronized (removedMembers) {
        return new HashSet<PersistentMemberID>(removedMembers);
      }
    }
  }

  /**
   * Callers should have already verified that debug output is enabled.
   *
   * @param infoMsg
   */
  public void dump(String infoMsg) {
    storage.getOnlineMembers();
    storage.getOfflineMembers();
    storage.getOfflineAndEqualMembers();
    storage.getMyInitializingID();
    storage.getMyPersistentID();
    final StringBuilder buf = new StringBuilder(2000);
    if (infoMsg != null) {
      buf.append(infoMsg);
      buf.append(": ");
    }
    buf.append("\nMY PERSISTENT ID:\n");
    buf.append(storage.getMyPersistentID());
    buf.append("\nMY INITIALIZING ID:\n");
    buf.append(storage.getMyInitializingID());

    buf.append("\nONLINE MEMBERS:\n");
    for (PersistentMemberID id : storage.getOnlineMembers()) {
      buf.append("\t");
      buf.append(id);
      buf.append("\n");
    }

    buf.append("\nOFFLINE MEMBERS:\n");
    for (PersistentMemberID id : storage.getOfflineMembers()) {
      buf.append("\t");
      buf.append(id);
      buf.append("\n");
    }

    buf.append("\nOFFLINE AND EQUAL MEMBERS:\n");
    for (PersistentMemberID id : storage.getOfflineAndEqualMembers()) {
      buf.append("\t");
      buf.append(id);
      buf.append("\n");
    }
    logger.debug(buf.toString());
  }

  /**
   * Wait for this bucket to be recovered from disk, at least to the point where it starts doing a
   * GII.
   *
   * This method will throw an exception if the recovery thread encountered an exception.
   */
  public void waitForPrimaryPersistentRecovery() {
    boolean interupted = false;
    while (true) {
      try {
        someMemberRecoveredLatch.await();
        break;
      } catch (InterruptedException e) {
        interupted = true;
      }
    }

    if (interupted) {
      Thread.currentThread().interrupt();
    }

    if (recoveryException != null) {
      StackTraceElement[] oldStack = recoveryException.getStackTrace();
      recoveryException.fillInStackTrace();
      ArrayList<StackTraceElement> newStack = new ArrayList<StackTraceElement>();
      newStack.addAll(Arrays.asList(oldStack));
      newStack.addAll(Arrays.asList(recoveryException.getStackTrace()));

      recoveryException.setStackTrace(newStack.toArray(new StackTraceElement[0]));
      throw recoveryException;
    }
  }



  /**
   * Overridden to fix bug 41336. We defer initialization of this member until after the atomic
   * bucket creation phase is over.
   */
  @Override
  public void setInitializing(PersistentMemberID newId) {
    if (atomicCreation) {
      if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
        logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
            "{}-{}: {} Deferring setInitializing until the EndBucketCreation phase for {}",
            shortDiskStoreId(), regionPath, regionPath, newId);
      }
    } else {
      super.setInitializing(newId);
    }
  }

  /**
   * Overridden to fix bug 41336. We defer initialization of this member until after the atomic
   * bucket creation phase is over.
   */
  @Override
  public void setOnline(boolean didGII, boolean wasAtomicCreation, PersistentMemberID newId)
      throws ReplyException {
    // This is slightly confusing. If we're currently in the middle of an atomic
    // creation, we will do nothing right now. Later, when endBucketCreation
    // is called, we will pass the "wasAtomicCreation" flag down to the super
    // class to ensure that it knows its coming online as part of an atomic creation.
    if (this.atomicCreation) {
      if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
        logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
            "{}-{}: {} Deferring setOnline until the EndBucketCreation phase for {}",
            shortDiskStoreId(), regionPath, regionPath, newId);
      }
    } else {
      super.setOnline(didGII, wasAtomicCreation, newId);
    }
  }

  /**
   * Finish the atomic creation of this bucket on multiple members This method is called with the
   * proxy bucket synchronized.
   */
  public void endBucketCreation(PersistentMemberID newId) {
    synchronized (lock) {
      if (!atomicCreation) {
        if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
          logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
              "{}-{}: {} In endBucketCreation - already online, skipping (possible concurrent endBucketCreation)",
              shortDiskStoreId(), regionPath, regionPath);
        }
        return;
      }

      if (logger.isDebugEnabled(LogMarker.PERSIST_ADVISOR_VERBOSE)) {
        logger.debug(LogMarker.PERSIST_ADVISOR_VERBOSE,
            "{}-{}: {} In endBucketCreation - now persisting the id {}", shortDiskStoreId(),
            regionPath, regionPath, newId);
      }
      atomicCreation = false;
    }
    super.setOnline(false, true, newId);
  }

  public boolean isAtomicCreation() {
    return this.atomicCreation;
  }

  public void setAtomicCreation(boolean atomicCreation) {
    if (getPersistentID() != null) {
      return;
    }
    synchronized (lock) {
      this.atomicCreation = atomicCreation;
    }
  }

  private BucketPersistenceAdvisor getColocatedPersistenceAdvisor() {
    PartitionedRegion colocatedRegion =
        ColocationHelper.getColocatedRegion(proxyBucket.getPartitionedRegion());
    if (colocatedRegion == null) {
      return null;
    }
    ProxyBucketRegion colocatedProxyBucket =
        colocatedRegion.getRegionAdvisor().getProxyBucketArray()[proxyBucket.getBucketId()];
    return colocatedProxyBucket.getPersistenceAdvisor();
  }
}
