package speiger.src.collections.objects.queues;

import java.util.Arrays;
import java.util.Comparator;
import java.util.function.Consumer;
import java.util.function.BiFunction;
import java.util.Objects;
import java.util.Optional;

import java.util.stream.Stream;
import java.util.stream.Collector;
import java.util.NoSuchElementException;
import java.util.function.Predicate;

import speiger.src.collections.objects.collections.ObjectIterator;
import speiger.src.collections.ints.functions.consumer.IntObjectConsumer;
import speiger.src.collections.objects.functions.consumer.ObjectObjectConsumer;
import speiger.src.collections.objects.functions.function.ObjectObjectUnaryOperator;
import speiger.src.collections.utils.ITrimmable;

/**
 * A Simple First In First Out Priority Queue that is a Good Replacement for a linked list (or ArrayDequeue)
 * Its specific implementation uses a backing array that grows and shrinks as it is needed.
 * @param <T> the keyType of elements maintained by this Collection
 */
public class ObjectArrayFIFOQueue<T> extends AbstractObjectPriorityQueue<T> implements ObjectPriorityDequeue<T>, ITrimmable
{
	/** Max Possible ArraySize without the JVM Crashing */
	private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
	/** The Minimum Capacity that is allowed */
	public static final int MIN_CAPACITY = 4;
	/** The Backing array */
	protected transient T[] array;
	/** The First Index pointer */
	protected int first;
	/** The Last Index pointer */
	protected int last;
	/** The Minimum Capacity of the Queue **/
	protected int minCapacity;
	
	/**
	 * Constructor using a initial array
	 * @param values the Array that should be used
	 */
	public ObjectArrayFIFOQueue(T[] values) {
		this(values, 0, values.length);
	}
	
	/**
	 * Constructor using a initial array
	 * @param values the Array that should be used
	 * @param size the amount of elements that are in the initial array
	 * @throws IllegalStateException if values is smaller then size
	 */
	public ObjectArrayFIFOQueue(T[] values, int size) {
		this(values, 0, size);
	}
	
	/**
	 * Constructor using a initial array
	 * @param values the Array that should be used
	 * @param offset where to begin in the initial array
	 * @param size the amount of elements that are in the initial array
	 * @throws IllegalStateException if values is smaller then size
	 */
	public ObjectArrayFIFOQueue(T[] values, int offset, int size) {
		if (values.length < size) throw new IllegalArgumentException("Initial array (" + values.length + ") is smaller then the expected size (" + size + ")");
		if(values.length <= 0) values = (T[])new Object[MIN_CAPACITY];
		else if(values.length < MIN_CAPACITY) values = Arrays.copyOf(values, MIN_CAPACITY);
		minCapacity = MIN_CAPACITY;
		array = values;
		first = offset;
		last = (offset + size) % array.length;
		if(array.length == size) expand();
	}
	
	/**
	 * Constructor with a Min Capacity
	 * @param capacity the initial capacity of the backing array
	 * @throws IllegalStateException if the initial size is smaller 0
	 */
	public ObjectArrayFIFOQueue(int capacity) {
		if (capacity < 0) throw new IllegalArgumentException("Initial capacity (" + capacity + ") is negative");
		array = (T[])new Object[Math.max(MIN_CAPACITY, capacity+1)];
		minCapacity = array.length;
	}
	
	/**
	 * Default Construtor
	 */
	public ObjectArrayFIFOQueue() {
		this(MIN_CAPACITY);
	}
	
	/**
	 * Creates a Collector for a ArrayFIFOQueue
	 * @param <T> the keyType of elements maintained by this Collection
	 * @return a collector
	 */
	public static <T> Collector<T, ObjectArrayFIFOQueue<T>, ObjectArrayFIFOQueue<T>> toQueue() {
		return Collector.of(ObjectArrayFIFOQueue::new, ObjectArrayFIFOQueue::enqueue, ObjectArrayFIFOQueue::merge);
	}
	
	/**
	 * Collects a Stream to a ArrayFIFOQueue
	 * @param <T> the keyType of elements maintained by this Collection
	 * @return a queue with the contents of the Stream
	 */
	public static <T> ObjectArrayFIFOQueue<T> toQueue(Stream<T> stream) {
		return stream.collect(ObjectArrayFIFOQueue::new, ObjectArrayFIFOQueue::enqueue, ObjectArrayFIFOQueue::merge);
	}
	
