// 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.ui.DialogWrapper;
import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.*;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.introduceField.IntroduceFieldHandler;
import com.intellij.refactoring.ui.NameSuggestionsField;
import com.intellij.refactoring.util.RadioUpDownListener;
import com.intellij.ui.components.JBRadioButton;
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.GroovyBundle;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElement;
import org.jetbrains.plugins.groovy.lang.psi.GroovyRecursiveElementVisitor;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrConstructorInvocation;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrField;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.GrVariable;
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.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMember;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.impl.GroovyNamesUtil;
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.PsiUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceContext;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceDialog;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceHandlerBase;
import org.jetbrains.plugins.groovy.refactoring.introduce.StringPartInfo;
import org.jetbrains.plugins.groovy.refactoring.ui.GrTypeComboBox;

import javax.swing.*;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;

public class GrIntroduceFieldDialog extends DialogWrapper implements GrIntroduceDialog<GrIntroduceFieldSettings>, GrIntroduceFieldSettings {
  private JPanel myContentPane;
  private NameSuggestionsField myNameField;
  private JRadioButton myPrivateRadioButton;
  private JRadioButton myProtectedRadioButton;
  private JRadioButton myPublicRadioButton;
  private JRadioButton myPropertyRadioButton;
  private JRadioButton myCurrentMethodRadioButton;
  private JRadioButton myFieldDeclarationRadioButton;
  private JRadioButton myClassConstructorSRadioButton;
  private JBRadioButton mySetUpMethodRadioButton;
  private JCheckBox myDeclareFinalCheckBox;
  private JCheckBox myReplaceAllOccurrencesCheckBox;
  private GrTypeComboBox myTypeComboBox;
  private JLabel myNameLabel;
  private JLabel myTypeLabel;
  private final boolean myIsStatic;
  private final boolean isInvokedInAlwaysInvokedConstructor;
  private final boolean hasLHSUsages;
  private final String myInvokedOnLocalVar;
  private final boolean myCanBeInitializedOutsideBlock;

  @Override
  public JComponent getPreferredFocusedComponent() {
    return myNameField;
  }

  private final GrIntroduceContext myContext;

