package speiger.src.collections.objects.sets;

import java.util.Arrays;
import java.util.Collection;
import java.util.function.Consumer;
import java.util.function.BiFunction;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.stream.Collector;
import java.util.Set;
import java.util.function.Predicate;
import speiger.src.collections.objects.collections.ObjectBidirectionalIterator;
import speiger.src.collections.objects.collections.ObjectCollection;
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.objects.lists.ObjectListIterator;
import speiger.src.collections.objects.utils.ObjectArrays;


/**
 * A Type Specific ArraySet implementation.
 * That is based around the idea of {@link java.util.List#indexOf(Object)} for no duplication.
 * Unless a array constructor is used the ArraySet does not allow for duplication.
 * This implementation does not shrink the backing array
 * @param <T> the keyType of elements maintained by this Collection
 */
public class ObjectArraySet<T> extends AbstractObjectSet<T> implements ObjectOrderedSet<T>
{
	/** The Backing Array */
	protected transient T[] data;
	/** The amount of elements stored in the array*/
	protected int size = 0;
	
	/**
	 * Default Constructor
	 */
	public ObjectArraySet() {
		data = (T[])ObjectArrays.EMPTY_ARRAY;
	}
	
	/**
	 * Minimum Capacity Constructor
	 * @param capacity the minimum capacity of the internal array
	 * @throws NegativeArraySizeException if the capacity is negative
	 */
	public ObjectArraySet(int capacity) {
		if(capacity < 0) throw new IllegalStateException("Size has to be 0 or greater");
		data = (T[])new Object[capacity];
	}
	
	/**
	 * Constructur using initial Array
	 * @param array the array that should be used for set.
	 */
	public ObjectArraySet(T[] array) {
		this(array, 0, array.length);
	}
	
	/**
	 * Constructur using initial Array
	 * @param array the array that should be used for set.
	 * @param length the amount of elements present within the array
	 * @throws NegativeArraySizeException if the length is negative
	 */
	public ObjectArraySet(T[] array, int length) {
		this(length);
		addAll(array, length);
	}
	
	/**
	 * Constructur using initial Array
	 * @param array the array that should be used for set.
	 * @param offset the starting offset of where the array should be copied from
	 * @param length the amount of elements present within the array
	 * @throws NegativeArraySizeException if the length is negative
	 */
	public ObjectArraySet(T[] array, int offset, int length) {
		this(length);
		addAll(array, offset, length);
	}
	
	/**
	 * A Helper constructor that allows to create a Set with exactly the same values as the provided collection.
	 * @param c the elements that should be added to the set.
	 * @note this slowly checks every element to remove duplicates
	 */
	public ObjectArraySet(Collection<? extends T> c) {
		this(c.size());
		addAll(c);
	}
	
	/**
	 * A Helper constructor that allows to create a Set with exactly the same values as the provided collection.
	 * @param c the elements that should be added to the set.
	 * @note this slowly checks every element to remove duplicates
	 */
	public ObjectArraySet(ObjectCollection<T> c) {
		this(c.size());
		addAll(c);
	}
	
	/**
	 * A Helper constructor that fast copies the element out of a set into the ArraySet.
	 * Since it is assumed that there is no duplication in the first place
	 * @param s the set the element should be taken from
	 */
	public ObjectArraySet(Set<? extends T> s) {
		this(s.size());
		for(T e : s)
			data[size++] = e;
	}
	
	/**
	 * A Helper constructor that fast copies the element out of a set into the ArraySet.
	 * Since it is assumed that there is no duplication in the first place
	 * @param s the set the element should be taken from
	 */
	public ObjectArraySet(ObjectSet<T> s) {
		this(s.size());
		for(ObjectIterator<T> iter = s.iterator();iter.hasNext();data[size++] = iter.next());
	}
	
	/**
	 * Creates a Collector for a ArraySet
	 * @param <T> the keyType of elements maintained by this Collection
	 * @return a collector
	 */
	public static <T> Collector<T, ObjectArraySet<T>, ObjectArraySet<T>> toList() {
		return Collector.of(ObjectArraySet::new, ObjectArraySet::add, ObjectArraySet::merge);
	}
	
	/**
	 * Collects a Stream to a ArraySet
	 * @param <T> the keyType of elements maintained by this Collection
	 * @return a set with the contents of the Stream
	 */
	public static <T> ObjectArraySet<T> toList(Stream<T> stream) {
		return stream.collect(ObjectArraySet::new, ObjectArraySet::add, ObjectArraySet::merge);
	}
	
	private ObjectArraySet<T> merge(ObjectArraySet<T> a) {
		addAll(a);
		return this;
	}
	
	@Override
	public boolean add(T o) {
		int index = findIndex(o);
		if(index == -1) {
			if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
			data[size++] = o;
			return true;
		}
		return false;
	}
	
