/*
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 * @copyright Ranides Atterwim
 * @license WTFPL
 * @url http://ranides.net/projects/assira
 */
package net.ranides.assira.collection.iterators;

import lombok.experimental.UtilityClass;
import net.ranides.assira.functional.Functions.EachFunction;
import net.ranides.assira.functional.checked.CheckedSupplier;
import net.ranides.assira.generic.CompareUtils;
import net.ranides.assira.generic.Ref;

import java.util.*;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Stream;

/**
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class IterableUtils {

    /**
     * Returns first element inside collection, or none if collection is empty.
     *
     * @param values values
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> first(Iterable<? extends T> values) {
        return IteratorUtils.first(values.iterator());
    }

    /**
     * Returns first element inside collection which satisfies predicate.
     * Returns none if collection is empty or no element was accepted by predicate.
     *
     * @param values values
     * @param predicate predicate
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> first(Iterable<? extends T> values, Predicate<? super T> predicate) {
        return IteratorUtils.first(values.iterator(), predicate);
    }

    /**
     * Returns last element inside collection, or none if collection is empty.
     * It uses iterator, so it respects order, but it must traverse whole collection.
     * If you can, lets try ListUtils.
     *
     * @param values values
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> last(Iterable<? extends T> values) {
        return IteratorUtils.last(values.iterator());
    }

    /**
     * Returns element at specified position or none if collection contains less elements.
     * It must traverse through all previous elements.
     * If you can, lets try ListUtils.
     *
     * @param values values
     * @param index index
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> at(Iterable<? extends T> values, int index) {
        return IteratorUtils.at(values.iterator(), index);
    }

    /**
     * Returns last element inside collection which satisfies predicate.
     * Returns none if collection is empty or no element was accepted by predicate.
     * It uses iterator, so it respects order, but it must traverse whole collection.
     * If you can, lets try ListUtils.
     *
     * @param values values
     * @param predicate predicate
     * @param <T> T
     * @return optional
     */
    public static <T> Ref<T> last(Iterable<? extends T> values, Predicate<? super T> predicate) {
        return IteratorUtils.last(values.iterator(), predicate);
    }

    /**
     * Counts number of elements for specified collection.
     * It must iterate through all elements, so if you can, check collection size directly.
     *
     * @param values values
     * @return int
     */
    public static int size(Iterable<?> values) {
        return IteratorUtils.size(values.iterator());
    }

    /**
     * Returns filtered view of provided collection.
     * Changes introduced to source collection are visible in view.
     *
     * @param values values
     * @param predicate predicate
     * @param <T> T
     * @return view
     */
    public static <T> Iterable<T> filter(Iterable<? extends T> values, Predicate<? super T> predicate) {
        return () -> IteratorUtils.filter(values.iterator(), predicate);
    }

    /**
     * Returns limited view of provided collection.
     * Limited iterable stops at first unmatched element.
     *
     * @param values values
     * @param predicate predicate
     * @param <T> T
     * @return view
     */
    public static <T> Iterable<T> limit(Iterable<? extends T> values, Predicate<? super T> predicate) {
        return () -> IteratorUtils.limit(values.iterator(), predicate);
    }

    /**
     * Returns limited view of provided collection.
     * Limited iterable returns no more than "limit" elements.
     *
     * @param values values
     * @param limit limit
     * @param <T> T
     * @return view
     */
    public static <T> Iterable<T> limit(Iterable<T> values, int limit) {
        return () -> IteratorUtils.limit(values.iterator(), limit);
    }

    /**
     * Returns view which maps every element using provided function.
     * Changes introduced to source collection are visible in view.
     *
     * @param values values
     * @param function function
     * @param <S> source type
     * @param <T> target type
     * @return view
     */
    public static <S,T> Iterable<T> map(Iterable<? extends S> values, EachFunction<? super S,? extends T> function) {
        return () -> IteratorUtils.map(values.iterator(), function);
    }

    /**
     * Returns view which maps every element using provided function.
     * Changes introduced to source collection are visible in view.
     *
     * @param values values
     * @param function function
     * @param <S> source type
     * @param <T> target type
     * @return view
     */
    public static <S,T> Iterable<T> map(Iterable<? extends S> values, Function<? super S,? extends T> function) {
        return () -> IteratorUtils.map(values.iterator(), function);
    }

    /**
     * Returns view which lazily expands all nested Iterable and merges into one.
     * Flat iterators move forward only as much as necessary (which means one element at time).
     *
     * @param values values
     * @param <T> T
     * @return view
     */
    public static <T> Iterable<T> flat(Iterable<? extends Iterable<? extends T>> values) {
        return () -> IteratorUtils.flat(map(values, Iterable::iterator).iterator());
    }

    /**
     * Returns view which lazily expands all nested Iterable and merges into one.
     * Flat iterators move forward only as much as necessary (which means one element at time).
     *
     * @param values values
     * @param <T> T
     * @return view
     */
    @SafeVarargs
    public static <T> Iterable<T> concat(Iterable<? extends T>... values) {
        return flat(Arrays.asList(values));
    }

    /**
     * Creates new sequential Stream from provided iterable.
     *
     * @param values values
     * @param <T> T
     * @return stream
     */
	public static <T> Stream<T> stream(Iterable<? extends T> values) {
        return IteratorUtils.stream(values.iterator());
	}

    /**
     * Inserts all elements from iterable into provided collection.
     *
     * @param values values
     * @param target target
     * @param <T> T
     * @param <C> C
     * @return target
     */
    public static <T, C extends Collection<? super T>> C collect(Iterable<? extends T> values, C target) {
        return IteratorUtils.collect(values.iterator(), target);
	}

    /**
     * Checks if provided collections contain equal elements.
     * Order of elements is important.
     *
     * @param a a
     * @param b b
     * @param <T> T
     * @return bool
     */
    public static <T> boolean equals(Iterable<? extends T> a, Iterable<? extends T> b) {
        return equals(a, b, CompareUtils::equals);
    }

    /**
     * Checks if provided collections contain equal elements using provided comparison function.
     * Order of elements is important.
     *
     * @param a a
     * @param b b
     * @param predicate predicate
     * @param <T> T
     * @return bool
     */
    public static <T> boolean equals(Iterable<? extends T> a, Iterable<? extends T> b, BiPredicate<? super T,? super T> predicate) {
        return IteratorUtils.equals(a.iterator(), b.iterator(), predicate);
    }

    /**
     * Checks if provided collections contain equal elements.
     * Order of elements is important.
     * It is three-value version, which returns +1, 0, -1
     *
     * @param a a
     * @param b b
     * @param <T> T
     * @return int
     */
    public static <T> int compare(Iterable<? extends T> a, Iterable<? extends T> b) {
        return compare(a, b, CompareUtils.comparator());
    }

    /**
     * Checks if provided collections contain equal elements using provided comparison function.
     * Order of elements is important.
     * It is three-value version, which returns +1, 0, -1
     *
     * @param a a
     * @param b b
     * @param cmp cmp
     * @param <T> T
     * @return int
     */
    public static <T> int compare(Iterable<? extends T> a, Iterable<? extends T> b, Comparator<? super T> cmp) {
        return IteratorUtils.compare(a.iterator(), b.iterator(), cmp);
    }

    /**
     * Iterates through all values and removes every element which is matched by predicate.
     *
     * @param values values
     * @param predicate predicate
     * @param <T> T
     */
    public static <T> void remove(Iterable<? extends T> values, Predicate<? super T> predicate) {
        IteratorUtils.remove(values.iterator(), predicate);
    }

    /**
     * Creates new collection which returns values generated by provided supplier.
     * Iterator stops when generator returns "optional.empty"
     *
     * Rethrows all unchecked exceptions from generator.
     * Wraps by RuntimeException all checked exceptions from generator.
     *
     * Probably supplier must be statefull.
     *
     * @param generator generator
     * @param <T> T
     * @return iterable
     */
    public static <T> Iterable<T> iterable(CheckedSupplier<Ref<T>, ?> generator) {
        return () -> IteratorUtils.finite(generator);
    }

    /**
     * Creates safe view of collection which will never throw any exception except NoSuchElementException.
     * All exceptions are silently ignored.
     * Behaviour of iterator methods in such case:
     *  - "hasNext" returns false
     *  - "next" throws NoSuchElementException
     *  - "remove" does nothing
     *
     * @param values values
     * @param <T> T
     * @return iterable
     */
    public static <T> Iterable<T> safe(Iterable<T> values) {
        return safe(values, Exception.class, e -> {});
    }

    /**
     * Creates safe view of collection which will never throw exception of specified "type".
     * Exceptions of "type" are silently ignored.
     * Behaviour of iterator methods in such case:
     *   - "hasNext" returns false
     *   - "next" throws NoSuchElementException
     *   - "remove" does nothing
     *
     * Please note, that other exceptions still will be propagated without change.
     *
     * Please note, that NoSuchElementException will be thrown as usuall.
     *
     * @param values values
     * @param type exception to ignore
     * @param <T> T
     * @param <E> E
     * @return iterable
     */
    public static <T, E extends Throwable> Iterable<T> safe(Iterable<T> values, Class<E> type) {
        return safe(values, type, e -> {});
    }

    /**
     * Creates safe view of collection which will never throw exception of specified "type".
     * Exceptions of "type" are sent to handler.
     *
     * Behaviour of iterator methods in such case:
     *   - "hasNext" returns false
     *   - "next" throws NoSuchElementException
     *   - "remove" does nothing
     *
     * Please note, that other exceptions still will be propagated without change.
     *
     * Please note, that NoSuchElementException will be thrown as usuall.
     *
     * @param values values
     * @param type exception to ignore
     * @param handler handler
     * @param <T> T
     * @param <E> E
     * @return iterable
     */
    public static <T, E extends Throwable> Iterable<T> safe(Iterable<T> values, Class<E> type, Consumer<E> handler) {
        return () -> IteratorUtils.safe(values.iterator(), type, handler);
    }

}
