package com.google.inject.internal;

import static com.google.common.collect.ImmutableList.toImmutableList;
import static java.lang.Math.min;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Primitives;
import com.google.inject.Binding;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.TypeLiteral;
import com.google.inject.spi.BindingSourceRestriction;
import com.google.inject.spi.UntargettedBinding;
import java.util.ArrayList;
import java.util.Formatter;
import java.util.List;
import java.util.Map;

// TODO(b/165344346): Migrate to use suggest hints API.
/** Helper class to find hints for {@link MissingImplementationError}. */
final class MissingImplementationErrorHints {

  private MissingImplementationErrorHints() {}

  /** When a binding is not found, show at most this many bindings with the same type */
  private static final int MAX_MATCHING_TYPES_REPORTED = 3;

  /** When a binding is not found, show at most this many bindings that have some similarities */
  private static final int MAX_RELATED_TYPES_REPORTED = 3;

  /**
   * If the key is unknown and it is one of these types, it generally means there is a missing
   * annotation.
   */
  private static final ImmutableSet<Class<?>> COMMON_AMBIGUOUS_TYPES =
      ImmutableSet.<Class<?>>builder()
          .add(Object.class)
          .add(String.class)
          .addAll(Primitives.allWrapperTypes())
          .build();

  static <T> ImmutableList<String> getSuggestions(Key<T> key, Injector injector) {
    ImmutableList.Builder<String> suggestions = ImmutableList.builder();
    TypeLiteral<T> type = key.getTypeLiteral();

    BindingSourceRestriction.getMissingImplementationSuggestion(GuiceInternal.GUICE_INTERNAL, key)
        .ifPresent(suggestions::add);

    // Keys which have similar strings as the desired key
    List<String> possibleMatches = new ArrayList<>();
    ImmutableList<Binding<T>> sameTypes =
        injector.findBindingsByType(type).stream()
            .filter(b -> !(b instanceof UntargettedBinding)) // These aren't valid matches
            .collect(toImmutableList());
    if (!sameTypes.isEmpty()) {
      suggestions.add("\nDid you mean?");
      int howMany = min(sameTypes.size(), MAX_MATCHING_TYPES_REPORTED);
      for (int i = 0; i < howMany; ++i) {
        Key<?> bindingKey = sameTypes.get(i).getKey();
        // TODO: Look into a better way to prioritize suggestions. For example, possbily
        // use levenshtein distance of the given annotation vs actual annotation.
        suggestions.add(
            Messages.format(
                "\n    * %s",
                formatSuggestion(bindingKey, injector.getExistingBinding(bindingKey))));
      }
      int remaining = sameTypes.size() - MAX_MATCHING_TYPES_REPORTED;
      if (remaining > 0) {
        String plural = (remaining == 1) ? "" : "s";
        suggestions.add(
            Messages.format(
                "\n    * %d more binding%s with other annotations.", remaining, plural));
      }
    } else {
      // For now, do a simple substring search for possibilities. This can help spot
      // issues when there are generics being used (such as a wrapper class) and the
      // user has forgotten they need to bind based on the wrapper, not the underlying
      // class. In the future, consider doing a strict in-depth type search.
      // TODO: Look into a better way to prioritize suggestions. For example, possbily
      // use levenshtein distance of the type literal strings.
      String want = type.toString();
      Map<Key<?>, Binding<?>> bindingMap = injector.getAllBindings();
      for (Key<?> bindingKey : bindingMap.keySet()) {
        Binding<?> binding = bindingMap.get(bindingKey);
        // Ignore untargeted bindings, those aren't valid matches.
        if (binding instanceof UntargettedBinding) {
          continue;
        }
        String have = bindingKey.getTypeLiteral().toString();
        if (have.contains(want) || want.contains(have)) {
          possibleMatches.add(formatSuggestion(bindingKey, bindingMap.get(bindingKey)));
          // TODO: Consider a check that if there are more than some number of results,
          // don't suggest any.
          if (possibleMatches.size() > MAX_RELATED_TYPES_REPORTED) {
            // Early exit if we have found more than we need.
            break;
          }
        }
      }

      if (!possibleMatches.isEmpty() && (possibleMatches.size() <= MAX_RELATED_TYPES_REPORTED)) {
        suggestions.add("\nDid you mean?");
        for (String possibleMatch : possibleMatches) {
          suggestions.add(Messages.format("\n    * %s", possibleMatch));
        }
      }
    }

    // If where are no possibilities to suggest, then handle the case of missing
    // annotations on simple types. This is usually a bad idea.
    if (sameTypes.isEmpty()
        && possibleMatches.isEmpty()
        && key.getAnnotationType() == null
        && COMMON_AMBIGUOUS_TYPES.contains(key.getTypeLiteral().getRawType())) {
      // We don't recommend using such simple types without annotations.
      suggestions.add("\nThe key seems very generic, did you forget an annotation?");
    }

    return suggestions.build();
  }

  private static String formatSuggestion(Key<?> key, Binding<?> binding) {
    Formatter fmt = new Formatter();
    fmt.format("%s bound ", Messages.convert(key));
    new SourceFormatter(binding.getSource(), fmt, /* omitPreposition= */ false).format();
    return fmt.toString();
  }
}