	private ObjectArrayFIFOQueue<T> merge(ObjectArrayFIFOQueue<T> a) {
		enqueueAll(a.toArray((T[])new Object[a.size()]));
		return this;
	}
	
	@Override
	public ObjectIterator<T> iterator() {
		return new Iter();
	}
	
	@Override
	public int size() {
		final int apparentLength = last - first;
		return apparentLength >= 0 ? apparentLength : array.length + apparentLength;
	}
	
	@Override
	public void clear() {
		if(first != last) {
			Arrays.fill(array, null);
			first = last = 0;
		}
		else if(first != 0) {
			first = last = 0;
		}
	}
	
	@Override
	public void enqueue(T e) {
		array[last++] = e;
		if(last == array.length) last = 0;
		if(last == first) expand();
	}
	
	@Override
	public void enqueueFirst(T e) {
		if(first == 0) first = array.length;
		array[--first] = e;
		if(first == last) expand();
	}
	
	@Override
	public T dequeue() {
		if(first == last) throw new NoSuchElementException();
		T data = array[first];
		array[first] = null;
		if(++first == array.length) first = 0;
		reduce();
		return data;
	}
	
	@Override
	public T dequeueLast() {
		if(first == last) throw new NoSuchElementException();
		if(last == 0) last = array.length;
		T data = array[--last];
		array[last] = null;
		reduce();
		return data;
	}
	
	@Override
	public T peek(int index) {
		if(first == last || index < 0 || index >= size()) throw new NoSuchElementException();
		index += first;
		return index >= array.length ? array[index-array.length] : array[index];
	}
	
	@Override
	public boolean contains(T e) {
		if(first == last) return false;
		for(int i = 0,m=size();i<m;i++) {
			if(e == array[(first + i) % array.length]) return true;
		}
		return false;
	}
	
	@Override
	public boolean removeFirst(T e) {
		if(first == last) return false;
		for(int i = 0,m=size();i<m;i++) {
			int index = (first + i) % array.length;
			if(e == array[index])
				return removeIndex(index);
		}
		return false;
	}
	
	@Override
	public boolean removeLast(T e) {
		if(first == last) return false;
		for(int i = size()-1;i>=0;i--) {
			int index = (first + i) % array.length;
			if(e == array[index])
				return removeIndex(index);
		}
		return false;
	}
	
	protected boolean removeIndex(int index) {
		if(first >= last ? index < first && index > last : index < first || index > last) return false;
		if(index == first) {
			array[first] = null;
			first++;
		}
		else if(index == last) {
			last--;
			array[last] = null;
		}
		else if(index > last) {
			System.arraycopy(array, first, array, first+1, (index - first));
			array[first] = null;
			first = ++first % array.length;
		}
		else if(index < first) {
			System.arraycopy(array, index+1, array, index, (last - index) - 1);
			array[last] = null;
			if(--last < 0) last += array.length;
		}
		else {
			if(index - first < last - index) {
				System.arraycopy(array, first, array, first+1, (index - first));
				array[first] = null;
				first = ++first % array.length;
			}
			else {
				System.arraycopy(array, index+1, array, index, (last - index) - 1);
				array[last] = null;
				if(--last < 0) last += array.length;
			}
		}
		reduce();
		return true;
	}
	
	@Override
	public void onChanged() {}
	
	@Override
	public ObjectArrayFIFOQueue<T> copy() {
		ObjectArrayFIFOQueue<T> queue = new ObjectArrayFIFOQueue<>();
		queue.first = first;
		queue.last = last;
		queue.minCapacity = minCapacity;
		queue.array = Arrays.copyOf(array, array.length);
		return queue;
	}
	
	@Override
	public Comparator<? super T> comparator() { return null; }
	
	@Override
	public void forEach(Consumer<? super T> action) {
		Objects.requireNonNull(action);
		if(first == last) return;
		for(int i = 0,m=size();i<m;i++)
			action.accept(array[(first + i) % array.length]);
		clearAndTrim(0);
	}
	
	@Override
	public void forEachIndexed(IntObjectConsumer<T> action) {
		Objects.requireNonNull(action);
		if(first == last) return;
		for(int i = 0,m=size();i<m;i++)
			action.accept(i, array[(first + i) % array.length]);
		clearAndTrim(0);
	}
	
	@Override
	public <E> void forEach(E input, ObjectObjectConsumer<E, T> action) {
		Objects.requireNonNull(action);
		if(first == last) return;
		for(int i = 0,m=size();i<m;i++)
			action.accept(input, array[(first + i) % array.length]);
		clearAndTrim(0);	
	}
	
