// 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.groovy.refactoring.introduce.variable;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiReference;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiUtilCore;
import com.intellij.refactoring.util.RefactoringUtil;
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.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.literals.GrStringInjection;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrAccessorMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import org.jetbrains.plugins.groovy.lang.psi.util.PsiUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceContext;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceHandlerBase;

import java.util.ArrayList;
import java.util.List;

/**
 * @author Max Medvedev
 */
public abstract class GrIntroduceLocalVariableProcessor {
  private static final Logger LOG = Logger.getInstance(GrIntroduceLocalVariableProcessor.class);

  private final GrIntroduceContext myContext;
  private final GroovyIntroduceVariableSettings mySettings;
  private final boolean myProcessUsages;
  private final PsiElement[] myOccurrences;
  private final GrExpression myExpression;

  public GrIntroduceLocalVariableProcessor(@NotNull GrIntroduceContext context,
                                           @NotNull GroovyIntroduceVariableSettings settings,
                                           PsiElement @NotNull [] occurrences,
                                           @NotNull GrExpression expression,
                                           boolean processUsages) {

    myContext = context;
    mySettings = settings;
    myProcessUsages = processUsages;
    myOccurrences = settings.replaceAllOccurrences() ? occurrences : new PsiElement[]{expression};
    myExpression = expression;
  }

  public @NotNull GrVariable processExpression(@NotNull GrVariableDeclaration declaration) {
    resolveLocalConflicts(myContext.getScope(), mySettings.getName());

    preprocessOccurrences();

    int expressionIndex = ArrayUtilRt.find(myOccurrences, myExpression);
    final PsiElement[] replaced = myProcessUsages ? processOccurrences() : myOccurrences;
    PsiElement replacedExpression = replaced[expressionIndex];
    GrStatement anchor = GrIntroduceHandlerBase.getAnchor(replaced, myContext.getScope());

    RefactoringUtil.highlightAllOccurrences(myContext.getProject(), replaced, myContext.getEditor());

    return insertVariableDefinition(declaration, anchor, replacedExpression);
  }

  protected abstract void refreshPositionMarker(PsiElement e);

  private static boolean isControlStatementBranch(GrStatement statement) {
    return statement.getParent() instanceof GrLoopStatement && statement == ((GrLoopStatement)statement.getParent()).getBody() ||
           statement.getParent() instanceof GrIfStatement &&
           (statement == ((GrIfStatement)statement.getParent()).getThenBranch() ||
            statement == ((GrIfStatement)statement.getParent()).getElseBranch());
  }

  private PsiElement[] processOccurrences() {

    List<PsiElement> result = new ArrayList<>();

    GrReferenceExpression templateRef =
      GroovyPsiElementFactory.getInstance(myContext.getProject()).createReferenceExpressionFromText(mySettings.getName());
    for (PsiElement occurrence : myOccurrences) {
      if (!(occurrence instanceof GrExpression)) {
        throw new IncorrectOperationException("Expression occurrence to be replaced is not instance of GroovyPsiElement");
      }

      final GrExpression replaced = ((GrExpression)occurrence).replaceWithExpression(templateRef, true);
      result.add(replaced);
    }

    return PsiUtilCore.toPsiElementArray(result);
  }

  private @NotNull GrExpression preprocessOccurrences() {
    GroovyRefactoringUtil.sortOccurrences(myOccurrences);
    if (myOccurrences.length == 0 || !(myOccurrences[0] instanceof GrExpression)) {
      throw new IncorrectOperationException("Wrong expression occurrence");
    }

    return (GrExpression)myOccurrences[0];
  }

  private static void resolveLocalConflicts(@NotNull PsiElement tempContainer, @NotNull String varName) {
    for (PsiElement child : tempContainer.getChildren()) {
      if (child instanceof GrReferenceExpression && !child.getText().contains(".")) {
        PsiReference psiReference = child.getReference();
        if (psiReference != null) {
          final PsiElement resolved = psiReference.resolve();
          if (resolved != null) {
            String fieldName = getFieldName(resolved);
            if (varName.equals(fieldName)) {
              GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(tempContainer.getProject());
              ((GrReferenceExpression)child).replaceWithExpression(factory.createExpressionFromText("this." + child.getText()), true);
            }
          }
        }
      }
      else {
        resolveLocalConflicts(child, varName);
      }
    }
  }

  private @NotNull GrVariable insertVariableDefinition(@NotNull GrVariableDeclaration declaration,
                                                       @NotNull GrStatement anchor,
                                                       @Nullable PsiElement expression) throws IncorrectOperationException {
    GrLabeledStatement labeledStatement = expression != null && expression.getParent() instanceof GrLabeledStatement ? (GrLabeledStatement)expression.getParent() : null;

    boolean expressionMustBeDeleted = expression != null && PsiUtil.isExpressionStatement(expression) && !isSingleGStringInjectionExpr(expression);
    boolean anchorEqualsExpression = anchor == expression || labeledStatement == anchor;

    String usedLabel = labeledStatement != null ? labeledStatement.getName() : null;

    if (expressionMustBeDeleted && !anchorEqualsExpression) {
      expression.delete();
    }

    boolean isInsideControlStatement = isControlStatementBranch(anchor);
    if (isInsideControlStatement) {
      anchor = insertBraces(anchor);
    }

    LOG.assertTrue(myOccurrences.length > 0);

    GrStatementOwner block = (GrStatementOwner)anchor.getParent();

    if (usedLabel != null && expressionMustBeDeleted && anchorEqualsExpression) {
      GrLabeledStatement definitionWithLabel = (GrLabeledStatement)GroovyPsiElementFactory.getInstance(anchor.getProject()).createStatementFromText(usedLabel + ": foo()");
      GrLabeledStatement inserted = insertStatement(definitionWithLabel, anchor, block, true);
      declaration = inserted.getStatement().replaceWithStatement(declaration);
    }
    else {
      declaration = insertStatement(declaration, anchor, block, expressionMustBeDeleted && anchorEqualsExpression);
    }

    final GrVariable variable = declaration.getVariables()[0];
    JavaCodeStyleManager.getInstance(declaration.getProject()).shortenClassReferences(declaration);


    PsiElement markerPlace = expressionMustBeDeleted  ? variable :
                             isInsideControlStatement ? declaration.getParent()
                                                      : expression;
    refreshPositionMarker(markerPlace);

    return variable;
  }

  private static <T extends GrStatement> T insertStatement(T declaration,
                                                           GrStatement anchor,
                                                           GrStatementOwner block,
                                                           boolean replaceAnchor) {
    if (replaceAnchor) {
      return (T)anchor.replace(declaration);
    }
    else {
      return (T)block.addStatementBefore(declaration, anchor);
    }
  }

  static @NotNull GrStatement insertBraces(@NotNull GrStatement anchor) {
    GrBlockStatement blockStatement = GroovyPsiElementFactory.getInstance(anchor.getProject()).createBlockStatement();

    blockStatement.getBlock().addStatementBefore(anchor, null);
    GrBlockStatement newBlockStatement = ((GrBlockStatement)anchor.replace(blockStatement));
    return newBlockStatement.getBlock().getStatements()[0];
  }

  private static boolean isSingleGStringInjectionExpr(PsiElement expression) {
    PsiElement parent = expression.getParent();
    return parent instanceof GrClosableBlock && parent.getParent() instanceof GrStringInjection;
  }

  private static @Nullable String getFieldName(@Nullable PsiElement element) {
    if (element instanceof GrAccessorMethod) element = ((GrAccessorMethod)element).getProperty();
    return element instanceof GrField ? ((GrField)element).getName() : null;
  }

}
