/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.ignite.internal.util;

import static java.util.Collections.addAll;
import static java.util.Collections.emptyIterator;
import static java.util.Collections.emptyList;
import static java.util.Collections.unmodifiableCollection;
import static java.util.Collections.unmodifiableSet;

import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
import it.unimi.dsi.fastutil.ints.IntSet;
import it.unimi.dsi.fastutil.ints.IntSets;
import java.util.AbstractCollection;
import java.util.AbstractList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Function;
import java.util.function.Predicate;
import org.jetbrains.annotations.Nullable;

/**
 * Utility class provides various method to work with collections.
 */
public final class CollectionUtils {
    /** Special object for determining that there is no next element. */
    private static final Object NO_NEXT_ELEMENT = new Object();

    /** Stub. */
    private CollectionUtils() {
        // No op.
    }

    /**
     * Tests if the given collection is either {@code null} or empty.
     *
     * @param col Collection to check.
     * @return Whether or not the given collection is {@code null} or empty.
     */
    public static boolean nullOrEmpty(@Nullable Collection<?> col) {
        return col == null || col.isEmpty();
    }

    /**
     * Tests if the given collection is either {@code null} or empty.
     *
     * @param c Collection to test.
     * @return Whether or not the given collection is {@code null} or empty.
     */
    public static boolean nullOrEmpty(@Nullable Iterable<?> c) {
        return c == null || (c instanceof Collection<?> ? ((Collection<?>) c).isEmpty() : !c.iterator().hasNext());
    }

    /**
     * Tests if the given collection is either {@code null} or empty.
     *
     * @param col Collection to check.
     * @return Whether or not the given collection is {@code null} or empty.
     */
    public static boolean nullOrEmpty(@Nullable Map<?, ?> col) {
        return col == null || col.isEmpty();
    }

    /**
     * Gets first element from given list or returns {@code null} if list is empty.
     *
     * @param list List to retrieve the first element.
     * @param <T> Type of the elements of the list.
     * @return The first element of the given list or {@code null} in case the list is empty.
     */
    public static <T> @Nullable T first(List<? extends T> list) {
        if (nullOrEmpty(list)) {
            return null;
        }

        return list.get(0);
    }

    /**
     * Gets first element from given collection or returns {@code null} if collection is empty.
     *
     * @param col Collection to retrieve the first element.
     * @param <T> Type of the elements of the collection.
     * @return The first element of the given collection or {@code null} in case the collection is empty.
     */
    public static <T> @Nullable T first(Collection<? extends T> col) {
        if (nullOrEmpty(col)) {
            return null;
        }

        return col.iterator().next();
    }

    /**
     * Returns first element from the given iterable or returns {@code null} if the list is empty.
     *
     * @param iterable Iterable to retrieve the first element.
     * @param <T> Type of the elements of the list.
     * @return The first element of the given iterable or {@code null} in case the iterable is null or empty.
     */
    public static <T> @Nullable T first(Iterable<? extends T> iterable) {
        if (iterable == null) {
            return null;
        }

        Iterator<? extends T> it = iterable.iterator();

        if (!it.hasNext()) {
            return null;
        }

        return it.next();
    }

    /**
     * Union set and items.
     *
     * @param set Set.
     * @param ts Items.
     * @param <T> Type of the elements of set and items..
     * @return Immutable union of set and items.
     */
    @SafeVarargs
    public static <T> Set<T> union(@Nullable Set<T> set, @Nullable T... ts) {
        if (nullOrEmpty(set)) {
            return ts == null || ts.length == 0 ? Set.of() : Set.of(ts);
        }

        if (ts == null || ts.length == 0) {
            return unmodifiableSet(set);
        }

        Set<T> res = new HashSet<>(set);

        addAll(res, ts);

        return unmodifiableSet(res);
    }

    /**
     * Concatenates collections.
     *
     * @param collections Collections.
     * @param <T> Type of the elements of lists.
     * @return Immutable collection concatenation.
     */
    @SafeVarargs
    public static <T> Collection<T> concat(Collection<T>... collections) {
        if (collections == null || collections.length == 0) {
            return List.of();
        }

        return new AbstractCollection<>() {
            /** {@inheritDoc} */
            @Override
            public Iterator<T> iterator() {
                return concat((Iterable<T>[]) collections).iterator();
            }

            /** {@inheritDoc} */
            @Override
            public int size() {
                int size = 0;

                for (int i = 0; i < collections.length; i++) {
                    size += collections[i].size();
                }

                return size;
            }

            /** {@inheritDoc} */
            @Override
            public boolean contains(Object o) {
                for (int i = 0; i < collections.length; i++) {
                    if (collections[i].contains(o)) {
                        return true;
                    }
                }

                return false;
            }
        };
    }

