// Copyright 2021 Code Intelligence GmbH
//
// 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.code_intelligence.jazzer.autofuzz;

import com.code_intelligence.jazzer.api.AutofuzzConstructionException;
import com.code_intelligence.jazzer.api.AutofuzzInvocationException;
import com.code_intelligence.jazzer.api.Consumer1;
import com.code_intelligence.jazzer.api.Consumer2;
import com.code_intelligence.jazzer.api.Consumer3;
import com.code_intelligence.jazzer.api.Consumer4;
import com.code_intelligence.jazzer.api.Consumer5;
import com.code_intelligence.jazzer.api.Function1;
import com.code_intelligence.jazzer.api.Function2;
import com.code_intelligence.jazzer.api.Function3;
import com.code_intelligence.jazzer.api.Function4;
import com.code_intelligence.jazzer.api.Function5;
import com.code_intelligence.jazzer.api.FuzzedDataProvider;
import com.code_intelligence.jazzer.runtime.HardToCatchError;
import com.code_intelligence.jazzer.utils.Utils;
import io.github.classgraph.ClassGraph;
import io.github.classgraph.ClassInfoList;
import io.github.classgraph.ScanResult;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
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.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import net.jodah.typetools.TypeResolver;
import net.jodah.typetools.TypeResolver.Unknown;

public class Meta {
  public static final boolean IS_DEBUG = isDebug();

  private static final Meta PUBLIC_LOOKUP_INSTANCE = new Meta(null);
  private static final boolean IS_TEST = isTest();
  private static final WeakHashMap<Class<?>, List<Class<?>>> implementingClassesCache =
      new WeakHashMap<>();
  private static final WeakHashMap<Class<?>, List<Class<?>>> nestedBuilderClassesCache =
      new WeakHashMap<>();
  private static final WeakHashMap<Class<?>, List<Method>> originalObjectCreationMethodsCache =
      new WeakHashMap<>();
  private static final WeakHashMap<Class<?>, List<Method>> cascadingBuilderMethodsCache =
      new WeakHashMap<>();

  private final AccessibleObjectLookup lookup;

  public Meta(Class<?> referenceClass) {
    lookup = new AccessibleObjectLookup(referenceClass);
  }

