/*
 * Copyright 2016 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package java.util;

import static javaemul.internal.InternalPreconditions.checkCriticalElement;
import static javaemul.internal.InternalPreconditions.checkCriticalState;
import static javaemul.internal.InternalPreconditions.checkNotNull;

import java.util.function.Consumer;
import java.util.function.DoubleConsumer;
import java.util.function.IntConsumer;
import java.util.function.LongConsumer;

/**
 * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Spliterators.html">
 * the official Java API doc</a> for details.
 *
 * Since it's hard to implement parallel algorithms in the browser environment
 * and to keep code simple, implementation does not provide splitting.
 */
public final class Spliterators {

  private abstract static class BaseSpliterator<T, S extends Spliterator<T>>
      implements Spliterator<T> {
    private final int characteristics;
    private long sizeEstimate;

    BaseSpliterator(long size, int characteristics) {
      this.sizeEstimate = size;
      this.characteristics = (characteristics & Spliterator.SIZED) != 0 ?
          characteristics | Spliterator.SUBSIZED : characteristics;
    }

    public int characteristics() {
      return characteristics;
    }

    public long estimateSize() {
      return sizeEstimate;
    }

    public S trySplit() {
      // see javadoc for java.util.Spliterator
      return null;
    }
  }

  /**
   * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Spliterators.AbstractSpliterator.html">
   * the official Java API doc</a> for details.
   */
  public abstract static class AbstractSpliterator<T>
      extends BaseSpliterator<T, Spliterator<T>> implements Spliterator<T> {

    protected AbstractSpliterator(long size, int characteristics) {
      super(size, characteristics);
    }
  }

  /**
   * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Spliterators.AbstractDoubleSpliterator.html">
   * the official Java API doc</a> for details.
   */
  public abstract static class AbstractDoubleSpliterator
      extends BaseSpliterator<Double, Spliterator.OfDouble> implements Spliterator.OfDouble {

    protected AbstractDoubleSpliterator(long size, int characteristics) {
      super(size, characteristics);
    }
  }

  /**
   * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Spliterators.AbstractIntSpliterator.html">
   * the official Java API doc</a> for details.
   */
  public abstract static class AbstractIntSpliterator
      extends BaseSpliterator<Integer, Spliterator.OfInt> implements Spliterator.OfInt {

    protected AbstractIntSpliterator(long size, int characteristics) {
      super(size, characteristics);
    }
  }

  /**
   * See <a href="https://docs.oracle.com/javase/8/docs/api/java/util/Spliterators.AbstractLongSpliterator.html">
   * the official Java API doc</a> for details.
   */
  public abstract static class AbstractLongSpliterator
      extends BaseSpliterator<Long, Spliterator.OfLong> implements Spliterator.OfLong {

    protected AbstractLongSpliterator(long size, int characteristics) {
      super(size, characteristics);
    }
  }

  @SuppressWarnings("unchecked")
  public static <T> Spliterator<T> emptySpliterator() {
    return (Spliterator<T>) EmptySpliterator.OF_REF;
  }

  public static Spliterator.OfDouble emptyDoubleSpliterator() {
    return EmptySpliterator.OF_DOUBLE;
  }

  public static Spliterator.OfInt emptyIntSpliterator() {
    return EmptySpliterator.OF_INT;
  }

  public static Spliterator.OfLong emptyLongSpliterator() {
    return EmptySpliterator.OF_LONG;
  }

  public static <T> Spliterator<T> spliterator(Object[] array, int characteristics) {
    return new ArraySpliterator<>(array, characteristics);
  }

  public static <T> Spliterator<T> spliterator(Object[] array, int fromIndex, int toIndex,
                                               int characteristics) {
    checkCriticalArrayBounds(fromIndex, toIndex, array.length);
    return new ArraySpliterator<>(array, fromIndex, toIndex, characteristics);
  }

  public static Spliterator.OfInt spliterator(int[] array, int characteristics) {
    return new IntArraySpliterator(array, characteristics);
  }

  public static Spliterator.OfInt spliterator(int[] array, int fromIndex, int toIndex,
                                              int characteristics) {
    checkCriticalArrayBounds(fromIndex, toIndex, array.length);
    return new IntArraySpliterator(array, fromIndex, toIndex, characteristics);
  }

