// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.psi.util;

import com.intellij.lang.jvm.types.JvmPrimitiveTypeKind;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.roots.ProjectRootModificationTracker;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.pom.java.JavaFeature;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.ObjectIntHashMap;
import com.intellij.util.containers.ObjectIntMap;
import org.intellij.lang.annotations.MagicConstant;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

import static com.intellij.psi.CommonClassNames.*;

public final class TypeConversionUtil {
  private static final Logger LOG = Logger.getInstance(TypeConversionUtil.class);

  private static final boolean[][] IS_ASSIGNABLE_BIT_SET = {
    {true, true, false, true, true, true, true}, // byte
    {false, true, false, true, true, true, true}, // short
    {false, false, true, true, true, true, true}, // char
    {false, false, false, true, true, true, true}, // int
    {false, false, false, false, true, true, true}, // long
    {false, false, false, false, false, true, true}, // float
    {false, false, false, false, false, false, true}, // double
  };

  private static final ObjectIntMap<PsiType> TYPE_TO_RANK_MAP = new ObjectIntHashMap<>();

  @MagicConstant(intValues = {BYTE_RANK, SHORT_RANK, CHAR_RANK, INT_RANK, FLOAT_RANK, DOUBLE_RANK, BOOL_RANK, STRING_RANK, UNKNOWN_RANK})
  @interface TypeRank {
  }
  public static final int BYTE_RANK = 1;
  public static final int SHORT_RANK = 2;
  public static final int CHAR_RANK = 3;
  public static final int INT_RANK = 4;
  public static final int LONG_RANK = 5;
  public static final int FLOAT_RANK = 6;
  public static final int DOUBLE_RANK = 7;
  private static final int BOOL_RANK = 10;
  private static final int STRING_RANK = 100;
  private static final int UNKNOWN_RANK = 1000;
  @TypeRank
  private static final int MAX_NUMERIC_RANK = DOUBLE_RANK;

  /**
   * This is extracted to a separate field as temporary work around a deadlock during class initialization (IDEA-309438).
   * Deadlock won't happen if PsiType class is initialized before initializing its inheritor PsiEllipsisType.
   */
  private static final PsiPrimitiveType NULL_TYPE_ACCESS = (PsiPrimitiveType)PsiTypes.nullType();
  
  public static final PsiType NULL_TYPE = new PsiEllipsisType(NULL_TYPE_ACCESS) {
    @Override
    public boolean isValid() {
      return true;
    }

    @Override
    public @NotNull String getPresentableText(boolean annotated) {
      return "FAKE TYPE";
    }
  };
  private static final Key<PsiElement> ORIGINAL_CONTEXT = Key.create("ORIGINAL_CONTEXT");
  private static final Key<PsiType> LOWER_BOUND = Key.create("LowBound");
  private static final Key<PsiType> UPPER_BOUND = Key.create("UpperBound");

  static {
    TYPE_TO_RANK_MAP.put(PsiTypes.byteType(), BYTE_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.shortType(), SHORT_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.charType(), CHAR_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.intType(), INT_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.longType(), LONG_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.floatType(), FLOAT_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.doubleType(), DOUBLE_RANK);
    TYPE_TO_RANK_MAP.put(PsiTypes.booleanType(), BOOL_RANK);
  }

  private TypeConversionUtil() { }

  /**
   * @return true if fromType can be casted to toType
   */
  public static boolean areTypesConvertible(@NotNull PsiType fromType, @NotNull PsiType toType) {
    return areTypesConvertible(fromType, toType, null);
  }

