/*******************************************************************************
 * Copyright (c) 2000, 2012 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
 *******************************************************************************/
package org.eclipse.jdi.internal;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

/**
 * This class is used to cache values. It uses soft references to store cached
 * values. Once a value is garbage collected by the VM, the corresponding entry
 * is removed from the cache on the next invocation of put() or get().
 *
 * Note that WeakHashMap can't be used for this purpose because in WeakHashMap
 * soft references are only used for the keys, and values may not have 'strong'
 * references to keys otherwise they will never be garbage collected.
 *
 */
public class ValueCache {
	/**
	 * Map to store <key, Reference> pairs, where Reference is a soft reference
	 * to an Object.
	 */
	private Map<Object, SoftReference<Object>> cacheTable = new Hashtable<>();
	/**
	 * Map to store <Reference, key> pairs, to find the cacheTable-key of a
	 * garbage collected Reference.
	 */
	private Map<SoftReference<Object>, Object> refTable = new Hashtable<>();

	/**
	 * The reference-queue that is registered with the soft references. The
	 * garbage collector will enqueue soft references that are garbage
	 * collected.
	 */
	private ReferenceQueue<Object> refQueue = new ReferenceQueue<>();

	/**
	 * Clean up all entries from the table for which the values were garbage
	 * collected.
	 */
	private void cleanup() {
		Reference<?> ref;
		while ((ref = refQueue.poll()) != null) {
			Object key = refTable.get(ref);
			if (key != null)
				cacheTable.remove(key);
			refTable.remove(ref);
		}
	}

	/**
	 * Put a new entry in the cache under the given key.
	 */
	public void put(Object key, Object value) {
		cleanup();
		SoftReference<Object> ref = new SoftReference<>(value, refQueue);
		cacheTable.put(key, ref);
		refTable.put(ref, key);
	}

	/**
	 * Get entry from the cache.
	 *
	 * @return Returns value that is cached under the given key, or null of one
	 *         of the following is true: - The value has not been cached. - The
	 *         value had been cached but is garbage collected.
	 */
	public Object get(Object key) {
		cleanup();
		Object value = null;
		SoftReference<?> ref = cacheTable.get(key);
		if (ref != null) {
			value = ref.get();
		}
		return value;
	}

	/**
	 * Returns a Collection view of the values contained in this cache.
	 */
	public Collection<Object> values() {
		cleanup();
		List<Object> returnValues = new ArrayList<>();
		synchronized (cacheTable) {
			Iterator<SoftReference<Object>> iter = cacheTable.values().iterator();
			SoftReference<Object> ref;
			Object value;
			while (iter.hasNext()) {
				ref = iter.next();
				value = ref.get();
				if (value != null) {
					returnValues.add(value);
				}
			}
		}
		return returnValues;
	}

	/**
	 * Returns a Collection view of the values contained in this cache that have
	 * the same runtime class as the given Class.
	 */
	public Collection<Object> valuesWithType(Class<?> type) {
		cleanup();
		List<Object> returnValues = new ArrayList<>();
		synchronized (cacheTable) {
			Iterator<SoftReference<Object>> iter = cacheTable.values().iterator();
			SoftReference<Object> ref;
			Object value;
			while (iter.hasNext()) {
				ref = iter.next();
				value = ref.get();
				if (value != null && value.getClass().equals(type)) {
					returnValues.add(value);
				}
			}
		}
		return returnValues;
	}

	/**
	 * Removes the key and its corresponding value from this cache.
	 *
	 * @return Returns The value to which the key had been mapped in this
	 *         hashtable, or null if the key did not have a mapping.
	 */
	public Object remove(Object key) {
		cleanup();
		Object value = null;
		SoftReference<?> ref = cacheTable.get(key);
		if (ref != null) {
			value = ref.get();
			refTable.remove(ref);
		}
		cacheTable.remove(key);
		return value;
	}
}
