private static @NotNull Substitution findDesiredSubstitution(@Nullable PsiType expected,
                                                               @Nullable PsiType actual,
                                                               @Nullable PsiType methodType,
                                                               int level) {
    if (expected == null || actual == null || methodType == null) return Substitution.BOTTOM;
    if (expected instanceof PsiArrayType && actual instanceof PsiArrayType && methodType instanceof PsiArrayType) {
      return findDesiredSubstitution(((PsiArrayType)expected).getComponentType(),
                                     ((PsiArrayType)actual).getComponentType(),
                                     ((PsiArrayType)methodType).getComponentType(),
                                     level);
    }
    if (expected.equals(actual)) return Substitution.TOP;
    if (!(methodType instanceof PsiClassType)) return Substitution.BOTTOM;
    PsiClass methodClass = ((PsiClassType)methodType).resolve();
    if (methodClass == null) return Substitution.BOTTOM;
    if (methodClass instanceof PsiTypeParameter) {
      if (!(expected instanceof PsiPrimitiveType)) {
        for (PsiClassType superType : methodClass.getSuperTypes()) {
          PsiSubstitutor substitutor = PsiSubstitutor.EMPTY.put((PsiTypeParameter)methodClass, expected);
          if (!substitutor.substitute(superType).isAssignableFrom(expected)) return Substitution.BOTTOM;
        }
        return new Substitution.TypeParameterSubstitution((PsiTypeParameter)methodClass, actual, expected);
      }
      return Substitution.BOTTOM;
    }
    if (!(expected instanceof PsiClassType) || !(actual instanceof PsiClassType)) return Substitution.BOTTOM;
    PsiClass expectedClass = ((PsiClassType)expected).resolve();
    PsiClass actualClass = ((PsiClassType)actual).resolve();
    if (expectedClass == null || actualClass == null || !actualClass.isEquivalentTo(methodClass)) return Substitution.BOTTOM;
    if (!expectedClass.isEquivalentTo(actualClass)) {
      if (level > 0) return Substitution.BOTTOM;
      methodType = trySubstitute(methodType, expectedClass);
      actual = trySubstitute(actual, expectedClass);
      if (methodType == null || actual == null) return Substitution.BOTTOM;
    }
    PsiType[] methodTypeParameters = ((PsiClassType)methodType).getParameters();
    PsiType[] expectedTypeParameters = ((PsiClassType)expected).getParameters();
    PsiType[] actualTypeParameters = ((PsiClassType)actual).getParameters();
    if (methodTypeParameters.length != expectedTypeParameters.length || methodTypeParameters.length != actualTypeParameters.length) {
      return Substitution.BOTTOM;
    }
    Substitution existing = Substitution.TOP;
    for (int i = 0; i < methodTypeParameters.length; i++) {
      Substitution substitution = findDesiredSubstitution(expectedTypeParameters[i], actualTypeParameters[i], methodTypeParameters[i], level+1);
      if (existing == Substitution.TOP) {
        existing = substitution;
      }
      else if (substitution != Substitution.TOP && !existing.equals(substitution)) {
        return Substitution.BOTTOM;
      }
    }
    return existing;
  }
private static @NotNull Substitution findDesiredSubstitution(@Nullable PsiType expected,
                                                               @Nullable PsiType actual,
                                                               @Nullable PsiType methodType,
                                                               int level)