  /**
   * @return true if fromType can be casted to toType
   */
  public static boolean areTypesConvertible(@NotNull PsiType fromType, @NotNull PsiType toType, @Nullable LanguageLevel languageLevel) {
    if (fromType == toType) return true;
    final boolean fromIsPrimitive = isPrimitiveAndNotNull(fromType);
    final boolean toIsPrimitive = isPrimitiveAndNotNull(toType);
    if (fromIsPrimitive || toIsPrimitive) {
      if (isVoidType(fromType) || isVoidType(toType)) return false;
      final int fromTypeRank = getTypeRank(fromType);
      final int toTypeRank = getTypeRank(toType);
      if (!toIsPrimitive) {
        if (fromTypeRank == toTypeRank) return true;
        if (toType instanceof PsiIntersectionType) {
          for (PsiType type : ((PsiIntersectionType)toType).getConjuncts()) {
            if (!areTypesConvertible(fromType, type)) return false;
          }
          return true;
        }
        // JLS 5.5: A value of a primitive type can be cast to a reference type by boxing conversion(see 5.1.7)
        if (!(toType instanceof PsiClassType)) return false;
        PsiClass toClass = ((PsiClassType)toType).resolve();
        if (toClass == null || toClass instanceof PsiTypeParameter) return false;
        PsiClassType boxedType = ((PsiPrimitiveType)fromType).getBoxedType(toClass.getManager(), toType.getResolveScope());
        return boxedType != null && areTypesConvertible(boxedType, toType);
      }
      if (!fromIsPrimitive) {
        // 5.5. Casting Contexts
        if ((fromTypeRank == SHORT_RANK || fromTypeRank == BYTE_RANK) && toTypeRank == CHAR_RANK) return false;

        fromType = uncapture(fromType); //starting from javac 9+
        if (fromType instanceof PsiClassType) {
          if (languageLevel == null) {
            languageLevel = ((PsiClassType)fromType).getLanguageLevel();
          }

          if (languageLevel.isAtLeast(LanguageLevel.JDK_1_7)) {
            final PsiClassType classType = (PsiClassType)fromType;
            final PsiClass psiClass = classType.resolve();
            if (psiClass == null) return false;
            final PsiClassType boxedType = ((PsiPrimitiveType)toType).getBoxedType(psiClass.getManager(), psiClass.getResolveScope());
            if (boxedType != null && isNarrowingReferenceConversionAllowed(fromType, boxedType)) {
              return true;
            }
          }
          //unboxing and widening with type parameters
          if (fromTypeRank == UNKNOWN_RANK &&
              isUnboxable((PsiPrimitiveType)toType, (PsiClassType)fromType, new HashSet<>())) {
            return true;
          }
        }
        return fromTypeRank == toTypeRank ||
               fromTypeRank < toTypeRank && toTypeRank <= MAX_NUMERIC_RANK;
      }
      return fromTypeRank == toTypeRank ||
             fromTypeRank <= MAX_NUMERIC_RANK && toTypeRank <= MAX_NUMERIC_RANK;
    }

    //type can be casted via widening reference conversion
    if (isAssignable(toType, fromType)) return true;

    if (isNullType(fromType) || isNullType(toType)) return true;

    // or narrowing reference conversion
    return isNarrowingReferenceConversionAllowed(fromType, toType);
  }

