package tc.oc.commons.core.util;

import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;

import com.google.common.reflect.TypeParameter;
import com.google.common.reflect.TypeToken;
import com.google.inject.TypeLiteral;
import tc.oc.commons.core.reflect.Methods;
import tc.oc.commons.core.reflect.Types;

public final class Optionals {
    private Optionals() {}

    public static boolean equals(Optional<?> a, Optional<?> b) {
        return a.isPresent() && b.isPresent() && a.get().equals(b.get());
    }

    public static boolean equals(Optional<?> a, Object b) {
        return a.isPresent() && a.get().equals(b);
    }

    public static boolean equals(Object a, Optional<?> b) {
        return equals(b, a);
    }

    public static boolean isInstance(Optional<?> value, Class<?> type) {
        return value.isPresent() && type.isInstance(value.get());
    }

    public static <T> Optional<T> cast(Optional<? super T> value, Class<T> type) {
        return isInstance(value, type) ? (Optional<T>) value : Optional.empty();
    }

    public static <T> Optional<T> cast(@Nullable Object obj, Class<T> type) {
        return type.isInstance(obj) ? Optional.ofNullable((T) obj)
                                    : Optional.empty();
    }

    public static <T> Optional<T> getIf(boolean condition, Supplier<T> supplier) {
        return condition ? Optional.of(supplier.get())
                         : Optional.empty();
    }

    public static <T, E extends Throwable> Optional<T> ofThrows(Class<E> exception, ThrowingSupplier<T, E> supplier) {
        try {
            return Optional.of(supplier.getThrows());
        } catch(Throwable e) {
            if(exception.isInstance(e)) {
                return Optional.empty();
            }
            throw (RuntimeException) e;
        }
    }

    public static <T> Optional<T> filter(T value, Predicate<? super T> filter) {
        return Optional.of(value).filter(filter);
    }

    public static boolean contains(Optional<?> container, Object value) {
        return container.isPresent() && container.get().equals(value);
    }

    public static <T> Set<T> toSet(Optional<T> t) {
        return t.isPresent() ? Collections.singleton(t.get())
                             : Collections.emptySet();
    }

    public static <T> Stream<T> stream(Optional<T> t) {
        return t.map(Stream::of).orElse(Stream.empty());
    }

    public static <T> Set<T> union(Stream<Optional<? extends T>> optionals) {
        return optionals.filter(Optional::isPresent)
                        .map(Optional::get)
                        .collect(Collectors.toSet());
    }

    public static <T> Set<T> union(Collection<Optional<? extends T>> optionals) {
        return union(optionals.stream());
    }

    public static <T> Set<T> union(Optional<? extends T>... optionals) {
        return union(Stream.of(optionals));
    }

    public static <T> Optional<T> first(Iterable<T> iterable) {
        final Iterator<T> iterator = iterable.iterator();
        return iterator.hasNext() ? Optional.of(iterator.next()) : Optional.empty();
    }

    public static <T> Optional<T> first(Stream<Optional<? extends T>> options) {
        return (Optional<T>) options.filter(Optional::isPresent)
                                    .findFirst()
                                    .orElse(Optional.empty());
    }

    public static <T> Optional<T> first(Collection<Optional<? extends T>> options) {
        return first(options.stream());
    }

    public static <T> Optional<T> first(Optional<? extends T>... options) {
        return first(Stream.of(options));
    }

    public static <T> TypeToken<Optional<T>> optionalType(TypeToken<T> type) {
        return new TypeToken<Optional<T>>(){}.where(new TypeParameter<T>(){}, type);
    }

    public static <T> TypeLiteral<Optional<T>> optionalType(TypeLiteral<T> type) {
        return Types.toLiteral(optionalType(Types.toToken(type)));
    }

    public static <T> TypeToken<Optional<? extends T>> optionalSubtypes(TypeToken<T> type) {
        return new TypeToken<Optional<? extends T>>(){}.where(new TypeParameter<T>(){}, type);
    }

    public static <T> TypeLiteral<Optional<? extends T>> optionalSubtypes(TypeLiteral<T> type) {
        return Types.toLiteral(optionalSubtypes(Types.toToken(type)));
    }

    private static final Method GET_METHOD = Methods.method(Optional.class, "get");

    public static <T> TypeToken<T> elementType(TypeToken<Optional<T>> optionalType) {
        return (TypeToken<T>) optionalType.method(GET_METHOD).getReturnType();
    }

    public static <T> TypeLiteral<T> elementType(TypeLiteral<Optional<T>> optionalType) {
        return (TypeLiteral<T>) optionalType.getReturnType(GET_METHOD);
    }

    public static <T> TypeToken<T> elementType(Type optionalType) {
        return elementType((TypeToken<Optional<T>>) TypeToken.of(optionalType));
    }

    /**
     * If the given {@link Optional} is present, return the result of combining its value
     * with the identity value, using the given combiner function. Otherwise, return the
     * identity value directly.
     */
    public static <T, R> R reduce(R identity, Optional<T> optional, BiFunction<? super R, ? super T, ? extends R> combiner) {
        return optional.<R>map(value -> combiner.apply(identity, value))
                       .orElse(identity);
    }

    public static <T, U> Optional<Pair<T, U>> both(Optional<T> t, Optional<U> u) {
        return mapBoth(t, u, Pair::of);
    }

    public static <T, U, R> Optional<R> mapBoth(Optional<T> t, Optional<U> u, BiFunction<T, U, R> mapper) {
        return flatMapBoth(t, u, mapper.andThen(Optional::of));
    }

    public static <T, U, R> Optional<R> flatMapBoth(Optional<T> t, Optional<U> u, BiFunction<T, U, Optional<R>> mapper) {
        return t.isPresent() && u.isPresent() ? mapper.apply(t.get(), u.get())
                                              : Optional.empty();
    }
}
