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

package net.ranides.assira.collection;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.functional.covariant.ProjectionFunction;
import net.ranides.assira.generic.Ref;

import java.util.*;
import java.util.function.Function;
import java.util.function.Predicate;

/**
 * Static methods working with Collection interface.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class CollectionUtils {

	/**
	 * Returns first element inside collection, or none if collection is empty.
	 * It uses iterator, so it respects ordering.
	 *
	 * @param values values
	 * @param <T> T
	 * @return optional
	 */
	public static <T> Ref<T> first(Collection<? 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.
	 * It uses iterator, so it respects ordering.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @param <T> T
	 * @return optional
	 */
	public static <T> Ref<T> first(Collection<? 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(Collection<? extends T> values) {
		return IteratorUtils.last(values.iterator());
	}

	/**
	 * 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(Collection<? extends T> values, Predicate<? super T> predicate) {
		return IteratorUtils.last(values.iterator(), predicate);
	}

	/**
	 * Returns filtered read-only view of provided collection.
	 * Changes introduced to source collection are visible in view.
	 *
	 * It is implemented on top of iterator, so it respects order.
	 * It has effective implementation of {@code contains}, so it can be used to filter  Sets.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @param <T> T
	 * @return view
	 */
    @SuppressWarnings("unchecked")
	public static <T> Collection<T> filter(Collection<? extends T> values, Predicate<? super T> predicate) {
		return new ACollection<T>() {
			@Override
			public Iterator<T> iterator() {
				return IteratorUtils.filter(values.iterator(), predicate);
			}

			@Override
			public int size() {
				return IteratorUtils.size(iterator());
			}
			
			@Override
			public boolean contains(Object value) {
				return predicate.test((T)value) && values.contains(value);
			}
		};
	}

	/**
	 * Returns read-only view which maps every element using provided function.
	 * Changes introduced to source collection are visible in view.
	 *
	 * It is implemented on top of iterator, so it respects order.
	 * It does not provide effective implementation of {@code contains} method.
	 *
	 * @param values values
	 * @param function function
	 * @param <S> source type
	 * @param <T> target type
	 * @return view
	 */
	public static <S,T> Collection<T> map(Collection<? extends S> values, Function<? super S,? extends T> function) {
        return new ACollection<T>() {
			@Override
			public Iterator<T> iterator() {
				return IteratorUtils.map(values.iterator(), function);
			}
			@Override
			public int size() {
				return values.size();
			}
		};
    }

	/**
	 * Returns mutable view which maps every element using provided projection.
	 * Changes introduced to source collection are visible in view and vice versa.
	 *
	 * It is implemented on top of iterator, so it respects order.
	 * It has effective implementation of {@code contains}, so it can be used to filter Sets.
	 * It allows removing and adding new elements.
	 *
	 * @param values values
	 * @param function function
	 * @return view
	 * @param <S> source type
	 * @param <T> target type
	 */
	public static <S,T> Collection<T> map(Collection<S> values, ProjectionFunction<S,T> function) {
        return new ACollection<T>() {
			@Override
			public Iterator<T> iterator() {
				return IteratorUtils.map(values.iterator(), function);
			}
			@Override
			public int size() {
				return values.size();
			}

            @Override
			@SuppressWarnings("unchecked")
            public boolean remove(Object value) {
				Object v;
				try {
					v = function.invert((T)value);
				} catch(ClassCastException _e) {
					return false;
				}
                return values.remove(v);
            }

            @Override
            public boolean add(T value) {
                return values.add(function.invert(value));
            }

            @Override
			@SuppressWarnings("unchecked")
            public boolean contains(Object value) {
				Object v;
				try {
					v = function.invert((T)value);
				} catch(ClassCastException _e) {
					return false;
				}
                return values.contains(v);
            }
            
		};
    }

	/**
	 * Checks if provided collections contain equal elements.
	 * Ignores order of elements.
	 * It tries to avoid O(N*N) comparisons.
	 * It uses {@code containsAll} if one of collection is Set.
	 *
     * It converts collections to sorted arrays in general case.
	 * It uses natural sort, which means that T must be Comparable if none collection is Set.
	 *
	 * @param a a
	 * @param b b
	 * @param <T> T
	 * @return bool
	 */
    public static <T> boolean equivalent(Collection<? extends T> a, Collection<? extends T> b) {
		if( a.size() != b.size() ){
            return false;
        }
		// Set powinien oferować dobrą implementację funkcji containsAll
		if(a instanceof Set<?>) {
			return a.containsAll(b);
		}
		if(b instanceof Set<?>) {
			return b.containsAll(a);
		}
		
        // najpierw spróbujmy tak sprawdzić - takie porównanie powinno być względnie tanie
        // (złożoność O(N)) w porówaniu do następnej metody
        if( a.equals(b) || b.equals(a) ) {
            return true;
        }
		
        // dużo kopiowania, na dodatek mamy sortowanie, czyli złożoność
        // na pamięć O(N) oraz operacje O(N*log(N)) + O(N)
        Object[] asort = a.toArray();
        Object[] bsort = b.toArray();
        Arrays.sort(asort);
        Arrays.sort(bsort);
        return Arrays.equals(asort, bsort);
	}

}
