/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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 org.seasar.framework.util.tiger;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * genericsを扱うためのユーティリティ・クラスです。
 * 
 * @author koichik
 */
public abstract class GenericUtil {

    /**
     * インスタンスを構築します。
     */
    protected GenericUtil() {
    }

    /**
     * <code>type</code>の原型が<code>clazz</code>に代入可能であれば<code>true</code>を、
     * それ以外の場合は<code>false</code>を返します。
     * 
     * @param type
     *            タイプ
     * @param clazz
     *            クラス
     * @return <code>type</code>の原型が<code>clazz</code>に代入可能であれば<code>true</code>
     */
    public static boolean isTypeOf(final Type type, final Class<?> clazz) {
        if (Class.class.isInstance(type)) {
            return clazz.isAssignableFrom(Class.class.cast(type));
        }
        if (ParameterizedType.class.isInstance(type)) {
            final ParameterizedType parameterizedType = ParameterizedType.class
                    .cast(type);
            return isTypeOf(parameterizedType.getRawType(), clazz);
        }
        return false;
    }

    /**
     * <code>type</code>の原型を返します。
     * <ul>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            タイプ
     * @return <code>type</code>の原型
     */
    public static Class<?> getRawClass(final Type type) {
        if (Class.class.isInstance(type)) {
            return Class.class.cast(type);
        }
        if (ParameterizedType.class.isInstance(type)) {
            final ParameterizedType parameterizedType = ParameterizedType.class
                    .cast(type);
            return getRawClass(parameterizedType.getRawType());
        }
        if (WildcardType.class.isInstance(type)) {
            final WildcardType wildcardType = WildcardType.class.cast(type);
            final Type[] types = wildcardType.getUpperBounds();
            return getRawClass(types[0]);
        }
        if (GenericArrayType.class.isInstance(type)) {
            final GenericArrayType genericArrayType = GenericArrayType.class
                    .cast(type);
            final Class<?> rawClass = getRawClass(genericArrayType
                    .getGenericComponentType());
            return Array.newInstance(rawClass, 0).getClass();
        }
        return null;
    }

    /**
     * <code>type</code>の型引数の配列を返します。
     * <p>
     * <code>type</code>がパラメータ化された型でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            タイプ
     * @return <code>type</code>の型引数の配列
     */
    public static Type[] getGenericParameter(final Type type) {
        if (ParameterizedType.class.isInstance(type)) {
            return ParameterizedType.class.cast(type).getActualTypeArguments();
        }
        if (GenericArrayType.class.isInstance(type)) {
            return getGenericParameter(GenericArrayType.class.cast(type)
                    .getGenericComponentType());
        }
        return null;
    }

    /**
     * 指定された位置の<code>type</code>の型引数を返します。
     * <p>
     * <code>type</code>がパラメータ化された型でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            タイプ
     * @param index
     *            位置
     * @return 指定された位置の<code>type</code>の型引数
     */
    public static Type getGenericParameter(final Type type, final int index) {
        if (!ParameterizedType.class.isInstance(type)) {
            return null;
        }
        final Type[] genericParameter = getGenericParameter(type);
        if (genericParameter == null) {
            return null;
        }
        return genericParameter[index];
    }

    /**
     * パラメータ化された型を要素とする配列の要素型を返します。
     * <p>
     * <code>type</code>がパラメータ化された型の配列でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された型を要素とする配列
     * @return パラメータ化された型を要素とする配列の要素型
     */
    public static Type getElementTypeOfArray(final Type type) {
        if (!GenericArrayType.class.isInstance(type)) {
            return null;
        }
        return GenericArrayType.class.cast(type).getGenericComponentType();
    }

    /**
     * パラメータ化された{@link Collection}の要素型を返します。
     * <p>
     * <code>type</code>がパラメータ化された{@link List}でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された{@link List}
     * @return パラメータ化された{@link List}の要素型
     */
    public static Type getElementTypeOfCollection(final Type type) {
        if (!isTypeOf(type, Collection.class)) {
            return null;
        }
        return getGenericParameter(type, 0);
    }

    /**
     * パラメータ化された{@link List}の要素型を返します。
     * <p>
     * <code>type</code>がパラメータ化された{@link List}でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された{@link List}
     * @return パラメータ化された{@link List}の要素型
     */
    public static Type getElementTypeOfList(final Type type) {
        if (!isTypeOf(type, List.class)) {
            return null;
        }
        return getGenericParameter(type, 0);
    }

