package cyclops.companion.rx2;


import com.oath.cyclops.react.Status;
import com.oath.cyclops.types.MonadicValue;
import com.oath.cyclops.types.Value;
import cyclops.companion.Futures;
import cyclops.control.Either;
import cyclops.control.Eval;
import cyclops.control.Future;
import cyclops.control.LazyEither;
import cyclops.data.Seq;
import cyclops.function.Function3;
import cyclops.function.Function4;
import io.reactivex.Maybe;
import io.reactivex.Single;
import lombok.experimental.UtilityClass;
import org.reactivestreams.Publisher;

import java.util.Iterator;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * Companion class for working with RxJava 2 Maybe types
 *
 * @author johnmcclean
 *
 */
@UtilityClass
public class Maybes {


    public static <T, R> Maybe< R> tailRec(T initial, Function<? super T, ? extends Maybe<? extends Either<T, R>>> fn) {
        Maybe<? extends Either<T, R>> next[] = new Maybe[1];
        next[0] = Maybe.just(Either.left(initial));
        boolean cont = true;
        do {
            cont = next[0].map(p -> p.fold(s -> {
                next[0] = fn.apply(s);
                return true;
            }, pr -> false)).blockingGet(false);
        } while (cont);
        return next[0].map(e->e.orElse(null));
    }

    public static <T> Maybe<T> fromPublisher(Publisher<T> maybe){
        return Single.fromPublisher(maybe).toMaybe();
    }



    public static <T> Future[] futures(Maybe<T>... futures){

        Future[] array = new Future[futures.length];
        for(int i=0;i<array.length;i++){
            array[i]=future(futures[i]);
        }
        return array;
    }
    public static <T> cyclops.control.Maybe<T> toMaybe(Maybe<T> future){
        return cyclops.control.Maybe.fromPublisher(future.toFlowable());
    }
    public static <T> Maybe<T> fromMaybe(cyclops.control.Maybe<T> future){
        return Single.fromPublisher(future).toMaybe();

    }
    public static <T> Maybe<T> fromValue(MonadicValue<T> future){
        return Single.fromPublisher(future).toMaybe();

    }
    public static <T> Future<T> future(Maybe<T> future){
        return Future.fromPublisher(future.toFlowable());
    }

    public static <R> LazyEither<Throwable,R> either(Maybe<R> either){
        return LazyEither.fromFuture(future(either));

    }

    public static <T> cyclops.control.Maybe<T> maybe(Maybe<T> opt){
        return cyclops.control.Maybe.fromFuture(future(opt));
    }
    public static <T> Eval<T> eval(Maybe<T> opt){
        return Eval.fromFuture(future(opt));
    }



    /**
     * Select the first Maybe to complete
     *
     * @see CompletableFuture#anyOf(CompletableFuture...)
     * @param fts Maybes to race
     * @return First Maybe to complete
     */
    public static <T> Maybe<T> anyOf(Maybe<T>... fts) {


        return Single.fromPublisher(Future.anyOf(futures(fts))).toMaybe();

    }
    /**
     * Wait until all the provided Future's to complete
     *
     * @see CompletableFuture#allOf(CompletableFuture...)
     *
     * @param fts Maybes to  wait on
     * @return Maybe that completes when all the provided Futures Complete. Empty Future result, or holds an Exception
     *         from a provided Future that failed.
     */
    public static <T> Maybe<T> allOf(Maybe<T>... fts) {

        return Single.fromPublisher(Future.allOf(futures(fts))).toMaybe();
    }
    /**
     * Block until a Quorum of results have returned as determined by the provided Predicate
     *
     * <pre>
     * {@code
     *
     * Maybe<ListX<Integer>> strings = Maybes.quorum(status -> status.getCompleted() >0, Maybe.deferred(()->1),Maybe.empty(),Maybe.empty());


    strings.get().size()
    //1
     *
     * }
     * </pre>
     *
     *
     * @param breakout Predicate that determines whether the block should be
     *            continued or removed
     * @param fts FutureWs to  wait on results from
     * @param errorHandler Consumer to handle any exceptions thrown
     * @return Future which will be populated with a Quorum of results
     */
    @SafeVarargs
    public static <T> Maybe<Seq<T>> quorum(Predicate<Status<T>> breakout, Consumer<Throwable> errorHandler, Maybe<T>... fts) {

        return Single.fromPublisher(Futures.quorum(breakout,errorHandler,futures(fts))).toMaybe();


    }
    /**
     * Block until a Quorum of results have returned as determined by the provided Predicate
     *
     * <pre>
     * {@code
     *
     * Maybe<ListX<Integer>> strings = Maybes.quorum(status -> status.getCompleted() >0, Maybe.deferred(()->1),Maybe.empty(),Maybe.empty());


    strings.get().size()
    //1
     *
     * }
     * </pre>
     *
     *
     * @param breakout Predicate that determines whether the block should be
     *            continued or removed
     * @param fts Maybes to  wait on results from
     * @return Maybe which will be populated with a Quorum of results
     */
    @SafeVarargs
    public static <T> Maybe<Seq<T>> quorum(Predicate<Status<T>> breakout, Maybe<T>... fts) {

        return Single.fromPublisher(Futures.quorum(breakout,futures(fts))).toMaybe();


    }
    /**
     * Select the first Future to return with a successful result
     *
     * <pre>
     * {@code
     * Maybe<Integer> ft = Maybe.empty();
      Maybe<Integer> result = Maybes.firstSuccess(Maybe.deferred(()->1),ft);

    ft.complete(10);
    result.get() //1
     * }
     * </pre>
     *
     * @param fts Maybes to race
     * @return First Maybe to return with a result
     */
    @SafeVarargs
    public static <T> Maybe<T> firstSuccess(Maybe<T>... fts) {
        return Single.fromPublisher(Future.firstSuccess(futures(fts))).toMaybe();

    }

