/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.apache.beam.sdk.values;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.List;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.collect.Lists;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.reflect.Invokable;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.reflect.Parameter;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.reflect.TypeResolver;
import org.apache.beam.vendor.guava.v26_0_jre.com.google.common.reflect.TypeToken;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
 * A description of a Java type, including actual generic parameters where possible.
 *
 * <p>To prevent losing actual type arguments due to erasure, create an anonymous subclass with
 * concrete types:
 *
 * <pre>{@code
 * TypeDescriptor<List<String>> typeDescriptor = new TypeDescriptor<List<String>>() {};
 * }</pre>
 *
 * <p>If the above were not an anonymous subclass, the type {@code List<String>} would be erased and
 * unavailable at run time.
 *
 * @param <T> the type represented by this {@link TypeDescriptor}
 */
@SuppressWarnings({
  "nullness" // TODO(https://github.com/apache/beam/issues/20497)
})
public abstract class TypeDescriptor<T> implements Serializable {

  // This class is just a wrapper for TypeToken
  private final TypeToken<T> token;

  /**
   * Creates a {@link TypeDescriptor} wrapping the provided token. This constructor is private so
   * Guava types do not leak.
   */
  private TypeDescriptor(TypeToken<T> token) {
    this.token = token;
  }

  /**
   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}. To use this
   * constructor properly, the type parameter must be a concrete type, for example {@code new
   * TypeDescriptor<List<String>>(){}}.
   */
  protected TypeDescriptor() {
    token = new TypeToken<T>(getClass()) {};
  }

  /**
   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}, which should
   * resolve to a concrete type in the context of the class {@code clazz}.
   *
   * <p>Unlike {@link TypeDescriptor#TypeDescriptor(Class)} this will also use context's of the
   * enclosing instances while attempting to resolve the type. This means that the types of any
   * classes instantiated in the concrete instance should be resolvable.
   */
  protected TypeDescriptor(Object instance) {
    TypeToken<?> unresolvedToken = new TypeToken<T>(getClass()) {};

    // While we haven't fully resolved the parameters, refine it using the captured
    // enclosing instance of the object.
    unresolvedToken = TypeToken.of(instance.getClass()).resolveType(unresolvedToken.getType());

    if (hasUnresolvedParameters(unresolvedToken.getType())) {
      for (Field field : instance.getClass().getDeclaredFields()) {
        Object fieldInstance = getEnclosingInstance(field, instance);
        if (fieldInstance != null) {
          unresolvedToken =
              TypeToken.of(fieldInstance.getClass()).resolveType(unresolvedToken.getType());
          if (!hasUnresolvedParameters(unresolvedToken.getType())) {
            break;
          }
        }
      }
    }

    // Once we've either fully resolved the parameters or exhausted enclosing instances, we have
    // the best approximation to the token we can get.
    @SuppressWarnings("unchecked")
    TypeToken<T> typedToken = (TypeToken<T>) unresolvedToken;
    token = typedToken;
  }

  private boolean hasUnresolvedParameters(Type type) {
    if (type instanceof TypeVariable) {
      return true;
    } else if (type instanceof ParameterizedType) {
      ParameterizedType param = (ParameterizedType) type;
      for (Type arg : param.getActualTypeArguments()) {
        if (hasUnresolvedParameters(arg)) {
          return true;
        }
      }
    }
    return false;
  }

  /**
   * Returns the enclosing instance if the field is synthetic and it is able to access it, or
   * {@literal null} if not.
   */
  private @Nullable Object getEnclosingInstance(Field field, Object instance) {
    if (!field.isSynthetic()) {
      return null;
    }

    boolean accessible = field.isAccessible();
    try {
      field.setAccessible(true);
      return field.get(instance);
    } catch (IllegalArgumentException | IllegalAccessException e) {
      // If we fail to get the enclosing instance field, do nothing. In the worst case, we won't
      // refine the type based on information in this enclosing class -- that is consistent with
      // previous behavior and is still a correct answer that can be fixed by returning the correct
      // type descriptor.
      return null;
    } finally {
      field.setAccessible(accessible);
    }
  }

  /**
   * Creates a {@link TypeDescriptor} representing the type parameter {@code T}, which should
   * resolve to a concrete type in the context of the class {@code clazz}.
   */
  @SuppressWarnings("unchecked")
  protected TypeDescriptor(Class<?> clazz) {
    TypeToken<T> unresolvedToken = new TypeToken<T>(getClass()) {};
    token = (TypeToken<T>) TypeToken.of(clazz).resolveType(unresolvedToken.getType());
  }

