/*
 * Copyright (c) 2011 Google Inc.
 *
 * 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 com.google.api.client.util;

import java.lang.reflect.Array;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.GenericDeclaration;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

/**
 * Utilities for working with Java types.
 *
 * @since 1.4
 * @author Yaniv Inbar
 */
public class Types {

  /**
   * Returns the parameterized type that is or extends the given type that matches the given super
   * class.
   *
   * <p>For example, if the input type is {@code HashMap<String,Integer>} and the input super class
   * is {@code Map.class}, it will return the extended parameterized type {@link Map}, but which
   * retains the actual type information from the original {@code HashMap}.
   *
   * @param type class or parameterized type
   * @param superClass super class
   * @return matching parameterized type or {@code null}
   */
  public static ParameterizedType getSuperParameterizedType(Type type, Class<?> superClass) {
    if (type instanceof Class<?> || type instanceof ParameterizedType) {
      outer:
      while (type != null && type != Object.class) {
        Class<?> rawType;
        if (type instanceof Class<?>) {
          // type is a class
          rawType = (Class<?>) type;
        } else {
          // current is a parameterized type
          ParameterizedType parameterizedType = (ParameterizedType) type;
          rawType = getRawClass(parameterizedType);
          // check if found Collection
          if (rawType == superClass) {
            // return the actual collection parameter
            return parameterizedType;
          }
          if (superClass.isInterface()) {
            for (Type interfaceType : rawType.getGenericInterfaces()) {
              // interface type is class or parameterized type
              Class<?> interfaceClass =
                  interfaceType instanceof Class<?>
                      ? (Class<?>) interfaceType
                      : getRawClass((ParameterizedType) interfaceType);
              if (superClass.isAssignableFrom(interfaceClass)) {
                type = interfaceType;
                continue outer;
              }
            }
          }
        }
        // move on to the super class
        type = rawType.getGenericSuperclass();
      }
    }
    return null;
  }

  /**
   * Returns whether a class is either assignable to or from another class.
   *
   * @param classToCheck class to check
   * @param anotherClass another class
   */
  public static boolean isAssignableToOrFrom(Class<?> classToCheck, Class<?> anotherClass) {
    return classToCheck.isAssignableFrom(anotherClass)
        || anotherClass.isAssignableFrom(classToCheck);
  }

  /**
   * Creates a new instance of the given class by invoking its default constructor.
   *
   * <p>The given class must be public and must have a public default constructor, and must not be
   * an array or an interface or be abstract. If an enclosing class, it must be static.
   */
  public static <T> T newInstance(Class<T> clazz) {
    // TODO(yanivi): investigate "sneaky" options for allocating the class that GSON uses, like
    // setting the constructor to be accessible, or possibly provide a factory method of a special
    // name
    try {
      return clazz.newInstance();
    } catch (IllegalAccessException e) {
      throw handleExceptionForNewInstance(e, clazz);
    } catch (InstantiationException e) {
      throw handleExceptionForNewInstance(e, clazz);
    }
  }

  private static IllegalArgumentException handleExceptionForNewInstance(
      Exception e, Class<?> clazz) {
    StringBuilder buf =
        new StringBuilder("unable to create new instance of class ").append(clazz.getName());
    ArrayList<String> reasons = new ArrayList<String>();
    if (clazz.isArray()) {
      reasons.add("because it is an array");
    } else if (clazz.isPrimitive()) {
      reasons.add("because it is primitive");
    } else if (clazz == Void.class) {
      reasons.add("because it is void");
    } else {
      if (Modifier.isInterface(clazz.getModifiers())) {
        reasons.add("because it is an interface");
      } else if (Modifier.isAbstract(clazz.getModifiers())) {
        reasons.add("because it is abstract");
      }
      if (clazz.getEnclosingClass() != null && !Modifier.isStatic(clazz.getModifiers())) {
        reasons.add("because it is not static");
      }
      // we don't know what visibility is necessary, but we can give a hint
      if (!Modifier.isPublic(clazz.getModifiers())) {
        reasons.add("possibly because it is not public");
      } else {
        try {
          clazz.getConstructor();
        } catch (NoSuchMethodException e1) {
          reasons.add("because it has no accessible default constructor");
        }
      }
    }
    // append reasons
    boolean and = false;
    for (String reason : reasons) {
      if (and) {
        buf.append(" and");
      } else {
        and = true;
      }
      buf.append(" ").append(reason);
    }
    return new IllegalArgumentException(buf.toString(), e);
  }

