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

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.BiFunction;
import java.util.function.BiPredicate;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import com.google.common.collect.HashMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Table;
import org.apache.commons.lang3.tuple.Pair;
import org.jetbrains.annotations.NotNull;

import ru.yandex.qe.dispenser.api.Keyable;
import ru.yandex.qe.dispenser.domain.index.LongIndexable;

public enum CollectionUtils {
    ;

    @NotNull
    public static <T> List<T> copyToUnmodifiableList(@NotNull final Collection<T> origin) {
        return Collections.unmodifiableList(new ArrayList<>(origin));
    }

    @NotNull
    public static <T> Set<T> copyToUnmodifiableSet(@NotNull final Collection<T> origin) {
        return Collections.unmodifiableSet(new HashSet<>(origin));
    }

    @NotNull
    public static <K, T> Map<K, Integer> count(@NotNull final Collection<T> col, @NotNull final Function<T, K> keyMapper,
                                               @NotNull final ToIntFunction<T> valueMapper) {
        return col.stream().collect(Collectors.toMap(keyMapper, valueMapper::applyAsInt, MoreFunctions.count()));
    }

    @NotNull
    public static <K, T> Map<K, Long> sum(@NotNull final Collection<T> col, @NotNull final Function<T, K> keyMapper,
                                          @NotNull final ToLongFunction<T> valueMapper) {
        return col.stream().collect(Collectors.toMap(keyMapper, valueMapper::applyAsLong, MoreFunctions.sum()));
    }

    @NotNull
    public static <T extends LongIndexable> Set<Long> ids(@NotNull final Collection<T> col) {
        return col.stream().map(LongIndexable::getId).collect(Collectors.toSet());
    }

    @NotNull
    public static <T extends LongIndexable> Map<Long, T> index(@NotNull final Collection<T> col) {
        return toMap(col, LongIndexable::getId);
    }

    @NotNull
    public static <T extends Keyable<K>, K extends Comparable<K>> Set<K> keys(@NotNull final Collection<T> col) {
        return map(col, Keyable::getKey);
    }

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

    @NotNull
    public static <K, T> Map<K, T> toMap(@NotNull final Collection<T> col, @NotNull final Function<T, K> keyMapper) {
        return StreamUtils.toMap(col.stream(), keyMapper);
    }

    @NotNull
    public static <K, T> Multimap<K, T> toMultimap(@NotNull final Collection<T> col, @NotNull final Function<T, K> keyMapper) {
        return StreamUtils.toMultimap(col.stream(), keyMapper);
    }

    @NotNull
    public static <K, V> Multimap<K, V> toMultimap(@NotNull final K key, @NotNull final V value) {
        return toMultimap(key, Collections.singleton(value));
    }

    @NotNull
    public static <K, V> Multimap<K, V> toMultimap(@NotNull final K key, @NotNull final Iterable<V> values) {
        final Multimap<K, V> multimap = HashMultimap.create();
        multimap.putAll(key, values);
        return multimap;
    }

    @NotNull
    public static <K, V> Multimap<K, V> toMultimap(@NotNull final Collection<K> keys, @NotNull final V value) {
        return keys.stream().collect(MoreCollectors.toLinkedMultimap(Function.identity(), k -> value));
    }

    @NotNull
    public static <R, C> Multimap<R, C> keyMultimap(@NotNull final Table<R, C, ?> table) {
        return table.cellSet().stream().collect(MoreCollectors.toLinkedMultimap(Table.Cell::getRowKey, Table.Cell::getColumnKey));
    }

    @NotNull
    public static <T, R> Set<R> map(@NotNull final Collection<T> list, @NotNull final Function<T, R> f) {
        return list.stream().map(f).collect(Collectors.toSet());
    }

    @NotNull
    public static <K, V, T> Stream<T> map(@NotNull final Map<K, V> map, @NotNull final BiFunction<K, V, T> f) {
        return map.entrySet().stream().map(e -> f.apply(e.getKey(), e.getValue()));
    }

    @NotNull
    public static <K, V, T> Stream<T> reduce(@NotNull final Map<K, V> map, @NotNull final BiFunction<K, V, T> f) {
        return map.entrySet().stream().map(e -> f.apply(e.getKey(), e.getValue()));
    }

    @NotNull
    public static <T> Set<T> filter(@NotNull final Collection<T> col, @NotNull final Predicate<T> filter) {
        return col.stream().filter(filter).collect(Collectors.toSet());
    }

    @NotNull
    public static <K, V> Map<K, V> filter(@NotNull final Map<K, V> map, @NotNull final BiPredicate<K, V> filter) {
        return map.entrySet().stream()
                .filter(e -> filter.test(e.getKey(), e.getValue()))
                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
    }

    @NotNull
    public static <K, V> Multimap<K, V> filter(@NotNull final Multimap<K, V> map, @NotNull final BiPredicate<K, V> filter) {
        return map.entries().stream()
                .filter(e -> filter.test(e.getKey(), e.getValue()))
                .collect(MoreCollectors.toLinkedMultimap(Map.Entry::getKey, Map.Entry::getValue));
    }

    @NotNull
    public static <T> T first(@NotNull final Iterable<T> col) {
        return Objects.requireNonNull(Iterables.getFirst(col, null), "Collection is empty!");
    }

    @NotNull
    public static <K, V> Multimap<K, V> subtractMultimaps(@NotNull final Multimap<K, V> minuendMap,
                                                          @NotNull final Multimap<K, V> subtrahendMap) {
        return Multimaps.filterEntries(minuendMap, e -> !subtrahendMap.containsEntry(e.getKey(), e.getValue()));
    }

    @NotNull
    public static <K, V> SetMultimap<K, V> subtractMultimaps(@NotNull final SetMultimap<K, V> minuendMap,
                                                             @NotNull final SetMultimap<K, V> subtrahendMap) {
        return Multimaps.filterEntries(minuendMap, e -> !subtrahendMap.containsEntry(e.getKey(), e.getValue()));
    }

    @NotNull
    public static <T> Set<T> setsUnion(@NotNull final Set<T>... sets) {
        return Arrays.stream(sets)
                .flatMap(Set::stream)
                .collect(Collectors.toSet());
    }

    public static <K, V> Map<K, Set<V>> multimapAsMap(@NotNull final SetMultimap<K, V> multimap, @NotNull final Collection<K> keys) {

        if (keys.isEmpty()) {
            return Collections.emptyMap();
        }

        final HashMap<K, Set<V>> result = new HashMap<>();

        for (final K key : keys) {
            result.put(key, multimap.get(key));
        }

        return result;
    }

    public static <T extends LongIndexable> List<Pair<T, T>> outerJoin(final Collection<T> left, final Collection<T> right) {
        final Map<Long, T> leftById = left.stream()
                .collect(Collectors.toMap(LongIndexable::getId, Function.identity(), (l, r) -> l));
        final Map<Long, T> rightById = right.stream()
                .collect(Collectors.toMap(LongIndexable::getId, Function.identity(), (l, r) -> l));
        return Stream.of(left, right)
                .flatMap(Collection::stream)
                .map(LongIndexable::getId)
                .distinct()
                .map(id -> Pair.of(leftById.get(id), rightById.get(id)))
                .collect(Collectors.toList());
    }
}
