@Override
  public PsiType[] guessContainerElementType(PsiExpression containerExpr, TextRange rangeToIgnore) {
    HashSet<PsiType> typesSet = new HashSet<>();

    PsiType type = containerExpr.getType();
    PsiType elemType;
    if ((elemType = getGenericElementType(type)) != null) return new PsiType[] {elemType};

    if (containerExpr instanceof PsiReferenceExpression) {
      PsiElement refElement = ((PsiReferenceExpression) containerExpr).resolve();
      if (refElement instanceof PsiVariable) {

        PsiFile file = refElement.getContainingFile();
        if (file == null) {
          file = containerExpr.getContainingFile(); // implicit variable in jsp
        }
        HashSet<PsiVariable> checkedVariables = new HashSet<>();
        addTypesByVariable(
            typesSet,
            (PsiVariable) refElement,
            file,
            checkedVariables,
            CHECK_USAGE | CHECK_DOWN,
            rangeToIgnore);
        checkedVariables.clear();
        addTypesByVariable(
            typesSet, (PsiVariable) refElement, file, checkedVariables, CHECK_UP, rangeToIgnore);
      }
    }

    return typesSet.toArray(PsiType.createArray(typesSet.size()));
  }
  private int substituteToTypeParameters(
      PsiTypeElement typeElement,
      PsiTypeElement inplaceTypeElement,
      PsiType[] paramVals,
      PsiTypeParameter[] params,
      TemplateBuilder builder,
      PsiSubstitutor rawingSubstitutor,
      boolean toplevel) {
    PsiType type = inplaceTypeElement.getType();
    List<PsiType> types = new ArrayList<>();
    for (int i = 0; i < paramVals.length; i++) {
      PsiType val = paramVals[i];
      if (val == null) return SUBSTITUTED_NONE;
      if (type.equals(val)) {
        types.add(myFactory.createType(params[i]));
      }
    }

    if (!types.isEmpty()) {
      Project project = typeElement.getProject();
      PsiType substituted = rawingSubstitutor.substitute(type);
      if (!CommonClassNames.JAVA_LANG_OBJECT.equals(substituted.getCanonicalText())
          && (toplevel || substituted.equals(type))) {
        types.add(substituted);
      }

      builder.replaceElement(
          typeElement,
          new TypeExpression(project, types.toArray(PsiType.createArray(types.size()))));
      return toplevel ? SUBSTITUTED_IN_REF : SUBSTITUTED_IN_PARAMETERS;
    }

    boolean substituted = false;
    PsiJavaCodeReferenceElement ref = typeElement.getInnermostComponentReferenceElement();
    PsiJavaCodeReferenceElement inplaceRef =
        inplaceTypeElement.getInnermostComponentReferenceElement();
    if (ref != null) {
      LOG.assertTrue(inplaceRef != null);
      PsiTypeElement[] innerTypeElements = ref.getParameterList().getTypeParameterElements();
      PsiTypeElement[] inplaceInnerTypeElements =
          inplaceRef.getParameterList().getTypeParameterElements();
      for (int i = 0; i < innerTypeElements.length; i++) {
        substituted |=
            substituteToTypeParameters(
                    innerTypeElements[i],
                    inplaceInnerTypeElements[i],
                    paramVals,
                    params,
                    builder,
                    rawingSubstitutor,
                    false)
                != SUBSTITUTED_NONE;
      }
    }

    return substituted ? SUBSTITUTED_IN_PARAMETERS : SUBSTITUTED_NONE;
  }
 @NotNull
 private static PsiType[] typesAtSite(
     @NotNull PsiType[] types1, @NotNull PsiSubstitutor siteSubstitutor1) {
   final PsiType[] types = PsiType.createArray(types1.length);
   for (int i = 0; i < types1.length; i++) {
     types[i] = siteSubstitutor1.substitute(types1[i]);
   }
   return types;
 }
 @NotNull
 public static PsiType[] typesByTypeElements(@NotNull PsiTypeElement[] typeElements) {
   PsiType[] types = PsiType.createArray(typeElements.length);
   for (int i = 0; i < types.length; i++) {
     types[i] = typeElements[i].getType();
   }
   if (types.length == 1 && types[0] instanceof PsiDiamondType) {
     return ((PsiDiamondType) types[0]).resolveInferredTypes().getTypes();
   }
   return types;
 }
  @Override
  public PsiType[] guessTypeToCast(
      PsiExpression expr) { // TODO : make better guess based on control flow
    LinkedHashSet<PsiType> types = new LinkedHashSet<>();

    ContainerUtil.addIfNotNull(types, getControlFlowExpressionType(expr));
    addExprTypesWhenContainerElement(types, expr);
    addExprTypesByDerivedClasses(types, expr);

    return types.toArray(PsiType.createArray(types.size()));
  }
 @NotNull
 @Override
 public PsiType[] getTypeArguments() {
   PsiType[] cachedTypes = myTypeParametersCachedTypes;
   if (cachedTypes == null) {
     cachedTypes = PsiType.createArray(myTypeParameters.length);
     for (int i = 0; i < cachedTypes.length; i++) {
       cachedTypes[cachedTypes.length - i - 1] = myTypeParameters[i].getType();
     }
     myTypeParametersCachedTypes = cachedTypes;
   }
   return cachedTypes;
 }
  public static PsiSubstitutor substituteByParameterName(
      final PsiClass psiClass, final PsiSubstitutor parentSubstitutor) {

    final Map<PsiTypeParameter, PsiType> substitutionMap = parentSubstitutor.getSubstitutionMap();
    final List<PsiType> result = new ArrayList<PsiType>(substitutionMap.size());
    for (PsiTypeParameter typeParameter : psiClass.getTypeParameters()) {
      final String name = typeParameter.getName();
      final PsiTypeParameter key =
          ContainerUtil.find(
              substitutionMap.keySet(),
              new Condition<PsiTypeParameter>() {
                @Override
                public boolean value(final PsiTypeParameter psiTypeParameter) {
                  return name.equals(psiTypeParameter.getName());
                }
              });
      if (key != null) {
        result.add(substitutionMap.get(key));
      }
    }
    return PsiSubstitutor.EMPTY.putAll(
        psiClass, result.toArray(PsiType.createArray(result.size())));
  }
  // method1 has more general parameter types thn method2
  private boolean secondMethodIsPreferable(
      @NotNull PsiMethod method1,
      @NotNull PsiSubstitutor substitutor1,
      @Nullable PsiElement resolveContext1,
      @NotNull PsiMethod method2,
      @NotNull PsiSubstitutor substitutor2,
      @Nullable PsiElement resolveContext2) {
    if (!method1.getName().equals(method2.getName())) return false;

    final Boolean custom =
        GrMethodComparator.checkDominated(method1, substitutor1, method2, substitutor2, this);
    if (custom != null) return custom;

    PsiType[] argTypes = myArgumentTypes;
    if (method1 instanceof GrGdkMethod && method2 instanceof GrGdkMethod) {
      method1 = ((GrGdkMethod) method1).getStaticMethod();
      method2 = ((GrGdkMethod) method2).getStaticMethod();
      if (myArgumentTypes != null) {
        argTypes = PsiType.createArray(argTypes.length + 1);
        System.arraycopy(myArgumentTypes, 0, argTypes, 1, myArgumentTypes.length);
        argTypes[0] = myThisType;
      }
    }

    if (myIsConstructor && argTypes != null && argTypes.length == 1) {
      if (method1.getParameterList().getParametersCount() == 0) return true;
      if (method2.getParameterList().getParametersCount() == 0) return false;
    }

    PsiParameter[] params1 = method1.getParameterList().getParameters();
    PsiParameter[] params2 = method2.getParameterList().getParameters();
    if (argTypes == null && params1.length != params2.length) return false;

    if (params1.length < params2.length) {
      if (params1.length == 0) return false;
      final PsiType lastType = params1[params1.length - 1].getType(); // varargs applicability
      return lastType instanceof PsiArrayType;
    }

    for (int i = 0; i < params2.length; i++) {
      final PsiType ptype1 = params1[i].getType();
      final PsiType ptype2 = params2[i].getType();
      PsiType type1 = substitutor1.substitute(ptype1);
      PsiType type2 = substitutor2.substitute(ptype2);

      if (argTypes != null && argTypes.length > i) {
        PsiType argType = argTypes[i];
        if (argType != null) {
          final boolean converts1 =
              TypesUtil.isAssignableWithoutConversions(
                  TypeConversionUtil.erasure(type1), argType, myPlace);
          final boolean converts2 =
              TypesUtil.isAssignableWithoutConversions(
                  TypeConversionUtil.erasure(type2), argType, myPlace);
          if (converts1 != converts2) {
            return converts2;
          }

          // see groovy.lang.GroovyCallable
          if (TypesUtil.resolvesTo(type1, CommonClassNames.JAVA_UTIL_CONCURRENT_CALLABLE)
              && TypesUtil.resolvesTo(type2, CommonClassNames.JAVA_LANG_RUNNABLE)) {
            if (InheritanceUtil.isInheritor(
                argType, GroovyCommonClassNames.GROOVY_LANG_GROOVY_CALLABLE)) return true;
          }
        }
      }

      if (!typesAgree(TypeConversionUtil.erasure(ptype1), TypeConversionUtil.erasure(ptype2)))
        return false;

      if (resolveContext1 != null && resolveContext2 == null) {
        return !(TypesUtil.resolvesTo(type1, CommonClassNames.JAVA_LANG_OBJECT)
            && TypesUtil.resolvesTo(type2, CommonClassNames.JAVA_LANG_OBJECT));
      }

      if (resolveContext1 == null && resolveContext2 != null) {
        return true;
      }
    }

    if (!(method1 instanceof SyntheticElement) && !(method2 instanceof SyntheticElement)) {
      final PsiType returnType1 = substitutor1.substitute(method1.getReturnType());
      final PsiType returnType2 = substitutor2.substitute(method2.getReturnType());

      if (!TypesUtil.isAssignableWithoutConversions(returnType1, returnType2, myPlace)
          && TypesUtil.isAssignableWithoutConversions(returnType2, returnType1, myPlace)) {
        return false;
      }
    }

    return true;
  }
  @NotNull
  private static PsiType getLeastUpperBound(
      PsiType type1, PsiType type2, Set<Pair<PsiType, PsiType>> compared, PsiManager manager) {
    if (type1 instanceof PsiCapturedWildcardType) {
      return getLeastUpperBound(
          ((PsiCapturedWildcardType) type1).getUpperBound(), type2, compared, manager);
    }
    if (type2 instanceof PsiCapturedWildcardType) {
      return getLeastUpperBound(
          type1, ((PsiCapturedWildcardType) type2).getUpperBound(), compared, manager);
    }

    if (type1 instanceof PsiWildcardType) {
      return getLeastUpperBound(
          ((PsiWildcardType) type1).getExtendsBound(), type2, compared, manager);
    }
    if (type2 instanceof PsiWildcardType) {
      return getLeastUpperBound(
          type1, ((PsiWildcardType) type2).getExtendsBound(), compared, manager);
    }

    if (type1 instanceof PsiArrayType && type2 instanceof PsiArrayType) {
      final PsiType componentType1 = ((PsiArrayType) type1).getComponentType();
      final PsiType componentType2 = ((PsiArrayType) type2).getComponentType();
      final PsiType componentType =
          getLeastUpperBound(componentType1, componentType2, compared, manager);
      if (componentType1 instanceof PsiPrimitiveType
          && componentType2 instanceof PsiPrimitiveType
          && componentType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
        final PsiElementFactory factory =
            JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
        final GlobalSearchScope resolveScope = GlobalSearchScope.allScope(manager.getProject());
        final PsiClassType cloneable =
            factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_CLONEABLE, resolveScope);
        final PsiClassType serializable =
            factory.createTypeByFQClassName(CommonClassNames.JAVA_IO_SERIALIZABLE, resolveScope);
        return PsiIntersectionType.createIntersection(componentType, cloneable, serializable);
      }
      return componentType.createArrayType();
    }
    if (type1 instanceof PsiIntersectionType) {
      Set<PsiType> newConjuncts = new LinkedHashSet<PsiType>();
      final PsiType[] conjuncts = ((PsiIntersectionType) type1).getConjuncts();
      for (PsiType type : conjuncts) {
        newConjuncts.add(getLeastUpperBound(type, type2, compared, manager));
      }
      return PsiIntersectionType.createIntersection(
          newConjuncts.toArray(PsiType.createArray(newConjuncts.size())));
    }
    if (type2 instanceof PsiIntersectionType) {
      return getLeastUpperBound(type2, type1, compared, manager);
    }
    if (type1 instanceof PsiClassType && type2 instanceof PsiClassType) {
      PsiClassType.ClassResolveResult classResolveResult1 =
          ((PsiClassType) type1).resolveGenerics();
      PsiClassType.ClassResolveResult classResolveResult2 =
          ((PsiClassType) type2).resolveGenerics();
      PsiClass aClass = classResolveResult1.getElement();
      PsiClass bClass = classResolveResult2.getElement();
      if (aClass == null || bClass == null) {
        return PsiType.getJavaLangObject(manager, GlobalSearchScope.allScope(manager.getProject()));
      }

      PsiClass[] supers = getLeastUpperClasses(aClass, bClass);
      if (supers.length == 0) {
        return PsiType.getJavaLangObject(manager, aClass.getResolveScope());
      }

      PsiClassType[] conjuncts = new PsiClassType[supers.length];
      for (int i = 0; i < supers.length; i++) {
        PsiClass aSuper = supers[i];
        PsiSubstitutor subst1 =
            TypeConversionUtil.getSuperClassSubstitutor(
                aSuper, aClass, classResolveResult1.getSubstitutor());
        PsiSubstitutor subst2 =
            TypeConversionUtil.getSuperClassSubstitutor(
                aSuper, bClass, classResolveResult2.getSubstitutor());
        PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;
        for (PsiTypeParameter parameter : PsiUtil.typeParametersIterable(aSuper)) {
          PsiType mapping1 = subst1.substitute(parameter);
          PsiType mapping2 = subst2.substitute(parameter);

          if (mapping1 != null && mapping2 != null) {
            substitutor =
                substitutor.put(
                    parameter,
                    getLeastContainingTypeArgument(
                        mapping1,
                        mapping2,
                        compared,
                        manager,
                        type1.equals(mapping1) && type2.equals(mapping2) ? aSuper : null,
                        parameter));
          } else {
            substitutor = substitutor.put(parameter, null);
          }
        }

        conjuncts[i] =
            JavaPsiFacade.getInstance(manager.getProject())
                .getElementFactory()
                .createType(aSuper, substitutor);
      }

      return PsiIntersectionType.createIntersection(conjuncts);
    }
    if (type2 instanceof PsiArrayType && !(type1 instanceof PsiArrayType)) {
      return getLeastUpperBound(type2, type1, compared, manager);
    }
    if (type1 instanceof PsiArrayType) {
      PsiElementFactory factory =
          JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
      GlobalSearchScope all = GlobalSearchScope.allScope(manager.getProject());
      PsiClassType serializable =
          factory.createTypeByFQClassName(CommonClassNames.JAVA_IO_SERIALIZABLE, all);
      PsiClassType cloneable =
          factory.createTypeByFQClassName(CommonClassNames.JAVA_LANG_CLONEABLE, all);
      PsiType arraySupers = PsiIntersectionType.createIntersection(serializable, cloneable);
      return getLeastUpperBound(arraySupers, type2, compared, manager);
    }

    return PsiType.getJavaLangObject(manager, GlobalSearchScope.allScope(manager.getProject()));
  }
  public void setupTypeElement(
      PsiTypeElement typeElement,
      ExpectedTypeInfo[] infos,
      PsiSubstitutor substitutor,
      TemplateBuilder builder,
      @Nullable PsiElement context,
      PsiClass targetClass) {
    LOG.assertTrue(typeElement.isValid());
    ApplicationManager.getApplication().assertWriteAccessAllowed();

    PsiManager manager = typeElement.getManager();
    GlobalSearchScope scope = typeElement.getResolveScope();
    Project project = manager.getProject();

    if (infos.length == 1 && substitutor != null && substitutor != PsiSubstitutor.EMPTY) {
      ExpectedTypeInfo info = infos[0];
      Map<PsiTypeParameter, PsiType> map = substitutor.getSubstitutionMap();
      PsiType[] vals = map.values().toArray(PsiType.createArray(map.size()));
      PsiTypeParameter[] params = map.keySet().toArray(new PsiTypeParameter[map.size()]);

      List<PsiType> types = matchingTypeParameters(vals, params, info);
      if (!types.isEmpty()) {
        ContainerUtil.addAll(
            types,
            ExpectedTypesProvider.processExpectedTypes(
                infos, new MyTypeVisitor(manager, scope), project));
        builder.replaceElement(
            typeElement,
            new TypeExpression(project, types.toArray(PsiType.createArray(types.size()))));
        return;
      } else {
        PsiElementFactory factory =
            JavaPsiFacade.getInstance(manager.getProject()).getElementFactory();
        PsiType type = info.getType();
        PsiType defaultType = info.getDefaultType();
        try {
          PsiTypeElement inplaceTypeElement =
              ((PsiVariable)
                      factory.createVariableDeclarationStatement("foo", type, null)
                          .getDeclaredElements()[0])
                  .getTypeElement();

          PsiSubstitutor rawingSubstitutor = getRawingSubstitutor(context, targetClass);
          int substitionResult =
              substituteToTypeParameters(
                  typeElement, inplaceTypeElement, vals, params, builder, rawingSubstitutor, true);
          if (substitionResult != SUBSTITUTED_NONE) {
            if (substitionResult == SUBSTITUTED_IN_PARAMETERS) {
              PsiJavaCodeReferenceElement refElement =
                  typeElement.getInnermostComponentReferenceElement();
              LOG.assertTrue(refElement != null && refElement.getReferenceNameElement() != null);
              type = getComponentType(type);
              LOG.assertTrue(type != null);
              defaultType = getComponentType(defaultType);
              LOG.assertTrue(defaultType != null);
              ExpectedTypeInfo info1 =
                  ExpectedTypesProvider.createInfo(
                      ((PsiClassType) defaultType).rawType(),
                      ExpectedTypeInfo.TYPE_STRICTLY,
                      ((PsiClassType) defaultType).rawType(),
                      info.getTailType());
              MyTypeVisitor visitor = new MyTypeVisitor(manager, scope);
              builder.replaceElement(
                  refElement.getReferenceNameElement(),
                  new TypeExpression(
                      project,
                      ExpectedTypesProvider.processExpectedTypes(
                          new ExpectedTypeInfo[] {info1}, visitor, project)));
            }

            return;
          }
        } catch (IncorrectOperationException e) {
          LOG.error(e);
        }
      }
    }

    PsiType[] types =
        infos.length == 0
            ? new PsiType[] {typeElement.getType()}
            : ExpectedTypesProvider.processExpectedTypes(
                infos, new MyTypeVisitor(manager, scope), project);
    builder.replaceElement(typeElement, new TypeExpression(project, types));
  }
  private Specifics isMoreSpecific(
      @NotNull MethodCandidateInfo info1,
      @NotNull MethodCandidateInfo info2,
      @MethodCandidateInfo.ApplicabilityLevelConstant int applicabilityLevel,
      @NotNull LanguageLevel languageLevel) {
    PsiMethod method1 = info1.getElement();
    PsiMethod method2 = info2.getElement();
    final PsiClass class1 = method1.getContainingClass();
    final PsiClass class2 = method2.getContainingClass();

    final PsiParameter[] params1 = method1.getParameterList().getParameters();
    final PsiParameter[] params2 = method2.getParameterList().getParameters();

    final PsiTypeParameter[] typeParameters1 = method1.getTypeParameters();
    final PsiTypeParameter[] typeParameters2 = method2.getTypeParameters();
    final PsiSubstitutor classSubstitutor1 =
        info1.getSubstitutor(false); // substitutions for method type parameters will be ignored
    final PsiSubstitutor classSubstitutor2 = info2.getSubstitutor(false);

    final int max = Math.max(params1.length, params2.length);
    PsiType[] types1 = PsiType.createArray(max);
    PsiType[] types2 = PsiType.createArray(max);
    final boolean varargsPosition =
        applicabilityLevel == MethodCandidateInfo.ApplicabilityLevel.VARARGS;
    for (int i = 0; i < max; i++) {
      ProgressManager.checkCanceled();
      PsiType type1 =
          params1.length > 0 ? params1[Math.min(i, params1.length - 1)].getType() : null;
      PsiType type2 =
          params2.length > 0 ? params2[Math.min(i, params2.length - 1)].getType() : null;
      if (varargsPosition) {
        if (type1 instanceof PsiEllipsisType
            && type2 instanceof PsiEllipsisType
            && params1.length == params2.length
            && class1 != null
            && (!JavaVersionService.getInstance().isAtLeast(class1, JavaSdkVersion.JDK_1_7)
                || ((PsiArrayType) type1)
                    .getComponentType()
                    .equalsToText(CommonClassNames.JAVA_LANG_OBJECT)
                || ((PsiArrayType) type2)
                    .getComponentType()
                    .equalsToText(CommonClassNames.JAVA_LANG_OBJECT))) {
          type1 = ((PsiEllipsisType) type1).toArrayType();
          type2 = ((PsiEllipsisType) type2).toArrayType();
        } else {
          type1 =
              type1 instanceof PsiEllipsisType ? ((PsiArrayType) type1).getComponentType() : type1;
          type2 =
              type2 instanceof PsiEllipsisType ? ((PsiArrayType) type2).getComponentType() : type2;
        }
      }

      types1[i] = type1;
      types2[i] = type2;
    }

    boolean sameBoxing = true;
    int[] boxingHappened = new int[2];
    for (int i = 0; i < types1.length; i++) {
      ProgressManager.checkCanceled();
      PsiType type1 = classSubstitutor1.substitute(types1[i]);
      PsiType type2 = classSubstitutor2.substitute(types2[i]);
      PsiType argType = i < getActualParameterTypes().length ? getActualParameterTypes()[i] : null;

      boolean boxingInFirst = false;
      if (isBoxingHappened(argType, type1, languageLevel)) {
        boxingHappened[0] += 1;
        boxingInFirst = true;
      }

      boolean boxingInSecond = false;
      if (isBoxingHappened(argType, type2, languageLevel)) {
        boxingHappened[1] += 1;
        boxingInSecond = true;
      }
      sameBoxing &= boxingInFirst == boxingInSecond;
    }
    if (boxingHappened[0] == 0 && boxingHappened[1] > 0) return Specifics.FIRST;
    if (boxingHappened[0] > 0 && boxingHappened[1] == 0) return Specifics.SECOND;

    if (sameBoxing) {
      final PsiSubstitutor siteSubstitutor1 = info1.getSiteSubstitutor();
      final PsiSubstitutor siteSubstitutor2 = info2.getSiteSubstitutor();

      final PsiType[] types2AtSite = typesAtSite(types2, siteSubstitutor2);
      final PsiType[] types1AtSite = typesAtSite(types1, siteSubstitutor1);

      final PsiSubstitutor methodSubstitutor1 =
          calculateMethodSubstitutor(
              typeParameters1, method1, siteSubstitutor1, types1, types2AtSite, languageLevel);
      boolean applicable12 =
          isApplicableTo(
              types2AtSite, method1, languageLevel, varargsPosition, methodSubstitutor1, method2);

      final PsiSubstitutor methodSubstitutor2 =
          calculateMethodSubstitutor(
              typeParameters2, method2, siteSubstitutor2, types2, types1AtSite, languageLevel);
      boolean applicable21 =
          isApplicableTo(
              types1AtSite, method2, languageLevel, varargsPosition, methodSubstitutor2, method1);

      if (!myLanguageLevel.isAtLeast(LanguageLevel.JDK_1_8)) {
        final boolean typeArgsApplicable12 =
            GenericsUtil.isTypeArgumentsApplicable(
                typeParameters1, methodSubstitutor1, myArgumentsList, !applicable21);
        final boolean typeArgsApplicable21 =
            GenericsUtil.isTypeArgumentsApplicable(
                typeParameters2, methodSubstitutor2, myArgumentsList, !applicable12);

        if (!typeArgsApplicable12) {
          applicable12 = false;
        }

        if (!typeArgsApplicable21) {
          applicable21 = false;
        }
      }

      if (applicable12 || applicable21) {

        if (applicable12 && !applicable21) return Specifics.SECOND;
        if (applicable21 && !applicable12) return Specifics.FIRST;

        final boolean abstract1 = method1.hasModifierProperty(PsiModifier.ABSTRACT);
        final boolean abstract2 = method2.hasModifierProperty(PsiModifier.ABSTRACT);
        if (abstract1 && !abstract2) {
          return Specifics.SECOND;
        }
        if (abstract2 && !abstract1) {
          return Specifics.FIRST;
        }
      }

      if (languageLevel.isAtLeast(LanguageLevel.JDK_1_8)
          && myArgumentsList instanceof PsiExpressionList
          && (typeParameters1.length == 0 || typeParameters2.length == 0)) {
        boolean toCompareFunctional = false;
        if (types1.length > 0 && types2.length > 0) {
          for (int i = 0; i < getActualParametersLength(); i++) {
            final PsiType type1 = types1[Math.min(i, types1.length - 1)];
            final PsiType type2 = types2[Math.min(i, types2.length - 1)];
            // from 15.12.2.5 Choosing the Most Specific Method
            // In addition, a functional interface type S is more specific than a functional
            // interface type T for an expression exp
            // if T is not a subtype of S and one of the following conditions apply.
            if (LambdaUtil.isFunctionalType(type1)
                && !TypeConversionUtil.erasure(type1).isAssignableFrom(type2)
                && LambdaUtil.isFunctionalType(type2)
                && !TypeConversionUtil.erasure(type2).isAssignableFrom(type1)) {
              types1AtSite[Math.min(i, types1.length - 1)] = PsiType.NULL;
              types2AtSite[Math.min(i, types2.length - 1)] = PsiType.NULL;
              toCompareFunctional = true;
            }
          }
        }

        if (toCompareFunctional) {
          final boolean applicable12ignoreFunctionalType =
              isApplicableTo(
                  types2AtSite,
                  method1,
                  languageLevel,
                  varargsPosition,
                  calculateMethodSubstitutor(
                      typeParameters1,
                      method1,
                      siteSubstitutor1,
                      types1,
                      types2AtSite,
                      languageLevel),
                  null);
          final boolean applicable21ignoreFunctionalType =
              isApplicableTo(
                  types1AtSite,
                  method2,
                  languageLevel,
                  varargsPosition,
                  calculateMethodSubstitutor(
                      typeParameters2,
                      method2,
                      siteSubstitutor2,
                      types2,
                      types1AtSite,
                      languageLevel),
                  null);

          if (applicable12ignoreFunctionalType || applicable21ignoreFunctionalType) {
            Specifics specifics = null;
            for (int i = 0; i < getActualParametersLength(); i++) {
              if (types1AtSite[Math.min(i, types1.length - 1)] == PsiType.NULL
                  && types2AtSite[Math.min(i, types2.length - 1)] == PsiType.NULL) {
                Specifics specific =
                    isFunctionalTypeMoreSpecific(
                        info1, info2, ((PsiExpressionList) myArgumentsList).getExpressions()[i], i);
                if (specific == Specifics.NEITHER) {
                  specifics = Specifics.NEITHER;
                  break;
                }

                if (specifics == null) {
                  specifics = specific;
                } else if (specifics != specific) {
                  specifics = Specifics.NEITHER;
                  break;
                }
              }
            }

            if (!applicable12ignoreFunctionalType && applicable21ignoreFunctionalType) {
              return specifics == Specifics.FIRST ? Specifics.FIRST : Specifics.NEITHER;
            }

            if (!applicable21ignoreFunctionalType && applicable12ignoreFunctionalType) {
              return specifics == Specifics.SECOND ? Specifics.SECOND : Specifics.NEITHER;
            }

            return specifics;
          }
        }
      }
    } else if (varargsPosition) {
      final PsiType lastParamType1 = classSubstitutor1.substitute(types1[types1.length - 1]);
      final PsiType lastParamType2 = classSubstitutor2.substitute(types2[types1.length - 1]);
      final boolean assignable1 = TypeConversionUtil.isAssignable(lastParamType2, lastParamType1);
      final boolean assignable2 = TypeConversionUtil.isAssignable(lastParamType1, lastParamType2);
      if (assignable1 && !assignable2) {
        return Specifics.FIRST;
      }
      if (assignable2 && !assignable1) {
        return Specifics.SECOND;
      }
    }

    if (class1 != class2) {
      if (class2.isInheritor(class1, true) || class1.isInterface() && !class2.isInterface()) {
        if (MethodSignatureUtil.isSubsignature(
            method1.getSignature(info1.getSubstitutor(false)),
            method2.getSignature(info2.getSubstitutor(false)))) {
          return Specifics.SECOND;
        } else if (method1.hasModifierProperty(PsiModifier.STATIC)
            && method2.hasModifierProperty(PsiModifier.STATIC)
            && boxingHappened[0] == 0) {
          return Specifics.SECOND;
        }
      } else if (MethodSignatureUtil.areErasedParametersEqual(
              method1.getSignature(PsiSubstitutor.EMPTY),
              method2.getSignature(PsiSubstitutor.EMPTY))
          && MethodSignatureUtil.isSubsignature(
              method2.getSignature(info2.getSubstitutor(false)),
              method1.getSignature(info1.getSubstitutor(false)))) {
        return Specifics.FIRST;
      } else if (class1.isInheritor(class2, true) || class2.isInterface()) {
        if (method1.hasModifierProperty(PsiModifier.STATIC)
            && method2.hasModifierProperty(PsiModifier.STATIC)
            && boxingHappened[0] == 0) {
          return Specifics.FIRST;
        }
      }
    }

    final boolean raw1 = PsiUtil.isRawSubstitutor(method1, classSubstitutor1);
    final boolean raw2 = PsiUtil.isRawSubstitutor(method2, classSubstitutor2);
    if (raw1 ^ raw2) {
      return raw1 ? Specifics.SECOND : Specifics.FIRST;
    }

    final boolean varargs1 = info1.isVarargs();
    final boolean varargs2 = info2.isVarargs();
    if (varargs1 ^ varargs2) {
      return varargs1 ? Specifics.SECOND : Specifics.FIRST;
    }

    return Specifics.NEITHER;
  }