// 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.field;

import com.intellij.codeInsight.TestFrameworks;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Comparing;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.codeStyle.GrReferenceAdjuster;
import org.jetbrains.plugins.groovy.lang.psi.GrQualifiedReference;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
import org.jetbrains.plugins.groovy.lang.psi.api.auxiliary.modifiers.GrModifier;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.*;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrCodeBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrOpenBlock;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrAssignmentExpression;
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.typedef.GrAnonymousClassDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrEnumTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.GrTypeDefinitionBody;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstantList;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMember;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMembersDeclaration;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.api.util.GrStatementOwner;
import org.jetbrains.plugins.groovy.lang.psi.impl.PsiImplUtil;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GroovyScriptClass;
import org.jetbrains.plugins.groovy.lang.psi.util.GroovyCommonClassNames;
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 org.jetbrains.plugins.groovy.refactoring.introduce.StringPartInfo;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

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

  private final GrIntroduceContext myContext;
  private final GrIntroduceFieldSettings mySettings;

  private @Nullable GrExpression myInitializer;
  private @Nullable GrVariable myLocalVariable;

  public GrIntroduceFieldProcessor(@NotNull GrIntroduceContext context,
                                   @NotNull GrIntroduceFieldSettings settings) {
    this.myContext = context;
    this.mySettings = settings;
  }

  public @Nullable GrVariable run() {
    PsiElement scope = myContext.getScope();
    final PsiClass targetClass = scope instanceof GroovyFileBase ? ((GroovyFileBase)scope).getScriptClass() : (PsiClass)scope;
    if (targetClass == null) return null;

    final GrVariableDeclaration declaration = insertField(targetClass);
    final GrVariable field = declaration.getVariables()[0];

    if (mySettings.removeLocalVar()) {
      myLocalVariable = GrIntroduceHandlerBase.resolveLocalVar(myContext);
      assert myLocalVariable != null : myContext.getExpression() + ", " + myContext.getVar() + ", " + myContext.getStringPart();
    }
    GrExpression originalInitializer = getInitializer();
    myInitializer = originalInitializer == null ? null : (GrExpression)originalInitializer.copy();

    List<PsiElement> replaced = processOccurrences(targetClass, field);

    switch (mySettings.initializeIn()) {
      case CUR_METHOD -> initializeInMethod(field, replaced);
      case FIELD_DECLARATION -> field.setInitializerGroovy(myInitializer);
      case CONSTRUCTOR -> initializeInConstructor(field, replaced);
      case SETUP_METHOD -> initializeInSetup(field, replaced);
    }

    JavaCodeStyleManager.getInstance(declaration.getProject()).shortenClassReferences(declaration);

    if (mySettings.removeLocalVar()) {
      GrIntroduceHandlerBase.deleteLocalVar(myLocalVariable);
    }

    return field;
  }

  private @NotNull List<PsiElement> processOccurrences(@NotNull PsiClass targetClass, @NotNull GrVariable field) {
    if (myContext.getStringPart() != null) {
      final GrExpression expr = myContext.getStringPart().replaceLiteralWithConcatenation(field.getName());
      final PsiElement occurrence = replaceOccurrence(field, expr, targetClass);
      updateCaretPosition(occurrence);
      return Collections.singletonList(occurrence);
    }

    if (mySettings.replaceAllOccurrences()) {
      GroovyRefactoringUtil.sortOccurrences(myContext.getOccurrences());
      ArrayList<PsiElement> result = new ArrayList<>();
      for (PsiElement occurrence : myContext.getOccurrences()) {
        result.add(replaceOccurrence(field, occurrence, targetClass));
      }
      return result;
    }

    GrVariable var = myContext.getVar();
    if (var != null) {
      GrExpression initializer = var.getInitializerGroovy();
      if (initializer != null) {
        return Collections.singletonList(replaceOccurrence(field, initializer, targetClass));
      }
      else {
        return Collections.emptyList();
      }
    }

    final GrExpression expression = myContext.getExpression();
    assert expression != null;
    return Collections.singletonList(replaceOccurrence(field, expression, targetClass));
  }

  private void updateCaretPosition(@NotNull PsiElement occurrence) {
    myContext.getEditor().getCaretModel().moveToOffset(occurrence.getTextRange().getEndOffset());
    myContext.getEditor().getSelectionModel().removeSelection();
  }

  protected @NotNull GrVariableDeclaration insertField(@NotNull PsiClass targetClass) {
    GrVariableDeclaration declaration = createField(targetClass);
    if (targetClass instanceof GrEnumTypeDefinition) {
      final GrEnumConstantList enumConstants = ((GrEnumTypeDefinition)targetClass).getEnumConstantList();
      return (GrVariableDeclaration)targetClass.addAfter(declaration, enumConstants);
    }

    if (targetClass instanceof GroovyScriptClass) {
      final GroovyFile file = ((GroovyScriptClass)targetClass).getContainingFile();
      PsiElement[] elements = file.getMethods();
      if (elements.length == 0) elements = file.getStatements();
      final PsiElement anchor = ArrayUtil.getFirstElement(elements);
      return (GrVariableDeclaration)file.addBefore(declaration, anchor);
    }

    if (targetClass instanceof GrTypeDefinition) {
      PsiElement anchor = getAnchorForDeclaration((GrTypeDefinition)targetClass);
      return (GrVariableDeclaration)targetClass.addAfter(declaration, anchor);
    }

    throw new IllegalArgumentException("Unexpected targetClass: " + targetClass.getClass());
  }

  private static @Nullable PsiElement getAnchorForDeclaration(@NotNull GrTypeDefinition targetClass) {
    final GrTypeDefinitionBody body = targetClass.getBody();
    if (body == null) return null;

    PsiElement anchor = body.getLBrace();
    final GrMembersDeclaration[] declarations = targetClass.getMemberDeclarations();
    for (GrMembersDeclaration declaration : declarations) {
      if (declaration instanceof GrVariableDeclaration) anchor = declaration;
      if (!(declaration instanceof GrVariableDeclaration)) return anchor;
    }

    return anchor;
  }

  void initializeInSetup(@NotNull GrVariable field, @NotNull Collection<PsiElement> replaced) {
    final PsiMethod setUpMethod = TestFrameworks.getInstance().findOrCreateSetUpMethod(((PsiClass)myContext.getScope()));
    assert setUpMethod instanceof GrMethod;

    final GrOpenBlock body = ((GrMethod)setUpMethod).getBlock();
    final GrStatement anchor = findAnchorForAssignment(body, replaced);
    generateAssignment(field, anchor, body, null);
  }

  void initializeInMethod(@NotNull GrVariable field, @NotNull List<PsiElement> replaced) {
    final PsiElement _scope = myContext.getScope();
    final PsiElement scope = _scope instanceof GroovyScriptClass ? ((GroovyScriptClass)_scope).getContainingFile() : _scope;

    final PsiElement place = replaced.get(0);

    final GrMember member = GrIntroduceFieldHandler.getContainer(place, scope);
    GrStatementOwner container = member instanceof GrMethod ? ((GrMethod)member).getBlock() :
                                 member instanceof GrClassInitializer ? ((GrClassInitializer)member).getBlock() :
                                 place.getContainingFile() instanceof GroovyFile ? ((GroovyFile)place.getContainingFile()) :
                                 null;
    assert container != null;

    final PsiElement anchor;
    if (mySettings.removeLocalVar()) {
      GrVariable variable = myLocalVariable;
      anchor = PsiTreeUtil.getParentOfType(variable, GrStatement.class);
    }
    else {
      anchor = GrIntroduceHandlerBase.findAnchor(replaced.toArray(PsiElement.EMPTY_ARRAY), container);
      GrIntroduceHandlerBase.assertStatement(anchor, myContext.getScope());
    }

    initializeInMethodInner(field, container, (GrStatement)anchor, replaced.get(0));
  }

  private void initializeInMethodInner(@NotNull GrVariable field,
                                       @Nullable GrStatementOwner container,
                                       @Nullable GrStatement anchor,
                                       @Nullable PsiElement occurence) {
    if (!mySettings.replaceAllOccurrences() && PsiUtil.isExpressionStatement(occurence) && Comparing.equal(anchor, occurence)) {
      generateAssignment(field, anchor, container, occurence);
    }
    else {
      generateAssignment(field, anchor, container, null);
    }
  }

  void initializeInConstructor(@NotNull GrVariable field, @NotNull Collection<PsiElement> replaced) {
    final PsiClass scope = (PsiClass)myContext.getScope();

    if (scope instanceof GrAnonymousClassDefinition) {
      initializeInAnonymousClassInitializer(field, (GrAnonymousClassDefinition)scope, replaced);
    }
    else {
      initializeInConstructor(field, scope, replaced);
    }
  }

  private void initializeInConstructor(@NotNull GrVariable field, @NotNull PsiClass scope, @NotNull Collection<PsiElement> replaced) {
    PsiMethod[] constructors = scope.getConstructors();
    if (constructors.length == 0) {
      constructors = new PsiMethod[]{generateConstructor(scope)};
    }

    for (PsiMethod constructor : constructors) {
      final GrConstructorInvocation invocation = PsiImplUtil.getChainingConstructorInvocation((GrMethod)constructor);
      if (invocation != null && invocation.isThisCall()) continue;
      final GrOpenBlock body = ((GrMethod)constructor).getBlock();
      final GrStatement anchor = findAnchorForAssignment(body, replaced);
      initializeInMethodInner(field, body, anchor, ContainerUtil.getFirstItem(replaced));
    }
  }

  private @NotNull PsiMethod generateConstructor(@NotNull PsiClass scope) {
    final String name = scope.getName();
    LOG.assertTrue(name != null, scope.getText());
    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myContext.getProject());
    final GrMethod
      constructor = factory.createConstructorFromText(name, ArrayUtilRt.EMPTY_STRING_ARRAY, ArrayUtilRt.EMPTY_STRING_ARRAY, "{}", scope);
    if (scope instanceof GroovyScriptClass) constructor.getModifierList().setModifierProperty(GrModifier.DEF, true);
    return (PsiMethod)scope.add(constructor);
  }

  private void initializeInAnonymousClassInitializer(@NotNull GrVariable field,
                                                     @NotNull GrAnonymousClassDefinition scope,
                                                     @NotNull Collection<PsiElement> replaced) {
    final GrClassInitializer[] initializers = scope.getInitializers();
    GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myContext.getProject());
    final GrClassInitializer initializer = initializers.length == 0 ? (GrClassInitializer)scope.add(factory.createClassInitializer())
                                                                    : initializers[0];

    final GrStatement anchor = findAnchorForAssignment(initializer.getBlock(), replaced);
    initializeInMethodInner(field, initializer.getBlock(), anchor, ContainerUtil.getFirstItem(replaced));
  }

  private void generateAssignment(@NotNull GrVariable field,
                                  @Nullable GrStatement anchor,
                                  @Nullable GrStatementOwner defaultContainer,
                                  @Nullable PsiElement occurrenceToDelete) {
    if (myInitializer == null || defaultContainer == null) return;

    GrAssignmentExpression init = (GrAssignmentExpression)GroovyPsiElementFactory.getInstance(myContext.getProject())
      .createExpressionFromText(mySettings.getName() + " = " + myInitializer.getText());

    GrStatementOwner block;
    if (anchor != null) {
      anchor = GroovyRefactoringUtil.addBlockIntoParent(anchor);
      LOG.assertTrue(anchor.getParent() instanceof GrStatementOwner);
      block = (GrStatementOwner)anchor.getParent();
    }
    else {
      block = defaultContainer;
    }

    init = (GrAssignmentExpression)block.addStatementBefore(init, anchor);
    replaceOccurrence(field, init.getLValue(), (PsiClass)myContext.getScope());

    if (occurrenceToDelete != null) {
      occurrenceToDelete.delete();
    }
  }

  private @Nullable GrExpression extractVarInitializer() {
    assert myLocalVariable != null;
    return myLocalVariable.getInitializerGroovy();
  }

  private static @Nullable GrStatement findAnchorForAssignment(final @Nullable GrCodeBlock block, @NotNull Collection<PsiElement> replaced) {
    if (block == null) return null;
    final List<PsiElement> elements = ContainerUtil.findAll(replaced, element -> PsiTreeUtil.isAncestor(block, element, true));
    if (elements.isEmpty()) return null;
    return (GrStatement)GrIntroduceHandlerBase.findAnchor(elements.toArray(PsiElement.EMPTY_ARRAY), block);
  }

  private @NotNull PsiElement replaceOccurrence(@NotNull GrVariable field, @NotNull PsiElement occurrence, @NotNull PsiClass containingClass) {
    boolean isOriginal = occurrence == myContext.getExpression();
    final GrReferenceExpression newExpr = createRefExpression(field, occurrence, containingClass);
    final PsiElement replaced;
    if (occurrence instanceof GrExpression) {
      replaced = ((GrExpression)occurrence).replaceWithExpression(newExpr, false);
    }
    else {
      replaced = occurrence.replace(newExpr);
    }

    if (replaced instanceof GrQualifiedReference<?>) {
      GrReferenceAdjuster.shortenReference((GrQualifiedReference<?>)replaced);
    }
    if (isOriginal) {
      updateCaretPosition(replaced);
    }
    return replaced;
  }

  private static @NotNull GrReferenceExpression createRefExpression(@NotNull GrVariable field,
                                                                    @NotNull PsiElement place,
                                                                    @NotNull PsiClass containingClass) {
    final String qname = containingClass instanceof GroovyScriptClass ? null : containingClass.getQualifiedName();
    final String prefix = qname != null ? qname + "." : "";
    final String refText;
    if (field.hasModifierProperty(PsiModifier.STATIC)) {
      refText = prefix + field.getName();
    }
    else {
      refText = prefix + "this." + field.getName();
    }

    return GroovyPsiElementFactory.getInstance(place.getProject()).createReferenceExpressionFromText(refText, place);
  }

  private @NotNull GrVariableDeclaration createField(@NotNull PsiClass targetClass) {
    final String name = mySettings.getName();
    final PsiType type = mySettings.getSelectedType();
    final String modifier = mySettings.getVisibilityModifier();

    List<String> modifiers = new ArrayList<>();
    if (targetClass instanceof GroovyScriptClass) {
      modifiers.add("@" + GroovyCommonClassNames.GROOVY_TRANSFORM_FIELD);
    }
    if (mySettings.isStatic()) modifiers.add(PsiModifier.STATIC);
    if (!PsiModifier.PACKAGE_LOCAL.equals(modifier)) modifiers.add(modifier);
    if (mySettings.declareFinal()) modifiers.add(PsiModifier.FINAL);

    final String[] arr_modifiers = ArrayUtilRt.toStringArray(modifiers);
    final GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(myContext.getProject());
    if (targetClass instanceof GroovyScriptClass) {
      return factory.createVariableDeclaration(arr_modifiers, ((GrExpression)null), type, name);
    }
    else {
      return factory.createFieldDeclaration(arr_modifiers, name, null, type);
    }
  }

  protected @Nullable GrExpression getInitializer() {
    if (mySettings.removeLocalVar()) {
      return extractVarInitializer();
    }

    GrExpression expression = myContext.getExpression();
    StringPartInfo stringPart = myContext.getStringPart();
    if (expression != null) {
      return expression;
    }
    else if (stringPart != null) {
      return stringPart.createLiteralFromSelected();
    }

    throw new IncorrectOperationException("cannot be here!");
  }
}
