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

package net.ranides.assira.collection;

import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.Set;
import java.util.function.IntPredicate;
import java.util.function.IntUnaryOperator;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.iterators.IntIterator;
import net.ranides.assira.collection.iterators.IntIteratorUtils;
import net.ranides.assira.collection.iterators.IteratorUtils;
import net.ranides.assira.functional.covariant.IntProjectionFunction;
import net.ranides.assira.functional.covariant.ProjectionFunction;

/**
 * Static methods working with IntCollection interface.
 * Generally, empty result is signalled by throwing exception because
 * we try to avoid boxing operations or OptionalInt wrappers.
 *
 * @author Ranides Atterwim {@literal <ranides@gmail.com>}
 */
@UtilityClass
public final class IntCollectionUtils {

	/**
	 * Returns first element inside collection, or throws if collection is empty.
	 * It uses iterator, so it respects ordering.
	 *
	 * @param values values
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int first(IntCollection values) {
		return IntIteratorUtils.first(values.iterator());
	}

	/**
	 * Returns first element inside collection which satisfies predicate.
	 * Throws if collection is empty or no element was accepted by predicate.
	 * It uses iterator, so it respects ordering.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int first(IntCollection values, IntPredicate predicate) {
		return IntIteratorUtils.first(values.iterator(), predicate);
	}

	/**
	 * Returns last element inside collection, or throws if collection is empty.
	 * It uses iterator, so it respects order, but it must traverse whole collection.
	 * If you can, lets try IntListUtils.
	 *
	 * @param values values
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int last(IntCollection values) {
		return IntIteratorUtils.last(values.iterator());
	}

	/**
	 * Returns last element inside collection which satisfies predicate.
	 * Throws 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 IntListUtils.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int last(IntCollection values, IntPredicate predicate) {
		return IntIteratorUtils.last(values.iterator(), predicate);
	}

	/**
	 * Returns filtered read-only view of provided IntCollection.
	 * Changes introduced to source collection are visible in view.
	 *
	 * It is implemented on top of IntIterator, so it respects order.
	 * It has effective implementation of {@code contains}, so it can be used to filter IntSets.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return view
	 */
	public static IntCollection filter(IntCollection values, IntPredicate predicate) {
		return new AIntCollection() {
			@Override
			public IntIterator iterator() {
				return IntIteratorUtils.filter(values.iterator(), predicate);
			}

			@Override
			public int size() {
				return IntIteratorUtils.size(iterator());
			}

			@Override
			public boolean contains(int k) {
				return values.contains(k) && predicate.test(k);
			}
		};
	}

	/**
	 * 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.
	 *
	 * Please note, that it allows only mapping preserving type "int".
	 * If you want to map to objects, please use CollectionUtils.
	 *
	 * @param values values
	 * @param function function
	 * @return view
	 */
	public static IntCollection map(IntCollection values, IntUnaryOperator function) {
        return new AIntCollection() {
			@Override
			public int size() {
				return values.size();
			}
			@Override
			public IntIterator iterator() {
				return IntIteratorUtils.map(values.iterator(), function);
			}
		};
    }

	/**
	 * 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.
	 *
	 * Please note, that it allows only mapping preserving type "int".
	 * If you want to map to objects, please use CollectionUtils.
	 *
	 * @param values values
	 * @param function function
	 * @return view
	 */
	public static IntCollection map(IntCollection values, IntProjectionFunction function) {
		return new AIntCollection() {
			@Override
			public IntIterator iterator() {
				return IntIteratorUtils.map(values.iterator(), function::applyAsInt);
			}
			@Override
			public int size() {
				return values.size();
			}

			@Override
			public boolean rem(int value) {
				return values.rem(function.invert(value));
			}

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

			@Override
			public boolean contains(int value) {
				return values.contains(function.invert(value));
			}

		};
	}

	/**
	 * 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.
	 *
	 * @param a a
	 * @param b b
	 * @return bool
	 */
	public static boolean equivalent(IntCollection a, IntCollection 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)
        int[] asort = a.toIntArray();
        int[] bsort = b.toIntArray();
        Arrays.sort(asort);
        Arrays.sort(bsort);
        return Arrays.equals(asort, bsort);
	}
	
}