  public GrIntroduceFieldDialog(final GrIntroduceContext context) {
    super(context.getProject(), true);
    myContext = context;

    final PsiClass clazz = (PsiClass)context.getScope();
    PsiElement scope = clazz instanceof GroovyScriptClass ? clazz.getContainingFile() : clazz;
    myIsStatic = GrIntroduceFieldHandler.shouldBeStatic(context.getPlace(), scope);

    initVisibility();

    ButtonGroup initialization = new ButtonGroup();
    ArrayList<JRadioButton> inits = new ArrayList<>();

    inits.add(myCurrentMethodRadioButton);
    inits.add(myFieldDeclarationRadioButton);
    inits.add(myClassConstructorSRadioButton);

    if (TestFrameworks.getInstance().isTestClass(clazz)) {
      inits.add(mySetUpMethodRadioButton);
    }
    else {
      mySetUpMethodRadioButton.setVisible(false);
    }

    for (JRadioButton init : inits) {
      initialization.add(init);
    }
    RadioUpDownListener.installOn(inits.toArray(new JRadioButton[0]));

    if (clazz instanceof GroovyScriptClass) {
      myClassConstructorSRadioButton.setEnabled(false);
    }

    myCanBeInitializedOutsideBlock = canBeInitializedOutsideBlock(context, clazz);
    final GrMember container = GrIntroduceFieldHandler.getContainer(context.getPlace(), scope);
    if (container == null) {
      myCurrentMethodRadioButton.setEnabled(false);
    }

    if (myCurrentMethodRadioButton.isEnabled()) {
      myCurrentMethodRadioButton.setSelected(true);
    }
    else {
      myFieldDeclarationRadioButton.setSelected(true);
    }

    myInvokedOnLocalVar  = context.getVar() == null ? getInvokedOnLocalVar(context.getExpression()) : context.getVar().getName();
    if (myInvokedOnLocalVar != null) {
      myReplaceAllOccurrencesCheckBox.setText(GroovyBundle.message("replace.all.occurrences.and.remove.variable.0", myInvokedOnLocalVar));
      if (context.getVar() != null) {
        myReplaceAllOccurrencesCheckBox.setEnabled(false);
        myReplaceAllOccurrencesCheckBox.setSelected(true);
      }
    }
    else if (context.getOccurrences().length == 1) {
      myReplaceAllOccurrencesCheckBox.setSelected(false);
      myReplaceAllOccurrencesCheckBox.setVisible(false);
    }

    myNameField.addDataChangedListener(() -> validateOKAction());

    ItemListener l = new ItemListener() {
      @Override
      public void itemStateChanged(ItemEvent e) {
        myNameField.requestFocusInWindow();
        checkErrors();

        if (myReplaceAllOccurrencesCheckBox.isSelected()) {
          PsiElement anchor = GrIntroduceHandlerBase.findAnchor(myContext.getOccurrences(), myContext.getScope());
          if (anchor != null && anchor != myContext.getScope() && anchor != ((GrTypeDefinition)myContext.getScope()).getBody()) {
            myCurrentMethodRadioButton.setEnabled(true);
          }
          else if (myCurrentMethodRadioButton.isEnabled()) {
            myCurrentMethodRadioButton.setEnabled(false);
            myFieldDeclarationRadioButton.setSelected(true);
          }
        }
        else if (!myCurrentMethodRadioButton.isEnabled()) {
          myCurrentMethodRadioButton.setEnabled(true);
        }
      }
    };
    myPrivateRadioButton.addItemListener(l);
    myProtectedRadioButton.addItemListener(l);
    myPublicRadioButton.addItemListener(l);
    myPropertyRadioButton.addItemListener(l);
    myCurrentMethodRadioButton.addItemListener(l);
    myFieldDeclarationRadioButton.addItemListener(l);
    myClassConstructorSRadioButton.addItemListener(l);
    myDeclareFinalCheckBox.addItemListener(l);
    myReplaceAllOccurrencesCheckBox.addItemListener(l);
    myTypeComboBox.addItemListener(l);

    isInvokedInAlwaysInvokedConstructor = container instanceof PsiMethod &&
                                          allOccurrencesInOneMethod(myContext.getOccurrences(), scope) &&
                                          isAlwaysInvokedConstructor((PsiMethod)container, clazz);
    hasLHSUsages = hasLhsUsages(myContext);

    setTitle(IntroduceFieldHandler.getRefactoringNameText());
    init();
    checkErrors();
  }

  private void checkErrors() {
    List<String> errors = new ArrayList<>();
    if (myCurrentMethodRadioButton.isSelected() && myDeclareFinalCheckBox.isSelected() && !isInvokedInAlwaysInvokedConstructor) {
      errors.add(GroovyRefactoringBundle.message("final.field.cant.be.initialized.in.cur.method"));
    }
    if (myDeclareFinalCheckBox.isSelected() && myReplaceAllOccurrencesCheckBox.isSelected() && myInvokedOnLocalVar != null && hasLHSUsages) {
      errors.add(GroovyRefactoringBundle.message("Field.cannot.be.final.because.replaced.variable.has.lhs.usages"));
    }
    if (!myCanBeInitializedOutsideBlock) {
      if (myFieldDeclarationRadioButton.isSelected()) {
        errors.add(GroovyRefactoringBundle.message("field.cannot.be.initialized.in.field.declaration"));
      }
      else if (myClassConstructorSRadioButton.isSelected()) {
        errors.add(GroovyRefactoringBundle.message("field.cannot.be.initialized.in.constructor(s)"));
      }
    }
    if (errors.isEmpty()) {
      setErrorText(null);
    }
    else {
      setErrorText(errorString(errors));
    }
  }

  private static @NlsSafe @NotNull String errorString(List<String> errors) {
    return StringUtil.join(errors, "\n");
  }

  private static boolean hasLhsUsages(@NotNull GrIntroduceContext context) {
    if (context.getVar() == null && !(context.getExpression() instanceof GrReferenceExpression)) return false;
    if (GrIntroduceHandlerBase.hasLhs(context.getOccurrences())) return true;
    return false;
  }