  public static Spliterator.OfLong spliterator(long[] array, int characteristics) {
    return new LongArraySpliterator(array, characteristics);
  }

  public static Spliterator.OfLong spliterator(long[] array, int fromIndex, int toIndex,
                                               int characteristics) {
    checkCriticalArrayBounds(fromIndex, toIndex, array.length);
    return new LongArraySpliterator(array, fromIndex, toIndex, characteristics);
  }

  public static Spliterator.OfDouble spliterator(double[] array, int characteristics) {
    return new DoubleArraySpliterator(array, characteristics);
  }

  public static Spliterator.OfDouble spliterator(double[] array, int fromIndex, int toIndex,
                                                 int characteristics) {
    checkCriticalArrayBounds(fromIndex, toIndex, array.length);
    return new DoubleArraySpliterator(array, fromIndex, toIndex, characteristics);
  }

  public static <T> Spliterator<T> spliterator(Collection<? extends T> c, int characteristics) {
    return new IteratorSpliterator<>(c, characteristics);
  }

  public static <T> Spliterator<T> spliterator(Iterator<? extends T> it, long size,
                                               int characteristics) {
    return new IteratorSpliterator<>(it, size, characteristics);
  }

  public static <T> Spliterator<T> spliteratorUnknownSize(Iterator<? extends T> it,
                                                          int characteristics) {
    return new IteratorSpliterator<>(it, characteristics);
  }

  public static Spliterator.OfInt spliterator(PrimitiveIterator.OfInt it, long size,
                                              int characteristics) {
    return new IntIteratorSpliterator(it, size, characteristics);
  }

  public static Spliterator.OfInt spliteratorUnknownSize(PrimitiveIterator.OfInt it,
                                                         int characteristics) {
    return new IntIteratorSpliterator(it, characteristics);
  }

  public static Spliterator.OfLong spliterator(PrimitiveIterator.OfLong it, long size,
                                               int characteristics) {
    return new LongIteratorSpliterator(it, size, characteristics);
  }

  public static Spliterator.OfLong spliteratorUnknownSize(PrimitiveIterator.OfLong it,
                                                          int characteristics) {
    return new LongIteratorSpliterator(it, characteristics);
  }

  public static Spliterator.OfDouble spliterator(PrimitiveIterator.OfDouble it, long size,
                                                 int characteristics) {
    return new DoubleIteratorSpliterator(it, size, characteristics);
  }

  public static Spliterator.OfDouble spliteratorUnknownSize(PrimitiveIterator.OfDouble it,
                                                            int characteristics) {
    return new DoubleIteratorSpliterator(it, characteristics);
  }

  public static <T> Iterator<T> iterator(Spliterator<? extends T> spliterator) {
    return new ConsumerIterator<>(spliterator);
  }

  public static PrimitiveIterator.OfDouble iterator(Spliterator.OfDouble spliterator) {
    return new DoubleConsumerIterator(spliterator);
  }

  public static PrimitiveIterator.OfInt iterator(Spliterator.OfInt spliterator) {
    return new IntConsumerIterator(spliterator);
  }

  public static PrimitiveIterator.OfLong iterator(Spliterator.OfLong spliterator) {
    return new LongConsumerIterator(spliterator);
  }

  private abstract static class EmptySpliterator<T, S extends Spliterator<T>, C>
      implements Spliterator<T> {

    static final Spliterator<Object> OF_REF = new EmptySpliterator.OfRef<>();
    static final Spliterator.OfDouble OF_DOUBLE = new EmptySpliterator.OfDouble();
    static final Spliterator.OfInt OF_INT = new EmptySpliterator.OfInt();
    static final Spliterator.OfLong OF_LONG = new EmptySpliterator.OfLong();

    public int characteristics() {
      return Spliterator.SIZED | Spliterator.SUBSIZED;
    }

    public long estimateSize() {
      return 0;
    }

    public void forEachRemaining(C consumer) {
      checkNotNull(consumer);
    }

    public boolean tryAdvance(C consumer) {
      checkNotNull(consumer);
      return false;
    }

    public S trySplit() {
      return null;
    }

    private static final class OfRef<T>
        extends EmptySpliterator<T, Spliterator<T>, Consumer<? super T>>
        implements Spliterator<T> {

      OfRef() { }
    }

    private static final class OfDouble
        extends EmptySpliterator<Double, Spliterator.OfDouble, DoubleConsumer>
        implements Spliterator.OfDouble {

      OfDouble() { }
    }

    private static final class OfInt
        extends EmptySpliterator<Integer, Spliterator.OfInt, IntConsumer>
        implements Spliterator.OfInt {

      OfInt() { }
    }

    private static final class OfLong
        extends EmptySpliterator<Long, Spliterator.OfLong, LongConsumer>
        implements Spliterator.OfLong {

      OfLong() { }
    }
  }

