/*******************************************************************************
 * Copyright (c) 2006, 2017 IBM Corporation 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:
 *     IBM Corporation - initial API and implementation
 *     Matthew Hall - bug 233306, 226289, 190881
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 *******************************************************************************/
package org.eclipse.core.databinding.observable.map;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.Realm;
import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.set.WritableSet;
import org.eclipse.core.runtime.Assert;

/**
 * A read-only observable map formed by the composition of two observable maps.
 * If map1 maps keys a:A to values b1:B, and map2 maps keys b2:B to values c:C,
 * the composite map maps keys a:A to values c:C. For example, map1 could map
 * Order objects to their corresponding Customer objects, and map2 could map
 * Customer objects to their "last name" property of type String. The composite
 * map of map1 and map2 would then map Order objects to their customers' last
 * names.
 *
 * <p>
 * This class is thread safe. All state accessing methods must be invoked from
 * the {@link Realm#isCurrent() current realm}. Methods for adding and removing
 * listeners may be invoked from any thread.
 * </p>
 *
 * @param <K>
 *            the type of the keys in this map
 * @param <I>
 *            the type of the intermediate values
 * @param <V>
 *            the type of the values in this map
 *
 * @since 1.1
 *
 */
public class CompositeMap<K, I, V> extends ObservableMap<K, V> {
	// adds that need to go through the second map and thus will be picked up by
	// secondMapListener.
	private Set<I> pendingAdds = new HashSet<>();

	// Removes that need to go through the second map and thus will be picked up
	// by secondMapListener. Maps from value being removed to key being removed.
	private Map<I, K> pendingRemoves = new HashMap<>();

	// Changes that need to go through the second map and thus will be picked up
	// by secondMapListener. Maps from old value to new value and new value to
	// old
	// value.
	private Map<I, I> pendingChanges = new HashMap<>();

	private BidiObservableMap<K, I> firstMap;
	private IObservableMap<I, V> secondMap;
	private WritableSetPlus<I> rangeSet = new WritableSetPlus<>();

	private IMapChangeListener<K, I> firstMapListener = event -> {
		MapDiff<? extends K, ? extends I> diff = event.diff;
		Set<I> rangeSetAdditions = new HashSet<>();
		Set<I> rangeSetRemovals = new HashSet<>();
		final Set<K> adds = new HashSet<>();
		final Set<K> changes = new HashSet<>();
		final Set<K> removes = new HashSet<>();
		final Map<K, V> oldValues = new HashMap<>();

		for (K addedKey : diff.getAddedKeys()) {
			I newValue1 = diff.getNewValue(addedKey);
			if (!rangeSet.contains(newValue1)) {
				pendingAdds.add(newValue1);
				rangeSetAdditions.add(newValue1);
			} else {
				adds.add(addedKey);
				wrappedMap.put(addedKey, secondMap.get(newValue1));
			}
		}
		for (K changedKey : diff.getChangedKeys()) {
			I oldValue1 = diff.getOldValue(changedKey);
			I newValue2 = diff.getNewValue(changedKey);
			boolean removed = firstMap.getKeys(oldValue1).isEmpty();
			boolean added = !rangeSet.contains(newValue2);
			if (removed) {
				pendingRemoves.put(oldValue1, changedKey);
				rangeSetRemovals.add(oldValue1);
			}
			if (added) {
				pendingAdds.add(newValue2);
				rangeSetAdditions.add(newValue2);
			}
			if (added || removed) {
				pendingChanges.put(oldValue1, newValue2);
				pendingChanges.put(newValue2, oldValue1);
			} else {
				changes.add(changedKey);
				oldValues.put(changedKey, secondMap.get(oldValue1));
				wrappedMap.put(changedKey, secondMap.get(newValue2));
			}
		}
		for (K removedKey : diff.getRemovedKeys()) {
			I oldValue2 = diff.getOldValue(removedKey);
			if (firstMap.getKeys(oldValue2).isEmpty()) {
				pendingRemoves.put(oldValue2, removedKey);
				rangeSetRemovals.add(oldValue2);
			} else {
				removes.add(removedKey);
				oldValues.put(removedKey, secondMap.get(oldValue2));
				wrappedMap.remove(removedKey);
			}
		}

		if (adds.size() > 0 || removes.size() > 0 || changes.size() > 0) {
			fireMapChange(new MapDiff<K, V>() {

				@Override
				public Set<K> getAddedKeys() {
					return adds;
				}

				@Override
				public Set<K> getChangedKeys() {
					return changes;
				}

				@Override
				public V getNewValue(Object key) {
					return wrappedMap.get(key);
				}

				@Override
				public V getOldValue(Object key) {
					return oldValues.get(key);
				}

				@Override
				public Set<K> getRemovedKeys() {
					return removes;
				}
			});
		}

		if (rangeSetAdditions.size() > 0 || rangeSetRemovals.size() > 0) {
			rangeSet.addAndRemove(rangeSetAdditions, rangeSetRemovals);
		}
	};