  /**
   * see JLS 5.1.5, JLS3 5.1.6
   */
  private static boolean isNarrowingReferenceConversionAllowed(@NotNull PsiType fromType, @NotNull PsiType toType) {
    toType = uncapture(toType);
    fromType = uncapture(fromType);

    if (toType instanceof PsiPrimitiveType || fromType instanceof PsiPrimitiveType) return fromType.equals(toType);
    //Done with primitives
    if (toType instanceof PsiDiamondType || fromType instanceof PsiDiamondType) return false;
    if (toType instanceof PsiArrayType && !(fromType instanceof PsiArrayType)) {
      if (fromType instanceof PsiClassType) {
        final PsiClass resolved = ((PsiClassType)fromType).resolve();
        if (resolved instanceof PsiTypeParameter) {
          for (final PsiClassType boundType : resolved.getExtendsListTypes()) {
            if (!isNarrowingReferenceConversionAllowed(boundType, toType)) return false;
          }
          return true;
        }
      }
      return isAssignable(fromType, toType);
    }
    if (fromType instanceof PsiArrayType) {
      if (toType instanceof PsiClassType) {
        final PsiClass resolved = ((PsiClassType)toType).resolve();
        if (resolved instanceof PsiTypeParameter) {
          for (final PsiClassType boundType : resolved.getExtendsListTypes()) {
            if (!areTypesConvertible(fromType, boundType)) return false;
          }
          return true;
        }
      }
      return toType instanceof PsiArrayType
             && isNarrowingReferenceConversionAllowed(((PsiArrayType)fromType).getComponentType(),
                                                      ((PsiArrayType)toType).getComponentType());
    }
    //Done with array types

    if (fromType instanceof PsiIntersectionType) {
      final PsiType[] conjuncts = ((PsiIntersectionType)fromType).getConjuncts();
      for (PsiType conjunct : conjuncts) {
        if (isNarrowingReferenceConversionAllowed(conjunct, toType)) return true;
      }
      return false;
    }
    if (toType instanceof PsiIntersectionType) {
      if (fromType instanceof PsiClassType && ((PsiClassType)fromType).getLanguageLevel().isAtLeast(LanguageLevel.JDK_1_8)) {
        for (PsiType conjunct : ((PsiIntersectionType)toType).getConjuncts()) {
          if (!isNarrowingReferenceConversionAllowed(fromType, conjunct)) return false;
        }
        return true;
      }
      return false;
    }

    if (fromType instanceof PsiDisjunctionType) {
      return isNarrowingReferenceConversionAllowed(((PsiDisjunctionType)fromType).getLeastUpperBound(), toType);
    }
    if (toType instanceof PsiDisjunctionType) {
      return false;
    }

    if (fromType instanceof PsiWildcardType) {
      final PsiWildcardType fromWildcard = (PsiWildcardType)fromType;
      final PsiType bound = fromWildcard.getBound();
      if (bound == null) return true;
      if (fromWildcard.isSuper()) {
        return isAssignable(toType, bound);
      }
      return isNarrowingReferenceConversionAllowed(bound, toType);
    }
    if (toType instanceof PsiWildcardType) {
      final PsiWildcardType toWildcard = (PsiWildcardType)toType;
      if (toWildcard.isSuper()) return false;
      final PsiType bound = toWildcard.getBound();
      return bound == null || isNarrowingReferenceConversionAllowed(fromType, bound);
    }

    if (isAssignable(fromType, toType)) return true;

    if (!(fromType instanceof PsiClassType) || !(toType instanceof PsiClassType)) return false;
    PsiClassType fromClassType = (PsiClassType)fromType;
    PsiClassType toClassType = (PsiClassType)toType;

    PsiClassType.ClassResolveResult fromResult = fromClassType.resolveGenerics();
    final PsiClass fromClass = fromResult.getElement();
    if (fromClass == null) return false;
    if (fromClass instanceof PsiTypeParameter) {
      return isNarrowingReferenceConversionAllowed(obtainSafeSuperType((PsiTypeParameter)fromClass), toType);
    }

    PsiClassType.ClassResolveResult toResult = toClassType.resolveGenerics();
    final PsiClass toClass = toResult.getElement();
    if (toClass == null) return false;
    if (toClass instanceof PsiTypeParameter) {
      return isNarrowingReferenceConversionAllowed(fromType, obtainSafeSuperType((PsiTypeParameter)toClass));
    }
    //Done with type parameters

    PsiManager manager = fromClass.getManager();
    final LanguageLevel languageLevel = toClassType.getLanguageLevel();
    //  jep-397
    if (JavaFeature.SEALED_CLASSES.isSufficient(languageLevel)) {
      if (fromClass.isInterface() || toClass.isInterface()) {
        if (fromClass.hasModifierProperty(PsiModifier.SEALED)) {
          if (!canConvertSealedTo(fromClass, toClass)) return false;
        }
        else if (toClass.hasModifierProperty(PsiModifier.SEALED)) {
          if (!canConvertSealedTo(toClass, fromClass)) return false;
        }
      }
    }
    if (!fromClass.isInterface()) {
      if (toClass.isInterface()) {
        return (!fromClass.hasModifierProperty(PsiModifier.FINAL) || fromClass.isInheritor(toClass, true)) &&
               checkSuperTypesWithDifferentTypeArguments(toResult, fromClass, manager, fromResult.getSubstitutor(), null, languageLevel);
      }
      else {
        if (manager.areElementsEquivalent(fromClass, toClass)) {
          return areSameParameterTypes(fromClassType, toClassType);
        }

        if (toClass.isInheritor(fromClass, true)) {
          return checkSuperTypesWithDifferentTypeArguments(fromResult, toClass, manager, toResult.getSubstitutor(), null, languageLevel);
        }
        if (fromClass.isInheritor(toClass, true)) {
          return checkSuperTypesWithDifferentTypeArguments(toResult, fromClass, manager, fromResult.getSubstitutor(), null, languageLevel);
        }

        return false;
      }
    }
    else if (!toClass.isInterface()) {
      if (!toClass.hasModifierProperty(PsiModifier.FINAL)) {
        return checkSuperTypesWithDifferentTypeArguments(fromResult, toClass, manager, toResult.getSubstitutor(), null, languageLevel);
      }
      else {
        PsiSubstitutor toSubstitutor = getMaybeSuperClassSubstitutor(fromClass, toClass, toResult.getSubstitutor());
        return toSubstitutor != null && areSameArgumentTypes(fromClass, fromResult.getSubstitutor(), toSubstitutor);
      }
    }
    else if (languageLevel.compareTo(LanguageLevel.JDK_1_5) < 0) {
      //In jls2 check for method in both interfaces with the same signature but different return types.
      Collection<HierarchicalMethodSignature> fromClassMethodSignatures = fromClass.getVisibleSignatures();
      Collection<HierarchicalMethodSignature> toClassMethodSignatures = toClass.getVisibleSignatures();

      for (HierarchicalMethodSignature fromMethodSignature : fromClassMethodSignatures) {
        for (HierarchicalMethodSignature toMethodSignature : toClassMethodSignatures) {
          if (fromMethodSignature.equals(toMethodSignature)) {
            final PsiType fromClassReturnType = fromMethodSignature.getMethod().getReturnType();
            final PsiType toClassReturnType = toMethodSignature.getMethod().getReturnType();
            if (fromClassReturnType != null
                && toClassReturnType != null
                && !fromClassReturnType.equals(toClassReturnType)) {
              return false;
            }
          }
        }
      }
      return true;
    }
    else {
      //In jls3 check for super interface with distinct type arguments
      PsiClassType.ClassResolveResult baseResult;
      PsiClass derived;
      PsiSubstitutor derivedSubstitutor;
      if (toClass.isInheritor(fromClass, true)) {
        baseResult = fromResult;
        derived = toClass;
        derivedSubstitutor = toResult.getSubstitutor();
      }
      else {
        baseResult = toResult;
        derived = fromClass;
        derivedSubstitutor = fromResult.getSubstitutor();
      }
      return checkSuperTypesWithDifferentTypeArguments(baseResult, derived, manager, derivedSubstitutor, null, languageLevel);
    }
  }