  private void initVisibility() {
    ButtonGroup visibility = new ButtonGroup();
    visibility.add(myPrivateRadioButton);
    visibility.add(myProtectedRadioButton);
    visibility.add(myPublicRadioButton);
    visibility.add(myPropertyRadioButton);

    if (myContext.getScope() instanceof GroovyScriptClass) {
      myPropertyRadioButton.setSelected(true);
      myPrivateRadioButton.setEnabled(false);
      myProtectedRadioButton.setEnabled(false);
      myPublicRadioButton.setEnabled(false);
      myPropertyRadioButton.setEnabled(false);
    }
    else {
      myPrivateRadioButton.setSelected(true);
    }
    RadioUpDownListener.installOn(myPrivateRadioButton, myProtectedRadioButton, myPublicRadioButton, myPropertyRadioButton);
  }

  private static boolean isAlwaysInvokedConstructor(@Nullable PsiMethod method, @NotNull PsiClass clazz) {
    if (method == null) return false;
    if (!method.isConstructor()) return false;
    final PsiMethod[] constructors = clazz.getConstructors();
    if (constructors.length == 1) return true;
    final GrConstructorInvocation invocation = PsiImplUtil.getChainingConstructorInvocation((GrMethod)method);
    if (invocation != null && invocation.isThisCall()) return false;

    for (PsiMethod constructor : constructors) {
      if (constructor == method) continue;
      final GrConstructorInvocation inv = PsiImplUtil.getChainingConstructorInvocation((GrMethod)constructor);
      if (inv == null || inv.isSuperCall()) return false;
    }
    return true;
  }

  private static boolean allOccurrencesInOneMethod(PsiElement @NotNull [] occurrences, PsiElement scope) {
    if (occurrences.length == 0) return true;
    GrMember container = GrIntroduceFieldHandler.getContainer(occurrences[0], scope);
    if (container == null) return false;
    for (int i = 1; i < occurrences.length; i++) {
      GrMember other = GrIntroduceFieldHandler.getContainer(occurrences[i], scope);
      if (other != container) return false;
    }
    return true;
  }

  @Override
  protected JComponent createCenterPanel() {
    myNameLabel.setLabelFor(myNameField);
    myTypeLabel.setLabelFor(myTypeComboBox);
    return myContentPane;
  }

  @Override
  public GrIntroduceFieldSettings getSettings() {
    return this;
  }

  @Override
  public @NotNull LinkedHashSet<String> suggestNames() {
    return new GrFieldNameSuggester(myContext, new GroovyFieldValidator(myContext), false).suggestNames();
  }

  private void createUIComponents() {
    final GrExpression expression = myContext.getExpression();
    final GrVariable var = myContext.getVar();
    final StringPartInfo stringPart = myContext.getStringPart();

    List<String> list = new ArrayList<>();
    if (var != null) {
      list.add(var.getName());
    }
    list.addAll(suggestNames());
    myNameField = new NameSuggestionsField(ArrayUtilRt.toStringArray(list), myContext.getProject(), GroovyFileType.GROOVY_FILE_TYPE);

    if (expression != null) {
      myTypeComboBox = GrTypeComboBox.createTypeComboBoxFromExpression(expression);
    }
    else if (stringPart != null) {
      myTypeComboBox = GrTypeComboBox.createTypeComboBoxFromExpression(stringPart.getLiteral());
    }
    else {
      myTypeComboBox = GrTypeComboBox.createTypeComboBoxWithDefType(var.getDeclaredType(), var);
    }

    GrTypeComboBox.registerUpDownHint(myNameField, myTypeComboBox);
  }

  @Override
  public boolean declareFinal() {
    return myDeclareFinalCheckBox.isSelected();
  }

  @Override
  public @NotNull Init initializeIn() {
    if (myCurrentMethodRadioButton.isSelected()) return Init.CUR_METHOD;
    if (myFieldDeclarationRadioButton.isSelected()) return Init.FIELD_DECLARATION;
    if (myClassConstructorSRadioButton.isSelected()) return Init.CONSTRUCTOR;
    if (mySetUpMethodRadioButton.isSelected()) return Init.SETUP_METHOD;
    throw new IncorrectOperationException("no initialization place is selected");
  }