    /**
     * Create a lazy concatenation of iterables.
     *
     * <p>NOTE: {@link Iterator#remove} - not supported.
     *
     * @param iterables Iterables.
     * @param <T> Type of the elements.
     * @return Concatenation of iterables.
     */
    @SafeVarargs
    public static <T> Iterable<T> concat(@Nullable Iterable<? extends T>... iterables) {
        if (iterables == null || iterables.length == 0) {
            return Collections::emptyIterator;
        }

        return () -> new Iterator<>() {
            /** Current index at {@code iterables}. */
            int idx = 0;

            /** Current iterator. */
            Iterator<? extends T> curr = emptyIterator();

            /** {@inheritDoc} */
            @Override
            public boolean hasNext() {
                while (!curr.hasNext() && idx < iterables.length) {
                    curr = iterables[idx++].iterator();
                }

                return curr.hasNext();
            }

            /** {@inheritDoc} */
            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                } else {
                    return curr.next();
                }
            }
        };
    }

    /**
     * Create a lazy concatenation of iterators.
     *
     * <p>NOTE: {@link Iterator#remove} - not supported.
     *
     * @param iterators Iterators.
     * @param <T> Type of the elements.
     * @return Concatenation of iterators.
     */
    @SafeVarargs
    public static <T> Iterator<T> concat(@Nullable Iterator<? extends T>... iterators) {
        if (iterators == null || iterators.length == 0) {
            return emptyIterator();
        }

        return new Iterator<>() {
            /** Current index at {@code iterators}. */
            int idx = 0;

            /** Current iterator. */
            Iterator<? extends T> curr = emptyIterator();

            /** {@inheritDoc} */
            @Override
            public boolean hasNext() {
                while (!curr.hasNext() && idx < iterators.length) {
                    curr = iterators[idx++];
                }

                return curr.hasNext();
            }

            /** {@inheritDoc} */
            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                } else {
                    return curr.next();
                }
            }
        };
    }

    /**
     * Create a lazy concatenation of iterators.
     *
     * <p>NOTE: {@link Iterator#remove} - not supported.
     *
     * @param iterators Iterators.
     * @param <T> Type of the elements.
     * @return Concatenation of iterators.
     */
    public static <T> Iterator<T> concat(@Nullable Collection<Iterator<? extends T>> iterators) {
        if (iterators == null || iterators.isEmpty()) {
            return emptyIterator();
        }

        return new Iterator<>() {
            /** Super iterator. */
            final Iterator<Iterator<? extends T>> it = iterators.iterator();

            /** Current iterator. */
            Iterator<? extends T> curr = emptyIterator();

            /** {@inheritDoc} */
            @Override
            public boolean hasNext() {
                while (!curr.hasNext() && it.hasNext()) {
                    curr = it.next();
                }

                return curr.hasNext();
            }

            /** {@inheritDoc} */
            @Override
            public T next() {
                if (!hasNext()) {
                    throw new NoSuchElementException();
                } else {
                    return curr.next();
                }
            }
        };
    }

    /**
     * Concatenates lists.
     *
     * @param lists Lists.
     * @param <T> Type of the elements of lists.
     * @return Immutable list concatenation.
     */
    @SafeVarargs
    public static <T> List<T> concat(List<T>... lists) {
        if (lists == null || lists.length == 0) {
            return List.of();
        }

        return new AbstractList<>() {
            /** {@inheritDoc} */
            @Override
            public T get(int index) {
                for (List<T> list : lists) {
                    if (index >= list.size()) {
                        index -= list.size();
                    } else {
                        return list.get(index);
                    }
                }

                throw new IndexOutOfBoundsException(index);
            }

            /** {@inheritDoc} */
            @Override
            public Iterator<T> iterator() {
                return concat((Iterable<T>[]) lists).iterator();
            }

            /** {@inheritDoc} */
            @Override
            public int size() {
                int size = 0;

                for (List<T> list : lists) {
                    size += list.size();
                }

                return size;
            }

            /** {@inheritDoc} */
            @Override
            public boolean contains(Object o) {
                for (List<T> list : lists) {
                    if (list.contains(o)) {
                        return true;
                    }
                }

                return false;
            }
        };
    }

    /**
     * Create a collection view that can only be read.
     *
     * @param collection Basic collection.
     * @param mapper Conversion function.
     * @param <T1> Base type of the collection.
     * @param <T2> Type for view.
     * @return Read-only collection view.
     */
    public static <T1, T2> Collection<T2> viewReadOnly(
            @Nullable Collection<? extends T1> collection,
            @Nullable Function<? super T1, ? extends T2> mapper
    ) {
        if (nullOrEmpty(collection)) {
            return emptyList();
        }

        if (mapper == null) {
            return unmodifiableCollection((Collection<T2>) collection);
        }

        return new AbstractCollection<>() {
            /** {@inheritDoc} */
            @Override
            public Iterator<T2> iterator() {
                Iterator<? extends T1> iterator = collection.iterator();

                return new Iterator<>() {
                    /** {@inheritDoc} */
                    @Override
                    public boolean hasNext() {
                        return iterator.hasNext();
                    }

                    /** {@inheritDoc} */
                    @Override
                    public T2 next() {
                        return mapper.apply(iterator.next());
                    }
                };
            }

            /** {@inheritDoc} */
            @Override
            public int size() {
                return collection.size();
            }

            /** {@inheritDoc} */
            @Override
            public boolean isEmpty() {
                return collection.isEmpty();
            }
        };
    }

    /**
     * Create a collection view that can only be read.
     *
     * @param collection Basic collection.
     * @param mapper Conversion function.
     * @param predicate Predicate to apply to each element of basic collection.
     * @param <T1> Base type of the collection.
     * @param <T2> Type for view.
     * @return Read-only collection view.
     */
    public static <T1, T2> Collection<T2> viewReadOnly(
            @Nullable Collection<? extends T1> collection,
            @Nullable Function<? super T1, ? extends T2> mapper,
            @Nullable Predicate<? super T1> predicate
    ) {
        if (collection == null) {
            return emptyList();
        }

        if (mapper == null && predicate == null) {
            return unmodifiableCollection((Collection<T2>) collection);
        }

        return new AbstractCollection<>() {
            /** {@inheritDoc} */
            @Override
            public Iterator<T2> iterator() {
                Iterator<? extends T1> iterator = collection.iterator();

                return new Iterator<>() {
                    @Nullable
                    T1 current = advance();

                    /** {@inheritDoc} */
                    @Override
                    public boolean hasNext() {
                        return current != NO_NEXT_ELEMENT;
                    }

                    /** {@inheritDoc} */
                    @Override
                    public T2 next() {
                        T1 current = this.current;

                        if (current == NO_NEXT_ELEMENT) {
                            throw new NoSuchElementException();
                        }

                        this.current = advance();

                        return mapper == null ? (T2) current : mapper.apply(current);
                    }

                    private @Nullable T1 advance() {
                        while (iterator.hasNext()) {
                            T1 next = iterator.next();

                            if (predicate == null || predicate.test(next)) {
                                return next;
                            }
                        }

                        return (T1) NO_NEXT_ELEMENT;
                    }
                };
            }

            /** {@inheritDoc} */
            @Override
            public int size() {
                if (predicate == null) {
                    return collection.size();
                }

                int count = 0;

                for (Iterator<T2> iterator = iterator(); iterator.hasNext(); iterator.next()) {
                    count++;
                }

                return count;
            }

            /** {@inheritDoc} */
            @Override
            public boolean isEmpty() {
                if (predicate == null) {
                    return collection.isEmpty();
                }

                return size() == 0;
            }
        };
    }

    /**
     * Difference of two sets.
     *
     * @param a First set.
     * @param b Second set.
     * @param <T> Type of the elements.
     * @return Immutable set of elements of the first without the second.
     */
    public static <T> Set<T> difference(@Nullable Set<T> a, @Nullable Set<T> b) {
        if (nullOrEmpty(a)) {
            return Set.of();
        } else if (nullOrEmpty(b)) {
            return unmodifiableSet(a);
        }

        // Lazy initialization.
        Set<T> res = null;

        for (T t : a) {
            if (!b.contains(t)) {
                if (res == null) {
                    res = new HashSet<>();
                }

                res.add(t);
            }
        }

        return res == null ? Set.of() : unmodifiableSet(res);
    }

    /**
     * Get unmodifiable copy set of specified collection.
     *
     * @param coll Collection of integer to copy.
     * @return Unmodifiable copy set.
     */
    public static IntSet setOf(Collection<Integer> coll) {
        return IntSets.unmodifiable(new IntOpenHashSet(coll));
    }
}