	private IMapChangeListener<I, V> secondMapListener = event -> {
		MapDiff<? extends I, ? extends V> diff = event.diff;
		final Set<K> adds = new HashSet<>();
		final Set<K> changes = new HashSet<>();
		final Set<K> removes = new HashSet<>();
		final Map<K, V> oldValues = new HashMap<>();
		final Map<K, V> newValues = new HashMap<>();
		Set<I> addedKeys = new HashSet<>(diff.getAddedKeys());
		Set<I> removedKeys = new HashSet<>(diff.getRemovedKeys());

		for (I addedKey : addedKeys) {
			Set<K> elements1 = firstMap.getKeys(addedKey);
			V newValue1 = diff.getNewValue(addedKey);
			if (pendingChanges.containsKey(addedKey)) {
				I oldKey = pendingChanges.remove(addedKey);
				V oldValue1;
				if (removedKeys.remove(oldKey)) {
					oldValue1 = diff.getOldValue(oldKey);
				} else {
					oldValue1 = secondMap.get(oldKey);
				}
				pendingChanges.remove(oldKey);
				pendingAdds.remove(addedKey);
				pendingRemoves.remove(oldKey);
				for (K element1 : elements1) {
					changes.add(element1);
					oldValues.put(element1, oldValue1);
					newValues.put(element1, newValue1);
					wrappedMap.put(element1, newValue1);
				}
			} else if (pendingAdds.remove(addedKey)) {
				for (K element2 : elements1) {
					adds.add(element2);
					newValues.put(element2, newValue1);
					wrappedMap.put(element2, newValue1);
				}
			} else {
				Assert.isTrue(false, "unexpected case"); //$NON-NLS-1$
			}
		}
		for (I changedKey : diff.getChangedKeys()) {
			Set<K> elements2 = firstMap.getKeys(changedKey);
			for (K element3 : elements2) {
				changes.add(element3);
				oldValues.put(element3, diff.getOldValue(changedKey));
				V newValue2 = diff.getNewValue(changedKey);
				newValues.put(element3, newValue2);
				wrappedMap.put(element3, newValue2);
			}
		}
		for (I removedKey : removedKeys) {
			K element4 = pendingRemoves.remove(removedKey);
			if (element4 != null) {
				if (pendingChanges.containsKey(removedKey)) {
					Object newKey = pendingChanges.remove(removedKey);
					pendingChanges.remove(newKey);
					pendingAdds.remove(newKey);
					pendingRemoves.remove(removedKey);
					changes.add(element4);
					oldValues.put(element4, diff.getOldValue(removedKey));
					V newValue3 = secondMap.get(newKey);
					newValues.put(element4, newValue3);
					wrappedMap.put(element4, newValue3);
				} else {
					removes.add(element4);
					V oldValue2 = diff.getOldValue(removedKey);
					oldValues.put(element4, oldValue2);
					wrappedMap.remove(element4);
				}
			} else {
				Assert.isTrue(false, "unexpected case"); //$NON-NLS-1$
			}
		}

		if (adds.size() > 0 || removes.size() > 0 || changes.size() > 0) {
			fireMapChange(new MapDiff<K, V>() {

				@Override
				public Set<K> getAddedKeys() {
					return adds;
				}

				@Override
				public Set<K> getChangedKeys() {
					return changes;
				}

				@Override
				public V getNewValue(Object key) {
					return newValues.get(key);
				}

				@Override
				public V getOldValue(Object key) {
					return oldValues.get(key);
				}

				@Override
				public Set<K> getRemovedKeys() {
					return removes;
				}
			});
		}
	};

	private static class WritableSetPlus<E> extends WritableSet<E> {
		void addAndRemove(Set<E> additions, Set<E> removals) {
			wrappedSet.removeAll(removals);
			wrappedSet.addAll(additions);
			fireSetChange(Diffs.createSetDiff(additions, removals));
		}
	}

	/**
	 * Creates a new composite map. Because the key set of the second map is
	 * determined by the value set of the given observable map
	 * <code>firstMap</code>, it cannot be passed in as an argument. Instead,
	 * the second map will be created by calling
	 * <code>secondMapFactory.createObservable(valueSet())</code>.
	 *
	 * @param firstMap
	 *            the first map
	 * @param secondMapFactory
	 *            a factory that creates the second map when given an observable
	 *            set representing the value set of <code>firstMap</code>.
	 */
	public CompositeMap(IObservableMap<K, I> firstMap,
			IObservableFactory<? super IObservableSet<I>, ? extends IObservableMap<I, V>> secondMapFactory) {
		super(firstMap.getRealm(), new HashMap<K, V>());
		this.firstMap = new BidiObservableMap<>(firstMap);
		this.firstMap.addMapChangeListener(firstMapListener);
		rangeSet.addAll(this.firstMap.values());
		this.secondMap = secondMapFactory.createObservable(rangeSet);
		secondMap.addMapChangeListener(secondMapListener);
		for (Entry<K, I> entry : this.firstMap.entrySet()) {
			wrappedMap.put(entry.getKey(), secondMap.get(entry.getValue()));
		}
	}

	/**
	 * @since 1.2
	 */
	@Override
	public Object getKeyType() {
		return firstMap.getKeyType();
	}

	/**
	 * @since 1.2
	 */
	@Override
	public Object getValueType() {
		return secondMap.getValueType();
	}

	@Override
	public synchronized void dispose() {
		super.dispose();
		if (firstMap != null) {
			firstMap.removeMapChangeListener(firstMapListener);
			firstMap = null;
		}
		if (secondMap != null) {
			secondMap.dispose();
			secondMap = null;
		}
	}

}