    /**
     * Perform a For Comprehension over a Maybe, accepting 3 generating functions.
     * This results in a four level nested internal iteration over the provided Maybes.
     *
     *  <pre>
     * {@code
     *
     *   import static cyclops.companion.reactor.Maybes.forEach4;
     *
          forEach4(Maybe.just(1),
                  a-> Maybe.just(a+1),
                  (a,b) -> Maybe.<Integer>just(a+b),
                  (a,b,c) -> Maybe.<Integer>just(a+b+c),
                  Tuple::tuple)
     *
     * }
     * </pre>
     *
     * @param value1 top level Maybe
     * @param value2 Nested Maybe
     * @param value3 Nested Maybe
     * @param value4 Nested Maybe
     * @param yieldingFunction Generates a result per combination
     * @return Maybe with a combined value generated by the yielding function
     */
    public static <T1, T2, T3, R1, R2, R3, R> Maybe<R> forEach4(Maybe<? extends T1> value1,
            Function<? super T1, ? extends Maybe<R1>> value2,
            BiFunction<? super T1, ? super R1, ? extends Maybe<R2>> value3,
            Function3<? super T1, ? super R1, ? super R2, ? extends Maybe<R3>> value4,
            Function4<? super T1, ? super R1, ? super R2, ? super R3, ? extends R> yieldingFunction) {


        Maybe<? extends R> res = value1.flatMap(in -> {

            Maybe<R1> a = value2.apply(in);
            return a.flatMap(ina -> {
                Maybe<R2> b = value3.apply(in, ina);
                return b.flatMap(inb -> {
                    Maybe<R3> c = value4.apply(in, ina, inb);
                    return c.map(in2 -> yieldingFunction.apply(in, ina, inb, in2));
                });

            });

        });
        return  narrow(res);
    }


    /**
     * Perform a For Comprehension over a Maybe, accepting 2 generating functions.
     * This results in a three level nested internal iteration over the provided Maybes.
     *
     *  <pre>
     * {@code
     *
     *   import static cyclops.companion.reactor.Maybes.forEach3;
     *
          forEach3(Maybe.just(1),
                  a-> Maybe.just(a+1),
                  (a,b) -> Maybe.<Integer>just(a+b),
                  Tuple::tuple)
     *
     * }
     * </pre>
     *
     * @param value1 top level Maybe
     * @param value2 Nested Maybe
     * @param value3 Nested Maybe
     * @param yieldingFunction Generates a result per combination
     * @return Maybe with a combined value generated by the yielding function
     */
    public static <T1, T2, R1, R2, R> Maybe<R> forEach3(Maybe<? extends T1> value1,
            Function<? super T1, ? extends Maybe<R1>> value2,
            BiFunction<? super T1, ? super R1, ? extends Maybe<R2>> value3,
            Function3<? super T1, ? super R1, ? super R2, ? extends R> yieldingFunction) {


        Maybe<? extends R> res = value1.flatMap(in -> {

            Maybe<R1> a = value2.apply(in);
            return a.flatMap(ina -> {
                Maybe<R2> b = value3.apply(in, ina);


                return b.map(in2 -> yieldingFunction.apply(in, ina, in2));


            });

        });
        return narrow(res);

    }



