/*
 * JBoss, Home of Professional Open Source
 * Copyright 2013, Red Hat, Inc. and individual contributors
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

package org.richfaces.fragment.common;

import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.WeakHashMap;

/**
 * Enhanced type resolution utilities. Originally based on
 * org.springframework.core.GenericTypeResolver.
 *
 * @author Jonathan Halterman
 */
public final class TypeResolver {
  private TypeResolver() {
  }

  /** An unknown type. */
  public static final class Unknown {
    private Unknown() {
    }
  }

  /** Cache of type variable/argument pairs */
  private static final Map<Class<?>, Reference<Map<TypeVariable<?>, Type>>> typeVariableCache = Collections.synchronizedMap(new WeakHashMap<Class<?>, Reference<Map<TypeVariable<?>, Type>>>());
  private static boolean cacheEnabled = true;

  /**
   * Enables the internal caching of resolved TypeVariables.
   */
  public static void enableCache() {
    cacheEnabled = true;
  }

  /**
   * Disables the internal caching of resolved TypeVariables.
   */
  public static void disableCache() {
    typeVariableCache.clear();
    cacheEnabled = false;
  }

  /**
   * Returns the raw class representing the type argument for the {@code type} using type variable
   * information from the {@code subType}. If no arguments can be resolved then
   * {@code Unknown.class} is returned.
   *
   * @param type to resolve argument for
   * @param subType to extract type variable information from
   * @return type argument for {@code type} else {@link Unknown.class} if no type arguments are
   *         declared
   * @throws IllegalArgumentException if more or less than one type argument is resolved for the
   *           {@code type}
   */
  public static <T, S extends T> Class<?> resolveRawArgument(Class<T> type, Class<S> subType) {
    return resolveRawArgument(resolveGenericType(type, subType), subType);
  }

  /**
   * Returns the raw class representing the type argument for the {@code genericType} using type
   * variable information from the {@code subType}. If {@code genericType} is an instance of class,
   * then {@code genericType} is returned. If no arguments can be resolved then
   * {@code Unknown.class} is returned.
   *
   * @param genericType to resolve argument for
   * @param subType to extract type variable information from
   * @return type argument for {@code genericType} else {@link Unknown.class} if no type arguments
   *         are declared
   * @throws IllegalArgumentException if more or less than one type argument is resolved for the
   *           {@code genericType}
   */
  public static Class<?> resolveRawArgument(Type genericType, Class<?> subType) {
    Class<?>[] arguments = resolveRawArguments(genericType, subType);
    if (arguments == null)
      return Unknown.class;

    if (arguments.length != 1)
      throw new IllegalArgumentException("Expected 1 type argument on generic type " + genericType
          + " but found " + arguments.length);

    return arguments[0];
  }

  /**
   * Returns an array of raw classes representing type arguments for the {@code type} using type
   * variable information from the {@code subType}. Arguments for {@code type} that cannot be
   * resolved are returned as {@code Unknown.class}. If no arguments can be resolved then
   * {@code null} is returned.
   *
   * @param type to resolve arguments for
   * @param subType to extract type variable information from
   * @return array of raw classes representing type arguments for the {@code type} else {@code null}
   *         if no type arguments are declared
   */
  public static <T, S extends T> Class<?>[] resolveRawArguments(Class<T> type, Class<S> subType) {
    return resolveRawArguments(resolveGenericType(type, subType), subType);
  }

  /**
   * Returns an array of raw classes representing type arguments for the {@code genericType} using
   * type variable information from the {@code subType}. Arguments for {@code genericType} that
   * cannot be resolved are returned as {@code Unknown.class}. If no arguments can be resolved then
   * {@code null} is returned.
   *
   * @param genericType to resolve arguments for
   * @param subType to extract type variable information from
   * @return array of raw classes representing type arguments for the {@code genericType} else
   *         {@code null} if no type arguments are declared
   */
  public static Class<?>[] resolveRawArguments(Type genericType, Class<?> subType) {
    Class<?>[] result = null;

    if (genericType instanceof ParameterizedType) {
      ParameterizedType paramType = (ParameterizedType) genericType;
      Type[] arguments = paramType.getActualTypeArguments();
      result = new Class[arguments.length];
      for (int i = 0; i < arguments.length; i++)
        result[i] = resolveRawClass(arguments[i], subType);
    } else if (genericType instanceof TypeVariable) {
      result = new Class[1];
      result[0] = resolveRawClass(genericType, subType);
    }

    return result;
  }

  /**
   * Returns the generic {@code type} using type variable information from the {@code subType} else
   * {@code null} if the generic type cannot be resolved.
   *
   * @param type to resolve generic type for
   * @param subType to extract type variable information from
   * @return generic {@code type} else {@code null} if it cannot be resolved
   */
  public static Type resolveGenericType(Class<?> type, Type subType) {
    Class<?> rawType;
    if (subType instanceof ParameterizedType)
      rawType = (Class<?>) ((ParameterizedType) subType).getRawType();
    else
      rawType = (Class<?>) subType;

    if (type.equals(rawType))
      return subType;

    Type result;
    if (type.isInterface()) {
      for (Type superInterface : rawType.getGenericInterfaces())
        if (superInterface != null && !superInterface.equals(Object.class))
          if ((result = resolveGenericType(type, superInterface)) != null)
            return result;
    }

    Type superClass = rawType.getGenericSuperclass();
    if (superClass != null && !superClass.equals(Object.class))
      if ((result = resolveGenericType(type, superClass)) != null)
        return result;

    return null;
  }

