// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package org.jetbrains.plugins.groovy.refactoring.introduce.parameter.java2groovy;

import com.intellij.codeInsight.ChangeContextUtil;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.search.LocalSearchScope;
import com.intellij.psi.search.searches.ReferencesSearch;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.IntroduceParameterRefactoring;
import com.intellij.refactoring.util.RefactoringUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.lang.lexer.GroovyTokenTypes;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.GroovyResolveResult;
import org.jetbrains.plugins.groovy.lang.psi.api.signatures.GrSignature;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrStatement;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariableDeclaration;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.params.GrParameter;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
import org.jetbrains.plugins.groovy.lang.psi.impl.signatures.GrClosureSignatureUtil;
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyPropertyUtils;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;

import java.util.*;

import static org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil.getNewName;

/**
 * @author Maxim.Medvedev
 */

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

  private final GrCall myContext;
  private final GrExpression myExpr;
  private final HashMap<GrExpression, String> myTempVars;
  private final GrExpression myInstanceRef;
  private final GrClosureSignatureUtil.ArgInfo<PsiElement>[] myActualArgs;
  private final PsiElement myToReplaceIn;
  private final Project myProject;
  private final int myReplaceFieldsWithGetters;
  private final PsiElement myParameterInitializer;
  private final PsiManager myManager;
  private final PsiParameter[] myParameters;
  private final GrSignature mySignature;

  private final Set<PsiParameter> myParamsToNotInline = new HashSet<>();

  public OldReferencesResolver(GrCall context,
                               GrExpression expr,
                               PsiElement toReplaceIn,
                               int replaceFieldsWithGetters,
                               PsiElement parameterInitializer,
                               final GrSignature signature,
                               final GrClosureSignatureUtil.ArgInfo<PsiElement>[] actualArgs, PsiParameter[] parameters) throws IncorrectOperationException {
    myContext = context;
    myExpr = expr;
    myReplaceFieldsWithGetters = replaceFieldsWithGetters;
    myParameterInitializer = parameterInitializer;
    myParameters = parameters;
    myTempVars = new HashMap<>();
    mySignature = signature;
    myActualArgs = actualArgs;
    myToReplaceIn = toReplaceIn;
    myProject = myContext.getProject();
    myManager = myContext.getManager();

    if (myContext instanceof GrMethodCallExpression methodCall) {
      final GrExpression methodExpression = methodCall.getInvokedExpression();
      if (methodExpression instanceof GrReferenceExpression) {
        myInstanceRef = ((GrReferenceExpression)methodExpression).getQualifierExpression();
      }
      else if (methodExpression instanceof GrMethodCall) {
        myInstanceRef = getQualifierFromGetterCall((GrMethodCall)methodExpression);
      }
      else {
        myInstanceRef = null;
      }
    }
    else {
      myInstanceRef = null;
    }
  }

  /**
   * checks for the case: qualifier.getFoo()(args)
   */
  private static @Nullable GrExpression getQualifierFromGetterCall(GrMethodCall methodExpression) {
    final GroovyResolveResult result = methodExpression.advancedResolve();
    if (!(result.getElement() instanceof GrAccessorMethod) || result.isInvokedOnProperty()) return null;

    final GrExpression invoked = methodExpression.getInvokedExpression();
    if (invoked instanceof GrReferenceExpression) return ((GrReferenceExpression)invoked).getQualifier();

    return null;
  }

  public void resolve() throws IncorrectOperationException {
    resolveOldReferences(myExpr, myParameterInitializer);

    Set<Map.Entry<GrExpression, String>> mappingsSet = myTempVars.entrySet();

    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myProject);

    for (Map.Entry<GrExpression, String> entry : mappingsSet) {
      GrExpression oldRef = entry.getKey();
      PsiElement newRef = factory.createExpressionFromText(entry.getValue());
      oldRef.replace(newRef);
    }
  }


  private void resolveOldReferences(PsiElement expr, PsiElement oldExpr) throws IncorrectOperationException {
    if (expr == null || !expr.isValid() || oldExpr == null) return;
    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myProject);
    PsiElement newExpr = expr;  // references continue being resolved in the children of newExpr

    if (oldExpr instanceof GrReferenceExpression oldRef) {
      if (isThisReferenceToContainingClass(oldExpr) || isSimpleSuperReference(oldExpr)) {
        if (myInstanceRef != null) {
          newExpr.replace(getInstanceRef(factory));
        }
        return;
      }

      newExpr = newExpr.replace(decodeReferenceExpression((GrReferenceExpression)newExpr, oldRef));
      //newExpr = ((GrReferenceExpression)newExpr).getReferenceNameElement();
      final GroovyResolveResult adv = oldRef.advancedResolve();
      final PsiElement scope = getClassContainingResolve(adv);
      final PsiElement owner = PsiUtil.getContextClass(oldExpr);

      if (myToReplaceIn instanceof GrClosableBlock || (owner != null && scope != null && PsiTreeUtil.isContextAncestor(owner, scope, false))) {

        final PsiElement subj = adv.getElement();

        // Parameters
        if (subj instanceof PsiParameter) {
          int index = ArrayUtil.indexOf(myParameters, subj);
          if (index < 0) return;
          if (index < myParameters.length) {
            newExpr = inlineParam(newExpr, getActualArg(index), ((PsiParameter)subj));
          }
        }
        // "naked" field and methods  (should become qualified)
        else if ((subj instanceof PsiField || subj instanceof PsiMethod) && oldRef.getQualifierExpression() == null) {

          PsiElement newResolved = newExpr instanceof GrReferenceExpression ? ((GrReferenceExpression)newExpr).resolve() : null;
          if (myInstanceRef != null || !subj.getManager().areElementsEquivalent(newResolved, subj)) {
            boolean isStatic = subj instanceof PsiField && ((PsiField)subj).hasModifierProperty(PsiModifier.STATIC) ||
                               subj instanceof PsiMethod && ((PsiMethod)subj).hasModifierProperty(PsiModifier.STATIC);

            String name = getNewName((PsiNamedElement)subj, adv.isInvokedOnProperty());
            boolean shouldBeAt = subj instanceof PsiField &&
                                 !PsiTreeUtil.isAncestor(((PsiMember)subj).getContainingClass(), newExpr, true) &&
                                 GroovyPropertyUtils.findGetterForField((PsiField)subj) != null;
            final GrReferenceExpression fromText = factory.createReferenceExpressionFromText("qualifier." + (shouldBeAt ? "@" : "") + name);
            if (isStatic) {
              final GrReferenceExpression qualifier = factory.createReferenceElementForClass(((PsiMember)subj).getContainingClass());
              newExpr = newExpr.replace(fromText);
              ((GrReferenceExpression)newExpr).setQualifier(qualifier);
              newExpr = ((GrReferenceExpression)newExpr).getReferenceNameElement();
            }
            else {
              if (myInstanceRef != null) {
                GrExpression instanceRef = getInstanceRef(factory);
                fromText.setQualifier(instanceRef);
                newExpr = newExpr.replace(fromText);
                newExpr = ((GrReferenceExpression)newExpr).getReferenceNameElement();
              }
            }
          }
        }

        if (subj instanceof PsiField) {
          // probably replacing field with a getter
          if (myReplaceFieldsWithGetters != IntroduceParameterRefactoring.REPLACE_FIELDS_WITH_GETTERS_NONE) {
            if (myReplaceFieldsWithGetters == IntroduceParameterRefactoring.REPLACE_FIELDS_WITH_GETTERS_ALL ||
                myReplaceFieldsWithGetters == IntroduceParameterRefactoring.REPLACE_FIELDS_WITH_GETTERS_INACCESSIBLE &&
                !JavaPsiFacade.getInstance(myProject).getResolveHelper().isAccessible((PsiMember)subj, newExpr, null)) {
              newExpr = replaceFieldWithGetter(newExpr, (PsiField)subj);
            }
          }
        }
      }
    }
    else {
      PsiClass refClass = oldExpr.getCopyableUserData(ChangeContextUtil.REF_CLASS_KEY);
      if (refClass != null && refClass.isValid()) {
        PsiReference ref = newExpr.getReference();
        if (ref != null) {
          final String qualifiedName = refClass.getQualifiedName();
          if (qualifiedName != null) {
            if (JavaPsiFacade.getInstance(refClass.getProject()).findClass(qualifiedName, oldExpr.getResolveScope()) != null) {
              newExpr = ref.bindToElement(refClass);
            }
          }
        }
      }
    }


    PsiElement[] oldChildren = oldExpr.getChildren();
    PsiElement[] newChildren = newExpr.getChildren();

    if (oldExpr instanceof GrNewExpression && newExpr instanceof GrNewExpression) { //special new-expression case
      resolveOldReferences(((GrNewExpression)newExpr).getReferenceElement(),
                           ((GrNewExpression)oldExpr).getReferenceElement());

      resolveOldReferences(((GrNewExpression)newExpr).getArgumentList(), ((GrNewExpression)oldExpr).getArgumentList());
      if (newChildren[1] instanceof GrArrayDeclaration) {
        for (GrExpression expression : ((GrArrayDeclaration)newChildren[1]).getBoundExpressions()) {
          resolveOldReferences(expression, oldChildren[1]);
        }
      }
    }
    else {
      if (oldExpr instanceof GrReferenceExpression && newExpr instanceof GrReferenceExpression) {
        final GrExpression oldQualifier = ((GrReferenceExpression)oldExpr).getQualifierExpression();
        final GrExpression newQualifier = ((GrReferenceExpression)newExpr).getQualifierExpression();
        if (oldQualifier != null && newQualifier != null) {
          resolveOldReferences(newQualifier, oldQualifier);
          return;
        }
      }

      if (oldChildren.length == newChildren.length) {
        for (int i = 0; i < newChildren.length; i++) {
          resolveOldReferences(newChildren[i], oldChildren[i]);
        }
      }
    }
  }

  private PsiElement inlineParam(PsiElement newExpr, GrExpression actualArg, PsiParameter parameter) {
    if (myParamsToNotInline.contains(parameter)) return newExpr;

    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myProject);

    if (myExpr instanceof GrClosableBlock) {
      int count = 0;
      for (PsiReference reference : ReferencesSearch.search(parameter, new LocalSearchScope(myParameterInitializer)).asIterable()) {
        count++;
        if (count > 1) break;
      }
      if (count > 1) {
        myParamsToNotInline.add(parameter);

        final PsiType type;
        if (parameter instanceof GrParameter) {
          type = ((GrParameter)parameter).getDeclaredType();
        }
        else {
          type = parameter.getType();
        }
        final GrVariableDeclaration declaration =
          factory.createVariableDeclaration(ArrayUtilRt.EMPTY_STRING_ARRAY, actualArg, type, parameter.getName());

        final GrStatement[] statements = ((GrClosableBlock)myExpr).getStatements();
        GrStatement anchor = statements.length > 0 ? statements[0] : null;
        return ((GrClosableBlock)myExpr).addStatementBefore(declaration, anchor);
      }
    }


    int copyingSafetyLevel = GroovyRefactoringUtil.verifySafeCopyExpression(actualArg);
    if (copyingSafetyLevel == RefactoringUtil.EXPR_COPY_PROHIBITED) {
      actualArg = factory.createExpressionFromText(getTempVar(actualArg));
    }
    newExpr = newExpr.replace(actualArg);
    return newExpr;
  }

  private static boolean isSimpleSuperReference(PsiElement oldExpr) {
    if (oldExpr instanceof GrReferenceExpression ref) {
      if (ref.getQualifier() == null) {
        PsiElement nameElement = ref.getReferenceNameElement();
        if (nameElement != null) {
          return nameElement.getNode().getElementType() == GroovyTokenTypes.kSUPER;
        }
      }
    }
    return false;
  }

  private boolean isThisReferenceToContainingClass(PsiElement oldExpr) {
    if (!(oldExpr instanceof GrReferenceExpression && PsiUtil.isThisReference(oldExpr))) return false;

    final GrReferenceExpression qualifier = (GrReferenceExpression)((GrReferenceExpression)oldExpr).getQualifier();
    if (qualifier == null) return true;

    final PsiClass contextClass = PsiUtil.getContextClass(myToReplaceIn);
    final PsiElement resolved = qualifier.resolve();
    return myManager.areElementsEquivalent(resolved, contextClass);
  }

  private @NotNull GrExpression getActualArg(int index) {
    if (myActualArgs == null || myActualArgs[index] == null) {
      final GrExpression[] arguments = myContext.getArgumentList().getExpressionArguments();
      if (index < arguments.length) return arguments[index];
      index -= arguments.length;
      final GrClosableBlock[] closureArguments = myContext.getClosureArguments();
      if (index < closureArguments.length) return closureArguments[index];
      throw new IncorrectOperationException("fail :(");
    }

    final GrClosureSignatureUtil.ArgInfo<PsiElement> argInfo = myActualArgs[index];
    final List<PsiElement> args = argInfo.args;
    if (argInfo.isMultiArg) {
      return GroovyRefactoringUtil.generateArgFromMultiArg(mySignature.getSubstitutor(), args, myParameters[index].getType(),
                                                           myContext.getProject());
    }
    else if (args.isEmpty()) {
      final PsiParameter parameter = myParameters[index];
      LOG.assertTrue(parameter instanceof GrParameter);
      final GrExpression initializer = ((GrParameter)parameter).getInitializerGroovy();
      LOG.assertTrue(initializer != null);
      return (GrExpression)initializer.copy();
    }
    else {
      return (GrExpression)args.get(0);
    }
  }

  private GrExpression getInstanceRef(GroovyPsiElementFactory factory) throws IncorrectOperationException {
    int copyingSafetyLevel = GroovyRefactoringUtil.verifySafeCopyExpression(myInstanceRef);

    GrExpression instanceRef = myInstanceRef;
    if (copyingSafetyLevel == RefactoringUtil.EXPR_COPY_PROHIBITED) {
      instanceRef = factory.createExpressionFromText(getTempVar(myInstanceRef));
    }
    return instanceRef;
  }

  private String getTempVar(GrExpression expr) throws IncorrectOperationException {
    String id = myTempVars.get(expr);
    if (id == null) {
      id = GroovyRefactoringUtil.createTempVar(expr, myContext, true);
      myTempVars.put(expr, id);
    }
    return id;
  }

  private static PsiElement replaceFieldWithGetter(PsiElement expr, PsiField psiField) throws IncorrectOperationException {
    if (RefactoringUtil.isAssignmentLHS(expr)) {
      // todo: warning
      return expr;
    }
    PsiElement newExpr = expr;

    PsiMethod getter = GroovyPropertyUtils.findGetterForField(psiField);

    if (getter != null) {
      if (JavaPsiFacade.getInstance(psiField.getProject()).getResolveHelper().isAccessible(getter, newExpr, null)) {

        GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(newExpr.getProject());
        String id = getter.getName();
        final PsiElement parent = newExpr.getParent();
        String qualifier = null;
        if (parent instanceof GrReferenceExpression) {
          final GrExpression qualifierExpression = ((GrReferenceExpression)parent).getQualifierExpression();
          if (qualifierExpression != null) {
            qualifier = qualifierExpression.getText();
          }
        }
        GrExpression getterCall;
        if (PsiTreeUtil.isAncestor(psiField.getContainingClass(), expr, true)) {
          getterCall = factory.createExpressionFromText((qualifier != null ? qualifier + "." : "") + id + "()");
        }
        else {
          getterCall = factory.createExpressionFromText((qualifier != null ? qualifier + "." : "") + psiField.getName());
        }
        if (parent != null) {
          newExpr = parent.replace(getterCall);
        }
        else {
          newExpr = expr.replace(getterCall);
        }
      }
      else {
        // todo: warning
      }
    }

    return newExpr;
  }

  private static @Nullable PsiElement getClassContainingResolve(final GroovyResolveResult result) {
    final PsiElement elem = result.getElement();
    if (elem != null) {
      if (elem instanceof PsiMember) {
        return ((PsiMember)elem).getContainingClass();
      }
      else {
        return PsiUtil.getContextClass(elem);
      }
    }
    return null;
  }

  private static GrReferenceExpression decodeReferenceExpression(GrReferenceExpression newExpr, GrReferenceExpression refExpr)
    throws IncorrectOperationException {
    PsiManager manager = refExpr.getManager();
    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(manager.getProject());

    GrExpression qualifier = refExpr.getQualifier();
    if (qualifier == null) {
      PsiMember refMember = refExpr.getCopyableUserData(ChangeContextUtil.REF_MEMBER_KEY);
      refExpr.putCopyableUserData(ChangeContextUtil.REF_MEMBER_KEY, null);

      if (refMember != null && refMember.isValid()) {
        PsiClass containingClass = refMember.getContainingClass();
        if (containingClass != null && refMember.hasModifierProperty(PsiModifier.STATIC)) {
          PsiElement refElement = newExpr.resolve();
          if (!manager.areElementsEquivalent(refMember, refElement)) {
            String qualifiedName = containingClass.getQualifiedName();
            if (qualifiedName != null) {
              newExpr.setQualifier(factory.createReferenceExpressionFromText(qualifiedName));
            }
          }
        }
      }
      else {
        PsiClass refClass = refExpr.getCopyableUserData(ChangeContextUtil.REF_CLASS_KEY);
        refExpr.putCopyableUserData(ChangeContextUtil.REF_CLASS_KEY, null);
        if (refClass != null && refClass.isValid()) {
          newExpr = (GrReferenceExpression)newExpr.bindToElement(refClass);
        }
      }
    }
    else {
      Boolean couldRemove = refExpr.getCopyableUserData(ChangeContextUtil.CAN_REMOVE_QUALIFIER_KEY);
      refExpr.putCopyableUserData(ChangeContextUtil.CAN_REMOVE_QUALIFIER_KEY, null);

      if (couldRemove == Boolean.FALSE && canRemoveQualifier(refExpr)) {
        GrReferenceExpression newRefExpr = (GrReferenceExpression)factory.createExpressionFromText(refExpr.getReferenceName());
        newExpr = (GrReferenceExpression)newExpr.replace(newRefExpr);
      }
    }
    return newExpr;
  }

  private static boolean canRemoveQualifier(GrReferenceExpression refExpr) {
    try {
      GrExpression qualifier = refExpr.getQualifier();
      if (!(qualifier instanceof GrReferenceExpression)) return false;

      PsiElement qualifierRefElement = ((GrReferenceExpression)qualifier).resolve();
      if (!(qualifierRefElement instanceof PsiClass)) return false;

      PsiElement refElement = refExpr.resolve();
      if (refElement == null) return false;

      final GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(refExpr.getProject());

      if (refExpr.getParent() instanceof GrMethodCallExpression methodCall) {
        GrMethodCallExpression newMethodCall =
          (GrMethodCallExpression)factory.createExpressionFromText(refExpr.getReferenceName() + "()", refExpr);
        newMethodCall.getArgumentList().replace(methodCall.getArgumentList());
        PsiElement newRefElement = ((GrReferenceExpression)newMethodCall.getInvokedExpression()).resolve();
        return refElement.equals(newRefElement);
      }
      else {
        GrReferenceExpression newRefExpr = (GrReferenceExpression)factory.createExpressionFromText(refExpr.getReferenceName(), refExpr);
        PsiElement newRefElement = newRefExpr.resolve();
        return refElement.equals(newRefElement);
      }
    }
    catch (IncorrectOperationException e) {
      return false;
    }
  }
}