  @SuppressWarnings("unchecked")
  public static <T1> void autofuzz(FuzzedDataProvider data, Consumer1<T1> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Consumer1.class, func.getClass());
    func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2> void autofuzz(FuzzedDataProvider data, Consumer2<T1, T2> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Consumer2.class, func.getClass());
    func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3> void autofuzz(FuzzedDataProvider data, Consumer3<T1, T2, T3> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Consumer3.class, func.getClass());
    func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3, T4> void autofuzz(
      FuzzedDataProvider data, Consumer4<T1, T2, T3, T4> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Consumer4.class, func.getClass());
    func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
        (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3, T4, T5> void autofuzz(
      FuzzedDataProvider data, Consumer5<T1, T2, T3, T4, T5> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Consumer5.class, func.getClass());
    func.accept((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
        (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3),
        (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4));
  }

  @SuppressWarnings("unchecked")
  public static <T1, R> R autofuzz(FuzzedDataProvider data, Function1<T1, R> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Function1.class, func.getClass());
    return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, R> R autofuzz(FuzzedDataProvider data, Function2<T1, T2, R> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Function2.class, func.getClass());
    return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3, R> R autofuzz(FuzzedDataProvider data, Function3<T1, T2, T3, R> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Function3.class, func.getClass());
    return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3, T4, R> R autofuzz(
      FuzzedDataProvider data, Function4<T1, T2, T3, T4, R> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Function4.class, func.getClass());
    return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
        (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3));
  }

  @SuppressWarnings("unchecked")
  public static <T1, T2, T3, T4, T5, R> R autofuzz(
      FuzzedDataProvider data, Function5<T1, T2, T3, T4, T5, R> func) {
    Class<?>[] types = TypeResolver.resolveRawArguments(Function5.class, func.getClass());
    return func.apply((T1) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 0),
        (T2) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 1),
        (T3) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 2),
        (T4) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 3),
        (T5) PUBLIC_LOOKUP_INSTANCE.consumeChecked(data, types, 4));
  }

  public static Object consume(FuzzedDataProvider data, Class<?> type) {
    return PUBLIC_LOOKUP_INSTANCE.consume(data, type, null);
  }

  static void rescanClasspath() {
    implementingClassesCache.clear();
  }

  private static boolean isTest() {
    String value = System.getenv("JAZZER_AUTOFUZZ_TESTING");
    return value != null && !value.isEmpty();
  }

  private static boolean isDebug() {
    String value = System.getenv("JAZZER_AUTOFUZZ_DEBUG");
    return value != null && !value.isEmpty();
  }

  private static int consumeArrayLength(FuzzedDataProvider data, int sizeOfElement) {
    // Spend at most half of the fuzzer input bytes so that the remaining arguments that require
    // construction still have non-trivial data to work with.
    int bytesToSpend = data.remainingBytes() / 2;
    return bytesToSpend / Math.max(sizeOfElement, 1);
  }

  private static String deepToString(Object obj) {
    if (obj == null) {
      return "null";
    }
    if (obj.getClass().isArray()) {
      return String.format("(%s[]) %s", obj.getClass().getComponentType().getName(),
          Arrays.deepToString((Object[]) obj));
    }
    return obj.toString();
  }

  private static String getDebugSummary(
      Executable executable, Object thisObject, Object[] arguments) {
    return String.format("%nMethod: %s::%s%s%nthis: %s%nArguments: %s",
        executable.getDeclaringClass().getName(), executable.getName(),
        Utils.getReadableDescriptor(executable), thisObject,
        Arrays.stream(arguments).map(Meta::deepToString).collect(Collectors.joining(", ")));
  }

  static Class<?> getRawType(Type genericType) {
    if (genericType instanceof Class<?>) {
      return (Class<?>) genericType;
    } else if (genericType instanceof ParameterizedType) {
      return getRawType(((ParameterizedType) genericType).getRawType());
    } else if (genericType instanceof WildcardType) {
      // TODO: Improve this.
      return Object.class;
    } else if (genericType instanceof TypeVariable<?>) {
      throw new AutofuzzError("Did not expect genericType to be a TypeVariable: " + genericType);
    } else if (genericType instanceof GenericArrayType) {
      return Array
          .newInstance(getRawType(((GenericArrayType) genericType).getGenericComponentType()), 0)
          .getClass();
    } else {
      throw new AutofuzzError("Got unexpected class implementing Type: " + genericType);
    }
  }

  public Object autofuzz(FuzzedDataProvider data, Method method) {
    return autofuzz(data, method, null);
  }

  // Renamed so that it doesn't clash with the static method consume, which we don't want to rename
  // as the api package depends on it by name.
  public Object consumeNonStatic(FuzzedDataProvider data, Class<?> type) {
    return consume(data, type, null);
  }

  Object autofuzz(FuzzedDataProvider data, Method method, AutofuzzCodegenVisitor visitor) {
    Object result;
    if (Modifier.isStatic(method.getModifiers())) {
      if (visitor != null) {
        // This group will always have two elements: The class name and the method call.
        visitor.pushGroup(
            String.format("%s.", method.getDeclaringClass().getCanonicalName()), "", "");
      }
      try {
        result = autofuzz(data, method, null, visitor);
      } finally {
        if (visitor != null) {
          visitor.popGroup();
        }
      }
    } else {
      if (visitor != null) {
        // This group will always have two elements: The thisObject and the method call.
        // Since the this object can be a complex expression, wrap it in parenthesis.
        visitor.pushGroup("(", ").", "");
      }
      try {
        Object thisObject = consume(data, method.getDeclaringClass(), visitor);
        if (thisObject == null) {
          throw new AutofuzzConstructionException();
        }
        result = autofuzz(data, method, thisObject, visitor);
      } finally {
        if (visitor != null) {
          visitor.popGroup();
        }
      }
    }
    return result;
  }

  public Object autofuzz(FuzzedDataProvider data, Method method, Object thisObject) {
    return autofuzz(data, method, thisObject, null);
  }

  Object autofuzz(
      FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
    if (visitor != null) {
      visitor.pushGroup(String.format("%s(", method.getName()), ", ", ")");
    }
    Object[] arguments = consumeArguments(data, method, visitor);
    if (visitor != null) {
      visitor.popGroup();
    }
    try {
      return method.invoke(thisObject, arguments);
    } catch (IllegalAccessException | IllegalArgumentException | NullPointerException e) {
      // We should ensure that the arguments fed into the method are always valid.
      throw new AutofuzzError(getDebugSummary(method, thisObject, arguments), e);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof HardToCatchError) {
        throw new AutofuzzInvocationException();
      }
      throw new AutofuzzInvocationException(e.getCause());
    }
  }

  Object autofuzzForConsume(
      FuzzedDataProvider data, Constructor<?> constructor, AutofuzzCodegenVisitor visitor) {
    try {
      return autofuzz(data, constructor, visitor);
    } catch (AutofuzzConstructionException e) {
      // Do not nest AutofuzzConstructionExceptions.
      throw e;
    } catch (AutofuzzInvocationException e) {
      // If an invocation fails during consume and thus while trying to construct a valid object,
      // the exception should not be reported as a finding, so we rewrap it.
      throw new AutofuzzConstructionException(e.getCause());
    } catch (Throwable t) {
      throw new AutofuzzConstructionException(t);
    }
  }

  Object autofuzzForConsume(
      FuzzedDataProvider data, Method method, Object thisObject, AutofuzzCodegenVisitor visitor) {
    try {
      return autofuzz(data, method, thisObject, visitor);
    } catch (AutofuzzConstructionException e) {
      // Do not nest AutofuzzConstructionExceptions.
      throw e;
    } catch (AutofuzzInvocationException e) {
      // If an invocation fails during consume and thus while trying to construct a valid object,
      // the exception should not be reported as a finding, so we rewrap it.
      throw new AutofuzzConstructionException(e.getCause());
    } catch (Throwable t) {
      throw new AutofuzzConstructionException(t);
    }
  }

  public <R> R autofuzz(FuzzedDataProvider data, Constructor<R> constructor) {
    return autofuzz(data, constructor, null);
  }

  <R> R autofuzz(
      FuzzedDataProvider data, Constructor<R> constructor, AutofuzzCodegenVisitor visitor) {
    if (visitor != null) {
      // getCanonicalName is correct also for nested classes.
      visitor.pushGroup(
          String.format("new %s(", constructor.getDeclaringClass().getCanonicalName()), ", ", ")");
    }
    Object[] arguments = consumeArguments(data, constructor, visitor);
    if (visitor != null) {
      visitor.popGroup();
    }
    try {
      return constructor.newInstance(arguments);
    } catch (InstantiationException | IllegalAccessException | IllegalArgumentException e) {
      // This should never be reached as the logic in consume should prevent us from e.g. calling
      // constructors of abstract classes or private constructors.
      throw new AutofuzzError(getDebugSummary(constructor, null, arguments), e);
    } catch (InvocationTargetException e) {
      if (e.getCause() instanceof HardToCatchError) {
        throw new AutofuzzInvocationException();
      }
      throw new AutofuzzInvocationException(e.getCause());
    }
  }

  // Invariant: The Java source code representation of the returned object visited by visitor must
  // represent an object of the same type as genericType. For example, a null value returned for
  // the genericType Class<java.lang.String> should lead to the generated code
  // "(java.lang.String) null", not just "null". This makes it possible to safely use consume in
  // recursive argument constructions.
  // Exception: Some Java libraries offer public methods that take private interfaces or abstract
  // classes as parameters. In this case, a cast to the parent type would cause an
  // IllegalAccessError. Since this case should be rare and there is no good alternative to
  // disambiguate overloads, we omit the cast in this case.
  Object consume(FuzzedDataProvider data, Type genericType, AutofuzzCodegenVisitor visitor) {
    Class<?> type = getRawType(genericType);
    if (type == byte.class || type == Byte.class) {
      byte result = data.consumeByte();
      if (visitor != null) {
        visitor.pushElement(String.format("(byte) %s", result));
      }
      return result;
    } else if (type == short.class || type == Short.class) {
      short result = data.consumeShort();
      if (visitor != null) {
        visitor.pushElement(String.format("(short) %s", result));
      }
      return result;
    } else if (type == int.class || type == Integer.class) {
      int result = data.consumeInt();
      if (visitor != null) {
        visitor.pushElement(Integer.toString(result));
      }
      return result;
    } else if (type == long.class || type == Long.class) {
      long result = data.consumeLong();
      if (visitor != null) {
        visitor.pushElement(String.format("%sL", result));
      }
      return result;
    } else if (type == float.class || type == Float.class) {
      float result = data.consumeFloat();
      if (visitor != null) {
        visitor.pushElement(String.format("%sF", result));
      }
      return result;
    } else if (type == double.class || type == Double.class) {
      double result = data.consumeDouble();
      if (visitor != null) {
        visitor.pushElement(Double.toString(result));
      }
      return result;
    } else if (type == boolean.class || type == Boolean.class) {
      boolean result = data.consumeBoolean();
      if (visitor != null) {
        visitor.pushElement(Boolean.toString(result));
      }
      return result;
    } else if (type == char.class || type == Character.class) {
      char result = data.consumeChar();
      if (visitor != null) {
        visitor.addCharLiteral(result);
      }
      return result;
    }
    // Sometimes, but rarely return null for non-primitive and non-boxed types.
    // TODO: We might want to return null for boxed types sometimes, but this is complicated by the
    //       fact that TypeUtils can't distinguish between a primitive type and its wrapper and may
    //       thus easily cause false-positive NullPointerExceptions.
    if (!type.isPrimitive() && data.consumeByte() == 0) {
      if (visitor != null) {
        if (type == Object.class) {
          visitor.pushElement("null");
        } else {
          visitor.pushElement(String.format("(%s) null", type.getCanonicalName()));
        }
      }
      return null;
    }
    if (type == String.class || type == CharSequence.class) {
      String result = data.consumeString(consumeArrayLength(data, 1));
      if (visitor != null) {
        visitor.addStringLiteral(result);
      }
      return result;
    } else if (type.isArray()) {
      if (type == byte[].class) {
        byte[] result = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
        if (visitor != null) {
          visitor.pushElement(IntStream.range(0, result.length)
                                  .mapToObj(i -> "(byte) " + result[i])
                                  .collect(Collectors.joining(", ", "new byte[]{", "}")));
        }
        return result;
      } else if (type == int[].class) {
        int[] result = data.consumeInts(consumeArrayLength(data, Integer.BYTES));
        if (visitor != null) {
          visitor.pushElement(Arrays.stream(result)
                                  .mapToObj(String::valueOf)
                                  .collect(Collectors.joining(", ", "new int[]{", "}")));
        }
        return result;
      } else if (type == short[].class) {
        short[] result = data.consumeShorts(consumeArrayLength(data, Short.BYTES));
        if (visitor != null) {
          visitor.pushElement(IntStream.range(0, result.length)
                                  .mapToObj(i -> "(short) " + result[i])
                                  .collect(Collectors.joining(", ", "new short[]{", "}")));
        }
        return result;
      } else if (type == long[].class) {
        long[] result = data.consumeLongs(consumeArrayLength(data, Long.BYTES));
        if (visitor != null) {
          visitor.pushElement(Arrays.stream(result)
                                  .mapToObj(e -> e + "L")
                                  .collect(Collectors.joining(", ", "new long[]{", "}")));
        }
        return result;
      } else if (type == boolean[].class) {
        boolean[] result = data.consumeBooleans(consumeArrayLength(data, 1));
        if (visitor != null) {
          visitor.pushElement(
              Arrays.toString(result).replace(']', '}').replace("[", "new boolean[]{"));
        }
        return result;
      } else {
        if (visitor != null) {
          visitor.pushGroup(
              String.format("new %s[]{", type.getComponentType().getName()), ", ", "}");
        }
        int remainingBytesBeforeFirstElementCreation = data.remainingBytes();
        Object firstElement = consume(data, type.getComponentType(), visitor);
        int remainingBytesAfterFirstElementCreation = data.remainingBytes();
        int sizeOfElementEstimate =
            remainingBytesBeforeFirstElementCreation - remainingBytesAfterFirstElementCreation;
        Object array = Array.newInstance(
            type.getComponentType(), consumeArrayLength(data, sizeOfElementEstimate));
        for (int i = 0; i < Array.getLength(array); i++) {
          if (i == 0) {
            Array.set(array, i, firstElement);
          } else {
            Array.set(array, i, consume(data, type.getComponentType(), visitor));
          }
        }
        if (visitor != null) {
          if (Array.getLength(array) == 0) {
            // We implicitly pushed the first element with the call to consume above, but it is not
            // part of the array.
            visitor.popElement();
          }
          visitor.popGroup();
        }
        return array;
      }
    } else if (type == ByteArrayInputStream.class || type == InputStream.class) {
      byte[] array = data.consumeBytes(consumeArrayLength(data, Byte.BYTES));
      if (visitor != null) {
        visitor.pushElement(IntStream.range(0, array.length)
                                .mapToObj(i -> "(byte) " + array[i])
                                .collect(Collectors.joining(
                                    ", ", "new java.io.ByteArrayInputStream(new byte[]{", "})")));
      }
      return new ByteArrayInputStream(array);
    } else if (type == Map.class) {
      ParameterizedType mapType = (ParameterizedType) genericType;
      if (mapType.getActualTypeArguments().length != 2) {
        throw new AutofuzzError(
            "Expected Map generic type to have two type parameters: " + mapType);
      }
      Type keyType = mapType.getActualTypeArguments()[0];
      Type valueType = mapType.getActualTypeArguments()[1];
      if (visitor != null) {
        // Do not use Collectors.toMap() since it cannot handle null values.
        // Also annotate the type of the entry stream since it might be empty, in which case type
        // inference on the accumulator could fail.
        visitor.pushGroup(
            String.format("java.util.stream.Stream.<java.util.AbstractMap.SimpleEntry<%s, %s>>of(",
                keyType.getTypeName(), valueType.getTypeName()),
            ", ",
            ").collect(java.util.HashMap::new, (map, e) -> map.put(e.getKey(), e.getValue()), java.util.HashMap::putAll)");
      }
      int remainingBytesBeforeFirstEntryCreation = data.remainingBytes();
      if (visitor != null) {
        visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")");
      }
      Object firstKey = consume(data, keyType, visitor);
      Object firstValue = consume(data, valueType, visitor);
      if (visitor != null) {
        visitor.popGroup();
      }
      int remainingBytesAfterFirstEntryCreation = data.remainingBytes();
      int sizeOfElementEstimate =
          remainingBytesBeforeFirstEntryCreation - remainingBytesAfterFirstEntryCreation;
      int mapSize = consumeArrayLength(data, sizeOfElementEstimate);
      Map<Object, Object> map = new HashMap<>(mapSize);
      for (int i = 0; i < mapSize; i++) {
        if (i == 0) {
          map.put(firstKey, firstValue);
        } else {
          if (visitor != null) {
            visitor.pushGroup("new java.util.AbstractMap.SimpleEntry<>(", ", ", ")");
          }
          map.put(consume(data, keyType, visitor), consume(data, valueType, visitor));
          if (visitor != null) {
            visitor.popGroup();
          }
        }
      }
      if (visitor != null) {
        if (mapSize == 0) {
          // We implicitly pushed the first entry with the call to consume above, but it is not
          // part of the array.
          visitor.popElement();
        }
        visitor.popGroup();
      }
      return map;
    } else if (type.isEnum()) {
      Enum<?> enumValue = (Enum<?>) data.pickValue(type.getEnumConstants());
      if (visitor != null) {
        visitor.pushElement(String.format("%s.%s", type.getName(), enumValue.name()));
      }
      return enumValue;
    } else if (type == Class.class) {
      if (visitor != null) {
        visitor.pushElement(String.format("%s.class", YourAverageJavaClass.class.getName()));
      }
      return YourAverageJavaClass.class;
    } else if (type == Method.class) {
      if (visitor != null) {
        throw new AutofuzzError("codegen has not been implemented for Method.class");
      }
      return data.pickValue(lookup.getAccessibleMethods(YourAverageJavaClass.class));
    } else if (type == Constructor.class) {
      if (visitor != null) {
        throw new AutofuzzError("codegen has not been implemented for Constructor.class");
      }
      return data.pickValue(lookup.getAccessibleConstructors(YourAverageJavaClass.class));
    } else if (type.isInterface() || Modifier.isAbstract(type.getModifiers())) {
      List<Class<?>> implementingClasses = implementingClassesCache.get(type);
      if (implementingClasses == null) {
        // TODO: We may be scanning multiple times. Instead, we should keep the ScanResult around
        //  for as long as there is enough memory.
        ClassGraph classGraph = new ClassGraph()
                                    .enableClassInfo()
                                    .ignoreClassVisibility()
                                    .ignoreMethodVisibility()
                                    .enableInterClassDependencies()
                                    .rejectPackages("jaz");
        if (!IS_TEST) {
          classGraph.rejectPackages("com.code_intelligence.jazzer");
        }
        try (ScanResult result = classGraph.scan()) {
          ClassInfoList children =
              type.isInterface() ? result.getClassesImplementing(type) : result.getSubclasses(type);
          implementingClasses = children.getStandardClasses()
                                    .filter(info -> !Modifier.isAbstract(info.getModifiers()))
                                    .filter(info -> lookup.isAccessible(info, info.getModifiers()))
                                    // Filter out anonymous and local classes, which can't be
                                    // instantiated in reproducers.
                                    .filter(info -> info.getName() != null)
                                    .loadClasses();
          implementingClassesCache.put(type, implementingClasses);
        }
      }
      if (implementingClasses.isEmpty()) {
        if (IS_DEBUG) {
          throw new AutofuzzConstructionException(String.format(
              "Could not find classes implementing %s on the classpath", type.getName()));
        } else {
          throw new AutofuzzConstructionException();
        }
      }
      if (visitor != null) {
        // See the "Exception" note in the method comment.
        if (Modifier.isPublic(type.getModifiers())) {
          // This group will always have a single element: The instance of the implementing class.
          visitor.pushGroup(String.format("(%s) ", type.getCanonicalName()), "", "");
        }
      }
      Object result = consume(data, data.pickValue(implementingClasses), visitor);
      if (visitor != null) {
        if (Modifier.isPublic(type.getModifiers())) {
          visitor.popGroup();
        }
      }
      return result;
    }
    Constructor<?>[] constructors = lookup.getAccessibleConstructors(type);
    if (constructors.length > 0) {
      Constructor<?> constructor = data.pickValue(constructors);
      boolean applySetters = constructor.getParameterCount() == 0;
      if (visitor != null && applySetters) {
        // Embed the instance creation and setters into an immediately invoked lambda expression to
        // turn them into an expression.
        String uniqueVariableName = visitor.uniqueVariableName();
        visitor.pushGroup(String.format("((java.util.function.Supplier<%1$s>) (() -> {%1$s %2$s = ",
                              type.getCanonicalName(), uniqueVariableName),
            String.format("; %s.", uniqueVariableName),
            String.format("; return %s;})).get()", uniqueVariableName));
      }
      Object obj = autofuzzForConsume(data, constructor, visitor);
      if (applySetters) {
        List<Method> potentialSetters = getPotentialSetters(type);
        if (!potentialSetters.isEmpty()) {
          List<Method> pickedSetters =
              data.pickValues(potentialSetters, data.consumeInt(0, potentialSetters.size()));
          for (Method setter : pickedSetters) {
            autofuzzForConsume(data, setter, obj, visitor);
          }
        }
        if (visitor != null) {
          visitor.popGroup();
        }
      }
      return obj;
    }
    // We are out of more or less canonical ways to construct an instance of this class and have to
    // resort to more heuristic approaches.

    // First, try to find nested classes with names ending in Builder and call a subset of their
    // chaining methods.
    List<Class<?>> nestedBuilderClasses = getNestedBuilderClasses(type);
    if (!nestedBuilderClasses.isEmpty()) {
      Class<?> pickedBuilder = data.pickValue(nestedBuilderClasses);
      List<Method> cascadingBuilderMethods = getCascadingBuilderMethods(pickedBuilder);
      List<Method> originalObjectCreationMethods = getOriginalObjectCreationMethods(pickedBuilder);

      int pickedMethodsNumber = data.consumeInt(0, cascadingBuilderMethods.size());
      List<Method> pickedMethods = data.pickValues(cascadingBuilderMethods, pickedMethodsNumber);
      Method builderMethod = data.pickValue(originalObjectCreationMethods);

      if (visitor != null) {
        // Group for the chain of builder methods.
        visitor.pushGroup("", ".", "");
      }
      Object builderObj = autofuzzForConsume(
          data, data.pickValue(lookup.getAccessibleConstructors(pickedBuilder)), visitor);
      for (Method method : pickedMethods) {
        builderObj = autofuzzForConsume(data, method, builderObj, visitor);
      }

      try {
        Object obj = autofuzzForConsume(data, builderMethod, builderObj, visitor);
        if (visitor != null) {
          visitor.popGroup();
        }
        return obj;
      } catch (Exception e) {
        throw new AutofuzzConstructionException(e);
      }
    }

    // We ran out of ways to construct an instance of the requested type. If in debug mode, report
    // more detailed information.
    if (IS_DEBUG) {
      String summary = String.format(
          "Failed to generate instance of %s:%nAccessible constructors: %s%nNested subclasses: %s%n",
          type.getName(),
          Arrays.stream(lookup.getAccessibleConstructors(type))
              .map(Utils::getReadableDescriptor)
              .collect(Collectors.joining(", ")),
          Arrays.stream(lookup.getAccessibleClasses(type))
              .map(Class::getName)
              .collect(Collectors.joining(", ")));
      throw new AutofuzzConstructionException(summary);
    } else {
      throw new AutofuzzConstructionException();
    }
  }

  private List<Class<?>> getNestedBuilderClasses(Class<?> type) {
    List<Class<?>> nestedBuilderClasses = nestedBuilderClassesCache.get(type);
    if (nestedBuilderClasses == null) {
      nestedBuilderClasses = Arrays.stream(lookup.getAccessibleClasses(type))
                                 .filter(cls -> cls.getName().endsWith("Builder"))
                                 .filter(cls -> !getOriginalObjectCreationMethods(cls).isEmpty())
                                 .collect(Collectors.toList());
      nestedBuilderClassesCache.put(type, nestedBuilderClasses);
    }
    return nestedBuilderClasses;
  }

  private List<Method> getOriginalObjectCreationMethods(Class<?> builder) {
    List<Method> originalObjectCreationMethods = originalObjectCreationMethodsCache.get(builder);
    if (originalObjectCreationMethods == null) {
      originalObjectCreationMethods =
          Arrays.stream(lookup.getAccessibleMethods(builder))
              .filter(m -> m.getReturnType() == builder.getEnclosingClass())
              .collect(Collectors.toList());
      originalObjectCreationMethodsCache.put(builder, originalObjectCreationMethods);
    }
    return originalObjectCreationMethods;
  }

  private List<Method> getCascadingBuilderMethods(Class<?> builder) {
    List<Method> cascadingBuilderMethods = cascadingBuilderMethodsCache.get(builder);
    if (cascadingBuilderMethods == null) {
      cascadingBuilderMethods = Arrays.stream(lookup.getAccessibleMethods(builder))
                                    .filter(m -> m.getReturnType() == builder)
                                    .collect(Collectors.toList());
      cascadingBuilderMethodsCache.put(builder, cascadingBuilderMethods);
    }
    return cascadingBuilderMethods;
  }

  private List<Method> getPotentialSetters(Class<?> type) {
    return Arrays.stream(lookup.getAccessibleMethods(type))
        .filter(method -> void.class.equals(method.getReturnType()))
        .filter(method -> method.getParameterCount() == 1)
        .filter(method -> method.getName().startsWith("set"))
        .collect(Collectors.toList());
  }

  public Object[] consumeArguments(
      FuzzedDataProvider data, Executable executable, AutofuzzCodegenVisitor visitor) {
    Object[] result;
    try {
      result = Arrays.stream(executable.getGenericParameterTypes())
                   .map(type -> consume(data, type, visitor))
                   .toArray();
      return result;
    } catch (AutofuzzConstructionException e) {
      // Do not nest AutofuzzConstructionExceptions.
      throw e;
    } catch (AutofuzzInvocationException e) {
      // If an invocation fails while creating the arguments for another invocation, the exception
      // should not be reported, so we rewrap it.
      throw new AutofuzzConstructionException(e.getCause());
    } catch (Throwable t) {
      throw new AutofuzzConstructionException(t);
    }
  }

  private Object consumeChecked(FuzzedDataProvider data, Class<?>[] types, int i) {
    if (types[i] == Unknown.class) {
      throw new AutofuzzError("Failed to determine type of argument " + (i + 1));
    }
    Object result;
    try {
      result = consumeNonStatic(data, types[i]);
    } catch (AutofuzzConstructionException e) {
      // Do not nest AutofuzzConstructionExceptions.
      throw e;
    } catch (AutofuzzInvocationException e) {
      // If an invocation fails while creating the arguments for another invocation, the exception
      // should not be reported, so we rewrap it.
      throw new AutofuzzConstructionException(e.getCause());
    } catch (Throwable t) {
      throw new AutofuzzConstructionException(t);
    }
    if (result != null && !types[i].isAssignableFrom(result.getClass())) {
      throw new AutofuzzError("consume returned " + result.getClass() + ", but need " + types[i]);
    }
    return result;
  }
}
