package graphql.collect;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import graphql.Internal;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.function.Predicate;

import static graphql.Assert.assertNotNull;

@Internal
@NullMarked
public final class ImmutableKit {

    public static <T> ImmutableList<T> emptyList() {
        return ImmutableList.of();
    }

    public static <T> ImmutableList<T> nonNullCopyOf(@Nullable Collection<T> collection) {
        return collection == null ? emptyList() : ImmutableList.copyOf(collection);
    }

    public static <K, V> ImmutableMap<K, V> emptyMap() {
        return ImmutableMap.of();
    }

    public static <K, V> ImmutableMap<K, V> addToMap(Map<K, V> existing, K newKey, V newVal) {
        return ImmutableMap.<K, V>builder().putAll(existing).put(newKey, newVal).build();
    }

    public static <T> ImmutableList<T> concatLists(List<T> l1, List<T> l2) {
        return ImmutableList.<T>builderWithExpectedSize(l1.size() + l2.size()).addAll(l1).addAll(l2).build();
    }

    /**
     * This is more efficient than `c.stream().map().collect()` because it does not create the intermediate objects needed
     * for the flexible style.  Benchmarking has shown this to outperform `stream()`.
     *
     * @param collection the collection to map
     * @param mapper     the mapper function
     * @param <T>        for two
     * @param <R>        for result
     *
     * @return a map immutable list of results
     */
    public static <T, R> ImmutableList<R> map(Collection<? extends T> collection, Function<? super T, ? extends R> mapper) {
        assertNotNull(collection);
        assertNotNull(mapper);
        ImmutableList.Builder<R> builder = ImmutableList.builderWithExpectedSize(collection.size());
        for (T t : collection) {
            R r = mapper.apply(t);
            builder.add(r);
        }
        return builder.build();
    }

    /**
     * This is more efficient than `c.stream().filter().collect()` because it does not create the intermediate objects needed
     * for the flexible style.  Benchmarking has shown this to outperform `stream()`.
     *
     * @param collection the collection to map
     * @param filter     the filter predicate
     * @param <T>        for two
     *
     * @return a map immutable list of results
     */
    public static <T> ImmutableList<T> filter(Collection<? extends T> collection, Predicate<? super T> filter) {
        assertNotNull(collection);
        assertNotNull(filter);
        return filterAndMap(collection, filter, Function.identity());
    }

    /**
     * This is more efficient than `c.stream().filter().map().collect()` because it does not create the intermediate objects needed
     * for the flexible style.  Benchmarking has shown this to outperform `stream()`.
     *
     * @param collection the collection to map
     * @param filter     the filter predicate
     * @param mapper     the mapper function
     * @param <T>        for two
     * @param <R>        for result
     *
     * @return a map immutable list of results
     */
    public static <T, R> ImmutableList<R> filterAndMap(Collection<? extends T> collection, Predicate<? super T> filter, Function<? super T, ? extends R> mapper) {
        assertNotNull(collection);
        assertNotNull(mapper);
        assertNotNull(filter);
        ImmutableList.Builder<R> builder = ImmutableList.builderWithExpectedSize(collection.size());
        for (T t : collection) {
            if (filter.test(t)) {
                R r = mapper.apply(t);
                builder.add(r);
            }
        }
        return builder.build();
    }

    public static <T> ImmutableList<T> flatMapList(Collection<List<T>> listLists) {
        ImmutableList.Builder<T> builder = ImmutableList.builder();
        for (List<T> t : listLists) {
            builder.addAll(t);
        }
        return builder.build();
    }


    /**
     * This will map a collection of items but drop any that are null from the input.
     * This is more efficient than `c.stream().map().collect()` because it does not create the intermediate objects needed
     * for the flexible style.  Benchmarking has shown this to outperform `stream()`.
     *
     * @param collection the collection to map
     * @param mapper     the mapper function
     * @param <T>        for two
     * @param <R>        for result
     *
     * @return a map immutable list of results
     */
    public static <T, R> ImmutableList<R> mapAndDropNulls(Collection<? extends T> collection, Function<? super T, ? extends R> mapper) {
        assertNotNull(collection);
        assertNotNull(mapper);
        ImmutableList.Builder<R> builder = ImmutableList.builderWithExpectedSize(collection.size());
        for (T t : collection) {
            R r = mapper.apply(t);
            if (r != null) {
                builder.add(r);
            }
        }
        return builder.build();
    }

    /**
     * This constructs a new Immutable list from an existing collection and adds a new element to it.
     *
     * @param existing    the existing collection
     * @param newValue    the new value to add
     * @param extraValues more values to add
     * @param <T>         for two
     *
     * @return an Immutable list with the extra items.
     */
    @SafeVarargs
    public static <T> ImmutableList<T> addToList(Collection<? extends T> existing, T newValue, T... extraValues) {
        assertNotNull(existing);
        assertNotNull(newValue);
        int expectedSize = existing.size() + 1 + extraValues.length;
        ImmutableList.Builder<T> newList = ImmutableList.builderWithExpectedSize(expectedSize);
        newList.addAll(existing);
        newList.add(newValue);
        for (T extraValue : extraValues) {
            newList.add(extraValue);
        }
        return newList.build();
    }

    /**
     * This constructs a new Immutable set from an existing collection and adds a new element to it.
     *
     * @param existing    the existing collection
     * @param newValue    the new value to add
     * @param extraValues more values to add
     * @param <T>         for two
     *
     * @return an Immutable Set with the extra items.
     */
    @SafeVarargs
    public static <T> ImmutableSet<T> addToSet(Collection<? extends T> existing, T newValue, T... extraValues) {
        assertNotNull(existing);
        assertNotNull(newValue);
        int expectedSize = existing.size() + 1 + extraValues.length;
        ImmutableSet.Builder<T> newSet = ImmutableSet.builderWithExpectedSize(expectedSize);
        newSet.addAll(existing);
        newSet.add(newValue);
        for (T extraValue : extraValues) {
            newSet.add(extraValue);
        }
        return newSet.build();
    }


    /**
     * Filters a variable args array to a list
     *
     * @param filter the predicate the filter with
     * @param args   the variable args
     * @param <T>    fot two
     *
     * @return a filtered list
     */
    @SafeVarargs
    public static <T> List<T> filterVarArgs(Predicate<? super T> filter, T... args) {
        if (args.length == 0) {
            return ImmutableList.of();
        }
        ImmutableList.Builder<T> builder = ImmutableList.builderWithExpectedSize(args.length);
        for (T arg : args) {
            if (filter.test(arg)) {
                builder.add(arg);
            }
        }
        return builder.build();
    }
}