/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, 
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package woolpack.utils;

import java.lang.reflect.Array;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;

/**
 * 静的メソッドの集まり。
 * @author nakamura
 *
 */
public class UtilsConstants {
	
	/**
	 * 恒等変換の{@link XmlTransformer}のファクトリのデフォルト値。
	 * 同時に動作する複数のスレッドで使用可能。
	 */
	public static final XmlTransformerFactory TRANSFORMER_FACTORY = 
		new XmlTransformerFactorySemaphoreImpl(new XmlTransformerFactoryImpl(), 5);
	
	/**
	 * 値の個数に関して原子的であるクラスの一覧のデフォルト値。
	 * 処理の原子性とは無関係。
	 */
	public static final Set<Class> ATOM_SET = Collections.unmodifiableSet(new HashSet<Class>(Arrays.asList(
			boolean.class,
			char.class,
			byte.class,
			short.class,
			int.class,
			long.class,
			float.class,
			double.class,
			Boolean.class,
			Character.class,
			Byte.class,
			Short.class,
			Integer.class,
			Long.class,
			Float.class,
			Double.class,
			BigInteger.class,
			BigDecimal.class,
			java.util.Date.class,
			java.sql.Date.class,
			String.class,
			Number.class
			)));

	private UtilsConstants(){}// カバレージがここを通過してはいけない
	
	/**
	 * 委譲先の{@link Switchable#get(Object)}の返却値が null の場合、デフォルト値を設定した{@link Switchable}を返す。
	 * 既に switchable にデフォルト値が定められている場合は引数の switchable を返す。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param defaultValue 委譲先の返却値が null の場合のデフォルト値。
	 * @param switchable 委譲先。本メソッドはこの引数の状態を変化させない。
	 * @return 委譲先の{@link Switchable#get(Object)}の返却値が null の場合、デフォルト値を設定し{@link Switchable}。
	 * @throws NullPointerException switchable が null の場合。
	 */
	public static <K,V> Switchable<K,V> nullToDefaultSwitchIfAbsent(final V defaultValue, final Switchable<K,V> switchable){
		if(switchable.getDefault() != null){
			return switchable;
		}else{
			return new Switchable<K,V>(){
				public V get(K key) {
					final V v = switchable.get(key);
					if(v != null){
						return v;
					}
					return defaultValue;
				}
				public Collection<K> keys() {
					return switchable.keys();
				}
				public V getDefault() {
					return defaultValue;
				}
			};
		}
	}
	
	/**
	 * 逆写像の{@link Map}を返す。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param before 逆写像の作成元。本メソッドはこの引数の状態を変化させない。
	 * @return 逆写像の{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播しない。
	 * @throws NullPointerException 引数が null の場合。
	 * @throws IllegalStateException 引数が単射でない場合。
	 */
	public static <K,V> Map<V,K> inverseMap(final Map<K,V> before){
		final Map<V,K> after = new HashMap<V,K>(before.size());
		{
			final Map<V,K> m = unoverwritableMap(after);
			for(K key:before.keySet()){
				m.put(before.get(key), key);
			}
		}
		return after;
	}
	
	/**
	 * 単射であることを維持する{@link Map}を返す。
	 * 単射とは「map#keySet()に含まれるすべての k0 と k1 について k0 != k1 ならば map#get(k0) != map#get(k1)」であることを意味する。
	 * または対偶をとって「map#keySet()に含まれるすべての k0 と k1 について map#get(k0) == map#get(k1) ならば k0 == k1」であることを意味する。
	 * 登録済の値を異なるキーで設定した場合は{@link IllegalStateException}を投げる。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param map 単射であることを維持する対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @return 単射であることを維持された{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static <K,V> Map<K,V> injectiveMap(final Map<K,V> map){
		return new InjectiveMap<K,V>(map);
	}
	
	/**
	 * キーに関して上書き禁止した{@link Map}を返す。
	 * 登録済のキーを設定した場合は{@link IllegalStateException}を投げる。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param map キーに関して上書き禁止する対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @return キーに関して上書き禁止された{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static <K,V> Map<K,V> unoverwritableMap(final Map<K,V> map){
		map.getClass();
		return new DelegationMap<K,V>(map){
			private void check(final K key){
				if(super.containsKey(key)){
					throw new IllegalStateException("already registered: " + key);
				}
			}
			@Override public V put(final K key, final V value){
				check(key);
				return super.put(key, value);
			}
			@Override public void putAll(final Map<? extends K,? extends V> t){
				for(final K key:t.keySet()){
					check(key);
				}
				super.putAll(t);
			}
		};
	}

	/**
	 * キー・値が null 値をとることを禁止する{@link Map}を返す。
	 * キーまたは値が null 値である組を設定した場合は{@link NullPointerException}を投げる。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param map キー・値がnull値をとることを禁止する対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @return キー・値がnull値をとることを禁止された{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static <K,V> Map<K,V> notNullMap(final Map<K,V> map){
		return new NotNullMap<K,V>(map);
	}
	
	/**
	 * キーの文字数がゼロであることを禁止する{@link Map}を返す。
	 * 文字数がゼロのキーを設定した場合は{@link StringIndexOutOfBoundsException}を投げる。
	 * @param <V> 値。
	 * @param map キーの文字数がゼロであることを禁止する対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @return キーの文字数がゼロであることを禁止された{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static <V> Map<String,V> keyNotEmptyMap(final Map<String,V> map){
		return new KeyNotEmptyMap<V>(map);
	}
	
	/**
	 * 類似の名前を同一とみなす{@link Map}を返す。
	 * 類似の名前とは、大文字区切り(先頭文字の大文字と小文字の区別なし)とアンダーバー区切り(大文字と小文字の区別なし)の相互変換の範囲とする。
	 * @param <V> 値。
	 * @param map 類似の名前を同一とみなす対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @return 類似の名前を同一とみなす{@link Map}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static <V> Map<String,V> similarKeyMap(final Map<String,V> map){
		return new SimilarPropertyNameMap<V>(map);
	}
	
	/**
	 * {@link ConcurrentMap}で定義しているメソッドのみを lock で同期化する{@link ConcurrentMap}を返す。
	 * DCLP(Double checked locking)パターンを使用しているためインスタンスが複数生成されることを抑止することはできないが、
	 * ひとつのインスタンスのみが複数のスレッドから参照されることを保証することができる。
	 * {@link Map}で定義されているメソッドは同期化しないので引数の map が同期化されている必要がある。
	 * @param <K> キー。
	 * @param <V> 値。
	 * @param map 対象の{@link Map}。本メソッドはこの引数の状態を変化させない。
	 * @param lock ロックオブジェクト。
	 * @return ロックオブジェクトで原子化した{@link ConcurrentMap}。返却値の状態変化と引数の状態変化は相互に伝播する。
	 * @throws NullPointerException 引数のいずれかが null の場合。
	 */
	public static <K,V> ConcurrentMap<K,V> concurrentMap(final Map<K,V> map, final Object lock){
		return new SynchronizedConcurrentMap<K,V>(map, lock);
	}
	
