/*
 * Copyright 2015-2017 the original author or authors.
 *
 * All rights reserved. This program and the accompanying materials are
 * made available under the terms of the Eclipse Public License v1.0 which
 * accompanies this distribution and is available at
 *
 * http://www.eclipse.org/legal/epl-v10.html
 */

package org.junit.jupiter.engine.execution;

import static org.junit.platform.commons.meta.API.Usage.Internal;
import static org.junit.platform.commons.util.ReflectionUtils.getWrapperType;
import static org.junit.platform.commons.util.ReflectionUtils.isAssignableTo;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.function.Function;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContextException;
import org.junit.platform.commons.meta.API;
import org.junit.platform.commons.util.Preconditions;

/**
 * {@code ExtensionValuesStore} is used inside implementations of
 * {@link ExtensionContext} to store and retrieve attributes.
 *
 * @since 5.0
 */
@API(Internal)
public class ExtensionValuesStore {

	private final ExtensionValuesStore parentStore;
	// TODO Consider using a ConcurrentHashMap for the internal store.
	private final Map<Object, StoredValue> storedValues = new HashMap<>(4);
	private final Object monitor = new Object();

	public ExtensionValuesStore(ExtensionValuesStore parentStore) {
		this.parentStore = parentStore;
	}

	Object get(Namespace namespace, Object key) {
		StoredValue storedValue = getStoredValue(namespace, key);
		return storedValue == null ? null : storedValue.value;
	}

	<T> T get(Namespace namespace, Object key, Class<T> requiredType) {
		Object value = get(namespace, key);
		return castToRequiredType(key, value, requiredType);
	}

	<K, V> Object getOrComputeIfAbsent(Namespace namespace, K key, Function<K, V> defaultCreator) {
		synchronized (this.monitor) {
			StoredValue storedValue = getStoredValue(namespace, key);
			if (storedValue == null) {
				storedValue = new StoredValue(defaultCreator.apply(key));
				putStoredValue(namespace, key, storedValue);
			}
			return storedValue.value;
		}
	}

	<K, V> V getOrComputeIfAbsent(Namespace namespace, K key, Function<K, V> defaultCreator, Class<V> requiredType) {
		Object value = getOrComputeIfAbsent(namespace, key, defaultCreator);
		return castToRequiredType(key, value, requiredType);
	}

	void put(Namespace namespace, Object key, Object value) {
		Preconditions.notNull(namespace, "Namespace must not be null");
		Preconditions.notNull(key, "key must not be null");

		synchronized (this.monitor) {
			putStoredValue(namespace, key, new StoredValue(value));
		}
	}

	Object remove(Namespace namespace, Object key) {
		synchronized (this.monitor) {
			StoredValue previous = this.storedValues.remove(new CompositeKey(namespace, key));
			return (previous != null ? previous.value : null);
		}
	}

	<T> T remove(Namespace namespace, Object key, Class<T> requiredType) {
		Object value = remove(namespace, key);
		return castToRequiredType(key, value, requiredType);
	}

	private StoredValue getStoredValue(Namespace namespace, Object key) {
		// NOTE: this method can be called from another ExtensionValuesStore
		// instance, so we must synchronize on the monitor.
		synchronized (this.monitor) {
			StoredValue storedValue = this.storedValues.get(new CompositeKey(namespace, key));
			if (storedValue != null) {
				return storedValue;
			}
			else if (this.parentStore != null) {
				return this.parentStore.getStoredValue(namespace, key);
			}
			else {
				return null;
			}
		}
	}

	private void putStoredValue(Namespace namespace, Object key, StoredValue storedValue) {
		CompositeKey compositeKey = new CompositeKey(namespace, key);
		this.storedValues.put(compositeKey, storedValue);
	}

	@SuppressWarnings("unchecked")
	private <T> T castToRequiredType(Object key, Object value, Class<T> requiredType) {
		if (value == null) {
			return null;
		}
		if (isAssignableTo(value, requiredType)) {
			if (requiredType.isPrimitive()) {
				return (T) getWrapperType(requiredType).cast(value);
			}
			return requiredType.cast(value);
		}
		// else
		throw new ExtensionContextException(
			String.format("Object stored under key [%s] is not of required type [%s]", key, requiredType.getName()));
	}

	private static class CompositeKey {

		private final Namespace namespace;
		private final Object key;

		private CompositeKey(Namespace namespace, Object key) {
			this.namespace = namespace;
			this.key = key;
		}

		@Override
		public boolean equals(Object o) {
			if (this == o) {
				return true;
			}
			if (o == null || getClass() != o.getClass()) {
				return false;
			}
			CompositeKey that = (CompositeKey) o;
			return this.namespace.equals(that.namespace) && this.key.equals(that.key);
		}

		@Override
		public int hashCode() {
			return Objects.hash(this.namespace, this.key);
		}
	}

	private static class StoredValue {

		private final Object value;

		private StoredValue(Object value) {
			this.value = value;
		}
	}

}

@@ -592,20 +592,16 @@
 va.util.
-Hash
 Map;%0Aimp
@@ -606,35 +606,39 @@
 mport java.util.
-Map
+Objects
 ;%0Aimport java.ut
@@ -632,39 +632,60 @@
 mport java.util.
-Objects
+concurrent.ConcurrentHashMap
 ;%0Aimport java.ut
@@ -1120,16 +1120,12 @@
 eve 
-attribut
+valu
 es.%0A
@@ -1250,76 +1250,8 @@
 re;%0A
-%09// TODO Consider using a ConcurrentHashMap for the internal store.%0A
 %09pri
@@ -1305,16 +1305,26 @@
 s = new 
+Concurrent
 HashMap%3C

Use ConcurrentHashMap within ExtensionValuesStore