// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.structuralsearch.impl.matcher;

import com.intellij.dupLocator.iterators.ArrayBackedNodeIterator;
import com.intellij.dupLocator.iterators.NodeIterator;
import com.intellij.dupLocator.iterators.SingleNodeIterator;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.impl.light.LightElement;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.javadoc.PsiDocTag;
import com.intellij.psi.javadoc.PsiDocTagValue;
import com.intellij.psi.javadoc.PsiDocToken;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.structuralsearch.MatchOptions;
import com.intellij.structuralsearch.MatchUtil;
import com.intellij.structuralsearch.StructuralSearchUtil;
import com.intellij.structuralsearch.impl.matcher.handlers.LiteralWithSubstitutionHandler;
import com.intellij.structuralsearch.impl.matcher.handlers.MatchingHandler;
import com.intellij.structuralsearch.impl.matcher.handlers.SubstitutionHandler;
import com.intellij.structuralsearch.impl.matcher.iterators.HierarchyNodeIterator;
import com.intellij.structuralsearch.impl.matcher.predicates.MatchPredicate;
import com.intellij.structuralsearch.impl.matcher.predicates.NotPredicate;
import com.intellij.structuralsearch.impl.matcher.predicates.RegExpPredicate;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.containers.ContainerUtil;
import com.siyeh.ig.psiutils.InstanceOfUtils;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class JavaMatchingVisitor extends JavaElementVisitor {
  public static final String[] MODIFIERS = {
    PsiModifier.PUBLIC, PsiModifier.PROTECTED, PsiModifier.PRIVATE, PsiModifier.STATIC, PsiModifier.ABSTRACT, PsiModifier.FINAL,
    PsiModifier.NATIVE, PsiModifier.SYNCHRONIZED, PsiModifier.STRICTFP, PsiModifier.TRANSIENT, PsiModifier.VOLATILE, PsiModifier.DEFAULT
  };
  private final GlobalMatchingVisitor myMatchingVisitor;
  private PsiClass myClazz;

  static {
    Arrays.sort(MODIFIERS);
  }

  public JavaMatchingVisitor(GlobalMatchingVisitor matchingVisitor) {
    myMatchingVisitor = matchingVisitor;
  }

  @Override
  public void visitComment(@NotNull PsiComment comment) {
    PsiComment other = null;

    final PsiElement element = myMatchingVisitor.getElement();
    if (!(element instanceof PsiComment)) {
      if (element instanceof PsiMember && PsiTreeUtil.skipWhitespacesAndCommentsForward(comment) instanceof PsiDeclarationStatement) {
        other = ObjectUtils.tryCast(element.getFirstChild(), PsiComment.class);
      }
    }
    else {
      other = (PsiComment)element;
    }

    if (!myMatchingVisitor.setResult(other != null)) {
      return;
    }

    final MatchingHandler handler = comment.getUserData(CompiledPattern.HANDLER_KEY);
    if (handler instanceof SubstitutionHandler substitutionHandler) {
      final RegExpPredicate predicate = substitutionHandler.findPredicate(RegExpPredicate.class);
      if (predicate != null) {
        predicate.setNodeTextGenerator(e -> JavaMatchUtil.getCommentText((PsiComment)e).trim());
        myMatchingVisitor.setResult(substitutionHandler.handle(other, myMatchingVisitor.getMatchContext()));
      }
      else {
        final String text = other.getText();
        final int length = text.length();
        final IElementType tokenType = other.getTokenType();
        int start = tokenType == JavaDocTokenType.DOC_COMMENT_START ? 3 : 2;
        while (start < length && text.charAt(start) == ' ') start++;
        int end = tokenType == JavaTokenType.END_OF_LINE_COMMENT || length < 4 ? length : length - 2;
        while (end > 0 && text.charAt(end - 1) == ' ') end--;
        myMatchingVisitor.setResult(substitutionHandler.handle(other, start, end, myMatchingVisitor.getMatchContext()));
      }
    }
    else if (handler instanceof LiteralWithSubstitutionHandler) {
      if (comment instanceof PsiDocComment) {
        myMatchingVisitor.setResult(handler.match(comment, other, myMatchingVisitor.getMatchContext()));
      } else {
        final LiteralWithSubstitutionHandler lwsHandler = (LiteralWithSubstitutionHandler) handler;
        int offset = other.getTokenType() == JavaDocTokenType.DOC_COMMENT_START ? 3 : 2;
        String commentText = other.getText();
        while (commentText.length() > offset && commentText.charAt(offset) <= ' ') {
          offset++;
        }
        myMatchingVisitor.setResult(lwsHandler.match(other, JavaMatchUtil.getCommentText(other).trim(), offset, myMatchingVisitor.getMatchContext()));
      }
    }
    else if (handler != null) {
      myMatchingVisitor.setResult(handler.match(comment, other, myMatchingVisitor.getMatchContext()));
    }
    else {
      myMatchingVisitor.setResult(myMatchingVisitor.matchText(MatchUtil.normalize(JavaMatchUtil.getCommentText(comment)),
                                                              MatchUtil.normalize(JavaMatchUtil.getCommentText(other))));
    }
  }

  @Override
  public final void visitModifierList(@NotNull PsiModifierList list) {
    final PsiModifierList other = (PsiModifierList)myMatchingVisitor.getElement();

    for (@PsiModifier.ModifierConstant String modifier : MODIFIERS) {
      if (!myMatchingVisitor.setResult(!list.hasModifierProperty(modifier) || other.hasModifierProperty(modifier))) {
        return;
      }
    }

    final PsiAnnotation[] annotations = list.getAnnotations();
    if (annotations.length > 0) {
      final Set<PsiAnnotation> annotationSet = ContainerUtil.newHashSet(annotations);

      for (PsiAnnotation annotation : annotations) {
        final PsiJavaCodeReferenceElement nameReferenceElement = annotation.getNameReferenceElement();

        if (nameReferenceElement != null && MatchOptions.MODIFIER_ANNOTATION_NAME.equals(nameReferenceElement.getText())) {
          final PsiAnnotationParameterList parameterList = annotation.getParameterList();
          final PsiNameValuePair[] attributes = parameterList.getAttributes();

          for (PsiNameValuePair pair : attributes) {
            final PsiAnnotationMemberValue value = pair.getValue();
            if (value == null) continue;

            if (value instanceof PsiArrayInitializerMemberValue) {
              boolean matchedOne = false;

              for (PsiAnnotationMemberValue v : ((PsiArrayInitializerMemberValue)value).getInitializers()) {
                if (annotationValueMatchesModifierList(other, v)) {
                  matchedOne = true;
                  break;
                }
              }

              if (!myMatchingVisitor.setResult(matchedOne)) {
                return;
              }
            }
            else {
              if (!myMatchingVisitor.setResult(annotationValueMatchesModifierList(other, value))) {
                return;
              }
            }
          }

          annotationSet.remove(annotation);
        }
      }

      if (!annotationSet.isEmpty()) {
        final PsiAnnotation[] otherAnnotations = other.getAnnotations();
        final List<PsiElement> unmatchedElements = new SmartList<>(otherAnnotations);
        myMatchingVisitor.getMatchContext().pushMatchedElementsListener(elements -> unmatchedElements.removeAll(elements));
        try {
          myMatchingVisitor.setResult(myMatchingVisitor.matchInAnyOrder(annotationSet.toArray(PsiAnnotation.EMPTY_ARRAY), otherAnnotations));
          other.putUserData(GlobalMatchingVisitor.UNMATCHED_ELEMENTS_KEY, unmatchedElements);
        }
        finally {
          myMatchingVisitor.getMatchContext().popMatchedElementsListener();
        }
      }
    }
  }

  private static boolean annotationValueMatchesModifierList(PsiModifierList list, PsiAnnotationMemberValue value) {
    @PsiModifier.ModifierConstant final String name = StringUtil.unquoteString(value.getText());
    if (MatchOptions.INSTANCE_MODIFIER_NAME.equals(name)) {
      return !list.hasModifierProperty(PsiModifier.STATIC) && !list.hasModifierProperty(PsiModifier.ABSTRACT) &&
             list.getParent() instanceof PsiMember;
    }
    return list.hasModifierProperty(name) && (!PsiModifier.PACKAGE_LOCAL.equals(name) || list.getParent() instanceof PsiMember);
  }

  @Override
  public void visitDocTag(@NotNull PsiDocTag tag) {
    final PsiDocTag other = (PsiDocTag)myMatchingVisitor.getElement();
    final CompiledPattern pattern = myMatchingVisitor.getMatchContext().getPattern();
    final boolean isTypedVar = pattern.isTypedVar(tag.getNameElement());

    if (!isTypedVar && !myMatchingVisitor.setResult(tag.getName().equals(other.getName()))) return;

    PsiElement psiDocTagValue = Arrays.stream(tag.getDataElements()).findFirst().orElse(null);

    if (psiDocTagValue != null) {
      if (psiDocTagValue instanceof PsiDocTagValue) {
        final PsiElement[] children = psiDocTagValue.getChildren();
        if (children.length == 1) {
          psiDocTagValue = children[0];
        }
      }

      if (pattern.isTypedVar(psiDocTagValue) && other.getDataElements().length == 0) {
        myMatchingVisitor.setResult(myMatchingVisitor.allowsAbsenceOfMatch(psiDocTagValue));
        return;
      }
    }

    if (isTypedVar) {
      myMatchingVisitor.setResult(myMatchingVisitor.handleTypedElement(tag.getNameElement(), other.getNameElement()));
    }

    myMatchingVisitor.setResult(myMatchingVisitor.matchOptionally(tag.getDataElements(), other.getDataElements()));
  }

  @Override
  public void visitDocTagValue(@NotNull PsiDocTagValue value) {
    myMatchingVisitor.setResult(substituteOrMatchText(value, myMatchingVisitor.getMatchContext()));
  }

  @Override
  public void visitDocToken(@NotNull PsiDocToken token) {
    myMatchingVisitor.setResult(substituteOrMatchText(token, myMatchingVisitor.getMatchContext()));
  }

  @Override
  public void visitDocComment(@NotNull PsiDocComment comment) {
    final PsiDocComment other = myMatchingVisitor.getElement(PsiDocComment.class);
    if (other == null) return;

    final PsiDocTag[] tags = comment.getTags();
    if (tags.length > 0 && !myMatchingVisitor.setResult(myMatchingVisitor.matchInAnyOrder(tags, other.getTags()))) return;
    visitComment(comment);
  }

  @Override
  public void visitElement(@NotNull PsiElement element) {
    myMatchingVisitor.setResult(myMatchingVisitor.matchText(element, myMatchingVisitor.getElement()));
  }

  @Override
  public void visitArrayInitializerExpression(@NotNull PsiArrayInitializerExpression expression) {
    final PsiArrayInitializerExpression other = getExpression(PsiArrayInitializerExpression.class, expression);
    if (other == null) return;
    myMatchingVisitor.setResult(myMatchingVisitor.matchSequentially(expression.getInitializers(), other.getInitializers()));
  }

  @Override
  public void visitClassInitializer(@NotNull PsiClassInitializer initializer) {
    final PsiClassInitializer other = (PsiClassInitializer)myMatchingVisitor.getElement();
    myMatchingVisitor.setResult(myMatchingVisitor.match(initializer.getModifierList(), other.getModifierList()) &&
                                myMatchingVisitor.matchSons(initializer.getBody(), other.getBody()));
  }

  @Override
  public void visitCodeBlock(@NotNull PsiCodeBlock block) {
    myMatchingVisitor.setResult(myMatchingVisitor.matchSons(block, myMatchingVisitor.getElement()));
  }

  @Override
  public void visitJavaToken(@NotNull PsiJavaToken token) {
    final PsiElement other = myMatchingVisitor.getElement();

    myMatchingVisitor.setResult((!(other instanceof PsiJavaToken) || token.getTokenType() == ((PsiJavaToken)other).getTokenType())
                                && (myMatchingVisitor.getMatchContext().getPattern().isTypedVar(token)
                                    ? myMatchingVisitor.handleTypedElement(token, other)
                                    : myMatchingVisitor.matchText(token, other)));
  }

  @Override
  public void visitAnnotation(@NotNull PsiAnnotation annotation) {
    final PsiAnnotation other = (PsiAnnotation)myMatchingVisitor.getElement();
    if (!myMatchingVisitor.setResult(myMatchingVisitor.match(annotation.getNameReferenceElement(), other.getNameReferenceElement()))) return;
    final PsiNameValuePair[] attributes = annotation.getParameterList().getAttributes();
    if (attributes.length == 0) {
      return;
    }
    final PsiNameValuePair[] otherAttributes = other.getParameterList().getAttributes();
    final List<PsiElement> unmatchedElements = new SmartList<>(otherAttributes);
    final MatchContext context = myMatchingVisitor.getMatchContext();
    context.pushMatchedElementsListener(elements -> unmatchedElements.removeAll(elements));
    try {
      if (myMatchingVisitor.setResult(myMatchingVisitor.matchInAnyOrder(attributes, otherAttributes))) {
        other.putUserData(GlobalMatchingVisitor.UNMATCHED_ELEMENTS_KEY, unmatchedElements);
      }
    } finally {
      context.popMatchedElementsListener();
    }
  }

  @Override
  public void visitNameValuePair(@NotNull PsiNameValuePair pair) {
    final PsiNameValuePair other = (PsiNameValuePair)myMatchingVisitor.getElement();

    final MatchContext context = myMatchingVisitor.getMatchContext();
    final PsiIdentifier nameIdentifier = pair.getNameIdentifier();
    final boolean isTypedVar = context.getPattern().isTypedVar(nameIdentifier);
    if (nameIdentifier != null) context.pushResult();
    final PsiIdentifier otherIdentifier = other.getNameIdentifier();
    try {
      final PsiAnnotationMemberValue value1 = pair.getValue();
      final PsiAnnotationMemberValue value2 = other.getValue();
      if (myMatchingVisitor.setResult(myMatchingVisitor.matchSequentially(getElementToMatch(value1), getElementToMatch(value2)) ||
                                      value1 instanceof PsiReferenceExpression && myMatchingVisitor.match(value1, value2))) {
        if (nameIdentifier != null) {
          myMatchingVisitor.setResult(isTypedVar ||
            myMatchingVisitor.matchText(nameIdentifier.getText(),
                                        otherIdentifier == null ? PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME : otherIdentifier.getText()));
        }
        else {
          myMatchingVisitor.setResult(otherIdentifier == null ||
                                      PsiAnnotation.DEFAULT_REFERENCED_METHOD_NAME.equals(otherIdentifier.getText()));
        }
      }
    } finally {
      final PsiIdentifier matchNode;
      if (otherIdentifier != null) {
        matchNode = otherIdentifier;
      }
      else {
        final PsiElementFactory factory = JavaPsiFacade.getElementFactory(other.getProject());
        final PsiAnnotation annotation =
          (PsiAnnotation)factory.createStatementFromText("@Anno(value=\"\")", other).getFirstChild().getFirstChild();
        matchNode = annotation.getParameterList().getAttributes()[0].getNameIdentifier();
      }
      if (nameIdentifier != null) myMatchingVisitor.scopeMatch(nameIdentifier, isTypedVar, matchNode);
    }
  }

  private static PsiElement getElementToMatch(PsiAnnotationMemberValue value) {
    if (value instanceof PsiArrayInitializerMemberValue arrayInitializer) {
      PsiAnnotationMemberValue[] initializers = arrayInitializer.getInitializers();
      if (initializers.length > 0) {
        return initializers[0];
      }
    }
    return value;
  }

  private boolean checkHierarchy(PsiMember element, PsiMember patternElement) {
    final MatchingHandler handler = myMatchingVisitor.getMatchContext().getPattern().getHandler(patternElement);
    if (handler instanceof SubstitutionHandler substitutionHandler) {
      if (!substitutionHandler.isSubtype()) {
        if (substitutionHandler.isStrictSubtype()) {
          // check if element is declared not in current class  (in ancestors)
          return element.getContainingClass() != myClazz;
        }
      }
      else {
        return true;
      }
    }

    // check if element is declared in current class (not in ancestors)
    return myClazz == null || element.getContainingClass() == myClazz;
  }

  @Override
  public void visitField(@NotNull PsiField field) {
    final PsiField other = myMatchingVisitor.getElement(PsiField.class);
    if (other == null) return;
    final PsiDocComment comment = field.getDocComment();
    if (comment != null && !myMatchingVisitor.setResult(myMatchingVisitor.match(comment, other.getDocComment()))) return;
    if (!myMatchingVisitor.setResult(checkHierarchy(other, field))) return;
    super.visitField(field);
  }

  @Override
  public void visitEnumConstant(@NotNull PsiEnumConstant enumConstant) {
    final PsiEnumConstant other = myMatchingVisitor.getElement(PsiEnumConstant.class);
    if (other == null) return;
    final PsiExpressionList argumentList = enumConstant.getArgumentList();
    if (argumentList != null && !myMatchingVisitor.setResult(myMatchingVisitor.matchSons(argumentList, other.getArgumentList()))) return;
    final PsiEnumConstantInitializer enumConstantInitializer = enumConstant.getInitializingClass();
    if (enumConstantInitializer != null &&
        !myMatchingVisitor.setResult(myMatchingVisitor.match(enumConstantInitializer, other.getInitializingClass()))) return;
    super.visitEnumConstant(enumConstant);
  }

  @Override
  public void visitAnonymousClass(@NotNull PsiAnonymousClass clazz) {
    final PsiAnonymousClass other = myMatchingVisitor.getElement(PsiAnonymousClass.class);
    if (other == null) return;
    final PsiElement classReference = clazz.getBaseClassReference();
    final boolean isTypedVar = myMatchingVisitor.getMatchContext().getPattern().isTypedVar(classReference);

    if (myMatchingVisitor.setResult((isTypedVar || myMatchingVisitor.match(clazz.getBaseClassReference(), other.getBaseClassReference())) &&
                                    myMatchingVisitor.matchSons(clazz.getArgumentList(), other.getArgumentList()) &&
                                    matchClasses(clazz, other)) && isTypedVar) {
      myMatchingVisitor.setResult(classReference instanceof LightElement || matchType(classReference, other.getBaseClassReference()));
    }
  }

  @Override
  public void visitLambdaExpression(@NotNull PsiLambdaExpression expression) {
    final PsiLambdaExpression other = getExpression(PsiLambdaExpression.class, expression);
    if (other == null) return;
    final PsiParameterList parameterList1 = expression.getParameterList();
    if (!myMatchingVisitor.setResult(
      parameterList1.isEmpty() || myMatchingVisitor.matchSons(parameterList1, other.getParameterList()))) return;
    final PsiElement body1 = getElementToMatch(expression.getBody());
    if (body1 == null) {
      return;
    }
    final PsiElement body2 = getElementToMatch(other.getBody());
    if (body1 instanceof PsiExpression && (body2 == null || body2 instanceof PsiStatement || body2 instanceof PsiComment)) {
      final PsiElement parent = body1.getParent();
      myMatchingVisitor.setResult(parent instanceof PsiStatement
                                  ? myMatchingVisitor.matchSequentially(parent, body2)
                                  : myMatchingVisitor.matchSequentially(body1, (body2 == null) ? other.getBody() : body2.getParent()));
    }
    else {
      myMatchingVisitor.setResult(myMatchingVisitor.matchSequentially(body1, body2));
    }
  }

  private PsiElement getElementToMatch(PsiElement element) {
    if (myMatchingVisitor.getMatchContext().getOptions().isLooseMatching()) {
      if (element instanceof PsiCodeBlock) {
        final List<PsiElement> list = PsiTreeUtil.getChildrenOfAnyType(element, PsiStatement.class, PsiComment.class);
        if (list.isEmpty()) return null;
        element = list.get(0);
        if (list.size() > 1) return element;
      }
      if (element instanceof PsiExpressionStatement expressionStatement) {
        element = expressionStatement.getExpression();
      }
      if (element instanceof PsiReturnStatement returnStatement) {
        element = returnStatement.getReturnValue();
      }
    }
    return element;
  }

  private boolean matchInAnyOrder(PsiReferenceList patternElements, PsiReferenceList matchElements) {
    if (patternElements == null) return myMatchingVisitor.isLeftLooseMatching() || matchElements == null;

    return myMatchingVisitor.matchInAnyOrder(
      patternElements.getReferenceElements(),
      (matchElements != null) ? matchElements.getReferenceElements() : PsiElement.EMPTY_ARRAY
    );
  }

  @Override
  public void visitRecordHeader(@NotNull PsiRecordHeader recordHeader) {
    final PsiRecordHeader other = myMatchingVisitor.getElement(PsiRecordHeader.class);
    myMatchingVisitor.setResult(myMatchingVisitor.matchSequentially(recordHeader.getRecordComponents(), other.getRecordComponents()));
  }

  private boolean matchClasses(PsiClass patternClass, PsiClass matchClass) {
    final PsiClass saveClazz = myClazz;
    myClazz = matchClass;
    final MatchContext context = myMatchingVisitor.getMatchContext();
    final JavaCompiledPattern javaPattern = (JavaCompiledPattern)context.getPattern();

    final Set<PsiElement> matchedElements = new HashSet<>();
    context.pushMatchedElementsListener(elements -> matchedElements.addAll(elements));
    try {
      final boolean templateIsInterface = patternClass.isInterface();
      if (templateIsInterface && !matchClass.isInterface()) return false;
      if (templateIsInterface && patternClass.isAnnotationType() && !matchClass.isAnnotationType()) return false;
      if (patternClass.isEnum() && !matchClass.isEnum()) return false;
      if (patternClass instanceof PsiTypeParameter != matchClass instanceof PsiTypeParameter) return false;
      if (patternClass.isRecord() && !matchClass.isRecord()) return false;

      if (!myMatchingVisitor.match(patternClass.getRecordHeader(), matchClass.getRecordHeader())) {
        return false;
      }
      if (!matchInAnyOrder(patternClass.getExtendsList(), matchClass.getExtendsList())) {
        return false;
      }

      // check if implements is in extended classes implements
      final PsiReferenceList implementsList = patternClass.getImplementsList();
      if (implementsList != null) {
        if (implementsList.getFirstChild() != null && matchClass.isInterface()) return false;
        final List<PsiJavaCodeReferenceElement> elements = new SmartList<>();
        for (PsiJavaCodeReferenceElement element : implementsList.getReferenceElements()) {
          final MatchingHandler handler = javaPattern.getHandler(element);
          if (handler instanceof SubstitutionHandler substitutionHandler &&
              (substitutionHandler.isSubtype() || substitutionHandler.isStrictSubtype())) {
            if (!matchWithinHierarchy(element, matchClass, substitutionHandler)) {
              return false;
            }
            continue;
          }
          elements.add(element);
        }
        if (!elements.isEmpty()) {
          final PsiReferenceList implementsList2 = matchClass.getImplementsList();
          final PsiElement[] matchElements = (implementsList2 == null) ? PsiElement.EMPTY_ARRAY : implementsList2.getReferenceElements();
          if (!myMatchingVisitor.matchInAnyOrder(elements.toArray(PsiElement.EMPTY_ARRAY), matchElements)) {
            return false;
          }
        }
      }

      final PsiField[] fields = PsiTreeUtil.getChildrenOfType(patternClass, PsiField.class);
      if (fields != null) {
        final PsiField[] fields2 = javaPattern.isRequestsSuperFields() ?
                                   matchClass.getAllFields() :
                                   matchClass.getFields();

        if (!myMatchingVisitor.matchInAnyOrder(fields, fields2)) {
          return false;
        }
      }

      final PsiMethod[] methods = PsiTreeUtil.getChildrenOfType(patternClass, PsiMethod.class);
      if (methods != null) {
        final PsiMethod[] methods2 = javaPattern.isRequestsSuperMethods() ?
                                     matchClass.getAllMethods() :
                                     matchClass.getMethods();

        if (!myMatchingVisitor.matchInAnyOrder(methods, methods2)) {
          return false;
        }
      }

      final PsiClass[] nestedClasses = PsiTreeUtil.getChildrenOfType(patternClass, PsiClass.class);
      if (nestedClasses != null) {
        final PsiClass[] nestedClasses2 = javaPattern.isRequestsSuperInners() ?
                                          matchClass.getAllInnerClasses() :
                                          matchClass.getInnerClasses();

        if (!myMatchingVisitor.matchInAnyOrder(nestedClasses, nestedClasses2)) {
          return false;
        }
      }

      final PsiClassInitializer[] initializers = patternClass.getInitializers();
      if (initializers.length > 0) {
        final PsiClassInitializer[] initializers2 = matchClass.getInitializers();

        if (!myMatchingVisitor.matchInAnyOrder(initializers, initializers2)) {
          return false;
        }
      }

      final List<PsiElement> unmatchedElements = new SmartList<>(PsiTreeUtil.getChildrenOfTypeAsList(matchClass, PsiMember.class));
      unmatchedElements.removeAll(matchedElements);
      MatchingHandler unmatchedSubstitutionHandler = null;
      for (PsiElement element = patternClass.getLBrace(); element != null; element = element.getNextSibling()) {
        if (element instanceof PsiTypeElement && element.getNextSibling() instanceof PsiErrorElement) {
          unmatchedSubstitutionHandler = javaPattern.getHandler(element);
          break;
        }
      }
      if (unmatchedSubstitutionHandler instanceof SubstitutionHandler handler) {
        for (PsiElement element : unmatchedElements) {
          handler.handle(element, context);
        }
      } else {
        matchClass.putUserData(GlobalMatchingVisitor.UNMATCHED_ELEMENTS_KEY, unmatchedElements);
      }

      return true;
    }
    finally {
      myClazz = saveClazz;
      context.popMatchedElementsListener();
    }
  }

  private boolean matchBody(PsiElement patternElement, PsiElement matchElement) {
    if (myMatchingVisitor.getMatchContext().getOptions().isLooseMatching()) {
      if (matchElement instanceof PsiBlockStatement) {
        final PsiCodeBlock codeBlock = ((PsiBlockStatement)matchElement).getCodeBlock();
        if (patternElement instanceof PsiBlockStatement || codeBlock.getStatementCount() == 1) {
          matchElement = codeBlock.getFirstChild();
        }
      }
      if (patternElement instanceof PsiBlockStatement) {
        patternElement = ((PsiBlockStatement)patternElement).getCodeBlock().getFirstChild();
      }
    }

    return myMatchingVisitor.matchSequentially(patternElement, matchElement);
  }

  @Override
  public void visitArrayAccessExpression(@NotNull PsiArrayAccessExpression expression) {
    final PsiArrayAccessExpression other = getExpression(PsiArrayAccessExpression.class, expression);
    if (other != null) {
      myMatchingVisitor.setResult(myMatchingVisitor.match(expression.getArrayExpression(), other.getArrayExpression()) &&
                                  myMatchingVisitor.match(expression.getIndexExpression(), other.getIndexExpression()));
    }
  }

  @Override
  public void visitMethodReferenceExpression(@NotNull PsiMethodReferenceExpression expression) {
    if (getExpression(PsiMethodReferenceExpression.class, expression) == null) return;
    super.visitMethodReferenceExpression(expression);
  }








  @Override
  public void visitReferenceExpression(@NotNull PsiReferenceExpression reference) {
    final MatchContext context = myMatchingVisitor.getMatchContext();
    final CompiledPattern pattern = context.getPattern();
    final PsiElement referenceNameElement = reference.getReferenceNameElement();
    assert referenceNameElement != null;
    MatchingHandler _handler = pattern.getHandlerSimple(referenceNameElement);
    boolean special = false;
    if (_handler == null) {
      _handler = pattern.getHandlerSimple(reference);
      special = reference.resolve() != null;
    }

    final PsiElement other = myMatchingVisitor.getElement();
    final PsiElement unwrapped = unwrap(other, context);
    final PsiExpression qualifier = reference.getQualifierExpression();
    if (_handler instanceof SubstitutionHandler handler && (qualifier == null || special)) {
      if (other instanceof PsiReferenceExpression && other.getParent() instanceof PsiMethodCallExpression) {
        myMatchingVisitor.setResult(false);
        return;
      }
      if (handler.isSubtype() || handler.isStrictSubtype()) {
        if (myMatchingVisitor.setResult(matchWithinHierarchy(reference, unwrapped, handler))) {
          handler.addResult(other, myMatchingVisitor.getMatchContext());
        }
      }
      else if (myMatchingVisitor.setResult(handler.validate(unwrapped, context))) {
        handler.addResult(other, context);
      }
      return;
    }

    final boolean multiMatch = unwrapped != null && reference.getContainingFile() == unwrapped.getContainingFile();
    if (!(unwrapped instanceof PsiReferenceExpression reference2)) {
      // when the same variable is used multiple times in a pattern, we need to check if they are the same,
      // but sometimes they are not normally comparable. In this case we fall back to a text comparison.
      // For example in the pattern `boolean equals(Object $x$) { return super.equals($x$); }`
      // the PsiReferenceExpression argument will be compared to the PsiIdentifier of the parameter.
      myMatchingVisitor.setResult(multiMatch && myMatchingVisitor.matchText(reference, unwrapped));
      return;
    }

    final PsiExpression qualifier2 = reference2.getQualifierExpression();
    if (multiMatch &&
        (qualifier == null || qualifier instanceof PsiThisExpression || qualifier instanceof PsiSuperExpression) &&
        (qualifier2 == null || qualifier2 instanceof PsiThisExpression || qualifier2 instanceof PsiSuperExpression)) {
      final PsiElement target = reference.resolve();
      if (target != null) {
        myMatchingVisitor.setResult(target == reference2.resolve());
        return;
      }
    }
    if (qualifier == null && qualifier2 == null) {
      myMatchingVisitor.setResult(myMatchingVisitor.matchText(referenceNameElement, reference2.getReferenceNameElement()));
      return;
    }

    // handle field selection
    if (myMatchingVisitor.setResult(!(other.getParent() instanceof PsiMethodCallExpression) && qualifier != null)) {
      final PsiElement referenceNameElement2 = reference2.getReferenceNameElement();

      if (pattern.isTypedVar(referenceNameElement)) {
        if (!myMatchingVisitor.setResult(myMatchingVisitor.handleTypedElement(referenceNameElement, referenceNameElement2))) return;
      }
      else {
        if (!myMatchingVisitor.setResult(myMatchingVisitor.matchText(referenceNameElement, referenceNameElement2))) return;
      }

      if (!myMatchingVisitor.setResult(qualifier instanceof PsiThisExpression && qualifier2 == null ||
                                       myMatchingVisitor.matchOptionally(qualifier, qualifier2))) return;
      if (qualifier2 == null) myMatchingVisitor.setResult(matchImplicitQualifier(qualifier, unwrapped, context));
    }
  }

  /** Removes parentheses from the element if it is a parenthesized expression. */
  private static PsiElement unwrap(PsiElement element, @NotNull MatchContext context) {
    return context.getOptions().isLooseMatching() && element instanceof PsiExpression
           ? PsiUtil.skipParenthesizedExprDown((PsiExpression)element)
           : element;
  }

  private boolean substituteOrMatchText(@NotNull PsiElement element, @NotNull MatchContext context) {
    final var handler = context.getPattern().getHandler(element);
    final PsiElement other = myMatchingVisitor.getElement();
    if (handler instanceof SubstitutionHandler substitutionHandler) {
      return substitutionHandler.handle(other, context);
    } else {
      return myMatchingVisitor.matchText(element, other);
    }
  }

  private static int getArrayDimensions(PsiElement element) {
    if (element == null) {
      return 0;
    }
    final PsiElement parent = element.getParent();
    if (parent instanceof PsiVariable variable) {
      final PsiType type = variable.getType();
      return type.getArrayDimensions();
    }
    else if (parent instanceof PsiMethod method) {
      final PsiType type = method.getReturnType();
