/*******************************************************************************
 * Copyright (c) 2008, 2017 Matthew Hall and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     Matthew Hall - initial API and implementation (bug 194734)
 *     Matthew Hall - bugs 265561, 262287, 268203, 268688, 301774
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 ******************************************************************************/

package org.eclipse.core.internal.databinding.property.set;

import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.set.AbstractObservableSet;
import org.eclipse.core.databinding.observable.set.SetDiff;
import org.eclipse.core.databinding.property.INativePropertyListener;
import org.eclipse.core.databinding.property.IPropertyObservable;
import org.eclipse.core.databinding.property.SimplePropertyEvent;
import org.eclipse.core.databinding.property.set.SimpleSetProperty;

/**
 * @param <S>
 *            type of the source object
 * @param <E>
 *            type of the elements in the set
 * @since 1.2
 *
 */
public class SimplePropertyObservableSet<S, E> extends AbstractObservableSet<E>
		implements IPropertyObservable<SimpleSetProperty<S, E>> {
	private S source;
	private SimpleSetProperty<S, E> property;

	private volatile boolean updating = false;

	private volatile int modCount = 0;

	private INativePropertyListener<S> listener;

	private Set<E> cachedSet;
	private boolean stale;

	/**
	 * @param realm
	 * @param source
	 * @param property
	 */
	public SimplePropertyObservableSet(Realm realm, S source, SimpleSetProperty<S, E> property) {
		super(realm);
		this.source = source;
		this.property = property;
	}

	@Override
	protected void firstListenerAdded() {
		if (!isDisposed() && listener == null) {
			listener = property.adaptListener(event -> {
				if (!isDisposed() && !updating) {
					getRealm().exec(() -> {
						if (event.type == SimplePropertyEvent.CHANGE) {
							modCount++;
							notifyIfChanged(event.diff);
						} else if (event.type == SimplePropertyEvent.STALE && !stale) {
							stale = true;
							fireStale();
						}
					});
				}
			});
		}

		getRealm().exec(() -> {
			cachedSet = new HashSet<>(getSet());
			stale = false;

			if (listener != null)
				listener.addTo(source);
		});
	}

	@Override
	protected void lastListenerRemoved() {
		if (listener != null)
			listener.removeFrom(source);

		cachedSet.clear();
		cachedSet = null;
		stale = false;
	}

	@Override
	protected Set<E> getWrappedSet() {
		return getSet();
	}

	@Override
	public Object getElementType() {
		return property.getElementType();
	}

	// Queries

	private Set<E> getSet() {
		return property.getSet(source);
	}

	@Override
	public boolean contains(Object o) {
		getterCalled();
		return getSet().contains(o);
	}

	@Override
	public boolean containsAll(Collection<?> c) {
		getterCalled();
		return getSet().containsAll(c);
	}

	@Override
	public boolean isEmpty() {
		getterCalled();
		return getSet().isEmpty();
	}

	@Override
	public Object[] toArray() {
		getterCalled();
		return getSet().toArray();
	}

	@Override
	public <T> T[] toArray(T[] a) {
		getterCalled();
		return getSet().toArray(a);
	}

	// Single change operations

	private void updateSet(Set<E> set, SetDiff<E> diff) {
		if (!diff.isEmpty()) {
			boolean wasUpdating = updating;
			updating = true;
			try {
				property.updateSet(source, diff);
				modCount++;
			} finally {
				updating = wasUpdating;
			}

			notifyIfChanged(null);
		}
	}

	@Override
	public boolean add(E o) {
		checkRealm();

		Set<E> set = getSet();
		if (set.contains(o))
			return false;

		SetDiff<E> diff = Diffs.createSetDiff(Collections.singleton(o), Collections.<E>emptySet());
		updateSet(set, diff);

		return true;
	}

	@Override
	public Iterator<E> iterator() {
		getterCalled();
		return new Iterator<E>() {
			int expectedModCount = modCount;
			Set<E> set = new HashSet<E>(getSet());
			Iterator<E> iterator = set.iterator();
			E last = null;

			@Override
			public boolean hasNext() {
				getterCalled();
				checkForComodification();
				return iterator.hasNext();
			}

			@Override
			public E next() {
				getterCalled();
				checkForComodification();
				last = iterator.next();
				return last;
			}

			@Override
			public void remove() {
				checkRealm();
				checkForComodification();

				SetDiff<E> diff = Diffs.createSetDiff(Collections.<E>emptySet(), Collections.singleton(last));
				updateSet(set, diff);

				iterator.remove(); // stay in sync

				last = null;
				expectedModCount = modCount;
			}

			private void checkForComodification() {
				if (expectedModCount != modCount)
					throw new ConcurrentModificationException();
			}
		};
	}

	@Override
	public boolean remove(Object o) {
		getterCalled();

		Set<E> set = getSet();
		if (!set.contains(o))
			return false;

		@SuppressWarnings("unchecked")
		// if o is contained, it is an E
		SetDiff<E> diff = Diffs.createSetDiff(Collections.<E>emptySet(), Collections.singleton((E) o));
		updateSet(set, diff);

		return true;
	}

	// Bulk change operations

	@Override
	public boolean addAll(Collection<? extends E> c) {
		getterCalled();

		if (c.isEmpty())
			return false;

		Set<E> set = getSet();
		if (set.containsAll(c))
			return false;

		Set<E> additions = new HashSet<E>(c);
		additions.removeAll(set);

		if (additions.isEmpty())
			return false;

		SetDiff<E> diff = Diffs.createSetDiff(additions, Collections.<E>emptySet());
		updateSet(set, diff);

		return true;
	}

	@Override
	public boolean removeAll(Collection<?> c) {
		getterCalled();

		if (c.isEmpty())
			return false;

		Set<E> set = getSet();
		if (set.isEmpty())
			return false;

		Set<Object> removals = new HashSet<Object>(c);
		removals.retainAll(set);
		@SuppressWarnings("unchecked")
		// because we have removed everything that is not an E
		Set<E> typedRemovals = (Set<E>) removals;

		if (removals.isEmpty())
			return false;

		SetDiff<E> diff = Diffs.createSetDiff(Collections.<E>emptySet(), typedRemovals);
		updateSet(set, diff);

		return true;
	}

	@Override
	public boolean retainAll(Collection<?> c) {
		getterCalled();

		Set<E> set = getSet();
		if (set.isEmpty())
			return false;

		if (c.isEmpty()) {
			clear();
			return true;
		}

		Set<E> removals = new HashSet<E>(set);
		removals.removeAll(c);

		if (removals.isEmpty())
			return false;

		SetDiff<E> diff = Diffs.createSetDiff(Collections.<E>emptySet(), removals);
		updateSet(set, diff);

		return true;
	}

	@Override
	public void clear() {
		getterCalled();

		Set<E> set = getSet();
		if (set.isEmpty())
			return;

		SetDiff<E> diff = Diffs.createSetDiff(Collections.<E>emptySet(), set);
		updateSet(set, diff);
	}

	private void notifyIfChanged(SetDiff<E> diff) {
		if (hasListeners()) {
			Set<E> oldSet = cachedSet;
			Set<E> newSet = cachedSet = new HashSet<E>(getSet());
			if (diff == null)
				diff = Diffs.computeSetDiff(oldSet, newSet);
			if (!diff.isEmpty() || stale) {
				stale = false;
				fireSetChange(diff);
			}
		}
	}

	@Override
	public boolean isStale() {
		getterCalled();
		return stale;
	}

	@Override
	public boolean equals(Object o) {
		getterCalled();
		return getSet().equals(o);
	}

	@Override
	public int hashCode() {
		getterCalled();
		return getSet().hashCode();
	}

	@Override
	public Object getObserved() {
		return source;
	}

	@Override
	public SimpleSetProperty<S, E> getProperty() {
		return property;
	}

	@Override
	public synchronized void dispose() {
		if (!isDisposed()) {
			if (listener != null)
				listener.removeFrom(source);
			property = null;
			source = null;
			listener = null;
			stale = false;
		}
		super.dispose();
	}
}
