private static boolean isAssignable(@NotNull PsiType left,
                                      @NotNull PsiType right,
                                      boolean allowUncheckedConversion,
                                      boolean capture) {
    if (left == right) return true;
    if (left instanceof PsiClassType && left.equalsToText(JAVA_LANG_OBJECT)) {
      if (right instanceof PsiMethodReferenceType || right instanceof PsiLambdaExpressionType) return false;
    }
    if (left.equals(right)) return true;

    if (isNullType(right)) {
      return !(left instanceof PsiPrimitiveType) || isNullType(left);
    }

    if (right instanceof PsiMethodReferenceType) {
      final PsiMethodReferenceExpression methodReferenceExpression = ((PsiMethodReferenceType)right).getExpression();
      if (left instanceof PsiLambdaExpressionType) {
        final PsiType rType = methodReferenceExpression.getFunctionalInterfaceType();
        final PsiType lType = ((PsiLambdaExpressionType)left).getExpression().getFunctionalInterfaceType();
        return Comparing.equal(rType, lType);
      }
      if (left instanceof PsiMethodReferenceType) {
        final PsiType rType = methodReferenceExpression.getFunctionalInterfaceType();
        final PsiType lType = ((PsiMethodReferenceType)left).getExpression().getFunctionalInterfaceType();
        return Comparing.equal(rType, lType);
      }
      return !(left instanceof PsiArrayType) && methodReferenceExpression.isAcceptable(left);
    }
    if (right instanceof PsiLambdaExpressionType) {
      final PsiLambdaExpression rLambdaExpression = ((PsiLambdaExpressionType)right).getExpression();
      if (left instanceof PsiLambdaExpressionType) {
        final PsiLambdaExpression lLambdaExpression = ((PsiLambdaExpressionType)left).getExpression();
        final PsiType rType = rLambdaExpression.getFunctionalInterfaceType();
        final PsiType lType = lLambdaExpression.getFunctionalInterfaceType();
        return Comparing.equal(rType, lType);
      }
      return !(left instanceof PsiArrayType) && rLambdaExpression.isAcceptable(left);
    }

    if (left instanceof PsiIntersectionType) {
      PsiType[] conjuncts = ((PsiIntersectionType)left).getConjuncts();
      for (PsiType conjunct : conjuncts) {
        if (!isAssignable(conjunct, right, allowUncheckedConversion, capture)) return false;
      }
      return true;
    }
    if (right instanceof PsiIntersectionType) {
      PsiType[] conjuncts = ((PsiIntersectionType)right).getConjuncts();
      for (PsiType conjunct : conjuncts) {
        if (isAssignable(left, conjunct, allowUncheckedConversion, capture)) return true;
      }
      return false;
    }

    if (right instanceof PsiCapturedWildcardType) {
      return isAssignable(left, ((PsiCapturedWildcardType)right).getUpperBound(capture), allowUncheckedConversion, capture);
    }

    if (left instanceof PsiCapturedWildcardType) {
      return left.equals(right) || isAssignable(((PsiCapturedWildcardType)left).getLowerBound(), right, allowUncheckedConversion, capture);
    }

    if (left instanceof PsiWildcardType) {
      return isAssignableToWildcard((PsiWildcardType)left, right);
    }
    if (right instanceof PsiWildcardType) {
      return isAssignableFromWildcard(left, (PsiWildcardType)right);
    }
    if (right instanceof PsiArrayType) {
      if (!(left instanceof PsiArrayType)) {
        if (left instanceof PsiPrimitiveType || PsiUtil.resolveClassInType(left) == null) return false;
        PsiClass lClass = PsiUtil.resolveClassInType(left);
        if (lClass == null) return false;
        if (lClass.isInterface()) {
          final String qualifiedName = lClass.getQualifiedName();
          return "java.io.Serializable".equals(qualifiedName) || "java.lang.Cloneable".equals(qualifiedName);
        }
        else {
          return left.equalsToText(JAVA_LANG_OBJECT);
        }
      }
      PsiType lCompType = ((PsiArrayType)left).getComponentType();
      PsiType rCompType = ((PsiArrayType)right).getComponentType();
      if (lCompType instanceof PsiPrimitiveType) {
        return lCompType.equals(rCompType);
      }
      return !(rCompType instanceof PsiPrimitiveType) && isAssignable(lCompType, rCompType, allowUncheckedConversion, capture);
    }

    if (left instanceof PsiDisjunctionType) {
      for (PsiType type : ((PsiDisjunctionType)left).getDisjunctions()) {
        if (isAssignable(type, right, allowUncheckedConversion, capture)) return true;
      }
      return false;
    }
    if (right instanceof PsiDisjunctionType) {
      return isAssignable(left, ((PsiDisjunctionType)right).getLeastUpperBound(), allowUncheckedConversion, capture);
    }

    if (left instanceof PsiArrayType) {
      if (right instanceof PsiClassType) {
        PsiClass aClass = ((PsiClassType)right).resolve();
        if (aClass instanceof PsiTypeParameter) {
          PsiType upperBound = getInferredUpperBoundForSynthetic((PsiTypeParameter)aClass);
          return upperBound != null && isAssignable(left, upperBound, allowUncheckedConversion, capture);
        }
      }
      return false;
    }
    if (right instanceof PsiPrimitiveType) {
      if (isVoidType(right)) return false;
      if (!(left instanceof PsiPrimitiveType)) {
        return left instanceof PsiClassType && isBoxable((PsiClassType)left, (PsiPrimitiveType)right);
      }
      int l = TYPE_TO_RANK_MAP.get(left);
      int r = TYPE_TO_RANK_MAP.get(right);
      int leftTypeIndex = (l==-1?0:l) - 1;
      int rightTypeIndex = (r==-1?0:r) - 1;
      return leftTypeIndex >= 0 &&
             rightTypeIndex >= 0 &&
             rightTypeIndex < IS_ASSIGNABLE_BIT_SET.length &&
             leftTypeIndex < IS_ASSIGNABLE_BIT_SET.length &&
             IS_ASSIGNABLE_BIT_SET[rightTypeIndex][leftTypeIndex];
    }
    if (!(right instanceof PsiClassType)) {
      return false; // must be TypeCook's PsiTypeVariable
    }
    if (left instanceof PsiPrimitiveType) {
      return isUnboxable((PsiPrimitiveType)left, (PsiClassType)right, new HashSet<>());
    }
    final PsiClassType.ClassResolveResult leftResult = PsiUtil.resolveGenericsClassInType(left);
    final PsiClassType.ClassResolveResult rightResult = PsiUtil.resolveGenericsClassInType(right);
    PsiClass leftResultElement = leftResult.getElement();
    PsiClass rightResultElement = rightResult.getElement();
    if (leftResultElement == null || rightResultElement == null) {
      if (left instanceof PsiClassType && left.equalsToText(JAVA_LANG_OBJECT)) {
        return true;
      }
      if (leftResultElement != rightResultElement) return false;
      // let's suppose 2 unknown classes, which could be the same to be the same
      String lText = left.getPresentableText();
      String rText = right.getPresentableText();
      if (lText.equals(rText)) return true;
      if (lText.length() > rText.length() && lText.endsWith(rText) &&
          lText.charAt(lText.length() - rText.length() - 1) == '.') {
        return true;
      }
      return rText.length() > lText.length()
             && rText.endsWith(lText)
             && rText.charAt(rText.length() - lText.length() - 1) == '.';
    }
    return isClassAssignable(leftResult, rightResult, allowUncheckedConversion, left.getResolveScope(), capture);
  }
private static boolean isAssignable(@NotNull PsiType left,
                                      @NotNull PsiType right,
                                      boolean allowUncheckedConversion,
                                      boolean capture)