  @Override
  public @NotNull String getVisibilityModifier() {
    if (myPrivateRadioButton.isSelected()) return PsiModifier.PRIVATE;
    if (myProtectedRadioButton.isSelected()) return PsiModifier.PROTECTED;
    if (myPublicRadioButton.isSelected()) return PsiModifier.PUBLIC;
    if (myPropertyRadioButton.isSelected()) return PsiModifier.PACKAGE_LOCAL;
    throw new IncorrectOperationException("no visibility selected");
  }

  @Override
  public boolean isStatic() {
    return myIsStatic;
  }

  @Override
  public boolean removeLocalVar() {
    return myInvokedOnLocalVar != null && myReplaceAllOccurrencesCheckBox.isSelected();
  }

  @Override
  public @NotNull String getName() {
    return myNameField.getEnteredName();
  }

  @Override
  public boolean replaceAllOccurrences() {
    return myReplaceAllOccurrencesCheckBox.isSelected();
  }

  @Override
  public PsiType getSelectedType() {
    return myTypeComboBox.getSelectedType();
  }

  private static @Nullable String getInvokedOnLocalVar(GrExpression expression) {
    if (expression instanceof GrReferenceExpression) {
      final PsiElement resolved = ((GrReferenceExpression)expression).resolve();
      if (PsiUtil.isLocalVariable(resolved)) {
        return ((GrVariable)resolved).getName();
      }
    }
    return null;
  }

  private static boolean canBeInitializedOutsideBlock(@NotNull GrIntroduceContext context, @NotNull PsiClass clazz) {
    final StringPartInfo part = context.getStringPart();
    GrExpression expression = context.getExpression();

    if (expression != null) {
      expression = (GrExpression)PsiUtil.skipParentheses(expression, false);
      if (expression == null) return false;

      if (expression instanceof GrReferenceExpression) {
        final PsiElement resolved = ((GrReferenceExpression)expression).resolve();
        if (PsiUtil.isLocalVariable(resolved)) {
          expression = ((GrVariable)resolved).getInitializerGroovy();
          if (expression == null) return false;
        }
      }

      ExpressionChecker visitor = new ExpressionChecker(clazz, expression);
      expression.accept(visitor);
      return visitor.isResult();
    }

    if (part != null) {
      for (GrStringInjection injection : part.getInjections()) {
        GroovyPsiElement scope = injection.getExpression() != null ? injection.getExpression() : injection.getClosableBlock();
        assert scope != null;
        ExpressionChecker visitor = new ExpressionChecker(clazz, scope);
        scope.accept(visitor);
        if (!visitor.isResult()) {
          return false;
        }
      }
      return true;
    }

    else {
      return false;
    }
  }

  private static final class ExpressionChecker extends GroovyRecursiveElementVisitor {
    private final PsiClass myClass;
    private final PsiElement myScope;

    private boolean result = true;

    private ExpressionChecker(@NotNull PsiClass aClass, @NotNull PsiElement scope) {
      myClass = aClass;
      myScope = scope;
    }

    @Override
    public void visitReferenceExpression(@NotNull GrReferenceExpression refExpr) {
      super.visitReferenceExpression(refExpr);
      final PsiElement resolved = refExpr.resolve();
      if (!(resolved instanceof GrVariable)) return;
      if (resolved instanceof GrField && myClass.getManager().areElementsEquivalent(myClass, ((GrField)resolved).getContainingClass())) {
        return;
      }
      if (resolved instanceof PsiParameter &&
          PsiTreeUtil.isAncestor(myScope, ((PsiParameter)resolved).getDeclarationScope(), false)) {
        return;
      }
      result = false;
    }

    private boolean isResult() {
      return result;
    }
  }

  private void validateOKAction() {
    setOKActionEnabled(GroovyNamesUtil.isIdentifier(getName()));
  }

  @Override
  protected void doOKAction() {
    final PsiClass clazz = (PsiClass)myContext.getScope();
    final String name = getName();
    String message = RefactoringBundle.message("field.exists", name, clazz.getQualifiedName());
    if (clazz.findFieldByName(name, true) != null &&
        Messages.showYesNoDialog(myContext.getProject(), message, IntroduceFieldHandler.getRefactoringNameText(), Messages.getWarningIcon()) != Messages.YES) {
      return;
    }
    super.doOKAction();
  }
}