    /**
     * パラメータ化された{@link Set}の要素型を返します。
     * <p>
     * <code>type</code>がパラメータ化された{@link Set}でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された{@link Set}
     * @return パラメータ化された{@link Set}の要素型
     */
    public static Type getElementTypeOfSet(final Type type) {
        if (!isTypeOf(type, Set.class)) {
            return null;
        }
        return getGenericParameter(type, 0);
    }

    /**
     * パラメータ化された{@link Map}のキーの型を返します。
     * <p>
     * <code>type</code>がパラメータ化された{@link Map}でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された{@link Map}
     * @return パラメータ化された{@link Map}のキーの型
     */
    public static Type getKeyTypeOfMap(final Type type) {
        if (!isTypeOf(type, Map.class)) {
            return null;
        }
        return getGenericParameter(type, 0);
    }

    /**
     * パラメータ化された{@link Map}の値の型を返します。
     * <p>
     * <code>type</code>がパラメータ化された{@link Map}でない場合は<code>null</code>を返します。
     * </p>
     * 
     * @param type
     *            パラメータ化された{@link Map}
     * @return パラメータ化された{@link Map}の値の型
     */
    public static Type getValueTypeOfMap(final Type type) {
        if (!isTypeOf(type, Map.class)) {
            return null;
        }
        return getGenericParameter(type, 1);
    }

    /**
     * パラメータ化された型(クラスまたはインタフェース)が持つ型変数をキー、型引数を値とする{@link Map}を返します。
     * 
     * @param clazz
     *            パラメータ化された型(クラスまたはインタフェース)
     * @return パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     */
    public static Map<TypeVariable<?>, Type> getTypeVariableMap(
            final Class<?> clazz) {
        final Map<TypeVariable<?>, Type> map = CollectionsUtil
                .newLinkedHashMap();

        final TypeVariable<?>[] typeParameters = clazz.getTypeParameters();
        for (TypeVariable<?> typeParameter : typeParameters) {
            map.put(typeParameter, getActualClass(typeParameter.getBounds()[0],
                    map));
        }

        final Class<?> superClass = clazz.getSuperclass();
        final Type superClassType = clazz.getGenericSuperclass();
        if (superClass != null) {
            gatherTypeVariables(superClass, superClassType, map);
        }

        final Class<?>[] interfaces = clazz.getInterfaces();
        final Type[] interfaceTypes = clazz.getGenericInterfaces();
        for (int i = 0; i < interfaces.length; ++i) {
            gatherTypeVariables(interfaces[i], interfaceTypes[i], map);
        }

        return map;
    }

    /**
     * パラメータ化された型(クラスまたはインタフェース)が持つ型変数および型引数を集めて<code>map</code>に追加します。
     * 
     * @param clazz
     *            クラス
     * @param type
     *            型
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     */
    protected static void gatherTypeVariables(final Class<?> clazz,
            final Type type, final Map<TypeVariable<?>, Type> map) {
        if (clazz == null) {
            return;
        }
        gatherTypeVariables(type, map);

        final Class<?> superClass = clazz.getSuperclass();
        final Type superClassType = clazz.getGenericSuperclass();
        if (superClass != null) {
            gatherTypeVariables(superClass, superClassType, map);
        }

        final Class<?>[] interfaces = clazz.getInterfaces();
        final Type[] interfaceTypes = clazz.getGenericInterfaces();
        for (int i = 0; i < interfaces.length; ++i) {
            gatherTypeVariables(interfaces[i], interfaceTypes[i], map);
        }
    }

    /**
     * パラメータ化された型(クラスまたはインタフェース)が持つ型変数および型引数を集めて<code>map</code>に追加します。
     * 
     * @param type
     *            型
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     */
    protected static void gatherTypeVariables(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (ParameterizedType.class.isInstance(type)) {
            final ParameterizedType parameterizedType = ParameterizedType.class
                    .cast(type);
            final TypeVariable<?>[] typeVariables = GenericDeclaration.class
                    .cast(parameterizedType.getRawType()).getTypeParameters();
            final Type[] actualTypes = parameterizedType
                    .getActualTypeArguments();
            for (int i = 0; i < actualTypes.length; ++i) {
                map.put(typeVariables[i], actualTypes[i]);
            }
        }
    }