  /** Returns a {@link TypeDescriptor} representing the given type. */
  public static <T> TypeDescriptor<T> of(Class<T> type) {
    return new SimpleTypeDescriptor<>(TypeToken.of(type));
  }

  /** Returns a {@link TypeDescriptor} representing the given type. */
  @SuppressWarnings("unchecked")
  public static TypeDescriptor<?> of(Type type) {
    return new SimpleTypeDescriptor<>((TypeToken<Object>) TypeToken.of(type));
  }

  /** Returns the {@link Type} represented by this {@link TypeDescriptor}. */
  public Type getType() {
    return token.getType();
  }

  /**
   * Returns the {@link Class} underlying the {@link Type} represented by this {@link
   * TypeDescriptor}.
   */
  public Class<? super T> getRawType() {
    return token.getRawType();
  }

  /** Returns the component type if this type is an array type, otherwise returns {@code null}. */
  public TypeDescriptor<?> getComponentType() {
    return new SimpleTypeDescriptor<>(token.getComponentType());
  }

  /** Returns the generic form of a supertype. */
  public final TypeDescriptor<? super T> getSupertype(Class<? super T> superclass) {
    return new SimpleTypeDescriptor<>(token.getSupertype(superclass));
  }

  /** Returns true if this type is known to be an array type. */
  public final boolean isArray() {
    return token.isArray();
  }

  /**
   * Returns a {@link TypeVariable} for the named type parameter. Throws {@link
   * IllegalArgumentException} if a type variable by the requested type parameter is not found.
   *
   * <p>For example, {@code new TypeDescriptor<List>(){}.getTypeParameter("T")} returns a {@code
   * TypeVariable<? super List>} representing the formal type parameter {@code T}.
   *
   * <p>Do not mistake the type parameters (formal type argument list) with the actual type
   * arguments. For example, if a class {@code Foo} extends {@code List<String>}, it does not make
   * sense to ask for a type parameter, because {@code Foo} does not have any.
   */
  public final TypeVariable<Class<? super T>> getTypeParameter(String paramName) {
    // Cannot convert TypeVariable<Class<? super T>>[] to TypeVariable<Class<? super T>>[]
    // due to how they are used here, so the result of getTypeParameters() cannot be used
    // without upcast.
    Class<?> rawType = getRawType();
    for (TypeVariable<?> param : rawType.getTypeParameters()) {
      if (param.getName().equals(paramName)) {
        @SuppressWarnings("unchecked")
        TypeVariable<Class<? super T>> typedParam = (TypeVariable<Class<? super T>>) param;
        return typedParam;
      }
    }
    throw new IllegalArgumentException(
        "No type parameter named " + paramName + " found on " + getRawType());
  }

  /** Returns true if this type is assignable from the given type. */
  public final boolean isSupertypeOf(TypeDescriptor<?> source) {
    return token.isSupertypeOf(source.token);
  }

  /** Return true if this type is a subtype of the given type. */
  public final boolean isSubtypeOf(TypeDescriptor<?> parent) {
    return token.isSubtypeOf(parent.token);
  }

  /** Returns a list of argument types for the given method, which must be a part of the class. */
  public List<TypeDescriptor<?>> getArgumentTypes(Method method) {
    Invokable<?, ?> typedMethod = token.method(method);

    List<TypeDescriptor<?>> argTypes = Lists.newArrayList();
    for (Parameter parameter : typedMethod.getParameters()) {
      argTypes.add(new SimpleTypeDescriptor<>(parameter.getType()));
    }
    return argTypes;
  }

  /**
   * Returns a {@link TypeDescriptor} representing the given type, with type variables resolved
   * according to the specialization in this type.
   *
   * <p>For example, consider the following class:
   *
   * <pre>{@code
   * class MyList implements List<String> { ... }
   * }</pre>
   *
   * <p>The {@link TypeDescriptor} returned by
   *
   * <pre>{@code
   * TypeDescriptor.of(MyList.class)
   *     .resolveType(Mylist.class.getMethod("get", int.class).getGenericReturnType)
   * }</pre>
   *
   * will represent the type {@code String}.
   */
  public TypeDescriptor<?> resolveType(Type type) {
    return new SimpleTypeDescriptor<>(token.resolveType(type));
  }

