package org.infinispan.server.hotrod.iteration;

import static org.infinispan.filter.CacheFilters.filterAndConvert;

import java.io.IOException;
import java.util.ArrayList;
import java.util.BitSet;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.infinispan.CacheStream;
import org.infinispan.commons.CacheException;
import org.infinispan.commons.logging.LogFactory;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.commons.util.CollectionFactory;
import org.infinispan.configuration.cache.CompatibilityModeConfiguration;
import org.infinispan.container.entries.CacheEntry;
import org.infinispan.filter.KeyValueFilterConverter;
import org.infinispan.filter.KeyValueFilterConverterFactory;
import org.infinispan.filter.ParamKeyValueFilterConverterFactory;
import org.infinispan.manager.EmbeddedCacheManager;
import org.infinispan.server.hotrod.HotRodTypeConverter;
import org.infinispan.server.hotrod.OperationStatus;
import org.infinispan.server.hotrod.logging.Log;
import org.infinispan.util.KeyValuePair;

/**
 * @author gustavonalle
 * @since 8.0
 */
class IterationSegmentsListener implements CacheStream.SegmentCompletionListener {
   private Set<Integer> finished = new HashSet<>();
   private Set<Integer> justFinished = new HashSet<>();

   Set<Integer> getFinished(boolean endOfIteration) {
      synchronized (this) {
         if (endOfIteration) {
            Set<Integer> segments = new HashSet<>(finished);
            segments.addAll(justFinished);
            return segments;
         } else {
            Set<Integer> diff = new HashSet<>(finished);
            diff.removeAll(justFinished);
            finished.clear();
            finished.addAll(justFinished);
            return diff;
         }
      }
   }

   @Override
   public void segmentCompleted(Set<Integer> segments) {
      if (!segments.isEmpty()) {
         synchronized (this) {
            justFinished = segments;
            finished.addAll(segments);
         }
      }
   }
}

class IterationState {
   final IterationSegmentsListener listener;
   final Iterator<CacheEntry<Object, Object>> iterator;
   final CacheStream<CacheEntry<Object, Object>> stream;
   final int batch;
   final CompatInfo compatInfo;
   final boolean metadata;

   IterationState(IterationSegmentsListener listener, Iterator<CacheEntry<Object, Object>> iterator, CacheStream<CacheEntry<Object, Object>> stream,
                  int batch, CompatInfo compatInfo, boolean metadata) {
      this.listener = listener;
      this.iterator = iterator;
      this.stream = stream;
      this.batch = batch;
      this.compatInfo = compatInfo;
      this.metadata = metadata;
   }
}


class CompatInfo {
   final boolean enabled;
   final Optional<HotRodTypeConverter> hotRodTypeConverter;

   CompatInfo(boolean enabled, Optional<HotRodTypeConverter> hotRodTypeConverter) {
      this.enabled = enabled;
      this.hotRodTypeConverter = hotRodTypeConverter;
   }

   static CompatInfo create(CompatibilityModeConfiguration config) {
      return new CompatInfo(config.enabled(), config.enabled() ?
            Optional.of(new HotRodTypeConverter(config.marshaller())) : Optional.empty());
   }
}

public class DefaultIterationManager implements IterationManager {
   private final EmbeddedCacheManager cacheManager;

   volatile Optional<Marshaller> marshaller = Optional.empty();

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

   private final Map<String, IterationState> iterationStateMap = CollectionFactory.makeConcurrentMap();
   private final Map<String, KeyValueFilterConverterFactory> filterConverterFactoryMap =
         CollectionFactory.makeConcurrentMap();

   public DefaultIterationManager(EmbeddedCacheManager cacheManager) {
      this.cacheManager = cacheManager;
   }

