/*******************************************************************************
 * 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, 303847
 *     Ovidio Mallo - bug 332367
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 ******************************************************************************/

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

import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.map.AbstractObservableMap;
import org.eclipse.core.databinding.observable.map.MapDiff;
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.map.SimpleMapProperty;

/**
 * @param <S>
 *            type of the source object
 * @param <K>
 *            type of the keys to the map
 * @param <V>
 *            type of the values in the map
 * @since 1.2
 */
public class SimplePropertyObservableMap<S, K, V> extends AbstractObservableMap<K, V>
		implements IPropertyObservable<SimpleMapProperty<S, K, V>> {
	private S source;
	private SimpleMapProperty<S, K, V> property;

	private volatile boolean updating = false;

	private volatile int modCount = 0;

	private INativePropertyListener<S> listener;

	private Map<K, V> cachedMap;
	private boolean stale;

	/**
	 * @param realm
	 * @param source
	 * @param property
	 */
	public SimplePropertyObservableMap(Realm realm, S source, SimpleMapProperty<S, K, V> property) {
		super(realm);
		this.source = source;
		this.property = property;
	}

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

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

	private void getterCalled() {
		ObservableTracker.getterCalled(this);
	}

	@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(() -> {
			cachedMap = new HashMap<>(getMap());
			stale = false;

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

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

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

	// Queries

	private Map<K, V> getMap() {
		return property.getMap(source);
	}

	// Single change operations

	private void updateMap(Map<K, V> map, MapDiff<K, V> diff) {
		if (!diff.isEmpty()) {
			boolean wasUpdating = updating;
			updating = true;
			try {
				property.updateMap(source, diff);
				modCount++;
			} finally {
				updating = wasUpdating;
			}

			notifyIfChanged(null);
		}
	}

	private EntrySet es = new EntrySet();

	@Override
	public Set<Map.Entry<K, V>> entrySet() {
		getterCalled();
		return es;
	}

	private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
		@Override
		public Iterator<Map.Entry<K, V>> iterator() {
			return new EntrySetIterator();
		}

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

	private class EntrySetIterator implements Iterator<Map.Entry<K, V>> {
		private volatile int expectedModCount = modCount;
		Map<K, V> map = new HashMap<>(getMap());
		Iterator<Map.Entry<K, V>> iterator = map.entrySet().iterator();
		Map.Entry<K, V> last = null;

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

		@Override
		public Map.Entry<K, V> next() {
			getterCalled();
			checkForComodification();
			last = iterator.next();
			return last;
		}

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

			MapDiff<K, V> diff = Diffs.createMapDiffSingleRemove(last.getKey(), last.getValue());
			updateMap(map, diff);

			iterator.remove(); // stay in sync

			last = null;
			expectedModCount = modCount;
		}

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

	@Override
	public Set<K> keySet() {
		getterCalled();
		// AbstractMap depends on entrySet() to fulfil keySet() API, so all
		// getterCalled() and comodification checks will still be handled
		return super.keySet();
	}

	@Override
	public boolean containsKey(Object key) {
		getterCalled();

		return getMap().containsKey(key);
	}

	@Override
	public V get(Object key) {
		getterCalled();

		return getMap().get(key);
	}

	@Override
	public V put(K key, V value) {
		checkRealm();

		Map<K, V> map = getMap();

		boolean add = !map.containsKey(key);

		V oldValue = map.get(key);

		MapDiff<K, V> diff;
		if (add)
			diff = Diffs.createMapDiffSingleAdd(key, value);
		else
			diff = Diffs.createMapDiffSingleChange(key, oldValue, value);

		updateMap(map, diff);

		return oldValue;
	}

	@Override
	public void putAll(Map<? extends K, ? extends V> m) {
		checkRealm();

		Map<K, V> map = getMap();

		Map<K, V> oldValues = new HashMap<K, V>();
		Map<K, V> newValues = new HashMap<K, V>();
		Set<K> changedKeys = new HashSet<K>();
		Set<K> addedKeys = new HashSet<K>();
		for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
			K key = entry.getKey();
			V newValue = entry.getValue();
			if (map.containsKey(key)) {
				changedKeys.add(key);
				oldValues.put(key, map.get(key));
			} else {
				addedKeys.add(key);
			}
			newValues.put(key, newValue);
		}

		MapDiff<K, V> diff = Diffs.createMapDiff(addedKeys, Collections.<K> emptySet(), changedKeys, oldValues,
				newValues);
		updateMap(map, diff);
	}

	@Override
	public V remove(Object key) {
		checkRealm();

		Map<K, V> map = getMap();
		if (!map.containsKey(key))
			return null;

		V oldValue = map.get(key);

		@SuppressWarnings("unchecked")
		// if we contain this key, then it is of
		// type K
		MapDiff<K, V> diff = Diffs.createMapDiffSingleRemove((K) key, oldValue);
		updateMap(map, diff);

		return oldValue;
	}

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

		Map<K, V> map = getMap();
		if (map.isEmpty())
			return;

		MapDiff<K, V> diff = Diffs.createMapDiffRemoveAll(new HashMap<K, V>(map));
		updateMap(map, diff);
	}

	@Override
	public Collection<V> values() {
		getterCalled();
		// AbstractMap depends on entrySet() to fulfil values() API, so all
		// getterCalled() and comodification checks will still be handled
		return super.values();
	}

	private void notifyIfChanged(MapDiff<K, V> diff) {
		if (hasListeners()) {
			Map<K, V> oldMap = cachedMap;
			Map<K, V> newMap = cachedMap = new HashMap<K, V>(getMap());
			if (diff == null)
				diff = Diffs.computeMapDiff(oldMap, newMap);
			if (!diff.isEmpty() || stale) {
				stale = false;
				fireMapChange(diff);
			}
		}
	}

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

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

	@Override
	public SimpleMapProperty<S, K, V> 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();
	}
}