  /**
   * Check if sealed class can be narrowed down to a given class.
   * Check performed only if sealed class or class that it should be narrowed down to is an interface.
   * 
   * Sealed class can be narrowed down to an interface in one of the following cases:
   * <ul>
   *  <li>sealed class implements interface
   *  <li>sealed class have at least one non-sealed subclass
   *  <li>at least one of final/sealed subclasses of sealed parent implement interface
   * </ul>
   *
   * <p>Note that sealed subclasses are checked recursively, e.g. in hierarchy:</p>
   *
   * <code>
   * <p>sealed class Parent {}
   * <p>sealed class A extends Parent {}
   * <p>final class C extends A {}
   * </code>
   * <p>all classes would be checked.</p>
   * <br>
   * <p>See JEP-397 for more details.</p>
   */
  public static boolean canConvertSealedTo(@NotNull PsiClass sealedClass, @NotNull PsiClass psiClass) {
    LOG.assertTrue(sealedClass.isInterface() || psiClass.isInterface());
    return canConvertSealedTo(sealedClass, psiClass, new HashSet<>());
  }
  
  private static boolean canConvertSealedTo(@NotNull PsiClass sealedClass,
                                            @NotNull PsiClass psiClass,
                                            @NotNull Set<PsiClass> visited) {
    if (visited.contains(sealedClass)) return true;
    visited.add(sealedClass);
    PsiReferenceList permitsList = sealedClass.getPermitsList();
    List<PsiClass> sealedSubClasses = new SmartList<>();
    boolean hasClassInheritors;
    if (permitsList == null) {
      Set<PsiClass> subClasses = findDirectSubClassesInFile(sealedClass);
      hasClassInheritors = subClasses.stream().anyMatch(subClass -> subClassExtendsClass(subClass, psiClass, sealedSubClasses));
    }
    else {
      hasClassInheritors = Arrays.stream(permitsList.getReferencedTypes())
        .map(t -> t.resolve())
        .anyMatch(subClass -> subClassExtendsClass(subClass, psiClass, sealedSubClasses));
    }
    return hasClassInheritors || sealedSubClasses.stream().anyMatch(subClass -> canConvertSealedTo(subClass, psiClass, visited));
  }