	@Override
	public boolean matchesAny(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0,m=size();i<m;i++) {
			if(filter.test(array[(first + i) % array.length])) return true;
		}
		return false;
	}
	
	@Override
	public boolean matchesNone(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0,m=size();i<m;i++) {
			if(filter.test(array[(first + i) % array.length])) return false;
		}
		return true;
	}
	
	@Override
	public boolean matchesAll(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0,m=size();i<m;i++) {
			if(!filter.test(array[(first + i) % array.length])) return false;
		}
		return true;
	}
	
	@Override
	public Optional<T> findFirst(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0,m=size();i<m;i++) {
			int index = (first + i) % array.length;
			if(filter.test(array[index])) {
				T data = array[index];
				removeIndex(index);
				return Optional.ofNullable(data);
			}
		}
		return Optional.empty();
	}
	
	@Override
	public <E> E reduce(E identity, BiFunction<E, T, E> operator) {
		Objects.requireNonNull(operator);
		E state = identity;
		for(int i = 0,m=size();i<m;i++) {
			state = operator.apply(state, array[(first + i) % array.length]);
		}
		return state;
	}
	
	@Override
	public Optional<T> reduce(ObjectObjectUnaryOperator<T, T> operator) {
		Objects.requireNonNull(operator);
		T state = null;
		boolean empty = true;
		for(int i = 0,m=size();i<m;i++) {
			if(empty) {
				empty = false;
				state = array[(first + i) % array.length];
				continue;
			}
			state = operator.apply(state, array[(first + i) % array.length]);
		}
		return empty ? Optional.empty() : Optional.ofNullable(state);
	}
	
	@Override
	public int count(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		int result = 0;
		for(int i = 0,m=size();i<m;i++) {
			if(filter.test(array[(first + i) % array.length])) result++;
		}
		return result;
	}
	
	@Override
	public boolean trim(int size) {
		int newSize = Math.max(Math.max(size, size()), minCapacity);
		if(newSize >= array.length) return false;
		T[] newArray = (T[])new Object[newSize];
		if(first <= last) System.arraycopy(array, first, newArray, 0, last - first);
		else {
			System.arraycopy(array, first, newArray, 0, array.length - first);
			System.arraycopy(array, 0, newArray, array.length - first, last);
		}
		first = 0;
		last = size();
		array = newArray;
		return true;
	}
	
	/**
	 * Trims the collection down to the requested size and clears all elements while doing so
	 * @param size the amount of elements that should be allowed
	 * @note this will enforce minimum size of the collection itself
	 */
	@Override
	public void clearAndTrim(int size) {
		int newSize = Math.max(minCapacity, size);
		if(array.length <= newSize) {
			clear();
			return;
		}
		first = last = 0;
		array = (T[])new Object[newSize];
	}
	
	@Override
	public <E> E[] toArray(E[] input) {
		if(input == null || input.length < size()) input = (E[])new Object[size()];
		if (first <= last) System.arraycopy(array, first, input, 0, last - first);
		else {
			System.arraycopy(array, first, input, 0, array.length - first);
			System.arraycopy(array, 0, input, array.length - first, last);
		}
		return input;
	}
	
	protected void reduce() {
		final int size = size();
		if (array.length > minCapacity && size <= array.length / 4) resize(size, Math.max(array.length / 2, minCapacity));
	}
	
	protected void expand() {
		resize(array.length, (int)Math.min(MAX_ARRAY_SIZE, 2L * array.length));
	}
	
	protected final void resize(int oldSize, int newSize) {
		T[] newArray = (T[])new Object[newSize];
		if(first >= last) {
			if(oldSize != 0)
			{
				System.arraycopy(array, first, newArray, 0, array.length - first);
				System.arraycopy(array, 0, newArray, array.length - first, last);
			}
		}
		else System.arraycopy(array, first, newArray, 0, last-first);
		first = 0;
		last = oldSize;
		array = newArray;
	}
	
	private class Iter implements ObjectIterator<T>
	{
		int index = first;
		@Override
		public boolean hasNext()
		{
			return index != last;
		}

		@Override
		public T next() {
			if(!hasNext()) throw new NoSuchElementException();
			T value = array[index];
			removeIndex(index);
			index = ++index % array.length;
			return value;
		}
	}
}