// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package org.jetbrains.plugins.javaFX.fxml;

import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.daemon.Validator;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightNamesUtil;
import com.intellij.codeInsight.daemon.impl.analysis.JavaGenericsUtil;
import com.intellij.codeInspection.util.InspectionMessage;
import com.intellij.ide.highlighter.JavaFileType;
import com.intellij.lang.ASTNode;
import com.intellij.lang.xml.XMLLanguage;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.cache.CacheManager;
import com.intellij.psi.impl.source.PostprocessReformattingAspect;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.search.UsageSearchContext;
import com.intellij.psi.search.searches.ClassInheritorsSearch;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.*;
import com.intellij.util.Processor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.xml.XmlAttributeDescriptor;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.*;
import org.jetbrains.plugins.javaFX.JavaFXBundle;
import org.jetbrains.plugins.javaFX.fxml.descriptors.JavaFxClassTagDescriptorBase;
import org.jetbrains.plugins.javaFX.fxml.descriptors.JavaFxPropertyTagDescriptor;
import org.jetbrains.plugins.javaFX.indexing.JavaFxControllerClassIndex;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;

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

  public static XmlProcessingInstruction createSingleImportInstruction(String qualifiedName, Project project) {
    final String importText = "<?import " + qualifiedName + "?>";
    final PsiElement child =
      PsiFileFactory.getInstance(project).createFileFromText("a.fxml", XMLLanguage.INSTANCE, importText).getFirstChild();
    return PsiTreeUtil.findChildOfType(child, XmlProcessingInstruction.class);
  }

  public static List<String> parseImports(XmlFile file) {
    return parseInstructions(file, "import");
  }

  public static List<String> parseInjectedLanguages(XmlFile file) {
    return parseInstructions(file, "language");
  }

  private static List<String> parseInstructions(XmlFile file, String instructionName) {
    final List<String> definedImports = new ArrayList<>();
    final XmlDocument document = file.getDocument();
    if (document != null) {
      final XmlProlog prolog = document.getProlog();

      final Collection<XmlProcessingInstruction>
        instructions = new ArrayList<>(PsiTreeUtil.findChildrenOfType(prolog, XmlProcessingInstruction.class));
      for (final XmlProcessingInstruction instruction : instructions) {
        final String instructionTarget = getInstructionTarget(instructionName, instruction);
        if (instructionTarget != null) {
          definedImports.add(instructionTarget);
        }
      }
    }
    return definedImports;
  }

  public static @Nullable String getInstructionTarget(String instructionName, XmlProcessingInstruction instruction) {
    final ASTNode node = instruction.getNode();
    ASTNode xmlNameNode = node.findChildByType(XmlTokenType.XML_NAME);
    ASTNode importNode = node.findChildByType(XmlTokenType.XML_TAG_CHARACTERS);
    if (!(xmlNameNode == null || !instructionName.equals(xmlNameNode.getText()) || importNode == null)) {
      return importNode.getText();
    }
    return null;
  }

  public static PsiClass findPsiClass(String name, PsiElement context) {
    final Project project = context.getProject();
    if (!StringUtil.getShortName(name).equals(name)) {
      final PsiClass psiClass = JavaPsiFacade.getInstance(project).findClass(name, GlobalSearchScope.allScope(project));
      if (psiClass != null) {
        return psiClass;
      }
      return findNestedPsiClass(name, context, project);
    }
    return findPsiClass(name, parseImports((XmlFile)context.getContainingFile()), context, project);
  }

  private static PsiClass findNestedPsiClass(String name, PsiElement context, Project project) {
    final int dotIndex = name.indexOf('.');
    if (dotIndex > 0) {
      final String outerName = name.substring(0, dotIndex);
      final PsiClass outerClass = findPsiClass(outerName, parseImports((XmlFile)context.getContainingFile()), context, project);
      if (outerClass != null) {
        final List<String> nameChain = StringUtil.split(name, ".", true, false);
        final List<String> nestedNames = nameChain.subList(1, nameChain.size());
        PsiClass aClass = outerClass;
        for (String nestedName : nestedNames) {
          aClass = aClass.findInnerClassByName(nestedName, true);
          if (aClass == null) return null;
        }
        return aClass;
      }
    }
    return null;
  }

  private static PsiClass findPsiClass(String name, List<String> imports, PsiElement context, Project project) {
    PsiClass psiClass = null;
    if (imports != null) {
      JavaPsiFacade psiFacade = JavaPsiFacade.getInstance(project);

      PsiFile file = context.getContainingFile();
      for (String anImport : imports) {
        if (StringUtil.getShortName(anImport).equals(name)) {
          psiClass = psiFacade.findClass(anImport, file.getResolveScope());
        }
        else if (StringUtil.endsWith(anImport, ".*")) {
          psiClass = psiFacade.findClass(StringUtil.trimEnd(anImport, "*") + name, file.getResolveScope());
        }
        if (psiClass != null) {
          return psiClass;
        }
      }
    }
    return null;
  }

  public static void insertImportWhenNeeded(XmlFile xmlFile,
                                            String shortName,
                                            String qualifiedName) {
    if (shortName != null && qualifiedName != null && findPsiClass(shortName, xmlFile.getRootTag()) == null) {
      final XmlDocument document = xmlFile.getDocument();
      if (document != null) {
        final XmlProcessingInstruction processingInstruction = createSingleImportInstruction(qualifiedName, xmlFile.getProject());
        final XmlProlog prolog = document.getProlog();
        if (prolog != null) {
          prolog.add(processingInstruction);
        }
        else {
          document.addBefore(processingInstruction, document.getRootTag());
        }
        PostprocessReformattingAspect.getInstance(xmlFile.getProject()).doPostponedFormatting(xmlFile.getViewProvider());
      }
    }
  }

  public static PsiClass getPropertyClass(PsiElement member) {
    final PsiClassType classType = getPropertyClassType(member);
    return classType != null ? classType.resolve() : null;
  }

  public static PsiClassType getPropertyClassType(PsiElement member) {
    return getPropertyClassType(member, JavaFxCommonNames.JAVAFX_BEANS_PROPERTY_OBJECT_PROPERTY);
  }

  public static PsiClassType getPropertyClassType(PsiElement member, final String superTypeFQN) {
    if (member instanceof PsiMember) {
      final PsiType type = PropertyUtilBase.getPropertyType((PsiMember)member);
      if (type instanceof PsiClassType) {
        final PsiClassType.ClassResolveResult resolveResult = ((PsiClassType)type).resolveGenerics();
        final PsiClass attributeClass = resolveResult.getElement();
        if (attributeClass != null) {
          final PsiClass objectProperty = JavaPsiFacade.getInstance(attributeClass.getProject())
            .findClass(superTypeFQN, attributeClass.getResolveScope());
          if (objectProperty != null) {
            final PsiSubstitutor superClassSubstitutor = TypeConversionUtil
              .getClassSubstitutor(objectProperty, attributeClass, resolveResult.getSubstitutor());
            if (superClassSubstitutor != null) {
              final PsiType propertyType = superClassSubstitutor.substitute(objectProperty.getTypeParameters()[0]);
              if (propertyType instanceof PsiClassType) {
                return (PsiClassType)propertyType;
              }
            }
            else {
              return (PsiClassType)type;
            }
          }
        }
      }
    }
    return null;
  }

  public static PsiMethod findStaticPropertySetter(String attributeName, XmlTag context) {
    final String packageName = StringUtil.getPackageName(attributeName);
    if (context != null && !StringUtil.isEmptyOrSpaces(packageName)) {
      final PsiClass classWithStaticProperty = findPsiClass(packageName, context);
      if (classWithStaticProperty != null) {
        return findStaticPropertySetter(attributeName, classWithStaticProperty);
      }
    }
    return null;
  }

  public static @Nullable PsiMethod findStaticPropertySetter(@NotNull String attributeName, @Nullable PsiClass classWithStaticProperty) {
    if (classWithStaticProperty == null) return null;
    final String setterName = PropertyUtilBase.suggestSetterName(StringUtil.getShortName(attributeName));
    final PsiMethod[] setters = classWithStaticProperty.findMethodsByName(setterName, true);
    for (PsiMethod setter : setters) {
      if (setter.hasModifierProperty(PsiModifier.PUBLIC) &&
          setter.hasModifierProperty(PsiModifier.STATIC) &&
          setter.getParameterList().getParametersCount() == 2) {
        return setter;
      }
    }
    return null;
  }

  public static PsiMethod findPropertyGetter(@NotNull PsiClass psiClass, @Nullable String propertyName) {
    if (StringUtil.isEmpty(propertyName)) return null;
    PsiMethod getter = findPropertyGetter(psiClass, propertyName, null);
    if (getter != null) {
      return getter;
    }
    return findPropertyGetter(psiClass, propertyName, PsiTypes.booleanType());
  }

  private static PsiMethod findPropertyGetter(final PsiClass psiClass, final String propertyName, final PsiType propertyType) {
    final String getterName = PropertyUtilBase.suggestGetterName(propertyName, propertyType);
    final PsiMethod[] getters = psiClass.findMethodsByName(getterName, true);
    for (PsiMethod getter : getters) {
      if (getter.hasModifierProperty(PsiModifier.PUBLIC) &&
          !getter.hasModifierProperty(PsiModifier.STATIC) &&
          PropertyUtilBase.isSimplePropertyGetter(getter)) {
        return getter;
      }
    }
    return null;
  }

  public static PsiMethod findObservablePropertyGetter(@NotNull PsiClass psiClass, @Nullable String propertyName) {
    if (StringUtil.isEmpty(propertyName)) return null;
    final PsiMethod[] getters = psiClass.findMethodsByName(propertyName + JavaFxCommonNames.PROPERTY_METHOD_SUFFIX, true);
    for (PsiMethod getter : getters) {
      if (getter.hasModifierProperty(PsiModifier.PUBLIC) &&
          !getter.hasModifierProperty(PsiModifier.STATIC) &&
          getter.getParameterList().isEmpty() &&
          InheritanceUtil.isInheritor(getter.getReturnType(), JavaFxCommonNames.JAVAFX_BEANS_VALUE_OBSERVABLE_VALUE)) {
        return getter;
      }
    }
    return null;
  }

  private static final Key<CachedValue<PsiClass>> INJECTED_CONTROLLER = Key.create("javafx.injected.controller");

  public static PsiClass getControllerClass(final PsiFile containingFile) {
    if (containingFile instanceof XmlFile) {
      final XmlTag rootTag = ((XmlFile)containingFile).getRootTag();
      final Project project = containingFile.getProject();
      if (rootTag != null) {
        XmlAttribute attribute = rootTag.getAttribute(FxmlConstants.FX_CONTROLLER);
        if (attribute != null) {
          final PsiClass controllerClass = findControllerClass(containingFile, project, attribute);
          if (controllerClass != null) {
            return controllerClass;
          }
        }
      }
      if (Registry.is("javafx.fxml.controller.from.loader", false)) {
        final CachedValuesManager manager = CachedValuesManager.getManager(containingFile.getProject());
        final PsiClass injectedControllerClass = manager.getCachedValue(
          containingFile, INJECTED_CONTROLLER, () -> computeInjectedControllerClass(containingFile), true);
        if (injectedControllerClass != null) {
          return injectedControllerClass;
        }
      }

      if (rootTag != null && FxmlConstants.FX_ROOT.equals(rootTag.getName())) {
        final XmlAttribute rootTypeAttr = rootTag.getAttribute(FxmlConstants.TYPE);
        if (rootTypeAttr != null) {
          return findControllerClass(containingFile, project, rootTypeAttr);
        }
      }
    }
    return null;
  }

  private static PsiClass findControllerClass(PsiFile containingFile, Project project, XmlAttribute attribute) {
    final String attributeValue = attribute.getValue();
    if (!StringUtil.isEmptyOrSpaces(attributeValue)) {
      final GlobalSearchScope customScope = GlobalSearchScope.projectScope(project).intersectWith(containingFile.getResolveScope());
      return JavaPsiFacade.getInstance(project).findClass(attributeValue, customScope);
    }
    return null;
  }

  public static boolean isEventHandlerProperty(@NotNull XmlAttribute attribute) {
    final PsiClass tagClass = getTagClass(attribute.getParent());
    return tagClass != null && getEventHandlerPropertyType(tagClass, attribute.getName()) != null;
  }

  public static @Nullable PsiClass getTagClass(@Nullable XmlAttributeValue xmlAttributeValue) {
    if (xmlAttributeValue != null) {
      final PsiElement parent = xmlAttributeValue.getParent();
      if (parent instanceof XmlAttribute) {
        final XmlTag xmlTag = ((XmlAttribute)parent).getParent();
        return getTagClass(xmlTag);
      }
    }
    return null;
  }

  public static @Nullable PsiClass getTagClass(@Nullable XmlTag xmlTag) {
    if (xmlTag != null) {
      final XmlElementDescriptor descriptor = xmlTag.getDescriptor();
      if (descriptor != null) {
        final PsiElement declaration = descriptor.getDeclaration();
        if (declaration instanceof PsiClass) {
          return (PsiClass)declaration;
        }
      }
    }
    return null;
  }

  public static @Nullable PsiElement getAttributeDeclaration(@Nullable XmlAttributeValue xmlAttributeValue) {
    if (xmlAttributeValue != null) {
      final PsiElement parent = xmlAttributeValue.getParent();
      if (parent instanceof XmlAttribute) {
        final XmlAttributeDescriptor descriptor = ((XmlAttribute)parent).getDescriptor();
        if (descriptor != null) {
          return descriptor.getDeclaration();
        }
      }
    }
    return null;
  }

  public static boolean isVisibleInFxml(@NotNull PsiMember psiMember) {
    return psiMember.hasModifierProperty(PsiModifier.PUBLIC) ||
           AnnotationUtil.isAnnotated(psiMember, JavaFxCommonNames.JAVAFX_FXML_ANNOTATION, 0);
  }

  public static @Nullable PsiMethod findValueOfMethod(final @NotNull PsiType psiType) {
    final PsiClass psiClass = PsiUtil.resolveClassInClassTypeOnly(psiType);
    return psiClass != null ? findValueOfMethod(psiClass) : null;
  }

  public static @Nullable PsiMethod findValueOfMethod(final @NotNull PsiClass psiClass) {
    return CachedValuesManager.getCachedValue(psiClass, () -> {
      final PsiMethod[] methods = psiClass.findMethodsByName(JavaFxCommonNames.VALUE_OF, true);
      for (PsiMethod method : methods) {
        if (method.hasModifierProperty(PsiModifier.STATIC)) {
          final PsiParameter[] parameters = method.getParameterList().getParameters();
          if (parameters.length == 1) {
            final PsiType type = parameters[0].getType();
            if (type.equalsToText(CommonClassNames.JAVA_LANG_STRING) || type.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
              if (psiClass.equals(PsiUtil.resolveClassInType(method.getReturnType()))) {
                return CachedValueProvider.Result.create(method, PsiModificationTracker.MODIFICATION_COUNT);
              }
            }
          }
        }
      }
      return CachedValueProvider.Result.create(null, PsiModificationTracker.MODIFICATION_COUNT);
    });
  }

  public static boolean isReadOnly(String attributeName, XmlTag tag) {
    if (findStaticPropertySetter(attributeName, tag) != null) return false;
    final XmlElementDescriptor descriptor = tag.getDescriptor();
    if (descriptor instanceof JavaFxClassTagDescriptorBase) {
      return ((JavaFxClassTagDescriptorBase)descriptor).isReadOnlyAttribute(attributeName);
    }
    return false;
  }

  public static boolean isExpressionBinding(@Nullable String value) {
    return value != null && value.startsWith("${") && value.endsWith("}");
  }

  public static boolean isIncorrectExpressionBinding(@Nullable String value) {
    if (value == null || !value.startsWith("$")) return false;
    if (value.length() == 1) return true;
    final boolean expressionStarts = value.startsWith("${");
    final boolean expressionEnds = value.endsWith("}");
    if (expressionStarts && expressionEnds && value.length() == 3) return true;
    if (expressionStarts != expressionEnds) return true;
    if (expressionStarts && value.indexOf('{', 2) >= 2) return true;
    if (expressionEnds && value.indexOf('}') < value.length() - 1) return true;
    return false;
  }

  public static @Nullable PsiType getWritablePropertyType(final @Nullable PsiType type, final @NotNull Project project) {
    final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(type);
    final PsiClass psiClass = resolveResult.getElement();
    if (psiClass != null) {
      final PsiClass propertyClass = JavaPsiFacade.getInstance(project).findClass(JavaFxCommonNames.JAVAFX_BEANS_PROPERTY,
                                                                                  GlobalSearchScope.allScope(project));
      if (propertyClass != null) {
        final PsiSubstitutor substitutor =
          TypeConversionUtil.getClassSubstitutor(propertyClass, psiClass, resolveResult.getSubstitutor());
        if (substitutor != null) {
          return substitutor.substitute(propertyClass.getTypeParameters()[0]);
        }
      }
    }
    return null;
  }

  private static @Nullable PsiType getDefaultPropertyExpectedType(@Nullable PsiClass aClass) {
    if (aClass == null) return null;
    return CachedValuesManager.getCachedValue(aClass, () -> {
      final PsiAnnotation annotation =
        AnnotationUtil.findAnnotationInHierarchy(aClass, Collections.singleton(JavaFxCommonNames.JAVAFX_BEANS_DEFAULT_PROPERTY));
      if (annotation != null) {
        final PsiAnnotationMemberValue memberValue = annotation.findAttributeValue(null);
        if (memberValue != null) {
          final String propertyName = StringUtil.unquoteString(memberValue.getText());
          final PsiMethod getter = findPropertyGetter(aClass, propertyName);
          if (getter != null) {
            final PsiType propertyType = eraseFreeTypeParameters(getter.getReturnType(), getter);
            return CachedValueProvider.Result.create(propertyType, PsiModificationTracker.MODIFICATION_COUNT);
          }
        }
      }
      return CachedValueProvider.Result.create(null, PsiModificationTracker.MODIFICATION_COUNT);
    });
  }

  public static String getDefaultPropertyName(@Nullable PsiClass aClass) {
    if (aClass == null) {
      return null;
    }
    final PsiAnnotation annotation = AnnotationUtil.findAnnotationInHierarchy(aClass,
                                                                              Collections.singleton(
                                                                                JavaFxCommonNames.JAVAFX_BEANS_DEFAULT_PROPERTY));
    if (annotation != null) {
      final PsiAnnotationMemberValue memberValue = annotation.findAttributeValue(null);
      if (memberValue != null) {
        return StringUtil.unquoteString(memberValue.getText());
      }
    }
    return null;
  }

  public static boolean isAbleToInstantiate(@NotNull PsiClass psiClass) {
    return isAbleToInstantiate(psiClass, message -> {
    });
  }

  public static boolean isAbleToInstantiate(@NotNull PsiClass psiClass, @NotNull Consumer<? super String> messageConsumer) {
    if (psiClass.isEnum() || hasNamedArgOrNoArgConstructor(psiClass)) return true;
    final PsiMethod valueOf = findValueOfMethod(psiClass);
    if (valueOf == null) {
      if (!hasBuilder(psiClass)) {
        messageConsumer.accept(JavaFXBundle.message("unable.to.instantiate"));
        return false;
      }
    }
    return true;
  }

  private static boolean hasNamedArgOrNoArgConstructor(@NotNull PsiClass psiClass) {
    if (psiClass.getConstructors().length == 0) return true;
    return CachedValuesManager.getCachedValue(psiClass, () -> {
      for (PsiMethod constructor : psiClass.getConstructors()) {
        final PsiParameter[] parameters = constructor.getParameterList().getParameters();
        if (parameters.length == 0) {
          return CachedValueProvider.Result.create(true, PsiModificationTracker.MODIFICATION_COUNT);
        }
        boolean annotated = true;
        for (PsiParameter parameter : parameters) {
          if (!AnnotationUtil.isAnnotated(parameter, JavaFxCommonNames.JAVAFX_BEANS_NAMED_ARG, 0)) {
            annotated = false;
            break;
          }
        }
        if (annotated) return CachedValueProvider.Result.create(true, PsiModificationTracker.MODIFICATION_COUNT);
      }
      return CachedValueProvider.Result.create(false, PsiModificationTracker.MODIFICATION_COUNT);
    });
  }

  public static boolean hasBuilder(final @NotNull PsiClass psiClass) {
    return CachedValuesManager.getCachedValue(psiClass, () -> {
      final Project project = psiClass.getProject();
      final PsiClass builderClass = JavaPsiFacade.getInstance(project).findClass(JavaFxCommonNames.JAVAFX_FXML_BUILDER,
                                                                                 GlobalSearchScope.allScope(project));
      if (builderClass != null) {
        final PsiMethod[] buildMethods = builderClass.findMethodsByName("build", false);
        if (buildMethods.length == 1 && buildMethods[0].getParameterList().isEmpty()) {
          if (ClassInheritorsSearch.search(builderClass).forEach(aClass -> {
            PsiType returnType = null;
            final PsiMethod method = MethodSignatureUtil.findMethodBySuperMethod(aClass, buildMethods[0], false);
            if (method != null) {
              returnType = method.getReturnType();
            }
            return !Comparing.equal(psiClass, PsiUtil.resolveClassInClassTypeOnly(returnType));
          })) {
            return CachedValueProvider.Result.create(false, PsiModificationTracker.MODIFICATION_COUNT);
          }
        }
      }
      return CachedValueProvider.Result.create(true, PsiModificationTracker.MODIFICATION_COUNT);
    });
  }

  public static boolean isClassAcceptable(@Nullable XmlTag targetTag, final @Nullable PsiClass fromClass) {
    return isClassAcceptable(targetTag, fromClass, (message, type) -> {
    });
  }

  public static boolean isClassAcceptable(@Nullable XmlTag targetTag, final @Nullable PsiClass fromClass,
                                          @NotNull BiConsumer<? super String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    if (targetTag == null || fromClass == null || !fromClass.isValid()) {
      return true;
    }
    final XmlElementDescriptor tagDescriptor = targetTag.getDescriptor();
    if (tagDescriptor instanceof JavaFxPropertyTagDescriptor) {
      final PsiClass containingClass = ((JavaFxPropertyTagDescriptor)tagDescriptor).getPsiClass();
      final PsiType targetType = getWritablePropertyType(containingClass, tagDescriptor.getDeclaration());
      return canCoerce(targetType, fromClass, targetTag, messageConsumer);
    }
    else if (tagDescriptor instanceof JavaFxClassTagDescriptorBase) {
      final PsiElement tagDeclaration = tagDescriptor.getDeclaration();
      if (tagDeclaration instanceof PsiClass defaultPropertyOwnerClass) {
        final XmlAttribute factoryAttr = targetTag.getAttribute(FxmlConstants.FX_FACTORY);
        if (factoryAttr != null) {
          defaultPropertyOwnerClass = getFactoryProducedClass((PsiClass)tagDeclaration, factoryAttr.getValue());
        }
        final PsiType targetType = getDefaultPropertyExpectedType(defaultPropertyOwnerClass);
        if (targetType != null) {
          return canCoerce(targetType, fromClass, targetTag, messageConsumer);
        }
        if (!isObservableCollection(defaultPropertyOwnerClass)) {
          return noDefaultPropertyError(messageConsumer);
        }
      }
    }
    return true;
  }

  private static boolean noDefaultPropertyError(@NotNull BiConsumer<? super String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    messageConsumer.accept("Parent tag has no default property",
                           Validator.ValidationHost.ErrorType.ERROR);
    return false;
  }

  private static boolean canCoerce(@Nullable PsiType targetType, @NotNull PsiClass fromClass, @NotNull PsiElement context,
                                   @NotNull BiConsumer<? super String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    if (targetType == null) return true;
    PsiType collectionItemType = JavaGenericsUtil.getCollectionItemType(targetType, fromClass.getResolveScope());
    if (collectionItemType == null && InheritanceUtil.isInheritor(targetType, JavaFxCommonNames.JAVAFX_BEANS_PROPERTY)) {
      collectionItemType = getWritablePropertyType(targetType, fromClass.getProject());
    }
    if (collectionItemType != null) {
      return canCoerceImpl(collectionItemType, fromClass, context, messageConsumer);
    }
    return canCoerceImpl(targetType, fromClass, context, messageConsumer);
  }

  private static @Nullable PsiType eraseFreeTypeParameters(@Nullable PsiType psiType, @NotNull PsiMember member) {
    final PsiClass containingClass = member.getContainingClass();
    return eraseFreeTypeParameters(psiType, containingClass);
  }

  private static @Nullable PsiType eraseFreeTypeParameters(@Nullable PsiType psiType, @Nullable PsiClass containingClass) {
    if (containingClass == null) return null;
    return JavaPsiFacade.getElementFactory(containingClass.getProject()).createRawSubstitutor(containingClass).substitute(psiType);
  }

  private static boolean canCoerceImpl(@NotNull PsiType targetType, @NotNull PsiClass fromClass, @NotNull PsiElement context,
                                       @NotNull BiConsumer<? super String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    if (targetType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT) ||
        targetType.equalsToText(CommonClassNames.JAVA_LANG_STRING) ||
        targetType.isAssignableFrom(PsiTypesUtil.getClassType(fromClass))) {
      return true;
    }
    final PsiClassType boxedTargetClass =
      targetType instanceof PsiPrimitiveType ? ((PsiPrimitiveType)targetType).getBoxedType(context) : null;
    if (boxedTargetClass != null && InheritanceUtil.isInheritor(boxedTargetClass, CommonClassNames.JAVA_LANG_NUMBER) ||
        InheritanceUtil.isInheritor(targetType, CommonClassNames.JAVA_LANG_NUMBER)) {
      if (Comparing.strEqual(fromClass.getQualifiedName(), CommonClassNames.JAVA_LANG_STRING) ||
          InheritanceUtil.isInheritor(fromClass, CommonClassNames.JAVA_LANG_NUMBER)) {
        return true;
      }
      return unrelatedTypesWarning(targetType, fromClass, messageConsumer);
    }
    final PsiMethod valueOfMethod = findValueOfMethod(targetType);
    final PsiType valueOfParameterType = valueOfMethod != null && valueOfMethod.getParameterList().getParametersCount() == 1 ?
                                         valueOfMethod.getParameterList().getParameters()[0].getType() : null;
    if (valueOfParameterType != null && valueOfParameterType.equalsToText(CommonClassNames.JAVA_LANG_OBJECT)) {
      return true;
    }
    if (Comparing.strEqual(fromClass.getQualifiedName(), CommonClassNames.JAVA_LANG_STRING)) {
      if (isPrimitiveOrBoxed(targetType) ||
          valueOfParameterType != null && valueOfParameterType.equalsToText(CommonClassNames.JAVA_LANG_STRING)) {
        return true;
      }
    }
    if (valueOfMethod != null) {
      return unrelatedTypesWarning(targetType, fromClass, messageConsumer);
    }
    return unableToCoerceError(targetType, fromClass, messageConsumer);
  }

  private static boolean unableToCoerceError(@NotNull PsiType targetType, @NotNull PsiClass fromClass,
                                             @NotNull BiConsumer<? super String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    messageConsumer.accept(
      JavaFXBundle.message("unable.to.coerce.error", HighlightNamesUtil.formatClass(fromClass), targetType.getCanonicalText()),
      Validator.ValidationHost.ErrorType.ERROR);
    return false;
  }

  private static boolean unrelatedTypesWarning(@NotNull PsiType targetType, @NotNull PsiClass fromClass,
                                               @NotNull BiConsumer<? super @InspectionMessage String, ? super Validator.ValidationHost.ErrorType> messageConsumer) {
    messageConsumer.accept(
      JavaFXBundle.message("conversion.between.unrelated.types.error", HighlightNamesUtil.formatClass(fromClass), targetType.getCanonicalText()),
      Validator.ValidationHost.ErrorType.WARNING);
    return true;
  }

  public static boolean isOutOfHierarchy(final XmlAttributeValue element) {
    XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class);
    while (tag != null) {
      if (FxmlConstants.FX_DEFINE.equals(tag.getName())) {
        return true;
      }
      tag = tag.getParentTag();
    }
    return false;
  }

  public static PsiType getWrappedPropertyType(final PsiField field, final Project project, final Map<String, PsiType> typeMap) {
    return CachedValuesManager.getCachedValue(field, () -> {
      final PsiType fieldType = field.getType();
      final PsiClassType.ClassResolveResult resolveResult = PsiUtil.resolveGenericsClassInType(fieldType);
      final PsiClass fieldClass = resolveResult.getElement();
      if (fieldClass == null) {
        final PsiType propertyType = eraseFreeTypeParameters(fieldType, field);
        return CachedValueProvider.Result.create(propertyType, PsiModificationTracker.MODIFICATION_COUNT);
      }
      PsiType substitute = null;
      for (String typeName : typeMap.keySet()) {
        if (InheritanceUtil.isInheritor(fieldType, typeName)) {
          substitute = typeMap.get(typeName);
          break;
        }
      }
      if (substitute == null) {
        if (!InheritanceUtil.isInheritor(fieldType, JavaFxCommonNames.JAVAFX_BEANS_VALUE_OBSERVABLE_VALUE)) {
          final PsiType propertyType = eraseFreeTypeParameters(fieldType, field);
          return CachedValueProvider.Result.create(propertyType, PsiModificationTracker.MODIFICATION_COUNT);
        }
        final PsiClass aClass = JavaPsiFacade.getInstance(project)
          .findClass(JavaFxCommonNames.JAVAFX_BEANS_VALUE_OBSERVABLE_VALUE, GlobalSearchScope.allScope(project));
        LOG.assertTrue(aClass != null);
        final PsiSubstitutor substitutor =
          TypeConversionUtil.getSuperClassSubstitutor(aClass, fieldClass, resolveResult.getSubstitutor());
        final PsiMethod[] values = aClass.findMethodsByName(JavaFxCommonNames.GET_VALUE, false);
        LOG.assertTrue(values.length == 1);
        substitute = substitutor.substitute(values[0].getReturnType());
      }

      final PsiType propertyType = eraseFreeTypeParameters(substitute, field);
      return CachedValueProvider.Result.create(propertyType, PsiModificationTracker.MODIFICATION_COUNT);
    });
  }

  public static @Nullable PsiType getWritablePropertyType(@Nullable PsiClass containingClass, @Nullable PsiElement declaration) {
    if (declaration instanceof PsiField) {
      return getWrappedPropertyType((PsiField)declaration, declaration.getProject(), JavaFxCommonNames.ourWritableMap);
    }
    if (declaration instanceof PsiMethod method) {
      if (!method.getParameterList().isEmpty()) {
        return getSetterArgumentType(method);
      }
      final String propertyName = PropertyUtilBase.getPropertyName(method);
      final PsiClass psiClass = containingClass != null ? containingClass : method.getContainingClass();
      if (propertyName != null && containingClass != null) {
