package org.infinispan.server.resp.commands.set;

import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletionStage;
import java.util.stream.Collectors;

import org.infinispan.commons.marshall.WrappedByteArray;
import org.infinispan.multimap.impl.EmbeddedSetCache;
import org.infinispan.multimap.impl.SetBucket;
import org.infinispan.server.resp.Consumers;
import org.infinispan.server.resp.Resp3Handler;
import org.infinispan.server.resp.RespCommand;
import org.infinispan.server.resp.RespRequestHandler;
import org.infinispan.server.resp.commands.Resp3Command;

import io.netty.channel.ChannelHandlerContext;

/**
 * {@link} https://redis.io/commands/sinter/
 *
 * Returns the members of the set resulting from the intersection of all the
 * given sets.
 *
 * @since 15.0
 */
public class SINTER extends RespCommand implements Resp3Command {
   static Set<WrappedByteArray> EMPTY_SET = new HashSet<>();

   public SINTER() {
      super(-2, 1, -1, 1);
   }

   @Override
   public CompletionStage<RespRequestHandler> perform(Resp3Handler handler,
         ChannelHandlerContext ctx,
         List<byte[]> arguments) {
      EmbeddedSetCache<byte[], WrappedByteArray> esc = handler.getEmbeddedSetCache();
      // Wrapping to exclude duplicate keys
      var uniqueKeys = getUniqueKeys(handler, arguments);
      var allEntries= esc.getAll(uniqueKeys);
      return handler.stageToReturn(
            allEntries
                  .thenApply((sets) -> sets.size()==uniqueKeys.size() ? intersect(sets.values(), 0) : checkTypesAndReturnEmpty(sets.values())),
            ctx, Consumers.COLLECTION_BULK_BICONSUMER);
   }

   public static Set<byte[]> getUniqueKeys(Resp3Handler handler, List<byte[]> arguments) {
      var wrappedArgs = arguments.stream().map(item -> new WrappedByteArray(item)).collect(Collectors.toSet());
      return wrappedArgs.stream().map(item -> item.getBytes()).collect(Collectors.toSet());
   }

   public static Set<WrappedByteArray> checkTypesAndReturnEmpty(Collection<SetBucket<WrappedByteArray>> buckets) {
      var iter = buckets.iterator();
      // access all items to check for error
      while (iter.hasNext()) {
      var minSet = iter.next();
      }
      return EMPTY_SET;
   }

   public static Set<WrappedByteArray> intersect(Collection<SetBucket<WrappedByteArray>> buckets,
         int limit) {
      var iter = buckets.iterator();

      // Return empty set if null or empty
      if (!iter.hasNext()) {
         return EMPTY_SET;
      }
      var minSet = iter.next();
      if (minSet.isEmpty()) {
         return EMPTY_SET;
      }

      // Find the smallest set in the sets list
      while (iter.hasNext() && !minSet.isEmpty()) {
         var el = iter.next();
         minSet = minSet.size() <= el.size() ? minSet : el;
      }

      // Build a set with all the elements in minSet and in all the rest of the sets
      // up to limit if non zero
      var result = new HashSet<WrappedByteArray>();
      for (var el : minSet.values()) {
         if (!buckets.stream().anyMatch(set -> !set.contains(el))) {
            result.add(el);
            if (limit > 0 && result.size() >= limit) {
               break;
            }
         }
      }
      return result;
   }
}
