package online.model;

import java.io.Serializable;
import java.lang.reflect.Array;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;

import core.config.Factory;

/**
 * ユニマップ
 *
 * @author Tadashi Nakayama
 * @param <K> キー
 * @param <V> 値
 */
public final class UniMap<K extends Serializable, V extends Serializable>
		implements Map<K, V>, Serializable {
	/** serialVersionUID */
	private static final long serialVersionUID = 1L;

	/** 記憶用マップ */
	private final Map<K, V[]> map = new HashMap<>();

	/** 値のクラス */
	private final Class<V> cls;

	/**
	 * コンストラクタ
	 *
	 * @param vals ダミー 指定しないこと
	 */
	@SafeVarargs
	public UniMap(final V... vals) {
		if (vals == null || vals.length != 0) {
			throw new IllegalArgumentException();
		}
		this.cls = Factory.cast(vals.getClass().getComponentType());
	}

	/**
	 * キーに対応した値が配列かを返す。
	 *
	 * @param key キー
	 * @return 配列(1次元以上)なら true
	 */
	public boolean isArrayValue(final K key) {
		final V[] obj = this.map.get(key);
		return obj != null && obj.getClass().getComponentType().isArray();
	}

	/**
	 * 配列化
	 *
	 * @param key キー
	 * @return 配列(1次元)化した場合 true を返す。
	 */
	public boolean toArrayValue(final K key) {
		if (Serializable.class.equals(this.cls)) {
			final V[] obj = this.map.get(key);
			if (obj != null) {
				if (!obj.getClass().getComponentType().isArray()) {
					this.map.put(key, getDimensionalValue(obj, null));
					return true;
				}
			}
		}
		return false;
	}

	/**
	 * 配列長取得
	 *
	 * @param key キー
	 * @return 配列長
	 */
	public int getArraySize(final K key) {
		final V[] obj = this.map.get(key);
		if (obj != null) {
			final var length = Array.getLength(obj);
			if (obj.getClass().getComponentType().isArray()) {
				if (0 < length && obj[0] != null) {
					return Array.getLength(obj[0]);
				}
			}
			return length;
		}
		return 0;
	}

	/**
	 * @see java.util.Map#containsValue(java.lang.Object)
	 */
	@Override
	public boolean containsValue(final Object arg0) {
		return this.map.keySet().stream().map(this::get).anyMatch(Predicate.isEqual(arg0));
	}

	/**
	 * @see java.util.Map#values()
	 */
	@Override
	public Collection<V> values() {
		final Collection<V> ret = new HashSet<>();
		for (final V[] obj : this.map.values()) {
			ret.add(toReal(obj));
		}
		return Collections.unmodifiableCollection(ret);
	}

	/**
	 * @see java.util.Map#putAll(java.util.Map)
	 */
	@Override
	public void putAll(final Map<? extends K, ? extends V> arg0) {
		if (UniMap.class.isInstance(arg0)) {
			this.map.putAll(Factory.<UniMap<? extends K, ? extends V>>cast(arg0).map);
		} else if (arg0 != null) {
			for (final var ent : arg0.entrySet()) {
				put(ent.getKey(), ent.getValue());
			}
		}
	}

	/**
	 * @see java.util.Map#entrySet()
	 */
	@Override
	public Set<Entry<K, V>> entrySet() {
		final var ret = new HashSet<Entry<K, V>>();
		for (final K key : this.map.keySet()) {
			ret.add(new UniMapEntry<>(this, key, get(key)));
		}
		return Collections.unmodifiableSet(ret);
	}

	/**
	 * @see java.util.Map#get(java.lang.Object)
	 */
	@Override
	public V get(final Object arg0) {
		return toReal(this.map.get(Factory.<K>cast(arg0)));
	}

	/**
	 * {@inheritDoc}
	 */
	@Override
	public V put(final K arg0, final V arg1) {
		return toReal(this.map.put(arg0, getDimensionalValue(arg1, null)));
	}

	/**
	 * 指定キー値と値を保存する。
	 *
	 * @param arg0 キー
	 * @param arg1 値
	 * @param clazz arg1がnull時の型
	 * @return 前の値
	 */
	V put(final K arg0, final V arg1, final Class<?> clazz) {
		return toReal(this.map.put(arg0, getDimensionalValue(arg1, clazz)));
	}

	/**
	 * 指定キーの値をダイレクトに取得する。
	 *
	 * @param arg0 キー
	 * @return 値
	 */
	V[] getRaw(final K arg0) {
		return this.map.get(arg0);
	}

	/**
	 * 指定キーの値をダイレクトに設定する。
	 *
	 * @param arg0 キー
	 * @param arg1 値
	 * @return 前の値
	 */
	V[] putRaw(final K arg0, final V[] arg1) {
		return this.map.put(arg0, arg1);
	}

	/**
	 * @see java.util.Map#remove(java.lang.Object)
	 */
	@Override
	public V remove(final Object arg0) {
		return toReal(this.map.remove(Factory.<K>cast(arg0)));
	}

	/**
	 * 値のクラスを取得
	 *
	 * @param <T> Type
	 * @param key キー値
	 * @return クラス
	 */
	public <T> Class<T> getValueClass(final K key) {
		// チェック
		return Factory.cast(Optional.ofNullable(this.map.get(key)).
				map(Object::getClass).map(Class::getComponentType).orElse(null));
	}

	/**
	 * 内容を表示する。
	 *
	 * @return 内容文字列
	 */
	@Override
	public String toString() {
		var first = true;
		final var sb = new StringBuilder("{");

		for (final K key : this.map.keySet()) {
			// カンマ付加
			if (!first) {
				sb.append(", ");
			}

			// キー付加
			sb.append(key);
			sb.append("=");

			// 値取得
			UniMapEntry.getObjectString(get(key), sb);
			first = false;
		}
		sb.append("}");

		return sb.toString();
	}

	/**
	 * 指定オブジェクトと指定クラスからクラスを判定する。
	 *
	 * @param obj 対象オブジェクト
	 * @param clazz 指定クラス
	 * @return クラスオブジェクト
	 */
	private Class<?> getClassType(final Object obj, final Class<?> clazz) {
		if (obj != null) {
			return obj.getClass();
		}
		if (clazz != null) {
			return clazz;
		}
		return this.cls;
	}

	/**
	 * 実際値取得
	 *
	 * @param obj 対象オブジェクト
	 * @return 実際値
	 */
	private V toReal(final V[] obj) {
		return (obj != null && 0 < obj.length) ? obj[0] : null;
	}

	/**
	 * 高次元値取得
	 *
	 * @param obj 対象オブジェクト
	 * @param clazz 指定クラス
	 * @return 高次元値
	 */
	private V[] getDimensionalValue(final Object obj, final Class<?> clazz) {
		final var dims = new int[getDimensions(obj, clazz)];
		dims[0] = 1;

		final V[] ret = Factory.cast(
				Array.newInstance(Factory.getComponentBaseClass(getClassType(obj, clazz)), dims));
		Array.set(ret, 0, obj);

		return ret;
	}

	/**
	 * 次元取得
	 *
	 * @param obj 対象オブジェクト
	 * @param clazz 対象クラス
	 * @return 次元数
	 */
	private int getDimensions(final Object obj, final Class<?> clazz) {
		int ret = 1;
		for (var cl = getClassType(obj, clazz);
				cl != null && cl.isArray(); cl = cl.getComponentType()) {
			ret++;
		}
		return ret;
	}

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

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

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

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

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

	/**
	 * エントリーセット用
	 *
	 * @param <K> キー
	 * @param <V> 値
	 */
	private static final class UniMapEntry<K extends Serializable, V extends Serializable>
				implements Entry<K, V> {
		/** 所属マップ */
		private final Map<K, V> m;
		/** キー */
		private final K k;
		/** 値 */
		private V v;

		/**
		 * コンストラクタ
		 *
		 * @param map 所属マップ
		 * @param key キー
		 * @param value 値
		 */
		UniMapEntry(final Map<K, V> map, final K key, final V value) {
			this.m = map;
			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;
		}

		/**
		 * {@inheritDoc}
		 */
		@Override
		public V setValue(final V arg0) {
			this.v = arg0;
			return this.m.put(getKey(), arg0);
		}

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

		/**
		 * オブジェクトの文字列を取得する。
		 *
		 * @param obj 対象オブジェクト
		 * @return 文字列バッファオブジェクト
		 */
		private StringBuilder getObjectString(final Object obj) {
			return getObjectString(obj, new StringBuilder());
		}

		/**
		 * オブジェクトの文字列を取得する。
		 *
		 * @param obj 対象オブジェクト
		 * @param sb 文字列バッファオブジェクト
		 * @return 文字列バッファオブジェクト
		 */
		static StringBuilder getObjectString(final Object obj, final StringBuilder sb) {
			if (obj == null || !obj.getClass().isArray()) {
				// 配列以外
				sb.append(obj);
			} else if (!obj.getClass().getComponentType().isArray()) {
				sb.append(Arrays.toString(Object[].class.cast(obj)));
			} else if (0 < Array.getLength(Object[].class.cast(obj))) {
				sb.append("[");
				var first = true;
				for (final var o : Object[].class.cast(obj)) {
					if (!first) {
						sb.append(", ");
					}
					getObjectString(o, sb);
					first = false;
				}
				sb.append("]");
			} else {
				var i = 0;
				for (var c = obj.getClass(); c.isArray(); c = c.getComponentType()) {
					sb.append("[");
					i++;
				}
				for (var j = 0; j < i; j++) {
					sb.append("]");
				}
			}
			return sb;
		}
	}
}