    /**
     * <code>type</code>の実際の型を返します。
     * <ul>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数で引数{@code map}のキーとして含まれている場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が型変数で引数{@code map}のキーとして含まれていない場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            タイプ
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return <code>type</code>の実際の型
     */
    public static Class<?> getActualClass(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (Class.class.isInstance(type)) {
            return Class.class.cast(type);
        }
        if (ParameterizedType.class.isInstance(type)) {
            return getActualClass(ParameterizedType.class.cast(type)
                    .getRawType(), map);
        }
        if (WildcardType.class.isInstance(type)) {
            return getActualClass(WildcardType.class.cast(type)
                    .getUpperBounds()[0], map);
        }
        if (TypeVariable.class.isInstance(type)) {
            final TypeVariable<?> typeVariable = TypeVariable.class.cast(type);
            if (map.containsKey(typeVariable)) {
                return getActualClass(map.get(typeVariable), map);
            }
            return getActualClass(typeVariable.getBounds()[0], map);
        }
        if (GenericArrayType.class.isInstance(type)) {
            final GenericArrayType genericArrayType = GenericArrayType.class
                    .cast(type);
            final Class<?> componentClass = getActualClass(genericArrayType
                    .getGenericComponentType(), map);
            return Array.newInstance(componentClass, 0).getClass();
        }
        return null;
    }

    /**
     * パラメータ化された型を要素とする配列の実際の要素型を返します。
     * <ul>
     * <li><code>type</code>がパラメータ化された型の配列でない場合は<code>null</code>を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された型を要素とする配列
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された型を要素とする配列の実際の要素型
     */
    public static Class<?> getActualElementClassOfArray(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!GenericArrayType.class.isInstance(type)) {
            return null;
        }
        return getActualClass(GenericArrayType.class.cast(type)
                .getGenericComponentType(), map);
    }

    /**
     * パラメータ化された{@link Collection}の実際の要素型を返します。
     * <ul>
     * <li><code>type</code>がパラメータ化された{@link Collection}でない場合は<code>null</code>
     * を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された{@link Collection}
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された{@link Collection}の実際の要素型
     */
    public static Class<?> getActualElementClassOfCollection(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!isTypeOf(type, Collection.class)) {
            return null;
        }
        return getActualClass(getGenericParameter(type, 0), map);
    }

    /**
     * パラメータ化された{@link List}の実際の要素型を返します。
     * <ul>
     * <li><code>type</code>がパラメータ化された{@link List}でない場合は<code>null</code>を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された{@link List}
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された{@link List}の実際の要素型
     */
    public static Class<?> getActualElementClassOfList(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!isTypeOf(type, List.class)) {
            return null;
        }
        return getActualClass(getGenericParameter(type, 0), map);
    }

    /**
     * パラメータ化された{@link Set}の実際の要素型を返します。
     * <ul>
     * <li><code>type</code>がパラメータ化された{@link Set}でない場合は<code>null</code>を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された{@link Set}
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された{@link Set}の実際の要素型
     */
    public static Class<?> getActualElementClassOfSet(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!isTypeOf(type, Set.class)) {
            return null;
        }
        return getActualClass(getGenericParameter(type, 0), map);
    }

    /**
     * パラメータ化された{@link Map}のキーの実際の型を返します。
     * <ul>
     * <li>キー型がパラメータ化された{@link Map}でない場合は<code>null</code>を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された{@link Map}
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された{@link Map}のキーの実際の型
     */
    public static Class<?> getActualKeyClassOfMap(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!isTypeOf(type, Map.class)) {
            return null;
        }
        return getActualClass(getGenericParameter(type, 0), map);
    }

    /**
     * パラメータ化された{@link Map}の値の実際の型を返します。
     * <ul>
     * <li><code>type</code>がパラメータ化された{@link Map}でない場合は<code>null</code>を返します。</li>
     * <li><code>type</code>が<code>Class</code>の場合はそのまま返します。</li>
     * <li><code>type</code>がパラメータ化された型の場合はその原型を返します。</li>
     * <li><code>type</code>がワイルドカード型の場合は(最初の)上限境界を返します。</li>
     * <li><code>type</code>が型変数の場合はその変数の実際の型引数を返します。</li>
     * <li><code>type</code>が配列の場合はその要素の実際の型の配列を返します。</li>
     * <li>その他の場合は<code>null</code>を返します。</li>
     * </ul>
     * 
     * @param type
     *            パラメータ化された{@link Map}
     * @param map
     *            パラメータ化された型が持つ型変数をキー、型引数を値とする{@link Map}
     * @return パラメータ化された{@link Map}の値の実際の型
     */
    public static Class<?> getActualValueClassOfMap(final Type type,
            final Map<TypeVariable<?>, Type> map) {
        if (!isTypeOf(type, Map.class)) {
            return null;
        }
        return getActualClass(getGenericParameter(type, 1), map);
    }

}
