/*******************************************************************************
 * Copyright (c) 2021 Joerg Kubitz.
 *
 * 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:
 *     Joerg Kubitz              - initial API and implementation
 *******************************************************************************/
package org.eclipse.e4.core.internal.contexts;

import java.util.concurrent.ConcurrentHashMap;
import java.util.function.BiConsumer;

/**
 * A wrapper around ConcurrentHashMap which allows null values.
 *
 * For null values a neutral Element is internally used.
 *
 * Faster then a synchronized Map.
 */
public class ConcurrentNeutralValueMap<K, V> {// implements subset of Map<K, V>
	private final ConcurrentHashMap<K, V> delegate = new ConcurrentHashMap<>();
	/** a value that is used for null elements **/
	private final V neutralValue;
	private static final NullValue NULL = new NullValue();

	/**
	 * @param neutralValue a Element that does not equal any other Value
	 */
	public ConcurrentNeutralValueMap(V neutralValue) {
		this.neutralValue = neutralValue;
	}

	@SuppressWarnings("unchecked")
	public ConcurrentNeutralValueMap() {
		this((V) NULL);
	}

	private V wrapValue(V value) {
		return value == null ? neutralValue : value;
	}

	private V unwrapValue(V v) {
		return v == neutralValue ? null : v;
	}

	public V get(Object key) {
		return unwrapValue(delegate.get(key));
	}

	private static final class NullValue {
		/** for {@link ConcurrentNeutralValueMap#toString()} **/
		@Override
		public String toString() {
			return "null"; //$NON-NLS-1$
		}

		@Override
		public boolean equals(Object obj) {
			// currently not called since ConcurrentHashMap checks for value1==value2
			// but just in case the Implementation changes:
			// in means of ConcurrentNeutralValueMap#equals() every NullValue equals
			return obj instanceof NullValue;
			// like java.util.Objects.equals(null, null) would return true,
		}

		/** for {@link ConcurrentNeutralValueMap#hashCode()} **/
		@Override
		public int hashCode() {
			// in means of ConcurrentNeutralValueMap#hashCode() we need a hashCode
			// even tough null.hashCode() would normally throw NullPointerException.
			return 0x55555555;
			// like java.util.Objects.hashCode(null) would return an int,
		}
	}

	/*
	 * Method overloads from java.lang.Object:
	 */

	/** return uses hashCode() of the neutralValue **/
	@Override
	public int hashCode() {
		return delegate.hashCode();
	}

	/** return does NOT use equals() of the neutralValue **/
	@Override
	public boolean equals(Object obj) {
		if (!(obj instanceof ConcurrentNeutralValueMap))
			return false;
		return delegate.equals(((ConcurrentNeutralValueMap<?, ?>) obj).delegate);
	}

	/** return uses toString() of the neutralValue **/
	@Override
	public String toString() {
		return delegate.toString();
	}

	/*
	 * Method overloads from java.util.Map:
	 */

	/**
	 * Like {@link java.util.Map#put} but we do not return a value.
	 *
	 * @see java.util.Map#put(Object, Object)
	 * @see ConcurrentHashMap#put(Object, Object)
	 * @see #putAndGetOld(Object, Object)
	 **/
	public void put(K key, V value) {
		delegate.put(key, wrapValue(value));
	}

	/**
	 * Like {@link java.util.Map#put} but we do not return a wrapped value.
	 *
	 * @see java.util.Map#put(Object, Object)
	 * @see Value
	 **/
	public Value<V> putAndGetOld(K key, V value) {
		return new Wrapped(delegate.put(key, wrapValue(value)));
	}

	/**
	 * @see java.util.Map#remove(Object)
	 * @see ConcurrentHashMap#remove(Object)
	 **/
	public V remove(Object key) {
		return unwrapValue(delegate.remove(key));
	}

	/**
	 * @see java.util.Map#size()
	 **/
	public int size() {
		return delegate.size();
	}

	/**
	 * @see java.util.Map#isEmpty()
	 **/
	public boolean isEmpty() {
		return delegate.isEmpty();
	}

	/**
	 * @see java.util.Map#clear()
	 **/
	public void clear() {
		delegate.clear();
	}

	/**
	 * Use is discouraged as the outcome might change concurrently
	 *
	 * @see java.util.Map#containsKey(Object)
	 * @see ConcurrentHashMap#containsKey(Object)
	 **/
	public boolean containsKey(Object key) {
		return delegate.containsKey(key);
	}

	/*
	 * Some helpful methods from ConcurrentHashMap which do not return V:
	 */

	/** @see ConcurrentHashMap#forEach(BiConsumer) **/
	public void forEach(BiConsumer<? super K, ? super V> action) {
		delegate.forEach( //
				(k, v) -> action.accept(k, unwrapValue(v) //
				));
	}

	/*
	 * Some methods from ConcurrentHashMap which return V. returning null is
	 * discouraged because it is not clear if that was a value or not:
	 */

	/**
	 * Like {@link ConcurrentHashMap#putIfAbsent(Object, Object)} but we do not
	 * return anything.
	 *
	 * @see ConcurrentHashMap#putIfAbsent(Object, Object)
	 **/
	public void putIfAbsent(K key, V value) {
		delegate.putIfAbsent(key, wrapValue(value));
	}

	public interface Value<V> {
		/**
		 * @return the result of map.contains(key)
		 */
		boolean isPresent();

		/**
		 * Callers are supposed to check #isPresent first to determine whether a
		 * returned null was a null value or the key was not present.
		 *
		 * @return the unwrapped result of map.get(key). May return null.
		 */
		V unwrapped();
	}

	private final class Wrapped implements Value<V> {
		V wrappedValue;

		private Wrapped(V wrappedValue) {
			this.wrappedValue = wrappedValue;
		}

		@Override
		public boolean isPresent() {
			return wrappedValue != null;
		}

		@Override
		public V unwrapped() {
			return unwrapValue(wrappedValue);
		}
	}

	/**
	 * @return a value that is wrapped into a Value Object.
	 * @see Value#isPresent()
	 * @see Value#unwrapped()
	 */
	public Value<V> getValue(K key) {
		return new Wrapped(delegate.get(key));
	}

}
