package cyclops.companion.vavr;

import com.oath.cyclops.anym.AnyMValue;
import com.oath.cyclops.ReactiveConvertableSequence;
import com.oath.cyclops.data.collections.extensions.CollectionX;
import com.oath.cyclops.types.Value;
import cyclops.control.Either;
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.VavrWitness;
import cyclops.monads.VavrWitness.option;
import cyclops.monads.WitnessType;
import cyclops.monads.XorM;
import cyclops.monads.transformers.jdk.OptionalT;
import cyclops.reactive.ReactiveSeq;
import cyclops.reactive.collections.mutable.ListX;
import io.vavr.control.Option;
import lombok.experimental.UtilityClass;
import org.reactivestreams.Publisher;

import java.util.OptionalDouble;
import java.util.OptionalInt;
import java.util.OptionalLong;
import java.util.function.BiFunction;
import java.util.function.Function;



/**
 * Utility class for working with JDK Optionals
 *
 * @author johnmcclean
 *
 */
@UtilityClass
public class Options {

    public static  <W1 extends WitnessType<W1>,T> XorM<W1,option,T> xorM(Option<T> type){
        return XorM.right(anyM(type));
    }
    public static  <W1 extends WitnessType<W1>,T> XorM<W1,option,T> xorM(T type){
        return XorM.right(anyM(Option.some(type)));
    }
    public static  <W1 extends WitnessType<W1>,T> XorM<W1,option,T> xorMNone(){
        return XorM.right(anyM(Option.none()));
    }
    public static <T,W extends WitnessType<W>> OptionalT<W, T> liftM(Option<T> opt, W witness) {
        return OptionalT.of(witness.adapter().unit(opt.toJavaOptional()));
    }
    public static <T> AnyMValue<option,T> anyM(Option<T> option) {
        return AnyM.ofValue(option, VavrWitness.option.INSTANCE);
    }
    public static <L, T, R> Option<R> tailRec(T initial, Function<? super T, ? extends Option<? extends io.vavr.control.Either<T, R>>> fn) {
        Option<? extends io.vavr.control.Either<T, R>> next[] = new Option[1];
        next[0] = Option.some(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> Option< R> tailRecEither(T initial, Function<? super T, ? extends Option<? extends Either<T, R>>> fn) {
        Option<? extends Either<T, R>> next[] = new Option[1];
        next[0] = Option.some(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));
    }

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

        return value1.flatMap(in -> {

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

            });

        });

    }

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

        return value1.flatMap(in -> {

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

            });

        });

    }

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

        return value1.flatMap(in -> {

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


        });

    }

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

        return value1.flatMap(in -> {

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



        });

    }

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

        return value1.flatMap(in -> {

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



    }

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

        return value1.flatMap(in -> {

            Option<R1> a = value2.apply(in);
            return a.filter(in2->filterFunction.apply(in,in2))
                    .map(in2 -> yieldingFunction.apply(in,  in2));
        });




    }


    public static Option<Double> optional(OptionalDouble d){
        return d.isPresent() ? Option.of(d.getAsDouble()) : Option.none();
    }
    public static Option<Long> optional(OptionalLong l){
        return l.isPresent() ? Option.of(l.getAsLong()) : Option.none();
    }
    public static Option<Integer> optional(OptionalInt l){
        return l.isPresent() ? Option.of(l.getAsInt()) : Option.none();
    }

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

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

    }
    public static <T> Option<ReactiveSeq<T>> sequence(final ReactiveSeq<Option<T>> opts) {
      return AnyM.sequence(opts.map(Options::anyM), option.INSTANCE)
        .map(ReactiveSeq::fromStream)
        .to(VavrWitness::option);

    }
    /**
     * 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 Options.
     *
     * <pre>
     * {@code
     *  Option<Integer> just = Option.of(10);
    Option<Integer> none = Option.empty();

     * Option<PersistentSetX<Integer>> opts = Option.accumulateJust(ListX.of(just, none, Option.of(1)), Reducers.toPersistentSetX());
    //Option.of(PersistentSetX.of(10, 1)));
     *
     * }
     * </pre>
     *
     * @param optionals Options to accumulate
     * @param reducer Reducer to accumulate values with
     * @return Option with reduced value
     */
    public static <T, R> Option<R> accumulatePresent(final Iterable<Option<T>> optionals, final Reducer<R,T> reducer) {
        return sequencePresent(optionals).map(s -> s.mapReduce(reducer));
    }
    /**
     * Accumulate the results only from those Options which have a value present, using the supplied mapping function to
     * convert the data from each Option 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
     *  Option<Integer> just = Option.of(10);
    Option<Integer> none = Option.empty();

     *  Option<String> opts = Option.accumulateJust(ListX.of(just, none, Option.of(1)), i -> "" + i,
    Monoids.stringConcat);
    //Option.of("101")
     *
     * }
     * </pre>
     *
     * @param optionals Options to accumulate
     * @param mapper Mapping function to be applied to the result of each Option
     * @param reducer Monoid to combine values from each Option
     * @return Option with reduced value
     */
    public static <T, R> Option<R> accumulatePresent(final Iterable<Option<T>> optionals, final Function<? super T, R> mapper,
                                                     final Monoid<R> reducer) {
        return sequencePresent(optionals).map(s -> s.map(mapper)
                .reduce(reducer));
    }
    /**
     * Accumulate the results only from those Options 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
     *  Option<Integer> just = Option.of(10);
    Option<Integer> none = Option.empty();

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

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

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

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



}
