/*******************************************************************************
 * Copyright (c) 2006, 2017 BEA Systems, Inc. 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:
 *    wharley@bea.com - initial API and implementation
 *    IBM Corporation - Bug 513790
 *******************************************************************************/
package org.eclipse.jdt.apt.core.internal.util;

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

/**
 * Manage a Map<T1, Set<T2>>, with reverse links so that it is possible to
 * efficiently find all T1s that have a particular T2 associated with them.
 * Access to the map is synchronized, so that it is possible to read and
 * write simultaneously from multiple threads.  Set semantics are preserved
 * in both directions, so there is no distinction between calling this a
 * Map<T1, Set<T2>> versus a Map<T2, Set<T1>>.  It is symmetric.
 * <p>
 * The map permits the null value for keys and for value elements.
 * <p>
 * Design invariants preserved by all operations on this map are as follows:
 * <ul>
 * <li> If a key exists, it has at least one value associated with it; that is,
 * for all k such that null != containsKey(k), getValues(k) returns a non-empty
 * set.</li>
 * <li> If a value exists, it has at least one key associated with it; that is,
 * for all v such that null != containsValue(v), getKeys(v) returns a non-empty
 * set.</li>
 */
public class ManyToMany<T1, T2> {

	private final Map<T1, Set<T2>> _forward = new HashMap<>();
	private final Map<T2, Set<T1>> _reverse = new HashMap<>();
	private boolean _dirty = false;

	/**
	 * Empty all maps.  If the maps previously contained entries,
	 * this will set the dirty bit.
	 * @return true if the maps contained any entries prior to being cleared
	 */
	public synchronized boolean clear() {
		boolean hadContent = !_forward.isEmpty() || !_reverse.isEmpty();
		_reverse.clear();
		_forward.clear();
		_dirty |= hadContent;
		return hadContent;
	}

	/**
	 * Sets the dirty bit to false.  Internal operations do not use the dirty
	 * bit; clearing it will not affect behavior of the map.  It's just there
	 * for the convenience of callers who don't want to keep track of every
	 * put() and remove().
	 */
	public synchronized void clearDirtyBit() {
		_dirty = false;
	}

	/**
	 * Equivalent to keySet().contains(key).
	 * @return true if the map contains the specified key.
	 */
	public synchronized boolean containsKey(T1 key) {
		return _forward.containsKey(key);
	}

	/**
	 * Is there a key that is mapped to the specified value?
	 * Search within the forward map.
	 * @return true if such a key exists
	 */
	public synchronized boolean containsKeyValuePair(T1 key, T2 value) {
		Set<T2> values = _forward.get(key);
		if (null == values) {
			return false;
		}
		return values.contains(value);
	}

	/**
	 * Equivalent to values().contains(value).
	 * @return true if the map contains the specified value (regardless
	 * of what key it might be associated with).
	 */
	public synchronized boolean containsValue(T2 value) {
		return _reverse.containsKey(value);
	}

	/**
	 * Search the reverse map for all keys that have been associated with
	 * a particular value.
	 * @return the set of keys that are associated with the specified value,
	 * or an empty set if the value does not exist in the map.
	 */
	public synchronized Set<T1> getKeys(T2 value) {
		Set<T1> keys = _reverse.get(value);
		if (null == keys) {
			return Collections.emptySet();
		}
		return new HashSet<>(keys);
	}

	/**
	 * Search the forward map for all values associated with a particular key.
	 * Returns a copy of the set of values.
	 * @return a copy of the set of values that are associated with the
	 * specified key, or an empty set if the key does not exist in the map.
	 */
	public synchronized Set<T2> getValues(T1 key) {
		Set<T2> values = _forward.get(key);
		if (null == values) {
			return Collections.emptySet();
		}
		return new HashSet<>(values);
	}

	/**
	 * @return a copy of the set of all keys (that is, all items of type T1).
	 * If the maps are empty, the returned set will be empty, not null.  The
	 * returned set can be modified by the caller without affecting the map.
	 * @see #getValueSet()
	 */
	public synchronized Set<T1> getKeySet() {
		Set<T1> keys = new HashSet<>(_forward.keySet());
		return keys;
	}

	/**
	 * @return a copy of the set of all values (that is, all items of type T2).
	 * If the maps are empty, the returned set will be empty, not null.  The
	 * returned set can be modified by the caller without affecting the map.
	 * @see #getKeySet()
	 */
	public synchronized Set<T2> getValueSet() {
		Set<T2> values = new HashSet<>(_reverse.keySet());
		return values;
	}

	/**
	 * Return the state of the dirty bit.  All operations that change the state
	 * of the maps, including @see #clear(), set the dirty bit if any content actually
	 * changed.  The only way to clear the dirty bit is to call @see #clearDirtyBit().
	 * @return true if the map content has changed since it was created or since
	 * the last call to clearDirtyBit().
	 * @see #clearDirtyBit()
	 */
	public synchronized boolean isDirty() {
		return _dirty;
	}

	/**
	 * Check whether <code>key</code> has an association to any values other
	 * than <code>value</code> - that is, whether the same key has been added
	 * with multiple values.  Equivalent to asking whether the intersection of
	 * <code>getValues(key)</code> and the set containing <code>value</code> is
	 * non-empty.
	 * @return true iff <code>key</code> is in the map and is associated
	 * with values other than <code>value</code>.
	 * @see #valueHasOtherKeys(Object, Object)
	 */
	public synchronized boolean keyHasOtherValues(T1 key, T2 value) {
		Set<T2> values = _forward.get(key);
		if (values == null)
			return false;
		int size = values.size();
		if (size == 0)
			return false;
		else if (size > 1)
			return true;
		else // size == 1
			return !values.contains(value);
	}