  private static @NotNull Set<PsiClass> findDirectSubClassesInFile(@NotNull PsiClass sealedClass) {
    Set<PsiClass> subClasses = new HashSet<>();

    if (sealedClass.isEnum()) {
      for (PsiField field : sealedClass.getFields()) {
        if (field instanceof PsiEnumConstant) {
          ContainerUtil.addIfNotNull(subClasses, ((PsiEnumConstant)field).getInitializingClass());
        }
      }
      return subClasses;
    }

    sealedClass.getContainingFile().accept(new JavaElementVisitor() {
      @Override
      public void visitJavaFile(@NotNull PsiJavaFile file) {
        for (PsiClass psiClass : file.getClasses()) {
          visitClass(psiClass);
        }
      }

      @Override
      public void visitClass(@NotNull PsiClass psiClass) {
        for (PsiClass inner : psiClass.getInnerClasses()) {
          visitClass(inner);
        }
        if (psiClass.isInheritor(sealedClass, false)) {
          subClasses.add(psiClass);
        }
      }
    });
    return subClasses;
  }

  private static boolean subClassExtendsClass(@Nullable PsiClass subClass,
                                              @NotNull PsiClass psiClass,
                                              @NotNull List<PsiClass> sealedClasses) {
    if (subClass == null) return false;
    if (subClass.hasModifierProperty(PsiModifier.SEALED)) {
      sealedClasses.add(subClass);
      return false;
    }
    //DON'T use `hasModifierProperty(PsiModifier.NON_SEALED)`, because compiled classes don't have this modifier
    return !subClass.hasModifierProperty(PsiModifier.FINAL) ||
           InheritanceUtil.isInheritorOrSelf(subClass, psiClass, true);
  }

  private static @NotNull PsiClassType obtainSafeSuperType(@NotNull PsiTypeParameter typeParameter) {
    final PsiClassType superType = typeParameter.getSuperTypes()[0];
    final PsiClassType.ClassResolveResult result = superType.resolveGenerics();
    final PsiClass superClass = result.getElement();
    if (superClass != null) {
      PsiElementFactory factory = JavaPsiFacade.getElementFactory(typeParameter.getProject());
      if (superClass instanceof PsiTypeParameter && InheritanceUtil.getCircularClass(superClass) != null) {
        return factory.createTypeByFQClassName(JAVA_LANG_OBJECT, typeParameter.getResolveScope());
      }
      final PsiSubstitutor substitutor = result.getSubstitutor().put(typeParameter, null);
      return factory.createType(superClass, substitutor);
    }
    return superType;
  }