  /**
   * Resolves the raw class for the {@code genericType}, using the type variable information from
   * the {@code subType} else {@link Unknown} if the raw class cannot be resolved.
   *
   * @param type to resolve raw class for
   * @param subType to extract type variable information from
   * @return raw class for the {@code genericType} else {@link Unknown} if it cannot be resolved
   */
  public static Class<?> resolveRawClass(Type genericType, Class<?> subType) {
    if (genericType instanceof Class) {
      return (Class<?>) genericType;
    } else if (genericType instanceof ParameterizedType) {
      return resolveRawClass(((ParameterizedType) genericType).getRawType(), subType);
    } else if (genericType instanceof GenericArrayType) {
      GenericArrayType arrayType = (GenericArrayType) genericType;
      Class<?> compoment = resolveRawClass(arrayType.getGenericComponentType(), subType);
      return Array.newInstance(compoment, 0).getClass();
    } else if (genericType instanceof TypeVariable) {
      TypeVariable<?> variable = (TypeVariable<?>) genericType;
      genericType = getTypeVariableMap(subType).get(variable);
      genericType = genericType == null ? resolveBound(variable) : resolveRawClass(genericType,
          subType);
    }

    return genericType instanceof Class ? (Class<?>) genericType : Unknown.class;
  }

  private static Map<TypeVariable<?>, Type> getTypeVariableMap(final Class<?> targetType) {
    Reference<Map<TypeVariable<?>, Type>> ref = typeVariableCache.get(targetType);
    Map<TypeVariable<?>, Type> map = ref != null ? ref.get() : null;

    if (map == null) {
      map = new HashMap<TypeVariable<?>, Type>();

      // Populate interfaces
      buildTypeVariableMap(targetType.getGenericInterfaces(), map);

      // Populate super classes and interfaces
      Type genericType = targetType.getGenericSuperclass();
      Class<?> type = targetType.getSuperclass();
      while (type != null && !Object.class.equals(type)) {
        if (genericType instanceof ParameterizedType)
          buildTypeVariableMap((ParameterizedType) genericType, map);
        buildTypeVariableMap(type.getGenericInterfaces(), map);

        genericType = type.getGenericSuperclass();
        type = type.getSuperclass();
      }

      // Populate enclosing classes
      type = targetType;
      while (type.isMemberClass()) {
        genericType = type.getGenericSuperclass();
        if (genericType instanceof ParameterizedType)
          buildTypeVariableMap((ParameterizedType) genericType, map);

        type = type.getEnclosingClass();
      }

      if (cacheEnabled)
        typeVariableCache.put(targetType, new WeakReference<Map<TypeVariable<?>, Type>>(map));
    }

    return map;
  }

  /**
   * Populates the {@code map} with with variable/argument pairs for the given {@code types}.
   */
  private static void buildTypeVariableMap(final Type[] types, final Map<TypeVariable<?>, Type> map) {
    for (Type type : types) {
      if (type instanceof ParameterizedType) {
        ParameterizedType parameterizedType = (ParameterizedType) type;
        buildTypeVariableMap(parameterizedType, map);
        Type rawType = parameterizedType.getRawType();
        if (rawType instanceof Class)
          buildTypeVariableMap(((Class<?>) rawType).getGenericInterfaces(), map);
      } else if (type instanceof Class) {
        buildTypeVariableMap(((Class<?>) type).getGenericInterfaces(), map);
      }
    }
  }

  /**
   * Populates the {@code typeVariableMap} with type arguments and parameters for the given
   * {@code type}.
   */
  private static void buildTypeVariableMap(ParameterizedType type,
      Map<TypeVariable<?>, Type> typeVariableMap) {
    if (type.getRawType() instanceof Class) {
      TypeVariable<?>[] typeVariables = ((Class<?>) type.getRawType()).getTypeParameters();
      Type[] typeArguments = type.getActualTypeArguments();

      for (int i = 0; i < typeArguments.length; i++) {
        TypeVariable<?> variable = typeVariables[i];
        Type typeArgument = typeArguments[i];

        if (typeArgument instanceof Class) {
          typeVariableMap.put(variable, typeArgument);
        } else if (typeArgument instanceof GenericArrayType) {
          typeVariableMap.put(variable, typeArgument);
        } else if (typeArgument instanceof ParameterizedType) {
          typeVariableMap.put(variable, typeArgument);
        } else if (typeArgument instanceof TypeVariable) {
          TypeVariable<?> typeVariableArgument = (TypeVariable<?>) typeArgument;
          Type resolvedType = typeVariableMap.get(typeVariableArgument);
          if (resolvedType == null)
            resolvedType = resolveBound(typeVariableArgument);
          typeVariableMap.put(variable, resolvedType);
        }
      }
    }
  }

  /**
   * Resolves the first bound for the {@code typeVariable}, returning {@code Unknown.class} if none
   * can be resolved.
   */
  public static Type resolveBound(TypeVariable<?> typeVariable) {
    Type[] bounds = typeVariable.getBounds();
    if (bounds.length == 0)
      return Unknown.class;

    Type bound = bounds[0];
    if (bound instanceof TypeVariable)
      bound = resolveBound((TypeVariable<?>) bound);

    return bound == Object.class ? Unknown.class : bound;
  }
}
