/*
 * Copyright 2015, 2020 StreamEx contributors
 *
 * 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 one.util.streamex.api;

import java.math.BigInteger;
import java.util.Iterator;
import java.util.function.BinaryOperator;
import java.util.function.LongPredicate;
import java.util.function.Predicate;
import java.util.function.UnaryOperator;

import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import one.util.streamex.DoubleStreamEx.DoubleEmitter;
import one.util.streamex.IntStreamEx.IntEmitter;
import one.util.streamex.LongStreamEx;
import one.util.streamex.LongStreamEx.LongEmitter;
import one.util.streamex.StreamEx;
import one.util.streamex.StreamEx.Emitter;
import one.util.streamex.TestHelpers;

import static java.util.Arrays.asList;
import static one.util.streamex.TestHelpers.checkSpliterator;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/**
 * @author Tagir Valeev
 */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class EmitterTest {
    // Like Java-9 Stream.iterate(seed, test, op)
    public static <T> Emitter<T> iterate(T seed, Predicate<? super T> test, UnaryOperator<T> op) {
        return test.test(seed) ? action -> {
            action.accept(seed);
            return iterate(op.apply(seed), test, op);
        } : null;
    }

    // Collatz sequence starting from given number
    public static Emitter<Integer> collatz(int start) {
        return action -> {
            action.accept(start);
            return start == 1 ? null : collatz(start % 2 == 0 ? start / 2 : start * 3 + 1);
        };
    }

    public static IntEmitter collatzInt(int start) {
        return action -> {
            action.accept(start);
            return start == 1 ? null : collatzInt(start % 2 == 0 ? start / 2 : start * 3 + 1);
        };
    }
    
    public static LongEmitter collatzLong(long start) {
        return action -> {
            action.accept(start);
            return start == 1 ? null : collatzLong(start % 2 == 0 ? start / 2 : start * 3 + 1);
        };
    }

    // Stream of Fibonacci numbers
    public static StreamEx<BigInteger> fibonacci() {
        return fibonacci(BigInteger.ONE, BigInteger.ZERO).stream();
    }

    private static Emitter<BigInteger> fibonacci(BigInteger first, BigInteger second) {
        return action -> {
            BigInteger next = first.add(second);
            action.accept(next);
            return fibonacci(second, next);
        };
    }

    // Perform scanLeft on the iterator
    public static <T> Emitter<T> scanLeft(Iterator<T> iter, T initial, BinaryOperator<T> reducer) {
        return action -> {
            if (!iter.hasNext())
                return null;
            T sum = reducer.apply(initial, iter.next());
            action.accept(sum);
            return scanLeft(iter, sum, reducer);
        };
    }

    public static Emitter<Integer> flatTest(int start) {
        return action -> {
            for (int i = 0; i < start; i++)
                action.accept(start);
            return start == 0 ? null : flatTest(start - 1);
        };
    }

    public static IntEmitter flatTestInt(int start) {
        return action -> {
            for (int i = 0; i < start; i++)
                action.accept(start);
            return start == 0 ? null : flatTestInt(start - 1);
        };
    }
    
    public static LongEmitter flatTestLong(int start) {
        return action -> {
            for (int i = 0; i < start; i++)
                action.accept(start);
            return start == 0 ? null : flatTestLong(start - 1);
        };
    }
    
    public static DoubleEmitter flatTestDouble(int start) {
        return action -> {
            for (int i = 0; i < start; i++)
                action.accept(start);
            return start == 0 ? null : flatTestDouble(start - 1);
        };
    }
    
    public static LongStreamEx primes() {
        return ((LongEmitter) (action -> {
            action.accept(2);
            return primes(3, x -> x % 2 != 0);
        })).stream();
    }
    
    private static LongEmitter primes(long start, LongPredicate isPrime) {
        return action -> {
            long nextPrime = LongStreamEx.range(start, Long.MAX_VALUE, 2).findFirst(isPrime).getAsLong();
            action.accept(nextPrime);
            return primes(nextPrime + 2, isPrime.and(x -> x % nextPrime != 0));
        };
    }
    
    @Test
    public void testEmitter() {
        assertEquals(asList(17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1), collatz(17).stream().toList());
        checkSpliterator("collatz", asList(17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1), collatz(17)::spliterator);
        assertArrayEquals(new int[] { 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1 },
            collatzInt(17).stream().toArray());
        checkSpliterator("collatzInt", asList(17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1),
            collatzInt(17)::spliterator);
        assertArrayEquals(new long[] { 17, 52, 26, 13, 40, 20, 10, 5, 16, 8, 4, 2, 1 },
            collatzLong(17).stream().toArray());
        checkSpliterator("collatzLong", asList(17L, 52L, 26L, 13L, 40L, 20L, 10L, 5L, 16L, 8L, 4L, 2L, 1L),
            collatzLong(17)::spliterator);
        assertTrue(collatz(17).stream().has(1));
        assertTrue(collatzInt(17).stream().has(1));
        assertFalse(collatzLong(17).stream().has(0));
        assertEquals(asList(1, 2, 3, 4, 5, 6, 7, 8, 9), iterate(1, x -> x < 10, x -> x + 1).stream().toList());

        // Extracting to variables is necessary to work-around javac <8u40 bug
        String expected = "354224848179261915075";
        String actual = fibonacci().skip(99).findFirst().get().toString();
        assertEquals(expected, actual);
        assertEquals(asList(1, 1, 2, 3, 5, 8, 13, 21, 34, 55), fibonacci().map(BigInteger::intValueExact).limit(10)
                .toList());

        assertEquals(asList("aa", "aabbb", "aabbbc"), scanLeft(asList("aa", "bbb", "c").iterator(), "", String::concat)
                .stream().toList());
        
        assertEquals(asList(4, 4, 4, 4, 3, 3, 3, 2, 2, 1), flatTest(4).stream().toList());
        assertEquals(asList(4, 4, 4, 4, 3, 3, 3, 2, 2), flatTest(4).stream().limit(9).toList());

        TestHelpers.checkSpliterator("flatTest", asList(4, 4, 4, 4, 3, 3, 3, 2, 2, 1),
            flatTest(4)::spliterator);
        TestHelpers.checkSpliterator("flatTest", asList(4, 4, 4, 4, 3, 3, 3, 2, 2, 1),
            flatTestInt(4)::spliterator);
        TestHelpers.checkSpliterator("flatTest", asList(4L, 4L, 4L, 4L, 3L, 3L, 3L, 2L, 2L, 1L),
            flatTestLong(4)::spliterator);
        TestHelpers.checkSpliterator("flatTest", asList(4.0, 4.0, 4.0, 4.0, 3.0, 3.0, 3.0, 2.0, 2.0, 1.0),
            flatTestDouble(4)::spliterator);
        
        assertEquals(7919L, primes().skip(999).findFirst().getAsLong());
    }
}