  private static boolean checkSuperTypesWithDifferentTypeArguments(@NotNull PsiClassType.ClassResolveResult baseResult,
                                                                   @NotNull PsiClass derived,
                                                                   @NotNull PsiManager manager,
                                                                   @NotNull PsiSubstitutor derivedSubstitutor,
                                                                   Set<? super PsiClass> visited,
                                                                   @NotNull LanguageLevel languageLevel) {
    if (visited != null && visited.contains(derived)) return true;

    if (languageLevel.compareTo(LanguageLevel.JDK_1_5) < 0) return true;
    PsiClass base = baseResult.getElement();
    PsiClass[] supers = derived.getSupers();
    if (manager.areElementsEquivalent(base, derived)) {
      derivedSubstitutor = getSuperClassSubstitutor(derived, derived, derivedSubstitutor);
      return areSameArgumentTypes(derived, baseResult.getSubstitutor(), derivedSubstitutor, 1);
    }
    PsiSubstitutor baseSubstitutor = getMaybeSuperClassSubstitutor(derived, base, baseResult.getSubstitutor());
    if (baseSubstitutor != null) {
      derivedSubstitutor = getSuperClassSubstitutor(derived, derived, derivedSubstitutor);
      if (!areSameArgumentTypes(derived, baseSubstitutor, derivedSubstitutor)) return false;
    }

    if (visited == null) {
      visited = new HashSet<>();
    }
    visited.add(derived);
    for (PsiClass aSuper : supers) {
      PsiSubstitutor s = getSuperClassSubstitutor(aSuper, derived, derivedSubstitutor);
      if (!checkSuperTypesWithDifferentTypeArguments(baseResult, aSuper, manager, s, visited, languageLevel)) return false;
    }

    return true;
  }

  private static boolean areSameParameterTypes(@NotNull PsiClassType type1, @NotNull PsiClassType type2) {
    PsiClassType.ClassResolveResult resolveResult1 = type1.resolveGenerics();
    PsiClassType.ClassResolveResult resolveResult2 = type2.resolveGenerics();
    final PsiClass aClass = resolveResult1.getElement();
    final PsiClass bClass = resolveResult2.getElement();
    return aClass != null &&
           bClass != null &&
           aClass.getManager().areElementsEquivalent(aClass, bClass) &&
           areSameArgumentTypes(aClass, resolveResult1.getSubstitutor(), resolveResult2.getSubstitutor(), 1);
  }

  private static boolean areSameArgumentTypes(@NotNull PsiClass aClass,
                                              @NotNull PsiSubstitutor substitutor1,
                                              @NotNull PsiSubstitutor substitutor2) {
    return areSameArgumentTypes(aClass, substitutor1, substitutor2, 0);
  }