  private static final class ConsumerIterator<T> implements Consumer<T>, Iterator<T> {
    private final Spliterator<? extends T> spliterator;
    private T nextElement;
    private boolean hasElement = false;

    ConsumerIterator(Spliterator<? extends T> spliterator) {
      this.spliterator = checkNotNull(spliterator);
    }

    @Override
    public void accept(T element) {
      nextElement = element;
    }

    @Override
    public boolean hasNext() {
      if (!hasElement) {
        hasElement = spliterator.tryAdvance(this);
      }
      return hasElement;
    }

    @Override
    public T next() {
      checkCriticalElement(hasNext());
      hasElement = false;
      T element = nextElement;
      nextElement = null;
      return element;
    }
  }

  private static final class DoubleConsumerIterator
      implements DoubleConsumer, PrimitiveIterator.OfDouble {

    private final Spliterator.OfDouble spliterator;
    private double nextElement;
    private boolean hasElement = false;

    DoubleConsumerIterator(Spliterator.OfDouble spliterator) {
      this.spliterator = checkNotNull(spliterator);
    }

    @Override
    public void accept(double d) {
      nextElement = d;
    }

    @Override
    public boolean hasNext() {
      if (!hasElement) {
        hasElement = spliterator.tryAdvance(this);
      }
      return hasElement;
    }

    @Override
    public double nextDouble() {
      checkCriticalElement(hasNext());
      hasElement = false;
      return nextElement;
    }
  }

  private static final class IntConsumerIterator
      implements IntConsumer, PrimitiveIterator.OfInt {

    private final Spliterator.OfInt spliterator;
    private int nextElement;
    private boolean hasElement = false;

    IntConsumerIterator(Spliterator.OfInt spliterator) {
      this.spliterator = checkNotNull(spliterator);
    }

    @Override
    public void accept(int i) {
      nextElement = i;
    }

    @Override
    public boolean hasNext() {
      if (!hasElement) {
        hasElement = spliterator.tryAdvance(this);
      }
      return hasElement;
    }

    @Override
    public int nextInt() {
      checkCriticalElement(hasNext());
      hasElement = false;
      return nextElement;
    }
  }

  private static final class LongConsumerIterator
      implements LongConsumer, PrimitiveIterator.OfLong {

    private final Spliterator.OfLong spliterator;
    private long nextElement;
    private boolean hasElement = false;

    LongConsumerIterator(Spliterator.OfLong spliterator) {
      this.spliterator = checkNotNull(spliterator);
    }

    @Override
    public void accept(long l) {
      nextElement = l;
    }

    @Override
    public boolean hasNext() {
      if (!hasElement) {
        hasElement = spliterator.tryAdvance(this);
      }
      return hasElement;
    }

    @Override
    public long nextLong() {
      checkCriticalElement(hasNext());
      hasElement = false;
      return nextElement;
    }
  }

  static class IteratorSpliterator<T> implements Spliterator<T> {
    private Collection<? extends T> collection;
    private Iterator<? extends T> it;
    private final int characteristics;
    private long estimateSize;

    IteratorSpliterator(Collection<? extends T> collection, int characteristics) {
      this.collection = checkNotNull(collection);
      this.characteristics = sizeKnownIteratorSpliteratorCharacteristics(characteristics);
    }

    IteratorSpliterator(Iterator<? extends T> it, long size, int characteristics) {
      this.it = checkNotNull(it);
      this.characteristics = sizeKnownIteratorSpliteratorCharacteristics(characteristics);
      this.estimateSize = size;
    }

