content: package in.oneton.idea.spring.assistant.plugin.suggestion.clazz;
  
  import com.intellij.openapi.diagnostic.Logger;
  import com.intellij.openapi.module.Module;
  import com.intellij.openapi.util.Key;
  import com.intellij.psi.PsiClass;
  import com.intellij.psi.PsiClassType;
  import com.intellij.psi.PsiType;
  import com.intellij.psi.util.CachedValue;
  import com.intellij.util.ConcurrencyUtil;
  import com.intellij.util.containers.ContainerUtil;
  import in.oneton.idea.spring.assistant.plugin.suggestion.Suggestion;
  import in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNode;
  import in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType;
  import in.oneton.idea.spring.assistant.plugin.suggestion.completion.FileType;
  import in.oneton.idea.spring.assistant.plugin.suggestion.completion.SuggestionDocumentationHelper;
  import org.jetbrains.annotations.NotNull;
  
  import javax.annotation.Nullable;
  import java.util.Collection;
  import java.util.List;
  import java.util.Set;
  import java.util.SortedSet;
  import java.util.concurrent.ConcurrentMap;
  
  import static com.intellij.psi.util.CachedValueProvider.Result.create;
  import static com.intellij.psi.util.CachedValuesManager.getCachedValue;
  import static in.oneton.idea.spring.assistant.plugin.misc.PsiCustomUtil.computeDependencies;
  import static in.oneton.idea.spring.assistant.plugin.misc.PsiCustomUtil.toValidPsiClass;
  import static in.oneton.idea.spring.assistant.plugin.misc.PsiCustomUtil.typeToFqn;
  import static in.oneton.idea.spring.assistant.plugin.suggestion.SuggestionNodeType.UNKNOWN_CLASS;
  import static in.oneton.idea.spring.assistant.plugin.suggestion.clazz.ClassSuggestionNodeFactory.newClassMetadata;
  import static java.util.Objects.requireNonNull;
  
  public class ClassMetadataProxy implements MetadataProxy {
  
    private static final Logger log = Logger.getInstance(ClassMetadataProxy.class);
  
    private static ConcurrentMap<String, Key<CachedValue<ClassMetadata>>> fqnToKey =
        ContainerUtil.newConcurrentMap();
  
    @NotNull
    private final PsiClass targetClass;
  
    @NotNull
    private final PsiClassType type;
  
    ClassMetadataProxy(@NotNull PsiClassType type) {
      this.type = type;
      targetClass = requireNonNull(toValidPsiClass(type));
    }
  
    @Nullable
    @Override
    public SuggestionDocumentationHelper findDirectChild(Module module, String pathSegment) {
      return doWithTargetAndReturn(module, target -> target.findDirectChild(module, pathSegment),
          null);
    }
  
    @Nullable
    @Override
    public Collection<? extends SuggestionDocumentationHelper> findDirectChildrenForQueryPrefix(
        Module module, String querySegmentPrefix) {
      return findDirectChildrenForQueryPrefix(module, querySegmentPrefix, null);
    }
  
    @Nullable
    @Override
    public Collection<? extends SuggestionDocumentationHelper> findDirectChildrenForQueryPrefix(
        Module module, String querySegmentPrefix, @Nullable Set<String> siblingsToExclude) {
      return doWithTargetAndReturn(module, target -> target
          .findDirectChildrenForQueryPrefix(module, querySegmentPrefix, siblingsToExclude), null);
    }
  
    @Nullable
    @Override
    public List<SuggestionNode> findDeepestSuggestionNode(Module module,
        List<SuggestionNode> matchesRootTillParentNode, String[] pathSegments,
        int pathSegmentStartIndex) {
      return doWithTargetAndReturn(module, target -> target
          .findDeepestSuggestionNode(module, matchesRootTillParentNode, pathSegments,
              pathSegmentStartIndex), null);
    }
  
    @Nullable
    @Override
    public SortedSet<Suggestion> findKeySuggestionsForQueryPrefix(Module module, FileType fileType,
        List<SuggestionNode> matchesRootTillMe, int numOfAncestors, String[] querySegmentPrefixes,
        int querySegmentPrefixStartIndex) {
      return doWithTargetAndReturn(module, target -> target
          .findKeySuggestionsForQueryPrefix(module, fileType, matchesRootTillMe, numOfAncestors,
              querySegmentPrefixes, querySegmentPrefixStartIndex), null);
    }
  
    @Nullable
    @Override
    public SortedSet<Suggestion> findKeySuggestionsForQueryPrefix(Module module, FileType fileType,
        List<SuggestionNode> matchesRootTillMe, int numOfAncestors, String[] querySegmentPrefixes,
        int querySegmentPrefixStartIndex, @Nullable Set<String> siblingsToExclude) {
      return doWithTargetAndReturn(module, target -> target
          .findKeySuggestionsForQueryPrefix(module, fileType, matchesRootTillMe, numOfAncestors,
              querySegmentPrefixes, querySegmentPrefixStartIndex, siblingsToExclude), null);
    }
  
    @Nullable
    @Override
    public SortedSet<Suggestion> findValueSuggestionsForPrefix(Module module, FileType fileType,
        List<SuggestionNode> matchesRootTillMe, String prefix) {
      return findValueSuggestionsForPrefix(module, fileType, matchesRootTillMe, prefix, null);
    }
  
    @Nullable
    @Override
    public SortedSet<Suggestion> findValueSuggestionsForPrefix(Module module, FileType fileType,
        List<SuggestionNode> matchesRootTillMe, String prefix,
        @Nullable Set<String> siblingsToExclude) {
      return doWithTargetAndReturn(module, target -> target
          .findValueSuggestionsForPrefix(module, fileType, matchesRootTillMe, prefix,
              siblingsToExclude), null);
    }
  
    @Nullable
    @Override
    public String getDocumentationForValue(Module module, String nodeNavigationPathDotDelimited,
        String originalValue) {
      return doWithTargetAndReturn(module,
          target -> target.getDocumentationForValue(module, nodeNavigationPathDotDelimited,
              originalValue),
          null);
    }
  
    @Override
    public boolean isLeaf(Module module) {
      return doWithTargetAndReturn(module, target -> target.isLeaf(module), true);
    }
  
    @NotNull
    @Override
    public SuggestionNodeType getSuggestionNodeType(Module module) {
      return doWithTargetAndReturn(module, ClassMetadata::getSuggestionNodeType, UNKNOWN_CLASS);
    }
  
    @Nullable
    @Override
    public PsiType getPsiType(Module module) {
      return doWithTargetAndReturn(module, target -> target.getPsiType(module), null);
    }
  
    @Override
    public boolean targetRepresentsArray() {
      return false;
    }
  
    @Override
    public boolean targetClassRepresentsIterable(Module module) {
      return doWithTargetAndReturn(module, IterableClassMetadata.class::isInstance, false);
    }
  
    <T> T doWithTargetAndReturn(Module module,
        TargetInvokerWithReturnValue<T> targetInvokerWithReturnValue, T defaultReturnValue) {
      ClassMetadata target = getTarget(module);
      if (target != null) {
        return targetInvokerWithReturnValue.invoke(target);
      }
      return defaultReturnValue;
    }
  
    private ClassMetadata getTarget(Module module) {
      String fqn = typeToFqn(module, type);
      if (fqn != null) {
        String userDataKeyRef = "spring_assistant_plugin_class_metadata:" + fqn;
        Key<CachedValue<ClassMetadata>> classMetadataKey =
            ConcurrencyUtil.cacheOrGet(fqnToKey, userDataKeyRef, Key.create(userDataKeyRef));
        return getCachedValue(targetClass, classMetadataKey, () -> {
          log.debug("Creating metadata instance for " + userDataKeyRef);
          Set<PsiClass> dependencies = computeDependencies(module, type);
          if (dependencies != null) {
            return create(newClassMetadata(type), dependencies);
          }
          return null;
        });
      }
      return null;
    }
  
  
    protected interface TargetInvokerWithReturnValue<T> {
      T invoke(ClassMetadata classMetadata);
    }
  
  }
  