   @Override
   public String start(String cacheName, Optional<BitSet> segments, Optional<KeyValuePair<String, List<byte[]>>> namedFactory, int batch, boolean metadata) {
      String iterationId = UUID.randomUUID().toString();
      CacheStream<CacheEntry<Object, Object>> stream = cacheManager.getCache(cacheName).getAdvancedCache().cacheEntrySet().stream();
      segments.map(bitSet -> stream.filterKeySegments(bitSet.stream().boxed().collect(Collectors.toSet())));

      IterationSegmentsListener segmentListener = new IterationSegmentsListener();
      CompatInfo compatInfo = CompatInfo.create(cacheManager.getCache(cacheName).getCacheConfiguration().compatibility());

      Stream<CacheEntry<Object, Object>> filteredStream;
      if (namedFactory.isPresent()) {
         KeyValueFilterConverterFactory factory = getFactory(namedFactory.get().getKey());
         List<byte[]> params = namedFactory.get().getValue();
         KeyValuePair<KeyValueFilterConverter, Boolean> filter = buildFilter(factory, params.toArray(new byte[params.size()][]));
         IterationFilter iterationFilter = new IterationFilter(compatInfo.enabled, Optional.of(filter.getKey()), marshaller, filter.getValue());
         filteredStream = filterAndConvert(stream.segmentCompletionListener(segmentListener), iterationFilter);
      } else {
         filteredStream = stream.segmentCompletionListener(segmentListener);
      }

      IterationState iterationState = new IterationState(segmentListener, filteredStream.iterator(), stream, batch, compatInfo, metadata);
      iterationStateMap.put(iterationId, iterationState);
      return iterationId;
   }

   private KeyValueFilterConverterFactory getFactory(String name) {
      KeyValueFilterConverterFactory factory = filterConverterFactoryMap.get(name);
      if (factory == null) {
         throw log.missingKeyValueFilterConverterFactory(name);
      }
      return factory;
   }

   private KeyValuePair<KeyValueFilterConverter, Boolean> buildFilter(KeyValueFilterConverterFactory factory, byte[][] params) {
      if (factory instanceof ParamKeyValueFilterConverterFactory) {
         ParamKeyValueFilterConverterFactory paramFactory = (ParamKeyValueFilterConverterFactory) factory;
         Object[] unmarshallParams;
         if (paramFactory.binaryParam()) {
            unmarshallParams = params;
         } else {
            unmarshallParams = unmarshallParams(params, factory);
         }
         return new KeyValuePair<>(paramFactory.getFilterConverter(unmarshallParams),
               paramFactory.binaryParam());
      } else {
         return new KeyValuePair<>(factory.getFilterConverter(), false);
      }
   }

   private Object[] unmarshallParams(byte[][] params, Object factory) {
      Marshaller m = marshaller.orElse(MarshallerBuilder.genericFromInstance(Optional.of(factory)));
      try {
         Object[] objectParams = new Object[params.length];
         int i = 0;
         for (byte[] param : params) {
            objectParams[i++] = m.objectFromByteBuffer(param);
         }
         return objectParams;
      } catch (IOException | ClassNotFoundException e) {
         throw new CacheException(e);
      }
   }


   @Override
   public IterableIterationResult next(String cacheName, String iterationId) {
      IterationState iterationState = iterationStateMap.get(iterationId);
      if (iterationState != null) {
         int i = 0;
         List<CacheEntry> entries = new ArrayList<>(iterationState.batch);
         while (i++ < iterationState.batch && iterationState.iterator.hasNext()) {
            entries.add(iterationState.iterator.next());
         }
         return new IterableIterationResult(iterationState.listener.getFinished(entries.isEmpty()), OperationStatus.Success,
               entries, iterationState.compatInfo, iterationState.metadata);
      } else {
         return new IterableIterationResult(Collections.emptySet(), OperationStatus.InvalidIteration,
               Collections.emptyList(), null, false);
      }
   }

   @Override
   public boolean close(String cacheName, String iterationId) {
      IterationState iterationState = iterationStateMap.get(iterationId);
      if (iterationState != null) {
         iterationState.stream.close();
         iterationStateMap.remove(iterationId);
         return true;
      }
      return false;
   }

   @Override
   public void addKeyValueFilterConverterFactory(String name, KeyValueFilterConverterFactory factory) {
      filterConverterFactoryMap.put(name, factory);
   }

   @Override
   public void removeKeyValueFilterConverterFactory(String name) {
      filterConverterFactoryMap.remove(name);
   }

   @Override
   public int activeIterations() {
      return iterationStateMap.size();
   }

   @Override
   public void setMarshaller(Optional<Marshaller> marshaller) {
      this.marshaller = marshaller;
   }
}