	@Override
	public T addOrGet(T o) {
		int index = findIndex(o);
		if(index != -1) return data[index];
		if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
		data[size++] = o;
		return o;
	}
	
	@Override
	public void addFirst(T o) {
		int index = findIndex(o);
		if(index == -1) {
			if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
			System.arraycopy(data, 0, data, 1, size++);
			data[0] = o;
		}
	}
	
	@Override
	public void addLast(T o) {
		int index = findIndex(o);
		if(index == -1) {
			if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
			data[size++] = o;
		}
	}
	
	@Override
	public boolean addAndMoveToFirst(T o) {
		int index = findIndex(o);
		if(index == -1) {
			if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
			System.arraycopy(data, 0, data, 1, size++);
			data[0] = o;
			return true;
		}
		else if(index != 0) {
			o = data[index];
			System.arraycopy(data, 0, data, 1, index);
			data[0] = o;
		}
		return false;
	}

	@Override
	public boolean addAndMoveToLast(T o) {
		int index = findIndex(o);
		if(index == -1) {
			if(data.length == size) data = Arrays.copyOf(data, size == 0 ? 2 : size * 2);
			data[size++] = o;
			return true;
		}
		else if(index != size - 1) {
			o = data[index];
			System.arraycopy(data, index+1, data, index, size - index - 1);
			data[size-1] = o;
		}
		return false;
	}

	@Override
	public boolean moveToFirst(T o) {
		int index = findIndex(o);
		if(index > 0) {
			o = data[index];
			System.arraycopy(data, 0, data, 1, index);
			data[0] = o;
			return true;
		}
		return false;
	}

	@Override
	public boolean moveToLast(T o) {
		int index = findIndex(o);
		if(index != -1 && index != size - 1) {
			o = data[index];
			System.arraycopy(data, index+1, data, index, size - index - 1);
			data[size-1] = o;
			return true;
		}
		return false;
	}
	
	@Override
	public boolean contains(Object e) {
		return findIndex(e) != -1;
	}
	
	@Override
	public T getFirst() {
		if(size == 0) throw new NoSuchElementException();
		return data[0];
	}
	
	@Override
	public T getLast() {
		if(size == 0) throw new NoSuchElementException();
		return data[size - 1];
	}
	
	@Override
	public boolean removeAll(ObjectCollection<T> c) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(!c.contains(data[i])) 
				data[j++] = data[i];
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean removeAll(ObjectCollection<T> c, Consumer<T> r) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(!c.contains(data[i])) data[j++] = data[i];
			else r.accept(data[i]);
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean retainAll(ObjectCollection<T> c) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(c.contains(data[i])) 
				data[j++] = data[i];
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean retainAll(ObjectCollection<T> c, Consumer<T> r) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(c.contains(data[i])) data[j++] = data[i];
			else r.accept(data[i]);
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean removeAll(Collection<?> c) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(!c.contains(data[i]))
				data[j++] = data[i];
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean retainAll(Collection<?> c) {
		int j = 0;
		for(int i = 0;i<size;i++) {
			if(c.contains(data[i]))
				data[j++] = data[i];
		}
		boolean result = j != size;
		Arrays.fill(data, j, size, null);
		size = j;
		return result;
	}
	
	@Override
	public boolean remove(Object o) {
		int index = findIndex(o);
		if(index != -1) {
			size--;
			if(index != size) System.arraycopy(data, index+1, data, index, size - index);
			data[size] = null;
			return true;
		}
		return false;
	}
	
	@Override
	public T removeFirst() {
		if(size == 0) throw new NoSuchElementException();
		T result = data[0];
		System.arraycopy(data, 1, data, 0, --size);
		data[size] = null;
		return result;
	}
	
	@Override
	public T removeLast() {
		if(size == 0) throw new NoSuchElementException();
		size--;
		T result = data[size];
		data[size] = null;
		return result;
	}
	@Override
	public void forEach(Consumer<? super T> action) {
		Objects.requireNonNull(action);
		for(int i = 0;i<size;action.accept(data[i++]));
	}
	
	@Override
	public void forEachIndexed(IntObjectConsumer<T> action) {
		Objects.requireNonNull(action);
		for(int i = 0;i<size;action.accept(i, data[i++]));
	}
	
	@Override
	public <E> void forEach(E input, ObjectObjectConsumer<E, T> action) {
		Objects.requireNonNull(action);
		for(int i = 0;i<size;i++)
			action.accept(input, data[i]);		
	}
	
