package org.infinispan.stream.impl.spliterators;

import java.util.Comparator;
import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.Consumer;
import java.util.function.Supplier;

import org.infinispan.commons.util.CloseableIterator;
import org.infinispan.commons.util.CloseableSpliterator;
import org.infinispan.commons.util.Closeables;

/**
 * A Spliterator using the provided iterator for supplying values.
 * Splits occur start at the batch size.  Each split gets subsequently bigger by increasing by the original
 * split size.  The batch size will never become higher than the configured max batch size
 */
public class IteratorAsSpliterator<T> implements CloseableSpliterator<T> {
   private CloseableIterator<? extends T> iterator;
   private final int characteristics;
   private final int batchIncrease;
   private final int maxBatchSize;

   private long estimateRemaining;
   private int currentBatchSize;

   public static class Builder<T> implements Supplier<IteratorAsSpliterator<T>> {
      private final CloseableIterator<? extends T> iterator;
      private int characteristics;
      private int batchIncrease = 1024;
      private int maxBatchSize = 51200;

      private long estimateRemaining = Long.MAX_VALUE;

      public Builder(Iterator<? extends T> iterator) {
         Objects.nonNull(iterator);
         this.iterator = Closeables.iterator(iterator);
      }

      public Builder(CloseableIterator<? extends T> closeableIterator) {
         Objects.nonNull(closeableIterator);
         this.iterator = closeableIterator;
      }

      /**
       * Sets the characteristics the subsequent spliterator will have.
       * @param characteristics
       * @return
       */
      public Builder<T> setCharacteristics(int characteristics) {
         this.characteristics = characteristics;
         return this;
      }

      /**
       * Sets the batch increase size.  This controls how much larger subsequent splits are.
       * The default value is 1024;
       * @param batchIncrease
       * @return this
       */
      public Builder<T> setBatchIncrease(int batchIncrease) {
         if (batchIncrease <= 0) {
            throw new IllegalArgumentException("The batchIncrease " + batchIncrease + " must be greater than 0");
         }
         this.batchIncrease = batchIncrease;
         return this;
      }

      /**
       * Sets the max batch size for a thread to use - This defaults to 51200
       * @param maxBatchSize
       * @return this
       */
      public Builder setMaxBatchSize(int maxBatchSize) {
         if (maxBatchSize <= 0) {
            throw new IllegalArgumentException("The maxBatchSize " + maxBatchSize + " must be greater than 0");
         }
         this.maxBatchSize = maxBatchSize;
         return this;
      }

      /**
       * Sets how many estimated elements are remaining for this iterator
       * This defaults to Long.MAX_VALUE.  It is heavily recommended to provide an exact or estimate value
       * to help with controlling parallelism
       * @param estimateRemaining
       * @return this
       */
      public Builder<T> setEstimateRemaining(long estimateRemaining) {
         this.estimateRemaining = estimateRemaining;
         return this;
      }

      @Override
      public IteratorAsSpliterator<T> get() {
         if (iterator == null) {
            throw new IllegalArgumentException("Iterator cannot be null");
         }
         if (batchIncrease > maxBatchSize) {
            throw new IllegalArgumentException("Max batch size " + maxBatchSize +
                    " cannot be larger than batchIncrease" + batchIncrease);
         }
         return new IteratorAsSpliterator<>(this);
      }
   }

   /**
    *
    * @param builder
    */
   private IteratorAsSpliterator(Builder<T> builder) {
      this.iterator = builder.iterator;
      this.characteristics = builder.characteristics;
      this.batchIncrease = builder.batchIncrease;
      this.maxBatchSize = builder.maxBatchSize;

      this.estimateRemaining = builder.estimateRemaining;
   }

   @Override
   public Spliterator<T> trySplit() {
      if (estimateRemaining > 1 && iterator.hasNext()) {
         int batch = currentBatchSize + batchIncrease;
         if (batch > estimateRemaining) {
            batch = (int) estimateRemaining;
         }
         if (batch > maxBatchSize) {
            batch = maxBatchSize;
         }
         Object[] array = new Object[batch];
         int i = 0;
         while (iterator.hasNext() && i < batch) {
            array[i] = iterator.next();
            i++;
         }
         currentBatchSize = batch;
         estimateRemaining -= i;
         return Spliterators.spliterator(array, 0, i, characteristics);
      }
      return null;
   }

   @Override
   public void forEachRemaining(Consumer<? super T> action) {
      if (action == null) {
         throw new NullPointerException();
      }
      iterator.forEachRemaining(action);
   }

   @Override
   public boolean tryAdvance(Consumer<? super T> action) {
      if (action == null) {
         throw new NullPointerException();
      }
      if (iterator.hasNext()) {
         action.accept(iterator.next());
         return true;
      }
      return false;
   }

   @Override
   public long estimateSize() {
      return estimateRemaining;
   }

   @Override
   public int characteristics() { return characteristics; }

   @Override
   public Comparator<? super T> getComparator() {
      if (hasCharacteristics(Spliterator.SORTED)) {
         return null;
      }
      throw new IllegalStateException();
   }

   @Override
   public void close() {
      iterator.close();
   }
}