  /**
   * Returns a set of {@link TypeDescriptor TypeDescriptor}, one for each superclass as well as each
   * interface implemented by this class.
   */
  @SuppressWarnings("rawtypes")
  public Iterable<TypeDescriptor> getTypes() {
    List<TypeDescriptor> interfaces = Lists.newArrayList();
    for (TypeToken<?> interfaceToken : token.getTypes()) {
      interfaces.add(new SimpleTypeDescriptor<>(interfaceToken));
    }
    return interfaces;
  }

  /** Returns a set of {@link TypeDescriptor}s, one for each interface implemented by this class. */
  @SuppressWarnings("rawtypes")
  public Iterable<TypeDescriptor> getInterfaces() {
    List<TypeDescriptor> interfaces = Lists.newArrayList();
    for (TypeToken<?> interfaceToken : token.getTypes().interfaces()) {
      interfaces.add(new SimpleTypeDescriptor<>(interfaceToken));
    }
    return interfaces;
  }

  /** Returns a set of {@link TypeDescriptor}s, one for each superclass (including this class). */
  @SuppressWarnings("rawtypes")
  public Iterable<TypeDescriptor> getClasses() {
    List<TypeDescriptor> classes = Lists.newArrayList();
    for (TypeToken<?> classToken : token.getTypes().classes()) {
      classes.add(new SimpleTypeDescriptor<>(classToken));
    }
    return classes;
  }

  /**
   * Returns a new {@code TypeDescriptor} where the type variable represented by {@code
   * typeParameter} are substituted by {@code type}. For example, it can be used to construct {@code
   * Map<K, V>} for any {@code K} and {@code V} type:
   *
   * <pre>{@code
   * static <K, V> TypeDescriptor<Map<K, V>> mapOf(
   *     TypeDescriptor<K> keyType, TypeDescriptor<V> valueType) {
   *   return new TypeDescriptor<Map<K, V>>() {}
   *       .where(new TypeParameter<K>() {}, keyType)
   *       .where(new TypeParameter<V>() {}, valueType);
   * }
   * }</pre>
   *
   * @param <X> The parameter type
   * @param typeParameter the parameter type variable
   * @param typeDescriptor the actual type to substitute
   */
  @SuppressWarnings("unchecked")
  public <X> TypeDescriptor<T> where(
      TypeParameter<X> typeParameter, TypeDescriptor<X> typeDescriptor) {
    return where(typeParameter.typeVariable, typeDescriptor.getType());
  }

  /**
   * A more general form of {@link #where(TypeParameter, TypeDescriptor)} that returns a new {@code
   * TypeDescriptor} by matching {@code formal} against {@code actual} to resolve type variables in
   * the current {@link TypeDescriptor}.
   */
  @SuppressWarnings("unchecked")
  public TypeDescriptor<T> where(Type formal, Type actual) {
    TypeResolver resolver = new TypeResolver().where(formal, actual);
    return (TypeDescriptor<T>) TypeDescriptor.of(resolver.resolveType(token.getType()));
  }

  /**
   * Returns whether this {@link TypeDescriptor} has any unresolved type parameters, as opposed to
   * being a concrete type.
   *
   * <p>For example:
   *
   * <pre>{@code
   * TypeDescriptor.of(new ArrayList<String>() {}.getClass()).hasUnresolvedTypeParameters()
   *   => false, because the anonymous class is instantiated with a concrete type
   *
   * class TestUtils {
   *   <T> ArrayList<T> createTypeErasedList() {
   *     return new ArrayList<T>() {};
   *   }
   * }
   *
   * TypeDescriptor.of(TestUtils.<String>createTypeErasedList().getClass())
   *   => true, because the type variable T got type-erased and the anonymous ArrayList class
   *   is instantiated with an unresolved type variable T.
   * }</pre>
   */
  public boolean hasUnresolvedParameters() {
    return hasUnresolvedParameters(getType());
  }

  @Override
  public String toString() {
    return token.toString();
  }

  /** Two type descriptor are equal if and only if they represent the same type. */
  @Override
  public boolean equals(@Nullable Object other) {
    if (!(other instanceof TypeDescriptor)) {
      return false;
    } else {
      @SuppressWarnings("unchecked")
      TypeDescriptor<?> descriptor = (TypeDescriptor<?>) other;
      return token.equals(descriptor.token);
    }
  }

  @Override
  public int hashCode() {
    return token.hashCode();
  }

  /**
   * A non-abstract {@link TypeDescriptor} for construction directly from an existing {@link
   * TypeToken}.
   */
  private static final class SimpleTypeDescriptor<T> extends TypeDescriptor<T> {
    SimpleTypeDescriptor(TypeToken<T> typeToken) {
      super(typeToken);
    }
  }
}
