package org.infinispan.query.indexmanager;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;

import org.hibernate.search.backend.FlushLuceneWork;
import org.hibernate.search.backend.IndexingMonitor;
import org.hibernate.search.backend.LuceneWork;
import org.hibernate.search.backend.PurgeAllLuceneWork;
import org.hibernate.search.indexes.spi.IndexManager;
import org.infinispan.commands.ReplicableCommand;
import org.infinispan.commons.util.Util;
import org.infinispan.query.logging.Log;
import org.infinispan.remoting.rpc.RpcManager;
import org.infinispan.remoting.transport.Address;
import org.infinispan.util.ByteString;
import org.infinispan.util.logging.LogFactory;

/**
 * The IndexingBackend which forwards all operations to a different node
 * using Infinispan's custom commands.
 *
 * @author Sanne Grinovero <sanne@hibernate.org> (C) 2014 Red Hat Inc.
 * @since 7.0
 */
final class RemoteIndexingBackend implements IndexingBackend {

   private static final Log log = LogFactory.getLog(RemoteIndexingBackend.class, Log.class);

   private final int GRACE_MILLISECONDS_FOR_REPLACEMENT = 4000;
   private final int POLLING_MILLISECONDS_FOR_REPLACEMENT = 4000;
   protected static final Set<Class<? extends LuceneWork>> SYNC_ONLY_WORKS =
         Util.asSet(PurgeAllLuceneWork.class, FlushLuceneWork.class);

   private final String cacheName;
   private final String indexName;
   private final Collection<Address> recipients;
   private final RpcManager rpcManager;
   private final Address masterAddress;
   private final boolean async;

   private volatile IndexingBackend replacement;

   public RemoteIndexingBackend(String cacheName, RpcManager rpcManager, String indexName, Address masterAddress, boolean async) {
      this.cacheName = cacheName;
      this.rpcManager = rpcManager;
      this.indexName = indexName;
      this.masterAddress = masterAddress;
      this.recipients = Collections.singleton(masterAddress);
      this.async = async;
   }

   @Override
   public void flushAndClose(IndexingBackend replacement) {
      if (replacement != null) {
         this.replacement = replacement;
      }
   }

   @Override
   public void applyWork(List<LuceneWork> workList, IndexingMonitor monitor, IndexManager indexManager) {
      IndexUpdateCommand command = new IndexUpdateCommand(ByteString.fromString(cacheName));
      //Use Search's custom Avro based serializer as it includes support for back/future compatibility
      byte[] serializedModel = indexManager.getSerializer().toSerializedModel(workList);
      command.setSerializedWorkList(serializedModel);
      command.setIndexName(this.indexName);
      try {
         log.applyingChangeListRemotely(workList);
         sendCommand(command, workList, shouldSendSync(workList));
      } catch (Exception e) {
         waitForReplacementBackend();
         if (replacement != null) {
            replacement.applyWork(workList, monitor, indexManager);
         } else {
            throw e;
         }
      }
   }

   /**
    * Decides whether a command should be sent sync or async
    */
   private boolean shouldSendSync(LuceneWork operation) {
      return !async || SYNC_ONLY_WORKS.contains(operation.getClass());
   }

   private boolean shouldSendSync(List<LuceneWork> operations) {
      return !async || (operations.size() == 1 && shouldSendSync(operations.get(0)));
   }


   @Override
   public void applyStreamWork(LuceneWork singleOperation, IndexingMonitor monitor, IndexManager indexManager) {
      final IndexUpdateStreamCommand streamCommand = new IndexUpdateStreamCommand(ByteString.fromString(cacheName));
      final List<LuceneWork> operations = Collections.singletonList(singleOperation);
      final byte[] serializedModel = indexManager.getSerializer().toSerializedModel(operations);
      streamCommand.setSerializedWorkList(serializedModel);
      streamCommand.setIndexName(this.indexName);
      try {
         sendCommand(streamCommand, operations, shouldSendSync(singleOperation));
      } catch (Exception e) {
         waitForReplacementBackend();
         if (replacement != null) {
            replacement.applyStreamWork(singleOperation, monitor, indexManager);
         } else {
            throw e;
         }
      }
   }

   /**
    * If some error happened, and a new IndexingBackend was provided, it is safe to
    * assume the error relates with the fact the current backend is no longer valid.
    * At this point we can still forward the work from the current stack to the next
    * backend, creating a linked list of forwards to the right backend:
    */
   private void waitForReplacementBackend() {
      int waitedMilliseconds = 0;
      try {
         while (replacement != null) {
            if (waitedMilliseconds >= GRACE_MILLISECONDS_FOR_REPLACEMENT) {
               return;
            }
            Thread.sleep(POLLING_MILLISECONDS_FOR_REPLACEMENT);
            waitedMilliseconds += POLLING_MILLISECONDS_FOR_REPLACEMENT;
         }
      } catch (InterruptedException e) {
         Thread.currentThread().interrupt();
      }
   }

   private void sendCommand(ReplicableCommand command, List<LuceneWork> workList, boolean sync) {
      rpcManager.invokeRemotely(recipients, command, rpcManager.getDefaultRpcOptions(sync));
      log.workListRemotedTo(workList, masterAddress);
   }

   @Override
   public boolean isMasterLocal() {
      return false;
   }

   public String toString() {
      return "RemoteIndexingBackend(" + masterAddress + ")";
   }

}
