package core.util;

import java.lang.ref.SoftReference;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.Supplier;

import core.config.Env;

/**
 * マップユーティリティ
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class MapUtil {

	/**
	 * コンストラクタ
	 */
	private MapUtil() {
		throw new AssertionError();
	}

	/**
	 * キー、バリュー配列マップ化
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param key キー配列
	 * @param val 値配列
	 * @return マップ
	 */
	public static <K, V> Map<K, V> toMap(final K[] key, final V[] val) {
		Map<K, V> ret = new HashMap<>();
		if (key != null && val != null) {
			for (int i = 0; i < key.length; i++) {
				ret.put(key[i], val[i]);
			}
		}
		return ret;
	}

	/**
	 * キャッシュリファレンスからマップを取得
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param cache キャッシュリファレンス
	 * @return マップ
	 */
	public static <K, V> ConcurrentMap<K, V> getCacheMap(
					final AtomicReference<SoftReference<ConcurrentMap<K, V>>> cache) {
		SoftReference<ConcurrentMap<K, V>> ref = cache.get();
		ConcurrentMap<K, V> map = ref != null ? ref.get() : null;
		while (map == null) {
			map = cacheAwareMap(new ConcurrentHashMap<K, V>());
			if (cache.compareAndSet(ref, new SoftReference<>(map))) {
				break;
			}
			ref = cache.get();
			map = ref.get();
		}
		return map;
	}

	/**
	 * キャッシュリファレンスからマップを取得
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param <E> ジェネリクス
	 * @param key キー
	 * @param cache キャッシュリファレンスマップ
	 * @return マップ
	 */
	public static <K, V, E> ConcurrentMap<K, V> getCacheMap(final E key,
					final ConcurrentMap<E, SoftReference<ConcurrentMap<K, V>>> cache) {
		SoftReference<ConcurrentMap<K, V>> ref = cache.get(key);
		ConcurrentMap<K, V> map = ref != null ? ref.get() : null;
		while (map == null) {
			map = cacheAwareMap(new ConcurrentHashMap<K, V>());
			if (ref == null) {
				if (cache.putIfAbsent(key, new SoftReference<>(map)) == null) {
					break;
				}
			} else {
				if (cache.replace(key, ref, new SoftReference<>(map))) {
					break;
				}
			}
			ref = cache.get(key);
			map = ref.get();
		}
		return map;
	}

	/**
	 * キャッシュマップ
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param map マップ
	 * @return キャッシュマップ
	 */
	public static <K, V> ConcurrentMap<K, V> cacheAwareMap(final ConcurrentMap<K, V> map) {
		return new CacheAwareMap<>(map);
	}

	/**
	 * 動的マップ
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 * @param map マップ
	 * @param sec 有効秒
	 * @return 動的マップ
	 */
	public static <K, V> ConcurrentMap<K, V> dynamicCacheMap(
					final ConcurrentMap<K, V> map, final int sec) {
		return new DynamicCacheMap<>(map, sec);
	}

	/**
	 * マップ値取得
	 * @param <K> Key
	 * @param <V> Value
	 * @param key キー
	 * @param map マップ
	 * @param supplier 供給
	 * @return マップのキー値がnullの場合、supplierの実行値を返す。
	 */
	public static <K, V> V get(final K key, final Map<K, V> map, final Supplier<V> supplier) {
		V value = map.get(key);
		return value == null ? supplier.get() : value;
	}

	/**
	 * マップ値取得
	 * @param <K> Key
	 * @param <V> Value
	 * @param <R> Return
	 * @param key キー
	 * @param map マップ
	 * @param function 変換
	 * @param supplier 供給
	 * @return マップのキー値がnullの場合、supplierの実行値を返す。
	 */
	public static <K, V, R> R get(final K key, final Map<K, V> map,
					final Function<V, R> function, final Supplier<R> supplier) {
		V value = map.get(key);
		return value == null ? supplier.get() : function.apply(value);
	}

	/**
	 * ラッパークラス
	 * @author Tadashi Nakayama
	 *
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 */
	private static final class CacheAwareMap<K, V> implements ConcurrentMap<K, V> {
		/** マップ */
		private final ConcurrentMap<K, V> map;

		/**
		 * コンストラクタ
		 * @param val マップ
		 */
		CacheAwareMap(final ConcurrentMap<K, V> val) {
			this.map = val;
		}

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

		/**
		 * @see java.util.Map#containsKey(java.lang.Object)
		 */
		@Override
		public boolean containsKey(final Object key) {
			return this.map.containsKey(key);
		}

		/**)
		 * @see java.util.Map#containsValue(java.lang.Object)
		 */
		@Override
		public boolean containsValue(final Object value) {
			return this.map.containsValue(value);
		}

		/**
		 * @see java.util.Map#entrySet()
		 */
		@Override
		public Set<Entry<K, V>> entrySet() {
			if (Env.isDevelop()) {
				return Collections.unmodifiableSet(this.map.entrySet());
			}
			return this.map.entrySet();
		}

		/**
		 * @see java.util.Map#get(java.lang.Object)
		 */
		@Override
		public V get(final Object key) {
			return this.map.get(key);
		}

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

		/**
		 * @see java.util.Map#keySet()
		 */
		@Override
		public Set<K> keySet() {
			return this.map.keySet();
		}

		/**
		 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V put(final K key, final V value) {
			return Env.isDevelop() ? null : this.map.put(key, value);
		}

		/**
		 * @see java.util.Map#putAll(java.util.Map)
		 */
		@Override
		public void putAll(final Map<? extends K, ? extends V> m) {
			if (!Env.isDevelop()) {
				this.map.putAll(m);
			}
		}

		/**
		 * @see java.util.Map#remove(java.lang.Object)
		 */
		@Override
		public V remove(final Object key) {
			return this.map.remove(key);
		}

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

		/**
		 * @see java.util.Map#values()
		 */
		@Override
		public Collection<V> values() {
			return this.map.values();
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #putIfAbsent(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V putIfAbsent(final K key, final V value) {
			if (!Env.isDevelop()) {
				return this.map.putIfAbsent(key, value);
			}
			return null;
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #remove(java.lang.Object, java.lang.Object)
		 */
		@Override
		public boolean remove(final Object key, final Object value) {
			return this.map.remove(key, value);
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #replace(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V replace(final K key, final V value) {
			if (!Env.isDevelop()) {
				return this.map.replace(key, value);
			}
			return null;
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #replace(java.lang.Object, java.lang.Object, java.lang.Object)
		 */
		@Override
		public boolean replace(final K key, final V oldValue, final V newValue) {
			return !Env.isDevelop() && this.map.replace(key, oldValue, newValue);
		}
	}

	/**
	 * ラッパークラス
	 * @author Tadashi Nakayama
	 *
	 * @param <K> ジェネリクス
	 * @param <V> ジェネリクス
	 */
	private static final class DynamicCacheMap<K, V> implements ConcurrentMap<K, V> {
		/** 有効時間（ミリセック） */
		private final int msec;
		/** マップ */
		private final ConcurrentMap<K, Value<V>> map = new ConcurrentHashMap<>();

		/**
		 * コンストラクタ
		 * @param val マップ
		 * @param sec 有効秒
		 */
		DynamicCacheMap(final ConcurrentMap<K, V> val, final int sec) {
			putAll(val);
			this.msec = sec * 1000;
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #putIfAbsent(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V putIfAbsent(final K key, final V value) {
			while (true) {
				long millis = System.currentTimeMillis();
				Value<V> newVal = new Value<>(value, millis + this.msec);
				Value<V> val = this.map.putIfAbsent(key, newVal);
				if (val == null) {
					return null;
				}

				if (val.isValid(millis)) {
					return val.getValue();
				}

				if (this.map.replace(key, val, newVal)) {
					return null;
				}
			}
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #remove(java.lang.Object, java.lang.Object)
		 */
		@Override
		public boolean remove(final Object key, final Object value) {
			Value<V> val = this.map.get(key);
			if (val != null && val.getValue().equals(value)) {
				if (val.isValid(System.currentTimeMillis())) {
					return this.map.remove(key, val);
				}
				this.map.remove(key, val);
			}
			return false;
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #replace(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V replace(final K key, final V value) {
			return toValue(this.map.replace(key, new Value<>(value, this.msec)));
		}

		/**
		 * @see java.util.concurrent.ConcurrentMap
		 * #replace(java.lang.Object, java.lang.Object, java.lang.Object)
		 */
		@Override
		public boolean replace(final K key, final V oldValue, final V newValue) {
			Value<V> val = this.map.get(key);
			if (val != null && val.getValue().equals(oldValue)) {
				if (val.isValid(System.currentTimeMillis())) {
					return this.map.replace(key, val, new Value<>(newValue, this.msec));
				}
				this.map.remove(key, val);
			}
			return false;
		}

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

		/**
		 * @see java.util.Map#containsKey(java.lang.Object)
		 */
		@Override
		public boolean containsKey(final Object key) {
			return isValidValue(this.map.get(key));
		}

		/**
		 * @see java.util.Map#containsValue(java.lang.Object)
		 */
		@Override
		public boolean containsValue(final Object value) {
			long time = System.currentTimeMillis();
			for (final Value<V> val : this.map.values()) {
				if (val.isValid(time) && val.getValue().equals(value)) {
					return true;
				}
			}
			return false;
		}

		/**
		 * @see java.util.Map#entrySet()
		 */
		@Override
		public Set<Entry<K, V>> entrySet() {
			long time = System.currentTimeMillis();
			Set<Entry<K, V>> ret = new HashSet<>();
			for (final Entry<K, Value<V>> val : this.map.entrySet()) {
				if (!val.getValue().isValid(time)) {
					ret.add(new DynamicCacheEntry<>(val.getKey(), val.getValue().getValue()));
				}
			}
			return Collections.unmodifiableSet(ret);
		}

		/**
		 * @see java.util.Map#get(java.lang.Object)
		 */
		@Override
		public V get(final Object key) {
			Value<V> val = this.map.get(key);
			if (val != null) {
				if (val.isValid(System.currentTimeMillis())) {
					return val.getValue();
				}
				this.map.remove(key, val);
			}
			return null;
		}

		/**
		 * @see java.util.Map#isEmpty()
		 */
		@Override
		public boolean isEmpty() {
			return size() == 0;
		}

		/**
		 * @see java.util.Map#keySet()
		 */
		@Override
		public Set<K> keySet() {
			long time = System.currentTimeMillis();
			Set<K> ret = new HashSet<>();
			for (final Entry<K, Value<V>> val : this.map.entrySet()) {
				if (!val.getValue().isValid(time)) {
					ret.add(val.getKey());
				}
			}
			return Collections.unmodifiableSet(ret);
		}

		/**
		 * @see java.util.Map#put(java.lang.Object, java.lang.Object)
		 */
		@Override
		public V put(final K key, final V value) {
			return toValue(this.map.put(key, new Value<>(value, this.msec)));
		}

		/**
		 * @see java.util.Map#putAll(java.util.Map)
		 */
		@Override
		public void putAll(final Map<? extends K, ? extends V> m) {
			long time = System.currentTimeMillis();
			for (final Entry<? extends K, ? extends V> me : m.entrySet()) {
				this.map.put(me.getKey(), new Value<V>(me.getValue(), time + this.msec));
			}
		}

		/**
		 * @see java.util.Map#remove(java.lang.Object)
		 */
		@Override
		public V remove(final Object key) {
			return toValue(this.map.remove(key));
		}

		/**
		 * @see java.util.Map#size()
		 */
		@Override
		public int size() {
			long time = System.currentTimeMillis();
			int ret = 0;
			for (final Value<V> val : this.map.values()) {
				if (val.isValid(time)) {
					ret++;
				}
			}
			return ret;
		}

		/**
		 * @see java.util.Map#values()
		 */
		@Override
		public Collection<V> values() {
			long time = System.currentTimeMillis();
			Collection<V> ret = new HashSet<>();
			for (final Value<V> val : this.map.values()) {
				if (!val.isValid(time)) {
					ret.add(val.getValue());
				}
			}
			return Collections.unmodifiableCollection(ret);
		}

		/**
		 * 値取得
		 * @param val 値
		 * @return 有効値
		 */
		private V toValue(final Value<V> val) {
			return val != null && val.isValid(System.currentTimeMillis()) ? val.getValue() : null;
		}

		/**
		 * 有効判断
		 * @param val 値
		 * @return 有効の場合 true を返す。
		 */
		private boolean isValidValue(final Value<V> val) {
			return val != null && val.isValid(System.currentTimeMillis());
		}

		/**
		 * バリュークラス
		 * @author Tadashi Nakayama
		 * @param <V> ジェネリクス
		 */
		private static final class Value<V> {
			/** 値 */
			private final V value;
			/** 作成時刻 */
			private final long time;

			/**
			 * コンストラクタ
			 * @param val 値
			 * @param msec 有効秒（ミリセック）
			 */
			Value(final V val, final int msec) {
				this(val, System.currentTimeMillis() + msec);
			}

			/**
			 * コンストラクタ
			 * @param val 値
			 * @param millis 有効未来時間（ミリセック）
			 */
			Value(final V val, final long millis) {
				if (val == null) {
					throw new NullPointerException();
				}

				this.value = val;
				this.time = millis;
			}

			/**
			 * 値取得
			 * @return 値
			 */
			public V getValue() {
				return this.value;
			}

			/**
			 * 有効判断
			 * @param millis 有効判断時間（ミリセック）
			 * @return 有効の場合 true を返す。
			 */
			public boolean isValid(final long millis) {
				return millis <= this.time;
			}

			/**
			 * @see java.lang.Object#toString()
			 */
			@Override
			public String toString() {
				return String.valueOf(this.value);
			}
		}

		/**
		 * エントリーセット用ネストクラス
		 *
		 * @param <K> キー
		 * @param <V> 値
		 */
		private static final class DynamicCacheEntry<K, V> implements Entry<K, V> {
			/** キー */
			private final K k;
			/** 値 */
			private final V v;

			/**
			 * コンストラクタ
			 *
			 * @param key キー
			 * @param value 値
			 */
			DynamicCacheEntry(final K key, final V value) {
				this.k = key;
				this.v = value;
			}

			/**
			 * @see java.util.Map.Entry#getKey()
			 */
			@Override
			public K getKey() {
				return this.k;
			}

			/**
			 * @see java.util.Map.Entry#getValue()
			 */
			@Override
			public V getValue() {
				return this.v;
			}

			/**
			 * @param arg0 設定値
			 * @return 既存値
			 * @see java.util.Map.Entry#setValue(java.lang.Object)
			 */
			@Override
			public V setValue(final V arg0) {
				throw new UnsupportedOperationException();
			}

			/**
			 * @see java.lang.Object#toString()
			 */
			@Override
			public String toString() {
				return getKey() + "=" + getValue();
			}
		}
	}
}
