package co.unruly.control;

import co.unruly.control.pair.Pair;

import java.util.Iterator;
import java.util.Objects;
import java.util.Spliterator;
import java.util.Spliterators;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.IntStream;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import static java.util.function.Function.identity;
import static java.util.stream.Stream.iterate;

public interface HigherOrderFunctions {

    /**
     * Takes a BiFunction, and reverses the order of the arguments
     */
    static <A, B, R> BiFunction<B, A, R> flip(BiFunction<A, B, R> f) {
        return (a, b) -> f.apply(b, a);
    }

    /**
     * Takes a list of functions (which take and return the same type) and composes
     * them into a single function, applying the provided functions in order
     */
    static <T> Function<T, T> compose(Function<T, T>... functions) {
        return compose(Stream.of(functions));
    }

    /**
     * Takes a Stream of functions (which take and return the same type) and composes
     * them into a single function, applying the provided functions in order
     */
    static <T> Function<T, T> compose(Stream<Function<T, T>> functions) {
        return functions.reduce(identity(), Function::andThen);
    }

    /**
     * Takes a list of predicates and composes them into a single predicate, which
     * passes when all passed-in predicates pass
     */
    static <T> Predicate<T> compose(Predicate<T>... functions) {
        return Stream.of(functions).reduce(__ -> true, Predicate::and);
    }

    /**
     * Turns a Consumer into a Function which applies the consumer and returns the input
     */
    static <T> Function<T, T> peek(Consumer<T> action) {
        return t -> {
            action.accept(t);
            return t;
        };
    }

    static <T> Stream<Pair<Integer, T>> withIndices(Stream<T> items) {
        return zip(iterate(0, x -> x + 1), items);
    }

    static <A, B> Stream<Pair<A, B>> zip(Stream<A> a, Stream<B> b) {
        return zip(a, b, Pair::of);
    }

    /**
     * Zips two streams together using the zipper function, resulting in a single stream of
     * items from each stream combined using the provided function.
     *
     * The resultant stream will have the length of the shorter of the two input streams.
     *
     * Sourced from https://stackoverflow.com/questions/17640754/zipping-streams-using-jdk8-with-lambda-java-util-stream-streams-zip
     */
    static <A , B, C> Stream<C> zip(Stream<A> a, Stream<B> b, BiFunction<A, B, C> zipper) {
        Objects.requireNonNull(zipper);
        Spliterator<? extends A> aSpliterator = Objects.requireNonNull(a).spliterator();
        Spliterator<? extends B> bSpliterator = Objects.requireNonNull(b).spliterator();

        // Zipping looses DISTINCT and SORTED characteristics
        int characteristics = aSpliterator.characteristics() & bSpliterator.characteristics() &
            ~(Spliterator.DISTINCT | Spliterator.SORTED);

        long zipSize = ((characteristics & Spliterator.SIZED) != 0)
            ? Math.min(aSpliterator.getExactSizeIfKnown(), bSpliterator.getExactSizeIfKnown())
            : -1;

        Iterator<A> aIterator = Spliterators.iterator(aSpliterator);
        Iterator<B> bIterator = Spliterators.iterator(bSpliterator);
        Iterator<C> cIterator = new Iterator<C>() {
            @Override
            public boolean hasNext() {
                return aIterator.hasNext() && bIterator.hasNext();
            }

            @Override
            public C next() {
                return zipper.apply(aIterator.next(), bIterator.next());
            }
        };

        Spliterator<C> split = Spliterators.spliterator(cIterator, zipSize, characteristics);
        return (a.isParallel() || b.isParallel())
            ? StreamSupport.stream(split, true)
            : StreamSupport.stream(split, false);
    }

    /**
     * Takes a value, and returns that same value, upcast to a suitable type. Inference is our friend here.
     */
    static <R, T extends R> R upcast(T fv) {
        return fv;
    }
}

@@ -79,103 +79,9 @@
 til.
-Iterator;%0Aimport java.util.Objects;%0Aimport java.util.Spliterator;%0Aimport java.util.Spliterators
+*
 ;%0Aim
@@ -229,43 +229,8 @@
 te;%0A
-import java.util.stream.IntStream;%0A
 impo
@@ -297,16 +297,16 @@
 pport;%0A%0A
-
 import s
@@ -349,16 +349,66 @@
 entity;%0A
+import static java.util.stream.Collectors.toList;%0A
 import s
@@ -3947,32 +3947,316 @@
  false);%0A    %7D%0A%0A
+    /**%0A     * Takes two lists, and returns a list of pairs forming the Cartesian product of those lists.%0A     */%0A    static %3CA, B%3E List%3CPair%3CA, B%3E%3E pairs(List%3CA%3E as, List%3CB%3E bs) %7B%0A        return as.stream().flatMap(a -%3E bs.stream().map(b -%3E Pair.of(a, b))).collect(toList());%0A    %7D%0A%0A
     /**%0A     * T

Function to extract the Cartesian product of two lists as a list of pairs