/*
 * 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 com.google.gwt.emultest.java8.util.stream;

import com.google.gwt.emultest.java.util.EmulTestBase;
import com.google.gwt.junit.DoNotRunWith;
import com.google.gwt.junit.Platform;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.DoubleSummaryStatistics;
import java.util.Iterator;
import java.util.List;
import java.util.OptionalDouble;
import java.util.Spliterator;
import java.util.function.DoubleSupplier;
import java.util.function.Supplier;
import java.util.stream.DoubleStream;
import java.util.stream.Stream;

/**
 * Tests {@link DoubleStream}.
 */
public class DoubleStreamTest extends EmulTestBase {

  public void testEmptyStream() {
    DoubleStream empty = DoubleStream.empty();
    assertEquals(0, empty.count());
    try {
      empty.count();
      fail("second terminal operation should have thrown IllegalStateEx");
    } catch (IllegalStateException expected) {
      // expected
    }

    assertEquals(0, DoubleStream.empty().limit(2).toArray().length);
    assertEquals(0L, DoubleStream.empty().count());
    assertEquals(0L, DoubleStream.empty().limit(2).count());

    assertFalse(DoubleStream.empty().findFirst().isPresent());
    assertFalse(DoubleStream.empty().findAny().isPresent());
    assertFalse(DoubleStream.empty().max().isPresent());
    assertFalse(DoubleStream.empty().min().isPresent());
    assertTrue(DoubleStream.empty().noneMatch(item -> true));
    assertTrue(DoubleStream.empty().allMatch(item -> true));
    assertFalse(DoubleStream.empty().anyMatch(item -> true));
    assertEquals(new double[0], DoubleStream.empty().toArray());
  }

  public void testStreamOfOne() {
    Supplier<DoubleStream> one = () -> DoubleStream.of(1);
    assertEquals(new double[] {1d}, one.get().toArray());
    assertEquals(1L, one.get().count());
    assertEquals(1d, one.get().findFirst().getAsDouble(), 0d);
    assertEquals(1d, one.get().findAny().getAsDouble(), 0d);
  }

  public void testBuilder() {
    DoubleStream s = DoubleStream.builder().add(1d).add(3d).add(2d).build();

    assertEquals(new double[] {1d, 3d, 2d}, s.toArray());

    DoubleStream.Builder builder = DoubleStream.builder();
    DoubleStream built = builder.build();
    assertEquals(0, built.count());
    try {
      builder.build();
      fail("build() after build() should fail");
    } catch (IllegalStateException expected) {
      // expected
    }
    try {
      builder.add(10d);
      fail("add() after build() should fail");
    } catch (IllegalStateException expected) {
      // expected
    }
  }

  public void testConcat() {
    Supplier<DoubleStream> adbc =
        () -> {
          return DoubleStream.concat(DoubleStream.of(1, 4), DoubleStream.of(2, 3));
        };

    assertEquals(new double[] {1d, 4d, 2d, 3d}, adbc.get().toArray());
    assertEquals(new double[] {1d, 2d, 3d, 4d}, adbc.get().sorted().toArray());

    List<String> closed = new ArrayList<>();
    DoubleStream first = DoubleStream.of(1d).onClose(() -> closed.add("first"));
    DoubleStream second = DoubleStream.of(2d).onClose(() -> closed.add("second"));

    DoubleStream concat = DoubleStream.concat(first, second);

    // read everything, make sure we saw it all and didn't close automatically
    double collectedAll = concat.sum();
    assertEquals(3d, collectedAll);
    assertEquals(0, closed.size());

    concat.close();
    assertEquals(Arrays.asList("first", "second"), closed);
  }

  // Java8 has the same bug that GWT previously had here, so we're excluding Java8 from running
  // this test. Presently, legacy dev mode is the only way to actually run this test using the
  // JVM's own implementation, so marking this DoNotRunWith(Devel) is sufficient to avoid this.
  @DoNotRunWith(Platform.Devel)
  public void testIterate() {
    assertEquals(
        new double[] {10d, 11d, 12d, 13d, 14d},
        DoubleStream.iterate(0d, l -> l + 1d).skip(10).limit(5).toArray());

    // Infinite stream, verify that it is correctly limited by a downstream step
    assertEquals(
        new double[] {0, 1, 2, 3, 4},
        DoubleStream.iterate(0, i -> i + 1).limit(5).toArray());

    // Check that the function is called the correct number of times
    int[] calledCount = {0};
    double[] array = DoubleStream.iterate(0, val -> {
      calledCount[0]++;
      return val + 1;
    }).limit(5).toArray();
    // Verify that the function was called for each value after the seed
    assertEquals(array.length - 1, calledCount[0]);
    // Sanity check the values returned
    assertEquals(new double[] {0, 1, 2, 3, 4}, array);
  }

