package org.infinispan.util;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Predicate;

import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.partitionhandling.AvailabilityMode;
import org.infinispan.remoting.transport.Address;
import org.infinispan.test.TestingUtil;
import org.infinispan.topology.CacheJoinInfo;
import org.infinispan.topology.CacheStatusResponse;
import org.infinispan.topology.CacheTopology;
import org.infinispan.topology.ClusterCacheStatus;
import org.infinispan.topology.ClusterTopologyManager;
import org.infinispan.topology.RebalancingStatus;
import org.infinispan.util.logging.Log;
import org.infinispan.util.logging.LogFactory;

public class BlockingClusterTopologyManager implements ClusterTopologyManager {
   private static final Log log = LogFactory.getLog(BlockingClusterTopologyManager.class);

   private final ClusterTopologyManager delegate;
   private final CopyOnWriteArrayList<Handle<CacheTopology>> topologyUpdates = new CopyOnWriteArrayList<>();
   private final CopyOnWriteArrayList<Handle<Integer>> topologyConfirmations = new CopyOnWriteArrayList<>();

   public static BlockingClusterTopologyManager replace(EmbeddedCacheManager cacheManager) {
      ClusterTopologyManager original = TestingUtil.extractGlobalComponent(cacheManager, ClusterTopologyManager.class);
      BlockingClusterTopologyManager bctm = new BlockingClusterTopologyManager(original);
      TestingUtil.replaceComponent(cacheManager, ClusterTopologyManager.class, bctm, true);

      try {
         Field cacheStatusMapField = original.getClass().getDeclaredField("cacheStatusMap");
         cacheStatusMapField.setAccessible(true);
         ConcurrentMap<String, ClusterCacheStatus> cacheStatusMap = (ConcurrentMap<String, ClusterCacheStatus>) cacheStatusMapField.get(original);

         Field clusterTopologyManagerField = ClusterCacheStatus.class.getDeclaredField("clusterTopologyManager");
         Field modifiers = Field.class.getDeclaredField("modifiers");
         modifiers.setAccessible(true);
         modifiers.setInt(clusterTopologyManagerField, clusterTopologyManagerField.getModifiers() & ~Modifier.FINAL);
         clusterTopologyManagerField.setAccessible(true);

         for (ClusterCacheStatus status : cacheStatusMap.values()) {
            clusterTopologyManagerField.set(status, bctm);
         }
      } catch (Exception e) {
         new IllegalStateException(e);
      }
      return bctm;
   }

   protected BlockingClusterTopologyManager(ClusterTopologyManager delegate) {
      this.delegate = delegate;
   }

   @Override
   public CacheStatusResponse handleJoin(String cacheName, Address joiner, CacheJoinInfo joinInfo, int viewId) throws Exception {
      return delegate.handleJoin(cacheName, joiner, joinInfo, viewId);
   }

   @Override
   public void handleLeave(String cacheName, Address leaver, int viewId) throws Exception {
      delegate.handleLeave(cacheName, leaver, viewId);
   }

   public Handle<Integer> startBlockingTopologyConfirmations(Predicate<Integer> condition) {
      Handle<Integer> handle = new Handle<>(condition);
      topologyConfirmations.add(handle);
      return handle;
   }

   @Override
   public void handleRebalancePhaseConfirm(String cacheName, Address node, int topologyId, Throwable throwable, int viewId) throws Exception {
      for (Handle<Integer> h : topologyConfirmations) {
         if (h.condition.test(topologyId)) {
            h.latch.blockIfNeeded();
         }
      }
      delegate.handleRebalancePhaseConfirm(cacheName, node, topologyId, throwable, viewId);
   }

   @Override
   public void broadcastRebalanceStart(String cacheName, CacheTopology cacheTopology, boolean totalOrder, boolean distributed) {
      delegate.broadcastRebalanceStart(cacheName, cacheTopology, totalOrder, distributed);
   }

   public Handle<CacheTopology> startBlockingTopologyUpdate(Predicate<CacheTopology> condition) {
      Handle<CacheTopology> handle = new Handle<>(condition);
      topologyUpdates.add(handle);
      return handle;
   }

   @Override
   public void broadcastTopologyUpdate(String cacheName, CacheTopology cacheTopology, AvailabilityMode availabilityMode, boolean totalOrder, boolean distributed) {
      for (Handle<CacheTopology> h : topologyUpdates) {
         if (h.condition.test(cacheTopology)) {
            log.tracef("Block if needed %s", cacheTopology);
            h.latch.blockIfNeeded();
         }
      }
      delegate.broadcastTopologyUpdate(cacheName, cacheTopology, availabilityMode, totalOrder, distributed);
   }

   @Override
   public void broadcastStableTopologyUpdate(String cacheName, CacheTopology cacheTopology, boolean totalOrder, boolean distributed) {
      delegate.broadcastStableTopologyUpdate(cacheName, cacheTopology, totalOrder, distributed);
   }

   @Override
   public boolean isRebalancingEnabled() {
      return delegate.isRebalancingEnabled();
   }

   @Override
   public boolean isRebalancingEnabled(String cacheName) {
      return delegate.isRebalancingEnabled(cacheName);
   }

   @Override
   public void setRebalancingEnabled(boolean enabled) {
      delegate.setRebalancingEnabled(enabled);
   }

   @Override
   public void setRebalancingEnabled(String cacheName, boolean enabled) {
      delegate.setRebalancingEnabled(cacheName, enabled);
   }

   @Override
   public RebalancingStatus getRebalancingStatus(String cacheName) {
      return delegate.getRebalancingStatus(cacheName);
   }

   @Override
   public void forceRebalance(String cacheName) {
      delegate.forceRebalance(cacheName);
   }

   @Override
   public void forceAvailabilityMode(String cacheName, AvailabilityMode availabilityMode) {
      delegate.forceAvailabilityMode(cacheName, availabilityMode);
   }

   @Override
   public void handleShutdownRequest(String cacheName) throws Exception {
      delegate.handleShutdownRequest(cacheName);
   }

   @Override
   public void broadcastShutdownCache(String cacheName, CacheTopology currentTopology, boolean totalOrder, boolean distributed) throws Exception {
      delegate.broadcastShutdownCache(cacheName, currentTopology, totalOrder, distributed);
   }

   public class Handle<T> {
      private final Predicate<T> condition;
      private final NotifierLatch latch = new NotifierLatch();

      public Handle(Predicate<T> condition) {
         this.condition = condition;
         latch.startBlocking();
      }

      public void stopBlocking() {
         log.tracef("Stop blocking");
         latch.stopBlocking();
         topologyUpdates.remove(this);
      }

      public void waitToBlock() throws InterruptedException {
         log.tracef("Waiting to block");
         latch.waitToBlock();
      }

      public void unblockOnce() {
         latch.unblockOnce();
      }
   }
}