  /** Returns whether the given type is an array. */
  public static boolean isArray(Type type) {
    return type instanceof GenericArrayType
        || type instanceof Class<?> && ((Class<?>) type).isArray();
  }

  /**
   * Returns the component type of the given array type, assuming {@link #isArray(Type)}.
   *
   * <p>Return type will either be class, parameterized type, generic array type, or type variable,
   * but not a wildcard type.
   *
   * @throws ClassCastException if {@link #isArray(Type)} is false
   */
  public static Type getArrayComponentType(Type array) {
    return array instanceof GenericArrayType
        ? ((GenericArrayType) array).getGenericComponentType()
        : ((Class<?>) array).getComponentType();
  }

  /**
   * Returns the raw class for the given parameter type as defined in {@link
   * ParameterizedType#getRawType()}.
   *
   * @param parameterType parameter type
   * @return raw class
   */
  public static Class<?> getRawClass(ParameterizedType parameterType) {
    return (Class<?>) parameterType.getRawType();
  }

  /**
   * Returns the only bound of the given wildcard type.
   *
   * @param wildcardType wildcard type
   * @return only bound or {@code Object.class} for none
   */
  public static Type getBound(WildcardType wildcardType) {
    Type[] lowerBounds = wildcardType.getLowerBounds();
    if (lowerBounds.length != 0) {
      return lowerBounds[0];
    }
    return wildcardType.getUpperBounds()[0];
  }

  /**
   * Resolves the actual type of the given type variable that comes from a field type based on the
   * given context list.
   *
   * <p>In case the type variable can be resolved partially, it will return the partially resolved
   * type variable.
   *
   * @param context context list, ordering from least specific to most specific type context, for
   *     example container class and then its field
   * @param typeVariable type variable
   * @return resolved or partially resolved actual type (type variable, class, parameterized type,
   *     or generic array type, but not wildcard type) or {@code null} if unable to resolve at all
   */
  public static Type resolveTypeVariable(List<Type> context, TypeVariable<?> typeVariable) {
    // determine where the type variable was declared
    GenericDeclaration genericDeclaration = typeVariable.getGenericDeclaration();
    if (genericDeclaration instanceof Class<?>) {
      Class<?> rawGenericDeclaration = (Class<?>) genericDeclaration;
      // check if the context extends that declaration
      int contextIndex = context.size();
      ParameterizedType parameterizedType = null;
      while (parameterizedType == null && --contextIndex >= 0) {
        parameterizedType =
            getSuperParameterizedType(context.get(contextIndex), rawGenericDeclaration);
      }
      if (parameterizedType != null) {
        // find the type variable's index in the declaration's type parameters
        TypeVariable<?>[] typeParameters = genericDeclaration.getTypeParameters();
        int index = 0;
        for (; index < typeParameters.length; index++) {
          TypeVariable<?> typeParameter = typeParameters[index];
          if (typeParameter.equals(typeVariable)) {
            break;
          }
        }
        // use that index to get the actual type argument
        Type result = parameterizedType.getActualTypeArguments()[index];
        if (result instanceof TypeVariable<?>) {
          // attempt to resolve type variable
          Type resolve = resolveTypeVariable(context, (TypeVariable<?>) result);
          if (resolve != null) {
            return resolve;
          }
          // partially resolved type variable is okay
        }
        return result;
      }
    }
    return null;
  }