  private static boolean areSameArgumentTypes(@NotNull PsiClass aClass,
                                              @NotNull PsiSubstitutor substitutor1,
                                              @NotNull PsiSubstitutor substitutor2,
                                              int level) {
    for (PsiTypeParameter typeParameter : PsiUtil.typeParametersIterable(aClass)) {
      PsiType typeArg1 = substitutor1.substitute(typeParameter);
      PsiType typeArg2 = substitutor2.substitute(typeParameter);
      if (typeArg1 == null || typeArg2 == null) return true;
      if (TypesDistinctProver.provablyDistinct(typeArg1, typeArg2, level)) return false;
    }

    return true;
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isPrimitiveAndNotNull(PsiType type) {
    return type instanceof PsiPrimitiveType && !isNullType(type);
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isEnumType(PsiType type) {
    type = uncapture(type);
    if (type instanceof PsiClassType) {
      final PsiClass psiClass = ((PsiClassType)type).resolve();
      if (psiClass instanceof PsiTypeParameter) {
        return InheritanceUtil.isInheritor(psiClass, true, JAVA_LANG_ENUM);
      }
      return psiClass != null && psiClass.isEnum();
    }
    return false;
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isNullType(PsiType type) {
    return PsiTypes.nullType().equals(type);
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isFloatOrDoubleType(@Nullable PsiType type) {
    return isFloatType(type) || isDoubleType(type);
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isDoubleType(@Nullable PsiType type) {
    type = uncapture(type);
    return PsiTypes.doubleType().equals(type) || PsiTypes.doubleType().equals(PsiPrimitiveType.getUnboxedType(type));
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isFloatType(@Nullable PsiType type) {
    type = uncapture(type);
    return PsiTypes.floatType().equals(type) || PsiTypes.floatType().equals(PsiPrimitiveType.getUnboxedType(type));
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isLongType(@Nullable PsiType type) {
    type = uncapture(type);
    return PsiTypes.longType().equals(type) || PsiTypes.longType().equals(PsiPrimitiveType.getUnboxedType(type));
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isVoidType(@Nullable PsiType type) {
    return PsiTypes.voidType().equals(type);
  }

  @Contract(value = "null -> false", pure = true)
  public static boolean isBooleanType(@Nullable PsiType type) {
    type = uncapture(type);
    return PsiTypes.booleanType().equals(type) || PsiTypes.booleanType().equals(PsiPrimitiveType.getUnboxedType(type));
  }

  @Contract(value = "null -> null", pure = true)
  private static PsiType uncapture(PsiType type) {
    while (type instanceof PsiCapturedWildcardType) {
      type = ((PsiCapturedWildcardType)type).getUpperBound();
    }
    return type;
  }

  @Contract(pure = true)
  public static boolean isNumericType(@TypeRank int typeRank) {
    return typeRank <= MAX_NUMERIC_RANK;
  }
  @Contract(pure = true)
  public static boolean isNumericType(PsiType type) {
    return type != null && isNumericType(getTypeRank(type));
  }
  @Contract(pure = true)
  public static boolean isIntegralNumberType(PsiType type) {
    return type != null && getTypeRank(type) <= LONG_RANK;
  }

  /**
   * @return 1..MAX_NUMERIC_TYPE if type is primitive numeric type,
   *         BOOL_TYPE for boolean,
   *         STRING_TYPE for String,
   *         Integer.MAX_VALUE for others
   */
  @TypeRank
  @Contract(pure = true)
  public static int getTypeRank(@NotNull PsiType type) {
    return getTypeRank(type, false);
  }

  /**
   * @return 1..MAX_NUMERIC_TYPE if type is primitive numeric type,
   *         BOOL_TYPE for boolean,
   *         STRING_TYPE for String,
   *         Integer.MAX_VALUE for others
   */
  @TypeRank
  @Contract(pure = true)
  private static int getTypeRank(@NotNull PsiType type, boolean refWiden) {
    type = uncapture(type);
    if (type.equalsToText(JAVA_LANG_STRING)) {
      return STRING_RANK;
    }
    if (refWiden) {
      type = toUpperBound(type);
    }
    PsiPrimitiveType unboxedType = PsiPrimitiveType.getUnboxedType(type);
    if (unboxedType != null) {
      type = unboxedType;
    }

    int rank = TYPE_TO_RANK_MAP.get(type);
    if (rank != -1) {
      return rank;
    }
    return UNKNOWN_RANK;
  }

  private static @NotNull PsiType toUpperBound(@NotNull PsiType type) {
    Set<PsiClass> visited = null;
    while (true) {
      PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(type);
      if (!(psiClass instanceof PsiTypeParameter)) return type;
      PsiClassType[] types = psiClass.getExtendsListTypes();
      if (types.length == 0) return type;
      type = types[0];
      if (visited == null) {
        visited = new HashSet<>();
        visited.add(psiClass);
      } else {
        if (!visited.add(psiClass)) return type;
      }
    }
  }

  /**
   * @param tokenType JavaTokenType enumeration
   * @param strict    true if operator result type should be convertible to the left operand
   * @return true if lOperand operator rOperand expression is syntactically correct
   */
  @Contract(pure = true)
  public static boolean isBinaryOperatorApplicable(@NotNull IElementType tokenType,
                                                   PsiExpression lOperand,
                                                   PsiExpression rOperand,
                                                   boolean strict) {
    if (lOperand == null || rOperand == null) return true;
    final PsiType ltype = lOperand.getType();
    final PsiType rtype = rOperand.getType();
    return isBinaryOperatorApplicable(tokenType, ltype, rtype, strict);
  }

  @Contract(pure = true, value = "_, null, _, _ -> true; _, _, null, _ -> true")
  public static boolean isBinaryOperatorApplicable(@NotNull IElementType tokenType, final PsiType ltype, final PsiType rtype, final boolean strict) {
    if (ltype == null || rtype == null) return true;
    int resultTypeRank = BOOL_RANK;
    boolean isApplicable = false;
    final int ltypeRank = getTypeRank(ltype, true);
    final int rtypeRank = getTypeRank(rtype, true);

    if (tokenType == JavaTokenType.LT || tokenType == JavaTokenType.LE || tokenType == JavaTokenType.GT || tokenType == JavaTokenType.GE) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
        isApplicable = ltypeRank <= MAX_NUMERIC_RANK && rtypeRank <= MAX_NUMERIC_RANK;
      }
    }
    else if (tokenType == JavaTokenType.EQEQ || tokenType == JavaTokenType.NE) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true) &&
          (isPrimitiveAndNotNull(ltype) || isPrimitiveAndNotNull(rtype))) {
        isApplicable = ltypeRank <= MAX_NUMERIC_RANK && rtypeRank <= MAX_NUMERIC_RANK
                       || ltypeRank == BOOL_RANK && rtypeRank == BOOL_RANK;
      }
      else {
        if (isPrimitiveAndNotNull(ltype)) {
          if (rtype instanceof PsiClassType) {
            final LanguageLevel languageLevel = ((PsiClassType)rtype).getLanguageLevel();
            return languageLevel.isAtLeast(LanguageLevel.JDK_1_5) &&
                   !languageLevel.isAtLeast(LanguageLevel.JDK_1_8) &&
                   areTypesConvertible(ltype, rtype);
          }
          return false;
        }
        if (isPrimitiveAndNotNull(rtype)) {
          if (ltype instanceof PsiClassType) {
            final LanguageLevel level = ((PsiClassType)ltype).getLanguageLevel();
            return level.isAtLeast(LanguageLevel.JDK_1_7) && !level.isAtLeast(LanguageLevel.JDK_1_8) && areTypesConvertible(rtype, ltype);
          }
          return false;
        }
        isApplicable = areTypesConvertible(ltype, rtype) || areTypesConvertible(rtype, ltype);
      }
    }
    else if (tokenType == JavaTokenType.PLUS) {
      if (ltype.equalsToText(JAVA_LANG_STRING)) {
        isApplicable = !isVoidType(rtype);
        resultTypeRank = STRING_RANK;
      }
      else if (rtype.equalsToText(JAVA_LANG_STRING)) {
        if (isVoidType(ltype)) {
          return false;
        }
        return !strict || ltype.isAssignableFrom(rtype);
      }
      else if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
        resultTypeRank = Math.max(ltypeRank, rtypeRank);
        isApplicable = ltypeRank <= MAX_NUMERIC_RANK && rtypeRank <= MAX_NUMERIC_RANK;
      }
    }
    else if (tokenType == JavaTokenType.ASTERISK || tokenType == JavaTokenType.DIV || tokenType == JavaTokenType.PERC ||
             tokenType == JavaTokenType.MINUS) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
        resultTypeRank = Math.max(ltypeRank, rtypeRank);
        isApplicable = ltypeRank <= MAX_NUMERIC_RANK && rtypeRank <= MAX_NUMERIC_RANK;
      }
    }
    else if (tokenType == JavaTokenType.LTLT || tokenType == JavaTokenType.GTGT || tokenType == JavaTokenType.GTGTGT) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
        isApplicable = ltypeRank <= LONG_RANK && rtypeRank <= LONG_RANK;
        resultTypeRank = INT_RANK;
      }
    }
    else if (tokenType == JavaTokenType.AND || tokenType == JavaTokenType.OR || tokenType == JavaTokenType.XOR) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
        isApplicable = ltypeRank <= LONG_RANK && rtypeRank <= LONG_RANK
                       || isBooleanType(ltype) && isBooleanType(rtype);
        resultTypeRank = ltypeRank <= LONG_RANK ? INT_RANK : BOOL_RANK;
      }
    }
    else if (tokenType == JavaTokenType.ANDAND || tokenType == JavaTokenType.OROR) {
      if (isPrimitiveAndNotNullOrWrapper(ltype, true) && isPrimitiveAndNotNullOrWrapper(rtype, true)) {
