package ru.yandex.qe.dispenser.domain.util;

import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Spliterators;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import com.google.common.collect.Iterators;
import com.google.common.collect.Multimap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import ru.yandex.qe.dispenser.api.Keyable;

public enum StreamUtils {
    ;

    @NotNull
    public static <T> Stream<T> stream(@NotNull final Iterable<T> iterable) {
        return iterable instanceof Collection ? ((Collection<T>) iterable).stream() : stream(iterable.iterator());
    }

    @NotNull
    public static <T> Stream<T> stream(@NotNull final Enumeration<T> enumeration) {
        return stream(Iterators.forEnumeration(enumeration));
    }

    @NotNull
    public static <T> Stream<T> stream(@NotNull final Iterator<T> iterator) {
        return StreamSupport.stream(Spliterators.spliteratorUnknownSize(iterator, 0), false);
    }

    @NotNull
    public static <T> Stream<T> instances(@NotNull final Collection<?> col, @NotNull final Class<T> clazz) {
        return instances(col.stream(), clazz);
    }

    @NotNull
    public static <T extends R, R> Stream<T> instances(@NotNull final R[] array, @NotNull final Class<T> clazz) {
        return instances(Arrays.stream(array), clazz);
    }

    @NotNull
    public static <T> Stream<T> instances(@NotNull final Stream<?> stream, @NotNull final Class<T> clazz) {
        return stream.filter(clazz::isInstance).map(clazz::cast);
    }

    @NotNull
    public static <T> Stream<T> ofNullable(@Nullable final Collection<T> col) {
        return Optional.ofNullable(col).map(Collection::stream).orElse(Stream.empty());
    }

    public static <T, R> void forEach(@NotNull final Stream<T> s1,
                                      @NotNull final Stream<R> s2,
                                      @NotNull final BiConsumer<? super T, ? super R> consumer) {
        final Iterator<T> it1 = s1.iterator();
        final Iterator<R> it2 = s2.iterator();
        while (it1.hasNext() && it2.hasNext()) {
            consumer.accept(it1.next(), it2.next());
        }
    }

    @NotNull
    public static <K extends Comparable<K>, T extends Keyable<K>> Map<K, T> keyIndex(@NotNull final Stream<T> stream) {
        return toMap(stream, Keyable::getKey);
    }

    @NotNull
    public static <K, T> Map<K, T> toMap(@NotNull final Stream<T> stream, @NotNull final Function<T, K> keyMapper) {
        return stream.collect(Collectors.toMap(keyMapper, Function.identity()));
    }

    @NotNull
    public static <K, T> Multimap<K, T> toMultimap(@NotNull final Stream<T> stream, @NotNull final Function<T, K> keyMapper) {
        return stream.collect(MoreCollectors.toLinkedMultimap(keyMapper, Function.identity()));
    }

    @NotNull
    public static <T> T requireSingle(@NotNull final Stream<T> stream) {
        return requireSingle(stream, null);
    }

    @NotNull
    public static <T> T requireSingle(@NotNull final Stream<T> stream, @Nullable final String message) {
        final List<T> list = stream.limit(2).collect(Collectors.toList());
        if (list.isEmpty()) {
            final String cause = "stream is empty";
            throw new IllegalStateException(message != null ? String.format("%s (%s)", message, cause) : cause + "!");
        }
        if (list.size() > 1) {
            final String cause = "stream has more than one element";
            throw new IllegalStateException(message != null ? String.format("%s (%s)", message, cause) : cause + "!");
        }
        return list.get(0);
    }

    @SafeVarargs
    public static <T> Stream<T> concat(@NotNull final Stream<T>... streams) {
        return Stream.of(streams).flatMap(Function.identity());
    }

    @NotNull
    public static <T> Stream<T> getAllDescendants(@NotNull final Collection<T> items,
                                                  @NotNull final Function<T, @NotNull Collection<T>> childSupplier) {
        return items.stream()
                .flatMap(item -> getAllDescendants(item, childSupplier));
    }

    @NotNull
    public static <T> Stream<T> getAllDescendants(@NotNull final T item,
                                                  @NotNull final Function<T, @NotNull Collection<T>> childSupplier) {
        return Stream.concat(Stream.of(item), getAllDescendants(childSupplier.apply(item), childSupplier));
    }
}
