/*******************************************************************************
 * 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 221704)
 *     Matthew Hall - bug 223114, 226289, 247875, 246782, 249526, 268022,
 *                    251424
 *     Stefan Xenos <sxenos@gmail.com> - Bug 335792
 *     Stefan Xenos <sxenos@gmail.com> - Bug 474065
 ******************************************************************************/

package org.eclipse.core.internal.databinding.observable.masterdetail;

import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.IObserving;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.map.IMapChangeListener;
import org.eclipse.core.databinding.observable.map.IObservableMap;
import org.eclipse.core.databinding.observable.map.ObservableMap;
import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory;
import org.eclipse.core.databinding.observable.value.IObservableValue;
import org.eclipse.core.databinding.observable.value.IValueChangeListener;
import org.eclipse.core.runtime.Assert;

/**
 * @param <M>
 *            type of the master observable
 * @param <K>
 *            type of the keys to the inner observable map
 * @param <V>
 *            type of the values in the inner observable map
 * @since 1.1
 *
 */
public class DetailObservableMap<M, K, V> extends ObservableMap<K, V>implements IObserving {
	private boolean updating = false;

	private IObservableValue<M> master;
	private IObservableFactory<? super M, IObservableMap<K, V>> detailFactory;

	private IObservableMap<K, V> detailMap;

	private Object detailKeyType;
	private Object detailValueType;

	private IValueChangeListener<M> masterChangeListener = event -> {
		if (isDisposed())
			return;
		ObservableTracker.setIgnore(true);
		try {
			Map<K, V> oldMap = new HashMap<>(wrappedMap);
			updateDetailMap();
			fireMapChange(Diffs.computeMapDiff(oldMap, wrappedMap));
		} finally {
			ObservableTracker.setIgnore(false);
		}
	};

	private IMapChangeListener<K, V> detailChangeListener = event -> {
		if (!updating) {
			fireMapChange(Diffs.unmodifiableDiff(event.diff));
		}
	};

	/**
	 * Constructs a new DetailObservableMap
	 *
	 * @param detailFactory
	 *            observable factory that creates IObservableMap instances given
	 *            the current value of master observable value
	 * @param master
	 * @param keyType
	 * @param valueType
	 *
	 */
	public DetailObservableMap(
			IObservableFactory<? super M, IObservableMap<K, V>> detailFactory,
			IObservableValue<M> master, Object keyType, Object valueType) {
		super(master.getRealm(), Collections.<K, V> emptyMap());
		Assert.isTrue(!master.isDisposed(), "Master observable is disposed"); //$NON-NLS-1$

		this.master = master;
		this.detailFactory = detailFactory;
		this.detailKeyType = keyType;
		this.detailValueType = valueType;

		master.addDisposeListener(staleEvent -> dispose());

		ObservableTracker.setIgnore(true);
		try {
			updateDetailMap();
		} finally {
			ObservableTracker.setIgnore(false);
		}
		master.addValueChangeListener(masterChangeListener);
	}

	private void updateDetailMap() {
		final M masterValue = master.getValue();
		if (detailMap != null) {
			detailMap.removeMapChangeListener(detailChangeListener);
			detailMap.dispose();
		}

		if (masterValue == null) {
			detailMap = null;
			wrappedMap = Collections.emptyMap();
		} else {
			ObservableTracker.setIgnore(true);
			try {
				detailMap = detailFactory.createObservable(masterValue);
			} finally {
				ObservableTracker.setIgnore(false);
			}
			DetailObservableHelper.warnIfDifferentRealms(getRealm(),
					detailMap.getRealm());
			wrappedMap = detailMap;

			if (detailKeyType != null) {
				Object innerKeyType = detailMap.getKeyType();

				Assert.isTrue(detailKeyType.equals(innerKeyType),
						"Cannot change key type in a nested observable map"); //$NON-NLS-1$
			}

			if (detailValueType != null) {
				Object innerValueType = detailMap.getValueType();

				Assert.isTrue(detailValueType.equals(innerValueType),
						"Cannot change value type in a nested observable map"); //$NON-NLS-1$
			}

			detailMap.addMapChangeListener(detailChangeListener);
		}
	}

	@Override
	public Object getKeyType() {
		return detailKeyType;
	}

	@Override
	public Object getValueType() {
		return detailValueType;
	}

	@Override
	public V put(final K key, final V value) {
		ObservableTracker.setIgnore(true);
		try {
			return detailMap.put(key, value);
		} finally {
			ObservableTracker.setIgnore(false);
		}
	}

	@Override
	public void putAll(final Map<? extends K, ? extends V> map) {
		ObservableTracker.setIgnore(true);
		try {
			detailMap.putAll(map);
		} finally {
			ObservableTracker.setIgnore(false);
		}
	}

	@Override
	public V remove(final Object key) {
		ObservableTracker.setIgnore(true);
		try {
			return detailMap.remove(key);
		} finally {
			ObservableTracker.setIgnore(false);
		}
	}

	@Override
	public void clear() {
		ObservableTracker.setIgnore(true);
		try {
			detailMap.clear();
		} finally {
			ObservableTracker.setIgnore(false);
		}
	}

	@Override
	public synchronized void dispose() {
		if (master != null) {
			master.removeValueChangeListener(masterChangeListener);
			master = null;
			masterChangeListener = null;
		}
		detailFactory = null;
		if (detailMap != null) {
			detailMap.removeMapChangeListener(detailChangeListener);
			detailMap.dispose();
			detailMap = null;
		}
		detailChangeListener = null;
		super.dispose();
	}

	@Override
	public Object getObserved() {
		if (detailMap instanceof IObserving) {
			return ((IObserving) detailMap).getObserved();
		}
		return null;
	}
}