	/**
	 * 変更禁止の{@link Iterable}を返す。
	 * @param <T> 汎用型。
	 * @param iterable 変更禁止対象の{@link Iterable}。本メソッドはこの引数の状態を変化させない。
	 * @return 変更禁止された{@link Iterable}。引数の状態変化はこの返却値に伝播する。
	 */
	public static <T> Iterable<T> unmodifiableIterable(final Iterable<T> iterable){
		return new Iterable<T>(){
			public Iterator<T> iterator() {
				final Iterator<T> it = iterable.iterator();
				return new Iterator<T>(){
					public boolean hasNext() {
						return it.hasNext();
					}
					public T next() {
						return it.next();
					}
					public void remove() {
						throw new UnsupportedOperationException();
					}
				};
			}
		};
	}
	
	/**
	 * オブジェクトを{@link Iterable}に変換する。
	 * 変換元が配列なら{@link Iterable}に変換する。
	 * 変換元が{@link Iterable}ならそのまま返す。
	 * 上記以外ならその値ひとつの{@link Iterable}を返す。
	 * @param value 変換元。
	 * @return 変換された{@link Iterable}。変換元が配列または{@link Iterable}の場合は返却値の状態変化と引数の状態変化は相互に伝播する。
	 */
	public static Iterable toIterable(final Object value){
		if(value instanceof Iterable){
			return (Iterable)value;
		}
		return toListPrivate(value);
	}
	
	/**
	 * オブジェクトを{@link Collection}に変換する。
	 * 変換元が配列なら{@link Collection}に変換する。
	 * 変換元が{@link Collection}ならそのまま返す。
	 * 上記以外ならその値ひとつの{@link Collection}を返す。
	 * @param value 変換元。
	 * @return 変換された{@link Collection}。変換元が配列または{@link Collection}の場合は返却値の状態変化と引数の状態変化は相互に伝播する。
	 */
	public static Collection toCollection(final Object value){
		if(value instanceof Collection){
			return (Collection)value;
		}
		return toListPrivate(value);
	}
	
	/**
	 * オブジェクトを{@link List}に変換する。
	 * 変換元が配列なら{@link List}に変換する。
	 * 変換元が{@link List}ならそのまま返す。
	 * 上記以外ならその値ひとつの{@link List}を返す。
	 * @param value 変換元。
	 * @return 変換された{@link List}。変換元が配列または{@link List}の場合は返却値の状態変化と引数の状態変化は相互に伝播する。
	 */
	public static List toList(final Object value){
		if(value instanceof List){
			return (List)value;
		}
		return toListPrivate(value);
	}
	
	private static List<Object> toListPrivate(final Object value){
		if(value != null && value.getClass().isArray()){
			if(value.getClass().getComponentType().isPrimitive()){
				return new AbstractList<Object>(){
					@Override
					public Object get(final int index) {
						return Array.get(value, index);
					}
					@Override
					public int size() {
						return Array.getLength(value);
					}
					@Override
					public Object set(final int index, final Object after){
						final Object before = get(index);
						Array.set(value, index, after);
						return before;
					}
				};
			}else{
				return Arrays.asList((Object[])value);
			}
		}
		return Collections.singletonList(value);
	}
	
	/**
	 * {@link Throwable#getCause()}をたどって{@link Throwable#getLocalizedMessage()}の一覧に変換する。
	 * 中間の{@link Throwable}の場合、
	 * メッセージが null でないかつメッセージが原因の{@link Throwable#toString()}と同一でない場合にメッセージとして追加する。
	 * 終端の{@link Throwable}の場合、
	 * メッセージが null の場合は{@link Throwable#toString()}をメッセージとして追加し、
	 * メッセージが null でない場合はそれをメッセージとして追加する。
	 * @param throwable 基点。
	 * @return メッセージの一覧。
	 * @throws NullPointerException 引数が null の場合。
	 */
	public static List<String> toMessageList(final Throwable throwable){
		final List<String> list = new ArrayList<String>();
		for(Throwable t = throwable; t != null; t = t.getCause()){
			final String s = t.getLocalizedMessage();
			if(t.getCause() == null){
				if(s != null){
					list.add(s);
				}else{
					list.add(t.toString());
				}
			}else{
				if(s != null && !t.getCause().toString().equals(s)){
					list.add(s);
				}
			}
		}
		return list;
	}
}
