content: /*
   * Copyright (C) 2012 Facebook, 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 io.airlift.drift.codec.metadata;
  
  import com.google.common.cache.CacheBuilder;
  import com.google.common.cache.CacheLoader;
  import com.google.common.cache.LoadingCache;
  import com.google.common.collect.ImmutableList;
  import com.google.common.collect.ImmutableSet;
  import com.google.common.reflect.TypeToken;
  import io.airlift.drift.annotations.ThriftField;
  
  import java.lang.annotation.Annotation;
  import java.lang.reflect.Array;
  import java.lang.reflect.Executable;
  import java.lang.reflect.Field;
  import java.lang.reflect.Method;
  import java.lang.reflect.Parameter;
  import java.lang.reflect.Type;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Optional;
  import java.util.Set;
  import java.util.concurrent.Future;
  
  import static com.google.common.base.Strings.emptyToNull;
  import static com.google.common.collect.ImmutableList.toImmutableList;
  import static java.lang.reflect.Modifier.isStatic;
  import static java.util.Arrays.stream;
  import static java.util.Collections.emptyList;
  import static java.util.Objects.requireNonNull;
  
  public final class ReflectionHelper
  {
      private ReflectionHelper()
      {
      }
  
      private static final Type MAP_KEY_TYPE;
      private static final Type MAP_VALUE_TYPE;
      private static final Type ITERATOR_TYPE;
      private static final Type ITERATOR_ELEMENT_TYPE;
      private static final Type OPTIONAL_TYPE;
      private static final Type FUTURE_RETURN_TYPE;
  
      static {
          try {
              Method mapPutMethod = Map.class.getMethod("put", Object.class, Object.class);
              MAP_KEY_TYPE = mapPutMethod.getGenericParameterTypes()[0];
              MAP_VALUE_TYPE = mapPutMethod.getGenericParameterTypes()[1];
  
              ITERATOR_TYPE = Iterable.class.getMethod("iterator").getGenericReturnType();
              ITERATOR_ELEMENT_TYPE = Iterator.class.getMethod("next").getGenericReturnType();
  
              OPTIONAL_TYPE = Optional.class.getMethod("get").getGenericReturnType();
  
              Method futureGetMethod = Future.class.getMethod("get");
              FUTURE_RETURN_TYPE = futureGetMethod.getGenericReturnType();
          }
          catch (NoSuchMethodException | SecurityException e) {
              throw new RuntimeException(e);
          }
      }
  
      public static boolean isArray(Type type)
      {
          return TypeToken.of(type).getComponentType() != null;
      }
  
      public static boolean isOptional(Type type)
      {
          return TypeToken.of(type).getRawType() == Optional.class;
      }
  
      public static Class<?> getArrayOfType(Type componentType)
      {
          // this creates an extra object but is the simplest way to get an array class
          Class<?> rawComponentType = TypeToken.of(componentType).getRawType();
          return Array.newInstance(rawComponentType, 0).getClass();
      }
  
      public static Type getMapKeyType(Type type)
      {
          return TypeToken.of(type).resolveType(MAP_KEY_TYPE).getType();
      }
  
      public static Type getMapValueType(Type type)
      {
          return TypeToken.of(type).resolveType(MAP_VALUE_TYPE).getType();
      }
  
      public static Type getIterableType(Type type)
      {
          return TypeToken.of(type).resolveType(ITERATOR_TYPE).resolveType(ITERATOR_ELEMENT_TYPE).getType();
      }
  
      public static Type getOptionalType(Type type)
      {
          return TypeToken.of(type).resolveType(OPTIONAL_TYPE).getType();
      }
  
      public static Type getFutureReturnType(Type type)
      {
          return TypeToken.of(type).resolveType(FUTURE_RETURN_TYPE).getType();
      }
  
      public static <T extends Annotation> Set<T> getEffectiveClassAnnotations(Class<?> type, Class<T> annotation)
      {
          // if the class is directly annotated, it is considered the only annotation
          if (type.isAnnotationPresent(annotation)) {
              return ImmutableSet.of(type.getAnnotation(annotation));
          }
  
          // otherwise find all annotations from all super classes and interfaces
          ImmutableSet.Builder<T> builder = ImmutableSet.builder();
          addEffectiveClassAnnotations(type, annotation, builder);
          return builder.build();
      }
  
      private static <T extends Annotation> void addEffectiveClassAnnotations(Class<?> type, Class<T> annotation, ImmutableSet.Builder<T> builder)
      {
          if (type.isAnnotationPresent(annotation)) {
              builder.add(type.getAnnotation(annotation));
              return;
          }
          if (type.getSuperclass() != null) {
              addEffectiveClassAnnotations(type.getSuperclass(), annotation, builder);
          }
          for (Class<?> anInterface : type.getInterfaces()) {
              addEffectiveClassAnnotations(anInterface, annotation, builder);
          }
      }
  
      public static Iterable<Method> getAllDeclaredMethods(Class<?> type)
      {
          ImmutableList.Builder<Method> methods = ImmutableList.builder();
  
          for (Class<?> clazz = type; clazz != null && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) {
              methods.addAll(ImmutableList.copyOf(clazz.getDeclaredMethods()));
          }
          return methods.build();
      }
  
      public static Iterable<Field> getAllDeclaredFields(Class<?> type)
      {
          ImmutableList.Builder<Field> fields = ImmutableList.builder();
          for (Class<?> clazz = type; clazz != null && !clazz.equals(Object.class); clazz = clazz.getSuperclass()) {
              fields.addAll(ImmutableList.copyOf(clazz.getDeclaredFields()));
          }
          return fields.build();
      }
  
      /**
       * Find methods that are tagged with a given annotation somewhere in the hierarchy
       */
      public static Collection<Method> findAnnotatedMethods(Class<?> type, Class<? extends Annotation> annotation)
      {
          List<Method> result = new ArrayList<>();
  
          // gather all publicly available methods
          // this returns everything, even if it's declared in a parent
          for (Method method : type.getMethods()) {
              // skip methods that are used internally by the vm for implementing covariance, etc
              if (method.isSynthetic() || method.isBridge() || isStatic(method.getModifiers())) {
                  continue;
              }
  
              // look for annotations recursively in super-classes or interfaces
              Method managedMethod = findAnnotatedMethod(
                      type,
                      annotation,
                      method.getName(),
                      method.getParameterTypes());
              if (managedMethod != null) {
                  result.add(managedMethod);
              }
          }
  
          return result;
      }
  
      @SuppressWarnings("PMD.EmptyCatchBlock")
      public static Method findAnnotatedMethod(Class<?> configClass, Class<? extends Annotation> annotation, String methodName, Class<?>... paramTypes)
      {
          try {
              Method method = configClass.getDeclaredMethod(methodName, paramTypes);
              if (method != null && method.isAnnotationPresent(annotation)) {
                  return method;
              }
          }
          catch (NoSuchMethodException e) {
              // ignore
          }
  
          if (configClass.getSuperclass() != null) {
              Method managedMethod = findAnnotatedMethod(
                      configClass.getSuperclass(),
                      annotation,
                      methodName,
                      paramTypes);
              if (managedMethod != null) {
                  return managedMethod;
              }
          }
  
          for (Class<?> iface : configClass.getInterfaces()) {
              Method managedMethod = findAnnotatedMethod(iface, annotation, methodName, paramTypes);
              if (managedMethod != null) {
                  return managedMethod;
              }
          }
  
          return null;
      }
  
      public static Collection<Field> findAnnotatedFields(Class<?> type, Class<? extends Annotation> annotation)
      {
          List<Field> result = new ArrayList<>();
  
          // gather all publicly available fields
          // this returns everything, even if it's declared in a parent
          for (Field field : type.getFields()) {
              if (field.isSynthetic() || isStatic(field.getModifiers())) {
                  continue;
              }
  
              if (field.isAnnotationPresent(annotation)) {
                  result.add(field);
              }
          }
  
          return result;
      }
  
      private static final LoadingCache<Executable, List<String>> PARAMETER_NAMES = CacheBuilder.newBuilder().build(
              new CacheLoader<Executable, List<String>>()
              {
                  @Override
                  public List<String> load(Executable executable)
                  {
                      return getParameterNames(executable);
                  }
              });
  
      public static List<String> extractParameterNames(Executable methodOrConstructor)
      {
          return PARAMETER_NAMES.getUnchecked(methodOrConstructor);
      }
  
      private static List<String> getParameterNames(Executable executable)
      {
          requireNonNull(executable, "executable is null");
  
          if (executable.getParameterCount() == 0) {
              return emptyList();
          }
  
          // first try to get the parameter names from the ThriftField annotations
          List<Optional<String>> parameterNamesFromThriftField = stream(executable.getParameters())
                  .map(ReflectionHelper::getThriftFieldParameterName)
                  .collect(toImmutableList());
          if (parameterNamesFromThriftField.stream().allMatch(Optional::isPresent)) {
              return parameterNamesFromThriftField.stream()
                      .map(Optional::get)
                      .collect(toImmutableList());
          }
  
          // otherwise get the parameter names from the class, but use any ThriftField annotations as overrides
          List<String> parameterNamesFromClass = stream(executable.getParameters())
                  .map(Parameter::getName)
                  .toList();
          ImmutableList.Builder<String> parameterNames = ImmutableList.builder();
          for (int i = 0; i < parameterNamesFromThriftField.size(); i++) {
              parameterNames.add(parameterNamesFromThriftField.get(i).orElse(parameterNamesFromClass.get(i)));
          }
          return parameterNames.build();
      }
  
      private static Optional<String> getThriftFieldParameterName(Parameter parameter)
      {
          return Optional.ofNullable(parameter.getAnnotation(ThriftField.class))
                  .flatMap(thriftField -> Optional.ofNullable(emptyToNull(thriftField.name())));
      }
  
      public static String extractFieldName(Method method)
      {
          requireNonNull(method, "method is null");
          return extractFieldName(method.getName());
      }
  
      public static String extractFieldName(String methodName)
      {
          requireNonNull(methodName, "methodName is null");
          if ((methodName.startsWith("get") || methodName.startsWith("set")) && methodName.length() > 3) {
              return Character.toLowerCase(methodName.charAt(3)) + methodName.substring(4);
          }
          else if (methodName.startsWith("is") && methodName.length() > 2) {
              return Character.toLowerCase(methodName.charAt(2)) + methodName.substring(3);
          }
          else {
              return methodName;
          }
      }
  
      public static Type resolveFieldType(Type structType, Type genericType)
      {
          return TypeToken.of(structType).resolveType(genericType).getType();
      }
  
      public static Type[] resolveFieldTypes(Type structType, Type[] genericTypes)
      {
          return stream(genericTypes)
                  .map(type -> resolveFieldType(structType, type))
                  .toArray(Type[]::new);
      }
  }
  