package cyclops.companion.vavr;

import com.oath.cyclops.ReactiveConvertableSequence;
import com.oath.cyclops.types.Value;
import cyclops.control.Eval;
import cyclops.conversion.vavr.FromCyclops;
import cyclops.conversion.vavr.ToCyclops;
import cyclops.function.Function3;
import cyclops.function.Function4;
import cyclops.function.Monoid;
import cyclops.function.Reducer;
import cyclops.monads.AnyM;
import cyclops.monads.Witness;
import cyclops.monads.WitnessType;
import cyclops.monads.transformers.EvalT;
import cyclops.reactive.ReactiveSeq;
import cyclops.reactive.collections.mutable.ListX;
import io.vavr.Lazy;
import org.reactivestreams.Publisher;

import java.util.function.BiFunction;
import java.util.function.Function;

public class Lazys {



    public static <L, T, R> Lazy<R> tailRec(T initial, Function<? super T, ? extends Lazy<? extends io.vavr.control.Either<T, R>>> fn) {
        Lazy<? extends io.vavr.control.Either<T, R>> next[] = new Lazy[1];
        next[0] = Lazy.of(()->io.vavr.control.Either.left(initial));
        boolean cont = true;
        do {
            cont = next[0].map(p -> p.fold(s -> {
                next[0] = fn.apply(s);
                return true;
            }, pr -> false)).getOrElse(false);
        } while (cont);
        return next[0].map(io.vavr.control.Either::get);
    }
    public static <T, R> Lazy< R> tailRecEither(T initial, Function<? super T, ? extends Lazy<? extends cyclops.control.Either<T, R>>> fn) {
        Lazy<? extends cyclops.control.Either<T, R>> next[] = new Lazy[1];
        next[0] = Lazy.of(()-> cyclops.control.Either.left(initial));
        boolean cont = true;
        do {
            cont = next[0].map(p -> p.visit(s -> {
                next[0] = fn.apply(s);
                return true;
            }, pr -> false)).getOrElse(false);
        } while (cont);
        return next[0].map(e->e.orElse(null));
    }