    /**
     * Perform a For Comprehension over a Maybe, accepting a generating function.
     * This results in a two level nested internal iteration over the provided Maybes.
     *
     *  <pre>
     * {@code
     *
     *   import static cyclops.companion.reactor.Maybes.forEach;
     *
          forEach(Maybe.just(1),
                  a-> Maybe.just(a+1),
                  Tuple::tuple)
     *
     * }
     * </pre>
     *
     * @param value1 top level Maybe
     * @param value2 Nested Maybe
     * @param yieldingFunction Generates a result per combination
     * @return Maybe with a combined value generated by the yielding function
     */
    public static <T, R1, R> Maybe<R> forEach(Maybe<? extends T> value1,
                                             Function<? super T, Maybe<R1>> value2,
                                             BiFunction<? super T, ? super R1, ? extends R> yieldingFunction) {

        Maybe<R> res = value1.flatMap(in -> {

            Maybe<R1> a = value2.apply(in);
            return a.map(ina -> yieldingFunction.apply(in, ina));


        });


        return narrow(res);

    }



    /**
     * Lazily combine this Maybe with the supplied value via the supplied BiFunction
     *
     * @param maybe Maybe to combine with another value
     * @param app Value to combine with supplied maybe
     * @param fn Combiner function
     * @return Combined Maybe
     */
    public static <T1, T2, R> Maybe<R> combine(Maybe<? extends T1> maybe, Value<? extends T2> app,
            BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(Single.fromPublisher(Future.fromPublisher(maybe.toFlowable())
                                .zip(app, fn)).toMaybe());
    }

    /**
     * Lazily combine this Maybe with the supplied Maybe via the supplied BiFunction
     *
     * @param maybe Maybe to combine with another value
     * @param app Maybe to combine with supplied maybe
     * @param fn Combiner function
     * @return Combined Maybe
     */
    public static <T1, T2, R> Maybe<R> combine(Maybe<? extends T1> maybe, Maybe<? extends T2> app,
            BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(Single.fromPublisher(Future.fromPublisher(maybe.toFlowable())
                                .zip(Future.fromPublisher(app.toFlowable()), fn)).toMaybe());
    }

    /**
     * Combine the provided Maybe with the first element (if present) in the provided Iterable using the provided BiFunction
     *
     * @param maybe Maybe to combine with an Iterable
     * @param app Iterable to combine with a Maybe
     * @param fn Combining function
     * @return Combined Maybe
     */
    public static <T1, T2, R> Maybe<R> zip(Maybe<? extends T1> maybe, Iterable<? extends T2> app,
            BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(Single.fromPublisher(Future.fromPublisher(maybe.toFlowable())
                                .zip(app, fn)).toMaybe());
    }

    /**
     * Combine the provided Maybe with the first element (if present) in the provided Publisher using the provided BiFunction
     *
     * @param maybe  Maybe to combine with a Publisher
     * @param fn Publisher to combine with a Maybe
     * @param app Combining function
     * @return Combined Maybe
     */
    public static <T1, T2, R> Maybe<R> zip(Maybe<? extends T1> maybe, BiFunction<? super T1, ? super T2, ? extends R> fn,
            Publisher<? extends T2> app) {
        Maybe<R> res = narrow(Single.fromPublisher(Future.fromPublisher(maybe.toFlowable()).zip(fn,app)).toMaybe());
        return res;
    }


    /**
     * Construct a Maybe from Iterable by taking the first value from Iterable
     *
     * @param t Iterable to populate Maybe from
     * @return Maybe containing first element from Iterable (or empty Maybe)
     */
    public static <T> Maybe<T> fromIterable(Iterable<T> t) {
        return narrow(Single.fromPublisher(Future.fromIterable(t)).toMaybe());
    }

    /**
     * Get an Iterator for the value (if any) in the provided Maybe
     *
     * @param pub Maybe to get Iterator for
     * @return Iterator over Maybe value
     */
    public static <T> Iterator<T> iterator(Maybe<T> pub) {
        return pub.toFlowable().blockingIterable().iterator();

    }

    public static <R> Maybe<R> narrow(Maybe<? extends R> apply) {
        return (Maybe<R>)apply;
    }




}