  /**
   * Returns the raw array component type to use -- for example for the first parameter of {@link
   * Array#newInstance(Class, int)} -- for the given component type.
   *
   * @param context context list, ordering from least specific to most specific type context, for
   *     example container class and then its field
   * @param componentType array component type or {@code null} for {@code Object.class} result
   * @return raw array component type
   */
  public static Class<?> getRawArrayComponentType(List<Type> context, Type componentType) {
    if (componentType instanceof TypeVariable<?>) {
      componentType = Types.resolveTypeVariable(context, (TypeVariable<?>) componentType);
    }
    if (componentType instanceof GenericArrayType) {
      Class<?> raw = getRawArrayComponentType(context, Types.getArrayComponentType(componentType));
      return Array.newInstance(raw, 0).getClass();
    }
    if (componentType instanceof Class<?>) {
      return (Class<?>) componentType;
    }
    if (componentType instanceof ParameterizedType) {
      return Types.getRawClass((ParameterizedType) componentType);
    }
    Preconditions.checkArgument(
        componentType == null, "wildcard type is not supported: %s", componentType);
    return Object.class;
  }

  /**
   * Returns the type parameter of {@link Iterable} that is assignable from the given iterable type.
   *
   * <p>For example, for the type {@code ArrayList<Integer>} -- or for a class that extends {@code
   * ArrayList<Integer>} -- it will return {@code Integer}.
   *
   * @param iterableType iterable type (must extend {@link Iterable})
   * @return type parameter, which may be any type
   */
  public static Type getIterableParameter(Type iterableType) {
    return getActualParameterAtPosition(iterableType, Iterable.class, 0);
  }

  /**
   * Returns the value type parameter of {@link Map} that is assignable from the given map type.
   *
   * <p>For example, for the type {@code Map<String, Integer>} -- or for a class that extends {@code
   * Map<String, Integer>} -- it will return {@code Integer}.
   *
   * @param mapType map type (must extend {@link Map})
   * @return type parameter, which may be any type
   */
  public static Type getMapValueParameter(Type mapType) {
    return getActualParameterAtPosition(mapType, Map.class, 1);
  }

  private static Type getActualParameterAtPosition(Type type, Class<?> superClass, int position) {
    ParameterizedType parameterizedType = Types.getSuperParameterizedType(type, superClass);
    if (parameterizedType == null) {
      return null;
    }
    Type valueType = parameterizedType.getActualTypeArguments()[position];
    // this is normally a type variable, except in the case where the class of iterableType is
    // superClass, e.g. Iterable<String>
    if (valueType instanceof TypeVariable<?>) {
      Type resolve = Types.resolveTypeVariable(Arrays.asList(type), (TypeVariable<?>) valueType);
      if (resolve != null) {
        return resolve;
      }
    }
    return valueType;
  }

  /**
   * Returns an iterable for an input iterable or array value.
   *
   * <p>If the input value extends {@link Iterable}, it will just return the input value. Otherwise,
   * it will return an iterable that can handle arrays of primitive and non-primitive component
   * type.
   *
   * @param value iterable (extends {@link Iterable}) or array value
   * @return iterable
   */
  @SuppressWarnings("unchecked")
  public static <T> Iterable<T> iterableOf(final Object value) {
    if (value instanceof Iterable<?>) {
      return (Iterable<T>) value;
    }
    Class<?> valueClass = value.getClass();
    Preconditions.checkArgument(valueClass.isArray(), "not an array or Iterable: %s", valueClass);
    Class<?> subClass = valueClass.getComponentType();
    if (!subClass.isPrimitive()) {
      return Arrays.<T>asList((T[]) value);
    }
    return new Iterable<T>() {

      public Iterator<T> iterator() {
        return new Iterator<T>() {

          final int length = Array.getLength(value);
          int index = 0;

          public boolean hasNext() {
            return index < length;
          }

          public T next() {
            if (!hasNext()) {
              throw new NoSuchElementException();
            }
            return (T) Array.get(value, index++);
          }

          public void remove() {
            throw new UnsupportedOperationException();
          }
        };
      }
    };
  }

  /**
   * Returns a new array of the given component type (possibly a Java primitive) that is a copy of
   * the content of the given collection.
   *
   * @param collection collection
   * @param componentType component type (possibly a Java primitive)
   * @return new array
   */
  public static Object toArray(Collection<?> collection, Class<?> componentType) {
    if (componentType.isPrimitive()) {
      Object array = Array.newInstance(componentType, collection.size());
      int index = 0;
      for (Object value : collection) {
        Array.set(array, index++, value);
      }
      return array;
    }
    return collection.toArray((Object[]) Array.newInstance(componentType, collection.size()));
  }

  private Types() {}
}