    /**
     * Lifts a vavr Lazy into a cyclops LazyT monad transformer (involves an observables conversion to
     * cyclops Lazy types)
     *
     */
    public static <T,W extends WitnessType<W>> EvalT<W, T> liftM(Lazy<T> opt, W witness) {
        return EvalT.of(witness.adapter().unit(ToCyclops.eval(opt)));
    }
    /**
     * Perform a For Comprehension over a Lazy, accepting 3 generating function.
     * This results in a four level nested internal iteration over the provided Lazys.
     *
     *  <pre>
     * {@code
     *
     *   import static com.oath.cyclops.reactor.Lazys.forEach4;
     *
    forEach4(Lazy.just(1),
    a-> Lazy.just(a+1),
    (a,b) -> Lazy.<Integer>just(a+b),
    a                  (a,b,c) -> Lazy.<Integer>just(a+b+c),
    Tuple::tuple)
     *
     * }
     * </pre>
     *
     * @param value1 top level Lazy
     * @param value2 Nested Lazy
     * @param value3 Nested Lazy
     * @param value4 Nested Lazy
     * @param yieldingFunction Generates a result per combination
     * @return Lazy with a combined value generated by the yielding function
     */
    public static <T1, T2, T3, R1, R2, R3, R> Lazy<R> forEach4(Lazy<? extends T1> value1,
                                                               Function<? super T1, ? extends Lazy<R1>> value2,
                                                               BiFunction<? super T1, ? super R1, ? extends Lazy<R2>> value3,
                                                               Function3<? super T1, ? super R1, ? super R2, ? extends Lazy<R3>> value4,
                                                               Function4<? super T1, ? super R1, ? super R2, ? super R3, ? extends R> yieldingFunction) {

        Eval<R> res = ToCyclops.eval(value1).flatMap(in -> {

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

            });

        });
        return FromCyclops.eval(res);
    }



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

        Eval<? extends R> res = ToCyclops.eval(value1).flatMap(in -> {

            Lazy<R1> a = value2.apply(in);
            return ToCyclops.eval(a).flatMap(ina -> {
                Lazy<R2> b = value3.apply(in, ina);
                return ToCyclops.eval(b).map(in2 -> yieldingFunction.apply(in, ina, in2));
            });


        });

        return FromCyclops.eval(Eval.narrow(res));

    }


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

        Eval<? extends R> res = ToCyclops.eval(value1).flatMap(in -> {

            Lazy<R1> a = value2.apply(in);
            return ToCyclops.eval(a).map(in2 -> yieldingFunction.apply(in, in2));
        });


        return FromCyclops.eval(Eval.narrow(res));
    }




    /**
     * Sequence operation, take a Collection of Lazys and turn it into a Lazy with a Collection
     * By constrast with {@link Lazys#sequencePresent(Iterable)}, if any Lazys are empty the result
     * is an empty Lazy
     *
     * <pre>
     * {@code
     *
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();
     *
     *  Lazy<ListX<Integer>> opts = Lazys.sequence(ListX.of(just, none, Lazy.of(1)));
    //Lazy.empty();
     *
     * }
     * </pre>
     *
     *
     * @param opts Maybes to Sequence
     * @return  Maybe with a List of values
     */
    public static <T> Lazy<ListX<T>> sequence(final Iterable<Lazy<T>> opts) {
        return sequence(ReactiveSeq.fromIterable(opts)).map(s -> s.to(ReactiveConvertableSequence::converter).listX());

    }
    /**
     * Sequence operation, take a Collection of Lazys and turn it into a Lazy with a Collection
     * Only successes are retained. By constrast with {@link Lazys#sequence(Iterable)} Lazy#empty types are
     * tolerated and ignored.
     *
     * <pre>
     * {@code
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();
     *
     * Lazy<ListX<Integer>> maybes = Lazys.sequencePresent(ListX.of(just, none, Lazy.of(1)));
    //Lazy.of(ListX.of(10, 1));
     * }
     * </pre>
     *
     * @param opts Lazys to Sequence
     * @return Lazy with a List of values
     */
    public static <T> Lazy<ListX<T>> sequencePresent(final Iterable<Lazy<T>> opts) {
        return sequence(ReactiveSeq.fromIterable(opts).filter(Lazy::isEvaluated)).map(s->s.to(ReactiveConvertableSequence::converter).listX());
    }
    /**
     * Sequence operation, take a Collection of Lazys and turn it into a Lazy with a Collection
     * By constrast with {@link Lazys#sequencePresent(Iterable)} if any Lazy types are empty
     * the return type will be an empty Lazy
     *
     * <pre>
     * {@code
     *
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();
     *
     *  Lazy<ListX<Integer>> maybes = Lazys.sequence(ListX.of(just, none, Lazy.of(1)));
    //Lazy.empty();
     *
     * }
     * </pre>
     *
     *
     * @param opts Maybes to Sequence
     * @return  Lazy with a List of values
     */
    public static <T> Lazy<ReactiveSeq<T>> sequence(final java.util.stream.Stream<Lazy<T>> opts) {
        return FromCyclops.eval(AnyM.sequence(opts.map(ToCyclops::eval).map(AnyM::fromEval), Witness.eval.INSTANCE)
                .map(ReactiveSeq::fromStream)
                .to(Witness::eval));

    }
  public static <T> Lazy<ReactiveSeq<T>> sequence(final ReactiveSeq<Lazy<T>> opts) {
    return FromCyclops.eval(AnyM.sequence(opts.map(ToCyclops::eval).map(AnyM::fromEval), Witness.eval.INSTANCE)
      .map(ReactiveSeq::fromStream)
      .to(Witness::eval));

  }
    /**
     * Accummulating operation using the supplied Reducer (@see cyclops2.Reducers). A typical use case is to accumulate into a Persistent Collection type.
     * Accumulates the present results, ignores empty Lazys.
     *
     * <pre>
     * {@code
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();

     * Lazy<PersistentSetX<Integer>> opts = Lazy.accumulateJust(ListX.of(just, none, Lazy.of(1)), Reducers.toPersistentSetX());
    //Lazy.of(PersistentSetX.of(10, 1)));
     *
     * }
     * </pre>
     *
     * @param futureals Lazys to accumulate
     * @param reducer Reducer to accumulate values with
     * @return Lazy with reduced value
     */
    public static <T, R> Lazy<R> accumulatePresent(final Iterable<Lazy<T>> futureals, final Reducer<R,T> reducer) {
        return sequencePresent(futureals).map(s -> s.mapReduce(reducer));
    }
    /**
     * Accumulate the results only from those Lazys which have a value present, using the supplied mapping function to
     * convert the data from each Lazy before reducing them using the supplied Monoid (a combining BiFunction/BinaryOperator and identity element that takes two
     * input values of the same type and returns the combined result) {@see cyclops2.Monoids }.
     *
     * <pre>
     * {@code
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();

     *  Lazy<String> opts = Lazy.accumulateJust(ListX.of(just, none, Lazy.of(1)), i -> "" + i,
    Monoids.stringConcat);
    //Lazy.of("101")
     *
     * }
     * </pre>
     *
     * @param futureals Lazys to accumulate
     * @param mapper Mapping function to be applied to the result of each Lazy
     * @param reducer Monoid to combine values from each Lazy
     * @return Lazy with reduced value
     */
    public static <T, R> Lazy<R> accumulatePresent(final Iterable<Lazy<T>> futureals, final Function<? super T, R> mapper,
                                                   final Monoid<R> reducer) {
        return sequencePresent(futureals).map(s -> s.map(mapper)
                .reduce(reducer));
    }
    /**
     * Accumulate the results only from those Lazys which have a value present, using the
     * supplied Monoid (a combining BiFunction/BinaryOperator and identity element that takes two
     * input values of the same type and returns the combined result) {@see cyclops2.Monoids }.
     *
     * <pre>
     * {@code
     *  Lazy<Integer> just = Lazy.of(10);
    Lazy<Integer> none = Lazy.empty();

     *  Lazy<String> opts = Lazy.accumulateJust(Monoids.stringConcat,ListX.of(just, none, Lazy.of(1)),
    );
    //Lazy.of("101")
     *
     * }
     * </pre>
     *
     * @param futureals Lazys to accumulate
     * @param reducer Monoid to combine values from each Lazy
     * @return Lazy with reduced value
     */
    public static <T> Lazy<T> accumulatePresent(final Monoid<T> reducer, final Iterable<Lazy<T>> futureals) {
        return sequencePresent(futureals).map(s -> s
                .reduce(reducer));
    }

    /**
     * Combine an Lazy with the provided value using the supplied BiFunction
     *
     * <pre>
     * {@code
     *  Lazys.combine(Lazy.of(10),Maybe.just(20), this::add)
     *  //Lazy[30]
     *
     *  private int add(int a, int b) {
    return a + b;
    }
     *
     * }
     * </pre>
     * @param f Lazy to combine with a value
     * @param v Value to combine
     * @param fn Combining function
     * @return Lazy combined with supplied value
     */
    public static <T1, T2, R> Lazy<R> combine(final Lazy<? extends T1> f, final Value<? extends T2> v,
                                              final BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(FromCyclops.eval(ToCyclops.eval(f)
                .zip(v, fn)));
    }
    /**
     * Combine an Lazy with the provided Lazy using the supplied BiFunction
     *
     * <pre>
     * {@code
     *  Lazys.combine(Lazy.of(10),Lazy.of(20), this::add)
     *  //Lazy[30]
     *
     *  private int add(int a, int b) {
    return a + b;
    }
     *
     * }
     * </pre>
     *
     * @param f Lazy to combine with a value
     * @param v Lazy to combine
     * @param fn Combining function
     * @return Lazy combined with supplied value, or empty Lazy if no value present
     */
    public static <T1, T2, R> Lazy<R> combine(final Lazy<? extends T1> f, final Lazy<? extends T2> v,
                                              final BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return combine(f, ToCyclops.eval(v),fn);
    }

    /**
     * Combine an Lazy with the provided Iterable (selecting one element if present) using the supplied BiFunction
     * <pre>
     * {@code
     *  Lazys.zip(Lazy.of(10),Arrays.asList(20), this::add)
     *  //Lazy[30]
     *
     *  private int add(int a, int b) {
    return a + b;
    }
     *
     * }
     * </pre>
     * @param f Lazy to combine with first element in Iterable (if present)
     * @param v Iterable to combine
     * @param fn Combining function
     * @return Lazy combined with supplied Iterable, or empty Lazy if no value present
     */
    public static <T1, T2, R> Lazy<R> zip(final Lazy<? extends T1> f, final Iterable<? extends T2> v,
                                          final BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(FromCyclops.eval(ToCyclops.eval(f)
                .zip(v, fn)));
    }

    /**
     * Combine an Lazy with the provided Publisher (selecting one element if present) using the supplied BiFunction
     * <pre>
     * {@code
     *  Lazys.zip(Flux.just(10),Lazy.of(10), this::add)
     *  //Lazy[30]
     *
     *  private int add(int a, int b) {
    return a + b;
    }
     *
     * }
     * </pre>
     *
     * @param p Publisher to combine
     * @param f  Lazy to combine with
     * @param fn Combining function
     * @return Lazy combined with supplied Publisher, or empty Lazy if no value present
     */
    public static <T1, T2, R> Lazy<R> zip(final Publisher<? extends T2> p, final Lazy<? extends T1> f,
                                          final BiFunction<? super T1, ? super T2, ? extends R> fn) {
        return narrow(FromCyclops.eval(ToCyclops.eval(f)
                .zip(fn,p)));
    }
    /**
     * Narrow covariant type parameter
     *
     * @param futureal Lazy with covariant type parameter
     * @return Narrowed Lazy
     */
    public static <T> Lazy<T> narrow(final Lazy<? extends T> futureal) {
        return (Lazy<T>) futureal;
    }


}
