/*******************************************************************************
 * 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)
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 ******************************************************************************/

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

import java.util.AbstractSet;
import java.util.Collections;
import java.util.HashMap;
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.IStaleListener;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.map.AbstractObservableMap;
import org.eclipse.core.databinding.observable.map.MapDiff;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.set.ISetChangeListener;
import org.eclipse.core.databinding.observable.set.SetChangeEvent;
import org.eclipse.core.databinding.observable.set.SetDiff;
import org.eclipse.core.databinding.property.IPropertyObservable;
import org.eclipse.core.databinding.property.value.DelegatingValueProperty;
import org.eclipse.core.internal.databinding.property.Util;

/**
 * @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 SetDelegatingValueObservableMap<S, K extends S, V> extends AbstractObservableMap<K, V>
		implements IPropertyObservable<DelegatingValueProperty<S, V>> {
	private IObservableSet<K> masterSet;
	private DelegatingValueProperty<S, V> detailProperty;
	private DelegatingCache<S, K, V> cache;

	private Set<Map.Entry<K, V>> entrySet;

	class EntrySet extends AbstractSet<Map.Entry<K, V>> {
		@Override
		public Iterator<Map.Entry<K, V>> iterator() {
			return new Iterator<Map.Entry<K, V>>() {
				final Iterator<K> it = masterSet.iterator();

				@Override
				public boolean hasNext() {
					return it.hasNext();
				}

				@Override
				public Map.Entry<K, V> next() {
					return new MapEntry(it.next());
				}

				@Override
				public void remove() {
					it.remove();
				}
			};
		}

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

	class MapEntry implements Map.Entry<K, V> {
		private final K key;

		MapEntry(K key) {
			this.key = key;
		}

		@Override
		public K getKey() {
			getterCalled();
			return key;
		}

		@Override
		public V getValue() {
			getterCalled();

			if (!masterSet.contains(key))
				return null;

			return cache.get(key);
		}

		@Override
		public V setValue(V value) {
			checkRealm();

			if (!masterSet.contains(key))
				return null;

			return cache.put(key, value);
		}

		@Override
		public boolean equals(Object o) {
			getterCalled();
			if (o == this)
				return true;
			if (o == null)
				return false;
			if (!(o instanceof Map.Entry))
				return false;
			Map.Entry<?, ?> that = (Map.Entry<?, ?>) o;
			return Util.equals(this.getKey(), that.getKey())
					&& Util.equals(this.getValue(), that.getValue());
		}

		@Override
		public int hashCode() {
			getterCalled();
			Object value = getValue();
			return (key == null ? 0 : key.hashCode())
					^ (value == null ? 0 : value.hashCode());
		}
	}

	private ISetChangeListener<K> masterListener = new ISetChangeListener<K>() {
		@Override
		public void handleSetChange(SetChangeEvent<? extends K> event) {
			if (isDisposed())
				return;

			cache.addAll(masterSet);

			// Need both obsolete and new elements to convert diff
			MapDiff<K, V> diff = convertDiff(event.diff);

			cache.retainAll(masterSet);

			fireMapChange(diff);
		}

		private MapDiff<K, V> convertDiff(SetDiff<? extends K> diff) {
			// Convert diff to detail value
			Map<K, V> oldValues = new HashMap<>();
			Map<K, V> newValues = new HashMap<>();

			for (K masterElement : diff.getRemovals()) {
				oldValues.put(masterElement, cache.get(masterElement));
			}
			for (K masterElement : diff.getAdditions()) {
				newValues.put(masterElement, cache.get(masterElement));
			}
			return Diffs.createMapDiff(diff.getAdditions(), diff.getRemovals(),
					Collections.<K> emptySet(), oldValues, newValues);
		}
	};

	private IStaleListener staleListener = staleEvent -> fireStale();

	/**
	 * @param keySet
	 * @param valueProperty
	 */
	public SetDelegatingValueObservableMap(IObservableSet<K> keySet,
			DelegatingValueProperty<S, V> valueProperty) {
		super(keySet.getRealm());
		this.masterSet = keySet;
		this.detailProperty = valueProperty;
		this.cache = new DelegatingCache<S, K, V>(getRealm(), valueProperty) {
			@Override
			void handleValueChange(K masterElement, V oldValue, V newValue) {
				fireMapChange(Diffs.createMapDiffSingleChange(masterElement,
						oldValue, newValue));
			}
		};
		cache.addAll(masterSet);

		masterSet.addSetChangeListener(masterListener);
		masterSet.addStaleListener(staleListener);
	}

	@Override
	public Set<Map.Entry<K, V>> entrySet() {
		getterCalled();
		if (entrySet == null)
			entrySet = new EntrySet();
		return entrySet;
	}

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

	@Override
	public V get(Object key) {
		getterCalled();
		return cache.get(key);
	}

	@Override
	public V put(K key, V value) {
		checkRealm();
		return cache.put(key, value);
	}

	@Override
	public boolean isStale() {
		return masterSet.isStale();
	}

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

	@Override
	public DelegatingValueProperty<S, V> getProperty() {
		return detailProperty;
	}

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

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

	@Override
	public synchronized void dispose() {
		if (masterSet != null) {
			masterSet.removeSetChangeListener(masterListener);
			masterSet.removeStaleListener(staleListener);
			masterSet = null;
		}

		if (cache != null) {
			cache.dispose();
			cache = null;
		}

		masterListener = null;
		detailProperty = null;

		super.dispose();
	}
}