  public void testGenerate() {
    // infinite, but if you limit it is already too short to skip much
    assertEquals(new double[0], DoubleStream.generate(makeGenerator()).limit(4).skip(5).toArray());

    assertEquals(
        new double[] {10d, 11d, 12d, 13d, 14d},
        DoubleStream.generate(makeGenerator()).skip(10).limit(5).toArray());
  }

  private DoubleSupplier makeGenerator() {
    return new DoubleSupplier() {
      double next = 0d;

      @Override
      public double getAsDouble() {
        return next++;
      }
    };
  }

  public void testToArray() {
    assertEquals(new double[0], DoubleStream.of().toArray());
    assertEquals(new double[] {1d}, DoubleStream.of(1d).toArray());
    assertEquals(new double[] {3d, 2d, 0d}, DoubleStream.of(3d, 2d, 0d).toArray());
  }

  public void testReduce() {
    double reduced = DoubleStream.of(1d, 2d, 4d).reduce(0, Double::sum);
    assertEquals(7d, reduced, 0d);

    reduced = DoubleStream.of().reduce(0, Double::sum);
    assertEquals(0d, reduced, 0d);

    OptionalDouble maybe = DoubleStream.of(1d, 4d, 8d).reduce(Double::sum);
    assertTrue(maybe.isPresent());
    assertEquals(13, maybe.getAsDouble(), 0d);
    maybe = DoubleStream.of().reduce(Double::sum);
    assertFalse(maybe.isPresent());
  }

