// Copyright 2000-2024 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.refs;

import com.intellij.openapi.util.TextRange;
import com.intellij.patterns.PlatformPatterns;
import com.intellij.patterns.StandardPatterns;
import com.intellij.patterns.XmlAttributeValuePattern;
import com.intellij.patterns.XmlPatterns;
import com.intellij.psi.*;
import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlProcessingInstruction;
import com.intellij.psi.xml.XmlTag;
import com.intellij.util.IncorrectOperationException;
import com.intellij.xml.XmlElementDescriptor;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.javaFX.fxml.FxmlConstants;
import org.jetbrains.plugins.javaFX.fxml.JavaFxFileTypeFactory;
import org.jetbrains.plugins.javaFX.fxml.JavaFxPsiUtil;

import static com.intellij.patterns.PlatformPatterns.virtualFile;
import static com.intellij.patterns.StandardPatterns.string;

public final class FxmlReferencesContributor extends PsiReferenceContributor {
  public static final JavaClassReferenceProvider CLASS_REFERENCE_PROVIDER = new JavaClassReferenceProvider() {
    {
      setOption(ALLOW_DOLLAR_NAMES, false);
    }
  };

  @Override
  public void registerReferenceProviders(@NotNull PsiReferenceRegistrar registrar) {
    final XmlAttributeValuePattern attributeValueInFxml = XmlPatterns.xmlAttributeValue().inVirtualFile(
      virtualFile().withExtension(JavaFxFileTypeFactory.FXML_EXTENSION));
    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.FX_CONTROLLER))
                                          .and(attributeValueInFxml),
                                        CLASS_REFERENCE_PROVIDER);

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
                                          .withParent(XmlPatterns.xmlAttribute().withName("type")
                                                        .withParent(XmlPatterns.xmlTag().withName(FxmlConstants.FX_ROOT)))
                                          .and(attributeValueInFxml),
                                        new MyJavaClassReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlTag().inVirtualFile(virtualFile().withExtension(JavaFxFileTypeFactory.FXML_EXTENSION)),
                                        new MyJavaClassReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.FX_ID))
                                          .and(attributeValueInFxml),
                                        new JavaFxFieldIdReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.SOURCE)
                                                                                     .withParent(XmlPatterns.xmlTag()
                                                                                                   .withName(FxmlConstants.FX_INCLUDE)))
                                          .and(attributeValueInFxml),
                                        new JavaFxSourceReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.SOURCE)
                                                                                     .withParent(XmlPatterns.xmlTag()
                                                                                                   .withName(FxmlConstants.FX_SCRIPT)))
                                          .and(attributeValueInFxml),
                                        new JavaFxSourceReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.SOURCE)
                                                                                     .withParent(XmlPatterns.xmlTag()
                                                                                                   .withName(string().oneOf(FxmlConstants.FX_REFERENCE, FxmlConstants.FX_COPY))))
                                          .and(attributeValueInFxml),
                                        new JavaFxComponentIdReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.FX_FACTORY))
                                          .and(attributeValueInFxml),
                                        new JavaFxFactoryReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withValue(string().startsWith("#"))
                                          .and(attributeValueInFxml),
                                        new JavaFxEventHandlerReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withValue(string().startsWith("@"))
                                          .withParent(StandardPatterns.not(XmlPatterns.xmlAttribute().withName(FxmlConstants.STYLESHEETS)))
                                          .and(attributeValueInFxml),
                                        new JavaFxLocationReferenceProvider(), PsiReferenceRegistrar.LOWER_PRIORITY);

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withValue(string().startsWith("$"))
                                          .withParent(XmlPatterns.xmlAttribute()
                                                        .andNot(XmlPatterns.xmlAttribute().withName(FxmlConstants.FX_VALUE)))
                                          .and(attributeValueInFxml),
                                        new JavaFxComponentIdReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
                                          .withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.URL_ATTR))
                                          .andNot(XmlPatterns.xmlAttributeValue().withValue(string().matches("^https?://.*")))
                                          .and(attributeValueInFxml),
                                        new JavaFxLocationReferenceProvider(false, "png", "jpg", "gif", "bmp"));
    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.STYLESHEETS)).and(attributeValueInFxml),
                                        new JavaFxLocationReferenceProvider(true, "css"));

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().withValue(string().startsWith("@"))
                                          .withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.VALUE)
                                                        .withParent(XmlPatterns.xmlTag().withName(FxmlConstants.URL_TAG)
                                                                      .withParent(XmlPatterns.xmlTag().withName(FxmlConstants.STYLESHEETS))))
                                          .and(attributeValueInFxml),
                                        new JavaFxLocationReferenceProvider(false, "css"));

    registrar.registerReferenceProvider(PlatformPatterns.psiElement(XmlProcessingInstruction.class).inVirtualFile(virtualFile().withExtension(JavaFxFileTypeFactory.FXML_EXTENSION)),
                                        new ImportReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue().and(attributeValueInFxml),
                                        new JavaFxColorReferenceProvider());

    registrar.registerReferenceProvider(XmlPatterns.xmlAttributeValue()
                                          .withParent(XmlPatterns.xmlAttribute().withName(FxmlConstants.FX_VALUE)
                                                        .withParent(XmlPatterns.xmlTag().withParent(XmlPatterns.xmlTag().withName(FxmlConstants.STYLESHEETS))))
                                          .and(attributeValueInFxml),
                                        new JavaFxLocationReferenceProvider(false, "css"));

    registrar.registerReferenceProvider(XmlPatterns.xmlAttribute().withLocalName(string().contains("."))
                                          .inVirtualFile(virtualFile().withExtension(JavaFxFileTypeFactory.FXML_EXTENSION)),
                                        new JavaFxStaticPropertyReferenceProvider());
  }

  private static final class MyJavaClassReferenceProvider extends JavaClassReferenceProvider {
    @Override
    public PsiReference @NotNull [] getReferencesByElement(@NotNull PsiElement element) {
      String name = element instanceof XmlAttributeValue ? ((XmlAttributeValue)element).getValue()
                                                         : ((XmlTag)element).getName();
      return getReferencesByString(name, element, 1);
    }

    @Override
    public PsiReference @NotNull [] getReferencesByString(@NotNull String str,
                                                          final @NotNull PsiElement position,
                                                          int offsetInPosition) {
      if (str.isEmpty()) return PsiReference.EMPTY_ARRAY;
      final PsiReference[] references = super.getReferencesByString(str, position, offsetInPosition);
      final int offset = position instanceof XmlTag ? 1 : 0;
      if (references.length <= offset) return PsiReference.EMPTY_ARRAY;
      final PsiReference[] results = new PsiReference[references.length - offset];
      for (int i = 0; i < results.length; i++) {
        results[i] = new JavaClassReferenceWrapper(references[i], position);
      }
      return results;
    }

    private static final class JavaClassReferenceWrapper implements PsiReference {
      private final PsiReference myReference;
      private final PsiElement myPosition;

      JavaClassReferenceWrapper(PsiReference reference, PsiElement position) {
        myReference = reference;
        myPosition = position;
      }

      @Override
      public @NotNull PsiElement getElement() {
        return myReference.getElement();
      }

      @Override
      public @NotNull TextRange getRangeInElement() {
        return myReference.getRangeInElement();
      }

      @Override
      public @Nullable PsiElement resolve() {
        final PsiElement resolve = myReference.resolve();
        if (resolve != null) {
          return resolve;
        }
        return getReferencedClass();
      }

      private PsiElement getReferencedClass() {
        if (myPosition instanceof XmlTag) {
          final XmlElementDescriptor descriptor = ((XmlTag)myPosition).getDescriptor();
          if (descriptor != null) {
            final PsiElement declaration = descriptor.getDeclaration();
            if (declaration instanceof PsiMethod &&
                ((PsiMethod)declaration).hasModifierProperty(PsiModifier.STATIC)) {
              final PsiClass containingClass = ((PsiMethod)declaration).getContainingClass();
              if (containingClass != null && myReference.getCanonicalText().equals(containingClass.getName())) {
                return containingClass;
              }
            }
          }
        }
        else if (myPosition instanceof XmlAttributeValue) {
          return JavaFxPsiUtil.findPsiClass(((XmlAttributeValue)myPosition).getValue(), myPosition);
        }
        return null;
      }

      @Override
      public @NotNull String getCanonicalText() {
        return myReference.getCanonicalText();
      }

      @Override
      public PsiElement handleElementRename(@NotNull String newElementName) throws IncorrectOperationException {
        String oldText = getOldName();
        final TextRange range = getRangeInElement();
        final String newText =
          oldText.substring(0, range.getStartOffset() - 1) + newElementName + oldText.substring(range.getEndOffset() - 1);
        return setNewName(newText);
      }

      @Override
      public PsiElement bindToElement(@NotNull PsiElement element)
        throws IncorrectOperationException {
        String oldText = getOldName();
        final TextRange range = getRangeInElement();
        final String newText = (element instanceof PsiPackage ? ((PsiPackage)element).getQualifiedName() : ((PsiClass)element).getName()) +
                               oldText.substring(range.getEndOffset() - 1);
        return setNewName(newText);
      }

      private PsiElement setNewName(String newText) {
        if (myPosition instanceof XmlTag) {
          return ((XmlTag)myPosition).setName(newText);
        }
        else {
          final XmlElementFactory xmlElementFactory = XmlElementFactory.getInstance(myPosition.getProject());
          final XmlAttribute xmlAttribute = xmlElementFactory.createXmlAttribute("attributeName", newText);
          final XmlAttributeValue valueElement = xmlAttribute.getValueElement();
          assert valueElement != null;
          return myPosition.replace(valueElement);
        }
      }

      private String getOldName() {
        return myPosition instanceof XmlTag ? ((XmlTag)myPosition).getName() : ((XmlAttributeValue)myPosition).getValue();
      }

      @Override
      public boolean isReferenceTo(@NotNull PsiElement element) {
        return myReference.isReferenceTo(element) || getReferencedClass() == element;
      }

      @Override
      public Object @NotNull [] getVariants() {
        return myReference.getVariants();
      }

      @Override
      public boolean isSoft() {
        return true;
      }
    }
  }
}