	@Override
	public boolean matchesAny(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0;i<size;i++) {
			if(filter.test(data[i])) return true;
		}
		return false;
	}
	
	@Override
	public boolean matchesNone(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0;i<size;i++) {
			if(filter.test(data[i])) return false;
		}
		return true;
	}
	
	@Override
	public boolean matchesAll(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0;i<size;i++) {
			if(!filter.test(data[i])) return false;
		}
		return true;
	}
	
	@Override
	public Optional<T> findFirst(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		for(int i = 0;i<size;i++) {
			if(filter.test(data[i])) return Optional.ofNullable(data[i]);
		}
		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;i<size;i++) {
			state = operator.apply(state, data[i]);
		}
		return state;
	}
	
	@Override
	public Optional<T> reduce(ObjectObjectUnaryOperator<T, T> operator) {
		Objects.requireNonNull(operator);
		T state = null;
		boolean empty = true;
		for(int i = 0;i<size;i++) {
			if(empty) {
				empty = false;
				state = data[i];
				continue;
			}
			state = operator.apply(state, data[i]);
		}
		return empty ? Optional.empty() : Optional.ofNullable(state);
	}
	
	@Override
	public int count(Predicate<T> filter) {
		Objects.requireNonNull(filter);
		int result = 0;
		for(int i = 0;i<size;i++) {
			if(filter.test(data[i])) result++;
		}
		return result;
	}
	
	protected int findIndex(Object o) {
		for(int i = size-1;i>=0;i--)
			if(Objects.equals(o, data[i])) return i;
		return -1;
	}
	
	@Override
	public ObjectBidirectionalIterator<T> iterator() {
		return new SetIterator(true, 0);
	}
	
	@Override
	public ObjectBidirectionalIterator<T> reverseIterator() {
		return new SetIterator(false, size);
	}
	
	@Override
	public ObjectBidirectionalIterator<T> iterator(T fromElement) {
		int index = findIndex(fromElement);
		if(index != -1) return new SetIterator(true, index);
		throw new NoSuchElementException();
	}
	
	public ObjectArraySet<T> copy() {
		ObjectArraySet<T> set = new ObjectArraySet<>();
		set.data = Arrays.copyOf(data, data.length);
		set.size = size;
		return set;
	}
	
	@Override
	public void clear() {
		size = 0;
	}
	
	@Override
	public int size() {
		return size;
	}
	
	@Override
	@Deprecated
	public Object[] toArray() {
		if(isEmpty()) return ObjectArrays.EMPTY_ARRAY;
		Object[] obj = new Object[size()];
		for(int i = 0;i<size();i++)
			obj[i] = data[i];
		return obj;
	}
	
	@Override
	public <E> E[] toArray(E[] a) {
		if(a == null) a = (E[])new Object[size()];
		else if(a.length < size()) a = (E[])ObjectArrays.newArray(a.getClass().getComponentType(), size());
		for(int i = 0;i<size();i++)
			a[i] = (E)data[i];
		if (a.length > size) a[size] = null;
		return a;
	}
		
	private class SetIterator implements ObjectListIterator<T> {
		boolean forward;
		int index;
		int lastReturned = -1;
		
		public SetIterator(boolean forward, int index) {
			this.forward = forward;
			this.index = index;
		}
		
		@Override
		public boolean hasNext() {
			return forward ? index < size() : index > 0;
		}
		
		@Override
		public T next() {
			if(!hasNext()) throw new NoSuchElementException();
			if(forward) {
				lastReturned = index;
				return data[index++];				
			}
			index--;
			return data[(lastReturned = index)];
		}
		
		@Override
		public boolean hasPrevious() {
			return forward ? index > 0 : index < size();
		}
		
		@Override
		public T previous() {
			if(!hasPrevious()) throw new NoSuchElementException();
			if(forward) {
				index--;
				return data[(lastReturned = index)];
			}
			lastReturned = index;
			return data[index++];
		}
		
		@Override
		public int nextIndex() {
			if(forward) return index;
			return size - index;
		}
		
		@Override
		public int previousIndex() {
			if(forward) return index-1;
			return (size - index)-1;
		}
		
		@Override
		public void remove() {
			if(lastReturned == -1) throw new IllegalStateException();
			ObjectArraySet.this.remove(data[lastReturned]);
			if(lastReturned < index) index--;
			lastReturned = -1;
		}
		
		@Override
		public void set(Object e) { throw new UnsupportedOperationException(); }
		
		@Override
		public void add(Object e) { throw new UnsupportedOperationException(); }
		
		@Override
		public int skip(int amount) {
			if(amount < 0) throw new IllegalStateException("Negative Numbers are not allowed");
			return forward ? moveForward(amount) : moveBackwards(amount);
		}
		
		@Override
		public int back(int amount) {
			if(amount < 0) throw new IllegalStateException("Negative Numbers are not allowed");
			return forward ? moveBackwards(amount) : moveForward(amount);
		}
		
		private int moveForward(int amount) {
			int steps = Math.min(amount, size() - index);
			index += steps;
			if(steps > 0) lastReturned = Math.min(index-1, size()-1);
			return steps;
		}
		
		private int moveBackwards(int amount) {
			int steps = Math.min(amount, index);
			index -= steps;
			if(steps > 0) lastReturned = Math.min(index, size()-1);
			return steps;
		}
	}
}