	/**
	 * Associate the specified value with the key.  Adds the entry
	 * to both the forward and reverse maps.  Adding the same value
	 * twice to a particular key has no effect.  Because this is a
	 * many-to-many map, adding a new value for an existing key does
	 * not change the existing association, it adds a new one.
	 * @param key can be null
	 * @param value can be null
	 * @return true if the key/value pair did not exist prior to being added
	 */
	public synchronized boolean put(T1 key, T2 value) {
		// Add to forward map
		Set<T2> values = _forward.get(key);
		if (null == values) {
			values = new HashSet<>();
			_forward.put(key, values);
		}
		boolean added = values.add(value);
		_dirty |= added;

		// Add to reverse map
		Set<T1> keys = _reverse.get(value);
		if (null == keys) {
			keys = new HashSet<>();
			_reverse.put(value, keys);
		}
		keys.add(key);

		assert checkIntegrity();
		return added;
	}

	/**
	 * Remove a particular key-value association.  This is the inverse
	 * of put(key, value).  If the key does not exist, or the value
	 * does not exist, or the association does not exist, this call
	 * has no effect.
	 * @return true if the key/value pair existed in the map prior to removal
	 */
	public synchronized boolean remove(T1 key, T2 value) {
		Set<T2> values = _forward.get(key);
		if (values == null) {
			assert checkIntegrity();
			return false;
		}
		boolean removed = values.remove(value);
		if (values.isEmpty()) {
			_forward.remove(key);
		}
		if (removed) {
			_dirty = true;
			// it existed, so we need to remove from reverse map as well
			Set<T1> keys = _reverse.get(value);
			keys.remove(key);
			if (keys.isEmpty()) {
				_reverse.remove(value);
			}
		}
		assert checkIntegrity();
		return removed;
	}

	/**
	 * Remove the key and its associated key/value entries.
	 * Calling removeKey(k) is equivalent to calling remove(k,v)
	 * for every v in getValues(k).
	 * @return true if the key existed in the map prior to removal
	 */
	public synchronized boolean removeKey(T1 key) {
		// Remove all back-references to key.
		Set<T2> values = _forward.get(key);
		if (null == values) {
			// key does not exist in map.
			assert checkIntegrity();
			return false;
		}
		for (T2 value : values) {
			Set<T1> keys = _reverse.get(value);
			if (null != keys) {
				keys.remove(key);
				if (keys.isEmpty()) {
					_reverse.remove(value);
				}
			}
		}
		// Now remove the forward references from key.
		_forward.remove(key);
		_dirty = true;
		assert checkIntegrity();
		return true;
	}

	/**
	 * Remove the value and its associated key/value entries.
	 * Calling removeValue(v) is equivalent to calling remove(k,v)
	 * for every k in getKeys(v).
	 * @return true if the value existed in the map prior to removal.
	 */
	public synchronized boolean removeValue(T2 value) {
		// Remove any forward references to value
		Set<T1> keys = _reverse.get(value);
		if (null == keys) {
			// value does not exist in map.
			assert checkIntegrity();
			return false;
		}
		for (T1 key : keys) {
			Set<T2> values = _forward.get(key);
			if (null != values) {
				values.remove(value);
				if (values.isEmpty()) {
					_forward.remove(key);
				}
			}
		}
		// Now remove the reverse references from value.
		_reverse.remove(value);
		_dirty = true;
		assert checkIntegrity();
		return true;
	}

	/**
	 * Check whether <code>value</code> has an association from any keys other
	 * than <code>key</code> - that is, whether the same value has been added
	 * with multiple keys.  Equivalent to asking whether the intersection of
	 * <code>getKeys(value)</code> and the set containing <code>key</code> is
	 * non-empty.
	 * @return true iff <code>value</code> is in the map and is associated
	 * with keys other than <code>key</code>.
	 * @see #keyHasOtherValues(Object, Object)
	 */
	public synchronized boolean valueHasOtherKeys(T2 value, T1 key) {
		Set<T1> keys = _reverse.get(value);
		if (keys == null)
			return false;
		int size = keys.size();
		if (size == 0)
			return false;
		else if (size > 1)
			return true;
		else // size == 1
			return !keys.contains(key);
	}

	/**
	 * Check the integrity of the internal data structures.  This is intended to
	 * be called within an assert, so that if asserts are disabled the integrity
	 * checks will not cause a performance impact.
	 * @return true if everything is okay.
	 * @throws IllegalStateException if there is a problem.
	 */
	private boolean checkIntegrity() {
		// For every T1->T2 mapping in the forward map, there should be a corresponding
		// T2->T1 mapping in the reverse map.
		for (Map.Entry<T1, Set<T2>> entry : _forward.entrySet()) {
			Set<T2> values = entry.getValue();
			if (values.isEmpty()) {
				throw new IllegalStateException("Integrity compromised: forward map contains an empty set"); //$NON-NLS-1$
			}
			for (T2 value : values) {
				Set<T1> keys = _reverse.get(value);
				if (null == keys || !keys.contains(entry.getKey())) {
					throw new IllegalStateException("Integrity compromised: forward map contains an entry missing from reverse map: " + value); //$NON-NLS-1$
				}
			}
		}
		// And likewise in the other direction.
		for (Map.Entry<T2, Set<T1>> entry : _reverse.entrySet()) {
			Set<T1> keys = entry.getValue();
			if (keys.isEmpty()) {
				throw new IllegalStateException("Integrity compromised: reverse map contains an empty set"); //$NON-NLS-1$
			}
			for (T1 key : keys) {
				Set<T2> values = _forward.get(key);
				if (null == values || !values.contains(entry.getKey())) {
					throw new IllegalStateException("Integrity compromised: reverse map contains an entry missing from forward map: " + key); //$NON-NLS-1$
				}
			}
		}
		return true;
	}

}
