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

package net.ranides.assira.collection.lists;

import lombok.experimental.UtilityClass;
import net.ranides.assira.collection.IntCollectionUtils;
import net.ranides.assira.collection.Swapper;
import net.ranides.assira.collection.arrays.NativeArray.NativeComparator;
import net.ranides.assira.collection.arrays.NativeArrayAllocator;
import net.ranides.assira.collection.arrays.NativeArraySearch;
import net.ranides.assira.collection.iterators.IntListIterator;
import net.ranides.assira.collection.iterators.RandomAccessIntIterator;
import net.ranides.assira.functional.covariant.IntProjectionFunction;

import java.util.List;
import java.util.NoSuchElementException;
import java.util.RandomAccess;
import java.util.function.IntFunction;
import java.util.function.IntPredicate;
import java.util.function.IntUnaryOperator;

/**
 * Static methods working with IntList 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 IntListUtils {

	/**
	 * Returns first element inside collection, or throws if collection is empty.
	 *
	 * @param values values
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int first(IntList values) {
		if(values.isEmpty()) {
			throw new NoSuchElementException();
		}
		return values.getInt(0);
	}

	/**
	 * Returns first element inside collection which satisfies predicate.
	 * Throws if collection is empty or no element was accepted by predicate.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int first(IntList values, IntPredicate predicate) {
		return IntCollectionUtils.first(values, predicate);
	}

	/**
	 * Returns first element inside collection which satisfies predicate and is located inside specified range.
	 * Throws if range is outside collection bounds or no element was accepted by predicate.
	 *
	 * @param values values
	 * @param begin begin
	 * @param end end
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int first(IntList values, int begin, int end, IntPredicate predicate) {
		return first(values.subList(begin, end), predicate);
	}

	/**
	 * Returns last element inside collection, or throws if collection is empty.
	 *
	 * @param values values
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int last(IntList values) {
		if(values.isEmpty()) {
			throw new NoSuchElementException();
		}
		return values.getInt(values.size()-1);
	}

	/**
	 * Returns last element inside collection which satisfies predicate.
	 * Throws if collection is empty or no element was accepted by predicate.
	 *
	 * It minimizes number of operations by using backward iterator.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int last(IntList values, IntPredicate predicate) {
		IntListIterator iterator = values.listIterator(values.size());

        while(iterator.hasPrevious()) {
            int v = iterator.previousInt();
            if( predicate.test(v)) {
                return v;
            }
        }
        throw new NoSuchElementException();
	}

	/**
	 * Returns last element inside collection which satisfies predicate and is located inside specified range.
	 * Throws if range is outside collection bounds or no element was accepted by predicate.
	 *
	 * It minimizes number of operations by using backward iterator.
	 *
	 * @param values values
	 * @param begin begin
	 * @param end end
	 * @param predicate predicate
	 * @return int
	 * @throws java.util.NoSuchElementException if no result
	 */
	public static int last(IntList values, int begin, int end, IntPredicate predicate) {
		return last(values.subList(begin, end), predicate);
	}

	/**
	 * Returns new list filtered by predicate.
	 * Returned list is a copy, not a view.
	 *
	 * @param values values
	 * @param predicate predicate
	 * @return new list
	 */
	public static IntList filter(IntList values, IntPredicate predicate) {
		IntList ret = new IntArrayList();
		for(int i=0,n=values.size(); i<n; i++) {
			int v = values.getInt(i);
			if(predicate.test(v)) {
				ret.add(v);
			}
		}
		return ret;
	}

	/**
	 * Returns view which maps every element using provided function.
	 * Changes introduced to source collection are visible in view.
	 *
	 * Returned view supports removing, but not set or add operations.
	 *
	 * Please note, that it allows mapping to any object.
	 *
	 * @param values values
	 * @param function function
	 * @param <T> target type
	 * @return view
	 */
	public static <T> List<T> map(IntList values, IntFunction<? extends T> function) {
		return new VirtualList<T>() {
			
			private static final long serialVersionUID = 1L;
			
			@Override
			public T get(int index) {
				return function.apply(values.get(index));
			}

			@Override
			public int size() {
				return values.size();
			}
			
			@Override
            public T remove(int index) {
                return function.apply(values.remove(index));
            }
		};
    }

	/**
	 * Returns view which maps every element using provided function.
	 * Changes introduced to source collection are visible in view.
	 *
	 * Returned view supports removing, but not set or add operations.
	 *
	 * Please note, that it allows only mapping preserving type "int".
	 *
	 * @param values values
	 * @param function function
	 * @return view
	 */
	public static IntList map(IntList values, IntUnaryOperator function) {
		return new AIntList() {
			
			private static final long serialVersionUID = 1L;
			
			@Override
			public int getInt(int index) {
				return function.applyAsInt(values.get(index));
			}

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

			@Override
			public void add(int index, int k) {
				throw new UnsupportedOperationException();
			}

			@Override
			public int removeInt(int index) {
				return function.applyAsInt(values.removeInt(index));
			}

			@Override
			public int set(int index, int k) {
				throw new UnsupportedOperationException();
			}
		};
    }

	/**
	 * Returns mutable view which maps every element using provided projection.
	 * Changes introduced to source collection are visible in view and vice versa.
	 *
	 * Returned view supports all operation: remove, set or add.
	 *
	 * @param values values
	 * @param function function
	 * @return view
	 */
	public static IntList map(IntList values, IntProjectionFunction function) {
		return new AIntList() {
			
			private static final long serialVersionUID = 1L;
			
			@Override
			public int getInt(int index) {
				return function.applyAsInt(values.get(index));
			}

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

            @Override
            public int set(int index, int element) {
                return function.applyAsInt(values.set(index, function.invert(element)));
            }

            @Override
            public void add(int index, int element) {
                values.add(index, function.invert(element));
            }

            @Override
            public int removeInt(int index) {
                return function.applyAsInt(values.remove(index));
            }
            
		};
    }

	/**
	 * Returns new virtual list of specified size, which contains elements calculated by "function" at every access.
	 *
	 * Virtual list does not support modifications.
	 *
	 * @param size size
	 * @param function function
	 * @return virtual
	 */
	public static IntList produce(int size, IntUnaryOperator function) {
		return IntVirtualList.of(size, function);
    }

	/**
	 * Returns sublist.
	 * Returned view is correctly marked as RandomAccess if source was marked by it.
	 *
	 * @param list list
	 * @param begin begin
	 * @param end end
	 * @return view
	 */
	public static IntList sublist(IntList list, int begin, int end) {
		NativeArrayAllocator.ensureFromTo(list.size(), begin, end);
		if(list instanceof RandomAccess) {
			return new RandomSubList(list, begin, end);
		} else {
			return new AIntList.SubList(list, begin, end);
		}
	}

	/**
	 * Returns swapper for specified list.
	 *
	 * Swappers can be used for example by sort algorithms or shuffle algorithms to make swaps in efficient way
	 * without knowing if they work on list or on array.
	 *
	 * @param list list
	 * @return swapper
	 */
	public static Swapper swapper(IntList list) {
	    return (a,b) -> swap(list,a,b);
    }

	/**
	 * Swaps elements inside list.
	 *
	 * @param list list
	 * @param a a
	 * @param b b
	 */
	public static void swap(IntList list, int a, int b) {
		int t = list.getInt(a);
		list.set(a, list.getInt(b));
		list.set(b, t);
	}

	/**
	 * Binary search operation.
	 * Returns index of "value", or negative index if not found.
	 * Negative index can be used as insertion position: -(index-1)
	 *
	 * List must be sorted in natural order.
	 *
	 * @param list list
	 * @param value value
	 * @return int
	 */
	public static int binarySearch(IntList list, int value) {
		return binarySearch(list, 0, list.size(), value);
	}

	/**
	 * Binary search operation inside specified range.
	 * Returns index of "value", or negative index if not found.
	 * Negative index can be used as insertion position: -(index-1)
	 *
	 * Returned index is not "begin-based" but "0-based" and can be used directly for list access.
	 * Of course, if begin or end is reached, returned index is clipped to specified range.
	 *
	 * List must be sorted in natural order.
	 *
	 * @param list list
	 * @param begin begin
	 * @param end end
	 * @param value value
	 * @return int
	 */
	public static int binarySearch(IntList list, int begin, int end, int value) {
		NativeComparator cmp = index -> Integer.compare(list.getInt(index), value);
		return NativeArraySearch.binarySearch(cmp, begin, end);
	}
	
	private static class RandomSubList extends AIntList.SubList implements RandomAccess {

		private static final long serialVersionUID = 1L;
		
		public RandomSubList(IntList list, int begin, int end) {
			super(list, begin, end);
		}

		@Override
		public IntListIterator listIterator(int offset) {
			return new RandomAccessIntIterator(offset){
				@Override
				protected int size() {
					return RandomSubList.this.size();
				}

				@Override
				protected int get(int index) {
					return RandomSubList.this.get(index);
				}

                @Override
                protected void set(int index, int value) {
                    RandomSubList.this.set(index, value);
                }

                @Override
                protected void add(int index, int value) {
                    RandomSubList.this.add(index, value);
                }

                @Override
                protected void remove(int index) {
                    RandomSubList.this.remove(index);
                }
				
			};
		}
		
	}
	
}