  @SuppressWarnings("ReturnValueIgnored")
  public void testFilter() {
    // unconsumed stream never runs filter
    boolean[] data = {false};
    DoubleStream.of(1d, 2d, 3d).filter(i -> data[0] |= true);
    assertFalse(data[0]);

    // Nothing's *defined* to care about the Spliterator characteristics, but the implementation
    // can't actually know the size before executing, so we check the characteristics explicitly.
    assertFalse(
        DoubleStream.of(1d, 2d, 3d)
            .filter(a -> a == 1d)
            .spliterator()
            .hasCharacteristics(Spliterator.SIZED | Spliterator.SUBSIZED));

    // one result
    assertEquals(
        new double[] {1d}, DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> a == 1).toArray());
    // zero results
    assertEquals(new double[0], DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> false).toArray());
    // two results
    assertEquals(
        new double[] {2d, 4d},
        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> a % 2 == 0).toArray());
    // all
    assertEquals(
        new double[] {1d, 2d, 3d, 4d, 3d},
        DoubleStream.of(1d, 2d, 3d, 4d, 3d).filter(a -> true).toArray());
  }

  @SuppressWarnings("ReturnValueIgnored")
  public void testMap() {
    // unconsumed stream never runs map
    int[] data = {0};
    DoubleStream.of(1d, 2d, 3d).map(i -> data[0]++);
    assertEquals(0, data[0]);

    assertEquals(new double[] {2d, 4d, 6d}, DoubleStream.of(1d, 2d, 3d).map(i -> i * 2).toArray());
  }

  @SuppressWarnings("ReturnValueIgnored")
  public void testPeek() {
    // unconsumed stream never peeks
    boolean[] data = {false};
    DoubleStream.of(1d, 2d, 3d).peek(i -> data[0] |= true);
    assertFalse(data[0]);

    // make sure we saw it all in order
    double[] items = new double[] {1d, 2d, 3d};
    List<Double> peeked = new ArrayList<>();
    DoubleStream.of(items)
        .peek(peeked::add)
        .forEach(
            item -> {
              // do nothing, just run
            });
    assertEquals(items.length, peeked.size());
    for (int i = 0; i < items.length; i++) {
      assertEquals(items[i], peeked.get(i), 0d);
    }
  }

  // same impl, no parallel in browser
  public void testFindFirstOrAny() {
    OptionalDouble any = DoubleStream.of(1d, 2d).findAny();
    assertTrue(any.isPresent());
    assertEquals(1d, any.getAsDouble(), 0d);
  }

  public void testAnyMatch() {
    // all
    assertTrue(DoubleStream.of(1d, 2d).anyMatch(s -> true));

    // some
    assertTrue(DoubleStream.of(1d, 2d).anyMatch(s -> s == 1d));

    // none
    assertFalse(DoubleStream.of(1d, 2d).anyMatch(s -> false));
  }

  public void testAllMatch() {
    // all
    assertTrue(DoubleStream.of(1d, 2d).allMatch(s -> true));

    // some
    assertFalse(DoubleStream.of(1d, 2d).allMatch(s -> s == 1d));

    // none
    assertFalse(DoubleStream.of(1d, 2d).allMatch(s -> false));
  }

  public void testNoneMatch() {
    // all
    assertFalse(DoubleStream.of(1d, 2d).noneMatch(s -> true));

    // some
    assertFalse(DoubleStream.of(1d, 2d).noneMatch(s -> s == 1d));

    // none
    assertTrue(DoubleStream.of(1d, 2d).noneMatch(s -> false));
  }

  public void testFlatMap() {
    assertEquals(0L, DoubleStream.empty().flatMap(value -> DoubleStream.of(1d)).count());
    assertEquals(0L, DoubleStream.of(1d).flatMap(value -> DoubleStream.empty()).count());
    assertEquals(0L, DoubleStream.of(1d).flatMap(value -> DoubleStream.of()).count());
    assertEquals(0L, DoubleStream.of().flatMap(value -> DoubleStream.of(1d)).count());
    assertEquals(1L, DoubleStream.of(1d).flatMap(value -> DoubleStream.of(1d)).count());

    DoubleStream values = DoubleStream.of(1d, 2d, 3d);

    assertEquals(
        new double[] {1d, 2d, 2d, 4d, 3d, 6d},
        values.flatMap(i -> DoubleStream.of(i, i * 2d)).toArray());
  }

  public void testMapToOthers() {
    Supplier<DoubleStream> s = () -> DoubleStream.of(1d, 2d, 10d);

    assertEquals(
        new String[] {"1", "2", "10"},
        s.get().mapToObj(DoubleStreamTest::toIntegralString).toArray(String[]::new));

    assertEquals(new long[] {1L, 2L, 10L}, s.get().mapToLong(i -> (long) i).toArray());

    assertEquals(new int[] {1, 2, 10}, s.get().mapToInt(i -> (int) i).toArray());
  }

  public void testDistinct() {
    double[] distinct = DoubleStream.of(1d, 2d, 3d, 2d).distinct().toArray();
    assertEquals(3, distinct.length);
    assertEquals(1d + 2d + 3d, distinct[0] + distinct[1] + distinct[2], 0d);
  }

  public void testSorted() {
    double[] sorted = DoubleStream.of(3d, 1d, 2d).sorted().toArray();
    assertEquals(new double[] {1d, 2d, 3d}, sorted);
  }

  public void testMinMax() {
    Supplier<DoubleStream> stream = () -> DoubleStream.of(2d, 3d, 4d, 1d);

    assertEquals(1d, stream.get().min().orElse(0), 0d);
    assertEquals(4d, stream.get().max().orElse(0), 0d);

    assertFalse(stream.get().filter(a -> false).max().isPresent());
    assertFalse(stream.get().filter(a -> false).min().isPresent());
  }

  public void testCountLimitSkip() {
    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d, 3d, 4d);

    assertEquals(4L, stream.get().count());

    assertEquals(4L, stream.get().limit(4).count());
    assertEquals(4L, stream.get().limit(5).count());
    assertEquals(3L, stream.get().limit(3).count());

    assertEquals(3L, stream.get().skip(1).limit(3).count());

    assertEquals(2L, stream.get().limit(3).skip(1).count());

    assertEquals(1L, stream.get().skip(3).count());

    assertEquals(new double[] {3d, 4d}, stream.get().skip(2).limit(3).toArray());
    assertEquals(new double[] {3d}, stream.get().skip(2).limit(1).toArray());

    assertEquals(new double[] {4d}, stream.get().skip(3).toArray());
    assertEquals(new double[] {}, stream.get().skip(5).toArray());

    assertEquals(new double[] {1d, 2d}, stream.get().limit(2).toArray());

    assertEquals(new double[] {2d}, stream.get().limit(2).skip(1).toArray());
  }

  public void testBoxed() {
    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d);
    Stream<Double> expected = stream.get().mapToObj(Double::valueOf);
    assertEquals(expected.toArray(), stream.get().boxed().toArray());
  }

  public void testSummaryStats() {
    Supplier<DoubleStream> stream = () -> DoubleStream.of(1d, 2d, 3d);
    DoubleSummaryStatistics summaryStats = stream.get().summaryStatistics();
    assertEquals(3L, summaryStats.getCount());
    assertEquals(1d, summaryStats.getMin(), 0d);
    assertEquals(2d, summaryStats.getAverage(), 0d);
    assertEquals(3d, summaryStats.getMax(), 0d);
    assertEquals(6d, summaryStats.getSum(), 0d);

    summaryStats.accept(6L);
    assertEquals(4L, summaryStats.getCount());
    assertEquals(1d, summaryStats.getMin(), 0d);
    assertEquals(3d, summaryStats.getAverage(), 0d);
    assertEquals(6d, summaryStats.getMax(), 0d);
    assertEquals(12d, summaryStats.getSum(), 0d);

    DoubleSummaryStatistics combinedSumStats = stream.get().summaryStatistics();
    combinedSumStats.combine(DoubleStream.of(4d, 5d, 6d, 0d).summaryStatistics());
    assertEquals(7L, combinedSumStats.getCount());
    assertEquals(0d, combinedSumStats.getMin(), 0d);
    assertEquals(3d, combinedSumStats.getAverage(), 0d);
    assertEquals(6d, combinedSumStats.getMax(), 0d);
    assertEquals(21d, combinedSumStats.getSum(), 0d);
  }

  public void testAverage() {
    assertFalse(DoubleStream.empty().average().isPresent());
    assertEquals(2.0d, DoubleStream.of(1d, 2d, 3d).average().getAsDouble(), 0d);
    assertEquals(0d, DoubleStream.of(1d, 2d, -3d).average().getAsDouble(), 0d);
    assertEquals(-2.0d, DoubleStream.of(-1d, -2d, -3d).average().getAsDouble(), 0d);
  }

  public void testSum() {
    assertEquals(6d, DoubleStream.of(1d, 2d, 3d).sum(), 0d);
    assertEquals(0d, DoubleStream.of(1d, 2d, -3d).sum(), 0d);
    assertEquals(-6d, DoubleStream.of(-1d, -2d, -3d).sum(), 0d);
  }

  public void testCollect() {
    // noinspection StringConcatenationInsideStringBufferAppend
    String val =
        DoubleStream.of(1d, 2d, 3d, 4d, 5d)
            .collect(
                StringBuilder::new,
                (stringBuilder, d) -> stringBuilder.append(toIntegralString(d)),
                StringBuilder::append)
            .toString();

    assertEquals("12345", val);
  }

  public void testForEach() {
    List<Double> vals = new ArrayList<>();
    DoubleStream.of(1d, 2d, 3d, 4d, 5d).forEach(vals::add);
    assertEquals(5, vals.size());
    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
  }

  public void testIterator() {
    List<Double> vals = new ArrayList<>();
    Iterator<Double> iterator = DoubleStream.of(1d, 2d, 3d, 4d, 5d).iterator();
    while (iterator.hasNext()) {
      vals.add(iterator.next());
    }
    assertEquals(5, vals.size());
    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
  }

  public void testSpliterator() {
    Spliterator<Double> spliterator = DoubleStream.of(1d, 2d, 3d, 4d, 5d).spliterator();
    assertEquals(5, spliterator.estimateSize());
    assertEquals(5, spliterator.getExactSizeIfKnown());

    List<Double> vals = new ArrayList<>();
    while (spliterator.tryAdvance(vals::add)) {
      // work is all done in the condition
    }

    assertEquals(5, vals.size());
    assertEquals(new Double[] {1d, 2d, 3d, 4d, 5d}, vals.toArray(new Double[vals.size()]));
  }

  // See https://github.com/gwtproject/gwt/issues/8615
  private static String toIntegralString(double value) {
    return "" + (int) value;
  }
}
