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

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.usageView.UsageInfo;
import com.intellij.util.CommonJavaRefactoringUtil;
import org.jetbrains.annotations.NotNull;

import java.util.List;

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

  private JavaUnresolvableLocalCollisionDetector() {
  }

  public static void findCollisions(final PsiElement element, final String newName, final List<? super UsageInfo> result) {
    if (!PsiUtil.isJvmLocalVariable(element)) {
      return;
    }

    PsiVariable variable = (PsiVariable)element;
    PsiElement scope;
    if (variable instanceof PsiLocalVariable local) {
      scope = CommonJavaRefactoringUtil.getVariableScope(local);
    }
    else {
      // element is a PsiParameter
      scope = ((PsiParameter)element).getDeclarationScope();
    }
    String oldName = variable.getName();
    LOG.assertTrue(scope != null, element.getClass().getName());
    boolean methodParameter = element instanceof PsiParameter parameter && parameter.getDeclarationScope() instanceof PsiMethod;
    
    PsiResolveHelper helper = PsiResolveHelper.getInstance(element.getProject());
    final CollidingVariableVisitor collidingNameVisitor = new CollidingVariableVisitor() {
      @Override
      public void visitCollidingElement(PsiVariable collidingVariable) {
        if (collidingVariable.equals(element)) return;
        if (collidingVariable.isUnnamed()) return;
        if (!methodParameter &&
            helper.resolveAccessibleReferencedVariable(newName, element) != collidingVariable &&
            helper.resolveAccessibleReferencedVariable(oldName, collidingVariable) != element) return;
        LocalHidesRenamedLocalUsageInfo collision = new LocalHidesRenamedLocalUsageInfo(collidingVariable, element);
        result.add(collision);
      }
    };

    visitLocalsCollisions(element, newName, scope, null, collidingNameVisitor);
  }

  public static void visitLocalsCollisions(PsiElement element, final String newName,
                                           PsiElement scope,
                                           PsiElement place,
                                           final CollidingVariableVisitor collidingNameVisitor) {
    if (scope == null) return;
    visitDownstreamCollisions(scope, place, newName, collidingNameVisitor);
    visitUpstreamLocalCollisions(element, scope, newName, collidingNameVisitor);
  }

  private static void visitDownstreamCollisions(PsiElement scope, PsiElement place, final String newName,
                                                final CollidingVariableVisitor collidingNameVisitor
  ) {
    ConflictingLocalVariablesVisitor collector =
      new ConflictingLocalVariablesVisitor(newName, collidingNameVisitor);
    if (place == null) {
      scope.accept(collector);
    }
    else {
      LOG.assertTrue(place.getParent() == scope);
      for (PsiElement sibling = place; sibling != null; sibling = sibling.getNextSibling()) {
        sibling.accept(collector);
      }
    }
  }

  public interface CollidingVariableVisitor {
    void visitCollidingElement(PsiVariable collidingVariable);
  }

  private static void visitUpstreamLocalCollisions(PsiElement element, PsiElement scope,
                                                  String newName,
                                                  final CollidingVariableVisitor collidingNameVisitor) {
    final PsiVariable collidingVariable =
      JavaPsiFacade.getInstance(scope.getProject()).getResolveHelper().resolveAccessibleReferencedVariable(newName, scope);
    if (collidingVariable instanceof PsiLocalVariable || collidingVariable instanceof PsiParameter) {
      final PsiElement commonParent = PsiTreeUtil.findCommonParent(element, collidingVariable);
      if (commonParent != null) {
        PsiElement current = element;
        while (current != null && current != commonParent) {
          if (current instanceof PsiMethod || current instanceof PsiClass) {
            return;
          }
          current = current.getParent();
        }
      }
      collidingNameVisitor.visitCollidingElement(collidingVariable);
    }
  }

  public static class ConflictingLocalVariablesVisitor extends JavaRecursiveElementWalkingVisitor {
    protected final String myName;
    protected CollidingVariableVisitor myCollidingNameVisitor;

    public ConflictingLocalVariablesVisitor(String newName, CollidingVariableVisitor collidingNameVisitor) {
      myName = newName;
      myCollidingNameVisitor = collidingNameVisitor;
    }

    @Override public void visitReferenceExpression(@NotNull PsiReferenceExpression expression) {
      visitElement(expression);
    }

    @Override
    public void visitField(@NotNull PsiField field) {
      if (myName.equals(field.getName())) {
        myCollidingNameVisitor.visitCollidingElement(field);
      }
    }

    @Override public void visitVariable(@NotNull PsiVariable variable) {
      if (myName.equals(variable.getName())) {
        myCollidingNameVisitor.visitCollidingElement(variable);
      }
    }
  }



}
