/**
   * Returns the target types that need to have conversion. The types contain first as many
   * constructor parameter types as we have and then the types of properties of object as given by
   * names of result-set.
   */
  @NotNull
  private static Optional<List<Type>> findTargetTypes(
      @NotNull Constructor<?> ctor, @NotNull List<String> resultSetColumns) {
    List<Type> constructorParameterTypes = asList(ctor.getGenericParameterTypes());

    int constructorParameterCount = constructorParameterTypes.size();

    if (constructorParameterCount > resultSetColumns.size()) {
      // We don't have enough columns in ResultSet to instantiate this constructor, discard it.
      return Optional.empty();

    } else if (constructorParameterCount == resultSetColumns.size()) {
      // We have exactly enough column in ResultSet. Use the constructor as it is.
      return Optional.of(constructorParameterTypes);

    } else {
      // Get the types of remaining properties
      ArrayList<Type> result = new ArrayList<>(resultSetColumns.size());
      result.addAll(constructorParameterTypes);

      List<String> propertyNames =
          resultSetColumns.subList(constructorParameterCount, resultSetColumns.size());
      for (String name : propertyNames) {
        Type type = PropertyAccessor.findPropertyType(ctor.getDeclaringClass(), name).orElse(null);
        if (type != null) result.add(type);
        else return Optional.empty();
      }

      return Optional.of(result);
    }
  }
  @NotNull
  private static Optional<TypeConversion> findEnumConversion(@NotNull Type target) {
    if (isEnum(target)) {
      @SuppressWarnings("rawtypes")
      Class<? extends Enum> cl = rawType(target).asSubclass(Enum.class);

      return Optional.ofNullable(
          TypeConversion.fromNonNullFunction(value -> Enum.valueOf(cl, value.toString())));
    }

    return Optional.empty();
  }
  @NotNull
  private static Optional<Constructor<?>> dalesbredConstructor(@NotNull Class<?> cl) {
    List<Constructor<?>> candidates =
        Stream.of(cl.getDeclaredConstructors())
            .filter(ctor -> ctor.isAnnotationPresent(DalesbredInstantiator.class))
            .collect(toList());

    if (candidates.size() == 1) return Optional.of(candidates.get(0));
    else if (candidates.size() > 1)
      throw new InstantiationFailureException(
          "only one constructor of "
              + cl.getName()
              + " can be marked with @DalesbredConstructor. Found "
              + candidates.size());
    else return Optional.empty();
  }
  /**
   * Returns the list of conversions that need to be performed to convert sourceTypes to
   * targetTypes, or empty if conversions can't be done.
   */
  @NotNull
  private Optional<List<TypeConversion>> resolveConversions(
      @NotNull NamedTypeList sourceTypes, @NotNull List<Type> targetTypes) {
    if (targetTypes.size() != sourceTypes.size()) return Optional.empty();

    ArrayList<TypeConversion> conversions = new ArrayList<>(targetTypes.size());

    for (int i = 0, len = targetTypes.size(); i < len; i++) {
      TypeConversion conversion =
          findConversionFromDbValue(sourceTypes.getType(i), targetTypes.get(i)).orElse(null);
      if (conversion != null) conversions.add(conversion);
      else return Optional.empty();
    }

    return Optional.of(conversions);
  }
  @NotNull
  private Optional<Instantiator<?>> findExplicitInstantiatorFor(
      Class<?> cl, @NotNull NamedTypeList types) throws InstantiationFailureException {
    Constructor<?> constructor = dalesbredConstructor(cl).orElse(null);

    if (constructor == null) return Optional.empty();

    try {
      ReflectionUtils.makeAccessible(constructor);
    } catch (SecurityException e) {
      throw new InstantiationFailureException(
          "Cannot instantiate "
              + constructor.getDeclaringClass().getName()
              + " using non-public constructor due to Security exception",
          e);
    }

    List<String> columnNames = types.getNames();
    List<Type> constructorParameterTypes = asList(constructor.getGenericParameterTypes());

    if (constructorParameterTypes.size() != columnNames.size())
      throw new InstantiationFailureException(
          String.format(
              "Cannot instantiate %s, constructor takes %d arguments, but result set has %d",
              constructor.getDeclaringClass().getName(),
              constructorParameterTypes.size(),
              columnNames.size()));

    ReflectionInstantiator<?> instantiator =
        resolveConversions(types, constructorParameterTypes)
            .map(
                conversions ->
                    new ReflectionInstantiator<>(constructor, conversions, Collections.emptyList()))
            .orElseThrow(
                () ->
                    new InstantiationFailureException(
                        "could not find a way to instantiate "
                            + constructor.getDeclaringClass().getName()
                            + " with parameters "
                            + types));

    return Optional.of(instantiator);
  }
  /**
   * Returns conversion for converting value of source to target, or returns null if there's no such
   * conversion.
   */
  @NotNull
  private Optional<TypeConversion> findConversionFromDbValue(
      @NotNull Type source, @NotNull Type target) {
    if (isAssignable(target, source)) return Optional.of(TypeConversion.identity());

    Optional<TypeConversion> directConversion =
        typeConversionRegistry.findConversionFromDbValue(source, target);
    if (directConversion.isPresent()) return directConversion;

    Optional<TypeConversion> arrayConversion = findArrayConversion(source, target);
    if (arrayConversion.isPresent()) return arrayConversion;

    Optional<TypeConversion> optionalConversion = findOptionalConversion(source, target);
    if (optionalConversion.isPresent()) return optionalConversion;

    Optional<TypeConversion> enumConversion = findEnumConversion(target);
    if (enumConversion.isPresent()) return enumConversion;

    return Optional.empty();
  }
  @NotNull
  private Optional<TypeConversion> findArrayConversion(@NotNull Type source, @NotNull Type target) {
    Class<?> rawTarget = rawType(target);

    if (isAssignable(Array.class, source)) {
      if (rawTarget.equals(Set.class))
        return Optional.of(
            SqlArrayConversion.sqlArray(typeParameter(target), this, LinkedHashSet::new));

      if (rawTarget.isAssignableFrom(List.class))
        return Optional.of(
            SqlArrayConversion.sqlArray(typeParameter(target), this, Function.identity()));

      if (rawTarget.isArray())
        return Optional.of(
            SqlArrayConversion.sqlArray(
                rawTarget.getComponentType(),
                this,
                list -> arrayOfType(rawTarget.getComponentType(), list)));
    }

    return Optional.empty();
  }
  /**
   * Returns an instantiator that uses given constructor and given types to create instances, or
   * empty if there are no conversions that can be made to instantiate the type.
   */
  @NotNull
  private <T> Optional<Instantiator<T>> implicitInstantiatorFrom(
      @NotNull Constructor<T> constructor, @NotNull NamedTypeList types) {
    if (!isPublic(constructor.getModifiers())) return Optional.empty();

    List<String> columnNames = types.getNames();
    return findTargetTypes(constructor, columnNames)
        .flatMap(
            targetTypes ->
                resolveConversions(types, targetTypes)
                    .map(
                        conversions ->
                            new ReflectionInstantiator<>(
                                constructor,
                                conversions,
                                createPropertyAccessorsForValuesNotCoveredByConstructor(
                                    constructor, columnNames))));
  }
  @NotNull
  private Optional<TypeConversion> findOptionalConversion(
      @NotNull Type source, @NotNull Type target) {
    Class<?> rawTarget = rawType(target);

    if (rawTarget == Optional.class) {
      return optionalConversion(source, typeParameter(target), Optional::ofNullable);

    } else if (rawTarget == OptionalInt.class) {
      return optionalConversion(source, Integer.class, OptionalUtils::optionalIntOfNullable);

    } else if (rawTarget == OptionalLong.class) {
      return optionalConversion(source, Long.class, OptionalUtils::optionalLongOfNullable);

    } else if (rawTarget == OptionalDouble.class) {
      return optionalConversion(source, Double.class, OptionalUtils::optionalDoubleOfNullable);

    } else {
      return Optional.empty();
    }
  }