    IteratorSpliterator(Iterator<? extends T> it, int characteristics) {
      this.it = checkNotNull(it);
      this.characteristics = sizeUnknownSpliteratorCharacteristics(characteristics);
      this.estimateSize = Long.MAX_VALUE;
    }

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

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

    @Override
    public void forEachRemaining(Consumer<? super T> consumer) {
      initIterator();
      it.forEachRemaining(consumer);
    }

    @Override
    public Comparator<? super T> getComparator() {
      checkSorted(characteristics);
      return null;
    }

    @Override
    public boolean tryAdvance(Consumer<? super T> consumer) {
      checkNotNull(consumer);
      initIterator();
      if (it.hasNext()) {
        consumer.accept(it.next());
        return true;
      }
      return false;
    }

    @Override
    public Spliterator<T> trySplit() {
      // see javadoc for java.util.Spliterator
      return null;
    }

    private void initIterator() {
      if (it == null) {
        it = collection.iterator();
        estimateSize = (long) collection.size();
      }
    }
  }

  private static final class DoubleIteratorSpliterator extends AbstractDoubleSpliterator {
    private final PrimitiveIterator.OfDouble it;

    DoubleIteratorSpliterator(PrimitiveIterator.OfDouble it, long size, int characteristics) {
      super(size, sizeKnownIteratorSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    DoubleIteratorSpliterator(PrimitiveIterator.OfDouble it, int characteristics) {
      super(Long.MAX_VALUE, sizeUnknownSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    @Override
    public void forEachRemaining(DoubleConsumer consumer) {
      it.forEachRemaining(consumer);
    }

    @Override
    public Comparator<? super Double> getComparator() {
      checkSorted(characteristics());
      return null;
    }

    @Override
    public boolean tryAdvance(DoubleConsumer consumer) {
      checkNotNull(consumer);
      if (it.hasNext()) {
        consumer.accept(it.nextDouble());
        return true;
      }
      return false;
    }
  }

  private static final class IntIteratorSpliterator extends AbstractIntSpliterator {
    private final PrimitiveIterator.OfInt it;

    IntIteratorSpliterator(PrimitiveIterator.OfInt it, long size, int characteristics) {
      super(size, sizeKnownIteratorSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    IntIteratorSpliterator(PrimitiveIterator.OfInt it, int characteristics) {
      super(Long.MAX_VALUE, sizeUnknownSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    @Override
    public void forEachRemaining(IntConsumer consumer) {
      it.forEachRemaining(consumer);
    }

    @Override
    public Comparator<? super Integer> getComparator() {
      checkSorted(characteristics());
      return null;
    }

    @Override
    public boolean tryAdvance(IntConsumer consumer) {
      checkNotNull(consumer);
      if (it.hasNext()) {
        consumer.accept(it.nextInt());
        return true;
      }
      return false;
    }
  }

  private static final class LongIteratorSpliterator extends AbstractLongSpliterator {
    private final PrimitiveIterator.OfLong it;

    LongIteratorSpliterator(PrimitiveIterator.OfLong it, long size, int characteristics) {
      super(size, sizeKnownIteratorSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    LongIteratorSpliterator(PrimitiveIterator.OfLong it, int characteristics) {
      super(Long.MAX_VALUE, sizeUnknownSpliteratorCharacteristics(characteristics));
      this.it = checkNotNull(it);
    }

    @Override
    public void forEachRemaining(LongConsumer consumer) {
      it.forEachRemaining(consumer);
    }

    @Override
    public Comparator<? super Long> getComparator() {
      checkSorted(characteristics());
      return null;
    }

    @Override
    public boolean tryAdvance(LongConsumer consumer) {
      checkNotNull(consumer);
      if (it.hasNext()) {
        consumer.accept(it.nextLong());
        return true;
      }
      return false;
    }
  }

  private abstract static class BaseArraySpliterator<T, S extends Spliterator<T>, C>
      implements Spliterator<T> {
    private int index;
    private final int limit;
    private final int characteristics;

    BaseArraySpliterator(int from, int limit, int characteristics) {
      this.index = from;
      this.limit = limit;
      this.characteristics = sizeKnownSpliteratorCharacteristics(characteristics);
    }

    public int characteristics() {
      return characteristics;
    }

    public long estimateSize() {
      return limit - index;
    }

    public void forEachRemaining(C consumer) {
      checkNotNull(consumer);
      while (index < limit) {
        consume(consumer, index++);
      }
    }

    public Comparator<? super T> getComparator() {
      checkSorted(characteristics);
      return null;
    }

    public boolean tryAdvance(C consumer) {
      checkNotNull(consumer);
      if (index < limit) {
        consume(consumer, index++);
        return true;
      }
      return false;
    }

    public S trySplit() {
      // see javadoc for java.util.Spliterator
      return null;
    }

    protected abstract void consume(C consumer, int index);
  }

  private static final class ArraySpliterator<T>
      extends BaseArraySpliterator<T, Spliterator<T>, Consumer<? super T>>
      implements Spliterator<T> {

    private final Object[] array;

    ArraySpliterator(Object[] array, int characteristics) {
      this(array, 0, array.length, characteristics);
    }

    ArraySpliterator(Object[] array, int from, int limit, int characteristics) {
      super(from, limit, characteristics);
      this.array = array;
    }

    @SuppressWarnings("unchecked")
    @Override
    protected void consume(Consumer<? super T> consumer, int index) {
      consumer.accept((T) array[index]);
    }
  }

  private static final class DoubleArraySpliterator
      extends BaseArraySpliterator<Double, Spliterator.OfDouble, DoubleConsumer>
      implements Spliterator.OfDouble {

    private final double[] array;

    DoubleArraySpliterator(double[] array, int characteristics) {
      this(array, 0, array.length, characteristics);
    }

    DoubleArraySpliterator(double[] array, int from, int limit, int characteristics) {
      super(from, limit, characteristics);
      this.array = array;
    }

    @Override
    protected void consume(DoubleConsumer consumer, int index) {
      consumer.accept(array[index]);
    }
  }

  private static final class IntArraySpliterator
      extends BaseArraySpliterator<Integer, Spliterator.OfInt, IntConsumer>
      implements Spliterator.OfInt {

    private final int[] array;

    IntArraySpliterator(int[] array, int characteristics) {
      this(array, 0, array.length, characteristics);
    }

    IntArraySpliterator(int[] array, int from, int limit, int characteristics) {
      super(from, limit, characteristics);
      this.array = array;
    }

    @Override
    protected void consume(IntConsumer consumer, int index) {
      consumer.accept(array[index]);
    }
  }

  private static final class LongArraySpliterator
      extends BaseArraySpliterator<Long, Spliterator.OfLong, LongConsumer>
      implements Spliterator.OfLong {

    private final long[] array;

    LongArraySpliterator(long[] array, int characteristics) {
      this(array, 0, array.length, characteristics);
    }

    LongArraySpliterator(long[] array, int from, int limit, int characteristics) {
      super(from, limit, characteristics);
      this.array = array;
    }

    @Override
    protected void consume(LongConsumer consumer, int index) {
      consumer.accept(array[index]);
    }
  }

  private static void checkSorted(int characteristics) {
    checkCriticalState((characteristics & Spliterator.SORTED) != 0);
  }

  private static int sizeKnownSpliteratorCharacteristics(int characteristics) {
    return characteristics | Spliterator.SIZED | Spliterator.SUBSIZED;
  }

  private static int sizeKnownIteratorSpliteratorCharacteristics(int characteristics) {
    return (characteristics & Spliterator.CONCURRENT) == 0 ?
        sizeKnownSpliteratorCharacteristics(characteristics) : characteristics;
  }

  private static int sizeUnknownSpliteratorCharacteristics(int characteristics) {
    return characteristics & ~(Spliterator.SIZED | Spliterator.SUBSIZED);
  }

  /**
   * We cant use InternalPreconditions.checkCriticalArrayBounds here because
   * Spliterators must throw only ArrayIndexOutOfBoundsException on range check by contract.
   */
  private static void checkCriticalArrayBounds(int start, int end, int length) {
    if (start > end || start < 0 || end > length) {
      throw new ArrayIndexOutOfBoundsException(
          "fromIndex: " + start + ", toIndex: " + end + ", length: " + length);
    }
  }

  private Spliterators() { }

}
