package net.ranides.assira.functional;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.Consumers.EachConsumer;
import net.ranides.assira.functional.Functions.*;
import net.ranides.assira.functional.special.AnyFunction;
import net.ranides.assira.generic.Ref;

import java.util.Optional;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * Static functional methods for Function interface
 */
@UtilityClass
public class FunctionUtils {
    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <R> R
     * @return functional
     */
    @SuppressWarnings("unchecked")
    public static <R> Function0<R> raw(Supplier<?> function) {
        Supplier<R> ready = Supplier.class.cast(function);
        return ready::get;
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <T> T
     * @param <R> R
     * @return functional
     */
    @SuppressWarnings("unchecked")
    public static <T, R> Function1<T, R> raw(Function<?, ?> function) {
        Function<T, R> ready = Function.class.cast(function);
        return ready::apply;
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <T> T
     * @param <U> U
     * @param <R> R
     * @return functional
     */
    @SuppressWarnings("unchecked")
    public static <T, U, R> Function2<T, U, R> raw(BiFunction<?, ?, ?> function) {
        BiFunction<T, U, R> ready = BiFunction.class.cast(function);
        return ready::apply;
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <A> A
     * @param <B> B
     * @param <C> C
     * @param <R> R
     * @return functional
     */
    @SuppressWarnings("unchecked")
    public static <A, B, C, R> Function3<A, B, C, R> raw(Function3<?, ?, ?, ?> function) {
        return Function3.class.cast(function);
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <A> A
     * @param <B> B
     * @param <C> C
     * @param <D> D
     * @param <R> R
     * @return functional
     */
    @SuppressWarnings("unchecked")
    public static <A, B, C, D, R> Function4<A, B, C, D, R> raw(Function4<?, ?, ?, ?, ?> function) {
        return Function4.class.cast(function);
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * @param function function
     * @param <R> R
     * @return function
     */
    @SuppressWarnings("unchecked")
    public static <R> AnyFunction<R> rawAny(AnyFunction<?> function) {
        return AnyFunction.class.cast(function);
    }

    /**
     * Sneaky raw cast.
     * It allows to avoid warnings in contexts where we know correctness of the code.
     *
     * This method casts optional objects to inferred type.
     * This method wraps other non-null objects in optional.
     *
     * Returns empty, if argument is empty or null.
     *
     * @param result result
     * @param <R> R
     * @return optional
     */
    @SuppressWarnings("unchecked")
    public static <R> Ref<R> rawOptional(Object result) {
        if(result instanceof Ref) {
            return Ref.class.cast(result);
        }
        else if(result instanceof Optional) {
            return Ref.ofOptional((Optional)result);
        }
        else {
            return Ref.class.cast(Ref.ofNullable(result));
        }
    }

    /**
     * Creates new function which casts argument into specified type and then passes it into callback.
     * If input argument has other type, it returns null instead of executing callback.
     *
     * Essentially, we could say, that it casts function in type-safe way, creating operation
     * which returns null value instead of throwing ClassCastExceptions at runtime.
     *
     * @param argument argument
     * @param function function
     * @param <T> T
     * @param <R> R
     * @return functional
     */
    public static <T, R> Function1<? extends T, R> castArgument(Class<T> argument, Function<? super T, R> function) {
        return input -> argument.isInstance(input) ? function.apply(argument.cast(input)) : null;
    }

    /**
     * Creates new function, which returns "other" instead of null.
     *
     * Essentially, we could say, that it creates null-safe version of callback.
     *
     * @param function function
     * @param other other
     * @param <T> T
     * @param <R> R
     * @return functional
     */
    public static <T, R> Function1<T, R> withDefault(Function<T, R> function, R other) {
        return arg -> Ref.ofNullable(function.apply(arg)).orElse(other);
    }

    /**
     * Creates new function, which returns "other" instead of optional.empty.
     *
     * Essentially, we could say, that it creates version of callback which will never return empty result.
     *
     * @param function function
     * @param other other
     * @param <T> T
     * @param <R> R
     * @return functional
     */
    public static <T, R> Function1<T, R> withOptional(Function<T, Ref<R>> function, R other) {
        return arg -> function.apply(arg).orElse(other);
    }

    /**
     * Creates identity function with side-effect provided by "consumer".
     *
     * Usefull if we want to pass function, but we don't care about generated results but side effects.
     *
     * @param consumer consumer
     * @param <T> T
     * @return functional
     */
    public static <T> Function1<? super T, T> asFunction(Consumer<? super T> consumer) {
        return input -> {
            consumer.accept(input);
            return input;
        };
    }

    /**
     * Creates function which ignores argument and always returns value generated by "supplier".
     *
     * Usefull if we want to pass function, which works like generator and doesn't care about arguments.
     *
     * @param supplier supplier
     * @param <T> T
     * @return functional
     */
    public static <T> Function1<Object, T> asFunction(Supplier<T> supplier) {
        return input -> supplier.get();
    }

    /**
     * Creates identity function with side-effect provided by "consumer".
     *
     * Usefull if we want to pass function, but we don't care about generated results but side effects.
     *
     * @param consumer consumer
     * @param <T> T
     * @return function
     */
    public static <T> EachFunction<? super T, T> asEachFunction(EachConsumer<? super T> consumer) {
        return (index, value) -> {
            consumer.accept(index, value);
            return value;
        };
    }

    /**
     * Creates composition:
     *      H(a) := function( arg1(a) )
     *
     * @param function function
     * @param arg1 arg1
     * @param <R> R
     * @param <A1> A1
     * @param <V1> V1
     * @return functional
     */
    public static <R, A1, V1> Function1<V1, R> compose(Function<A1, R> function, Function<V1, A1> arg1) {
        return input -> function.apply(arg1.apply(input));
    }

    /**
     * Creates composition:
     *      H(a,b) := function( arg1(a), arg2(b) )
     *
     * @param function function
     * @param arg1 arg1
     * @param arg2 arg2
     * @param <R> R
     * @param <A1> A1
     * @param <A2> A2
     * @param <V> V
     * @return functional
     */
    public static <R, A1, A2, V> Function1<V, R> compose(
        BiFunction<A1, A2, R> function,
        Function<V, A1> arg1,
        Function<V, A2> arg2
    ) {
        return arg -> function.apply(arg1.apply(arg), arg2.apply(arg));
    }

    /**
     * Creates composition:
     *      H(a,b,c) := function( arg1(a), arg2(b), arg3(c) )
     *
     * @param function function
     * @param arg1 arg1
     * @param arg2 arg2
     * @param arg3 arg3
     * @param <R> R
     * @param <A1> A1
     * @param <A2> A2
     * @param <A3> A3
     * @param <V> V
     * @return functional
     */
    public static <R, A1, A2, A3, V> Function1<V, R> compose(
        Function3<A1, A2, A3, R> function,
        Function<V, A1> arg1,
        Function<V, A2> arg2,
        Function<V, A3> arg3
    ) {
        return arg -> function.apply(arg1.apply(arg), arg2.apply(arg), arg3.apply(arg));
    }

    /**
     * Creates composition:
     *      H(a,b,c,d) := function( arg1(a), arg2(b), arg3(c), arg4(d) )
     *
     * @param function function
     * @param arg1 arg1
     * @param arg2 arg2
     * @param arg3 arg3
     * @param arg4 arg4
     * @param <R> R
     * @param <A1> A1
     * @param <A2> A2
     * @param <A3> A3
     * @param <A4> A4
     * @param <V> V
     * @return function
     */
    public static <R, A1, A2, A3, A4, V> Function1<V, R> compose(
        Function4<A1, A2, A3, A4, R> function,
        Function<V, A1> arg1,
        Function<V, A2> arg2,
        Function<V, A3> arg3,
        Function<V, A4> arg4
    ) {
        return arg -> function.apply(arg1.apply(arg), arg2.apply(arg), arg3.apply(arg), arg4.apply(arg));
    }


}
