// 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.extract.method;

import com.intellij.openapi.editor.event.DocumentEvent;
import com.intellij.openapi.editor.event.DocumentListener;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.ui.DialogWrapper;
import com.intellij.openapi.ui.Splitter;
import com.intellij.openapi.ui.ValidationInfo;
import com.intellij.openapi.util.NlsContexts;
import com.intellij.psi.*;
import com.intellij.refactoring.BaseRefactoringProcessor;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.ui.ComboBoxVisibilityPanel;
import com.intellij.refactoring.ui.MethodSignatureComponent;
import com.intellij.refactoring.util.RefactoringUIUtil;
import com.intellij.ui.EditorTextField;
import com.intellij.ui.IdeBorderFactory;
import com.intellij.util.ArrayUtil;
import com.intellij.util.containers.HashingStrategy;
import com.intellij.util.containers.MultiMap;
import com.intellij.util.ui.JBUI;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyFileType;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.lang.psi.impl.synthetic.GroovyScriptClass;
import org.jetbrains.plugins.groovy.lang.psi.impl.utils.DuplicatesUtil;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringBundle;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;
import org.jetbrains.plugins.groovy.refactoring.extract.ExtractUtil;
import org.jetbrains.plugins.groovy.refactoring.extract.GrParameterTablePanel;
import org.jetbrains.plugins.groovy.refactoring.extract.InitialInfo;
import org.jetbrains.plugins.groovy.refactoring.ui.GrMethodSignatureComponent;
import org.jetbrains.plugins.groovy.refactoring.ui.GroovyComboboxVisibilityPanel;
import org.jetbrains.plugins.groovy.settings.GroovyApplicationSettings;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.EventListenerList;
import java.awt.event.KeyEvent;
import java.util.EventListener;
import java.util.List;
import java.util.Map;

public class GroovyExtractMethodDialog extends DialogWrapper {
  private final ExtractMethodInfoHelper myHelper;

  private final EventListenerList myListenerList = new EventListenerList();

  private JPanel contentPane;
  private EditorTextField myNameField;
  private JCheckBox myCbSpecifyType;
  private JLabel myNameLabel;
  private MethodSignatureComponent mySignature;
  private ComboBoxVisibilityPanel<String> myVisibilityPanel;
  private Splitter mySplitter;
  private JCheckBox myForceReturnCheckBox;
  private GrParameterTablePanel myParameterTablePanel;
  private final Project myProject;

  public GroovyExtractMethodDialog(InitialInfo info, PsiClass owner) {
    super(info.getProject(), true);
    myProject = info.getProject();
    myHelper = new ExtractMethodInfoHelper(info, "", owner, false);

    myParameterTablePanel.init(myHelper);

    setModal(true);
    setTitle(GroovyExtractMethodHandler.getRefactoringName());
    init();
    setUpNameField();
    setUpDialog();
    update();
  }

  @Override
  protected void init() {
    super.init();
    mySplitter.setOrientation(true);
    mySplitter.setShowDividerIcon(false);
    mySplitter.setFirstComponent(myParameterTablePanel);
    mySplitter.setSecondComponent(mySignature);
  }

  @Override
  protected void doOKAction() {
    myHelper.setForceReturn(myForceReturnCheckBox.isSelected());
    String name = getEnteredName();
    if (name == null) return;
    if (!validateMethod(myHelper)) {
      return;
    }
    final GroovyApplicationSettings settings = GroovyApplicationSettings.getInstance();
    if (myCbSpecifyType.isEnabled()) {
      settings.EXTRACT_METHOD_SPECIFY_TYPE = myCbSpecifyType.isSelected();
    }
    if (myForceReturnCheckBox.isEnabled()) {
      settings.FORCE_RETURN = myForceReturnCheckBox.isSelected();
    }
    settings.EXTRACT_METHOD_VISIBILITY = myVisibilityPanel.getVisibility();
    super.doOKAction();
  }

  private void setUpDialog() {
    myCbSpecifyType.setMnemonic(KeyEvent.VK_T);
    myCbSpecifyType.setFocusable(false);
    myCbSpecifyType.setSelected(true);
    if (GroovyApplicationSettings.getInstance().EXTRACT_METHOD_SPECIFY_TYPE != null) {
      myCbSpecifyType.setSelected(GroovyApplicationSettings.getInstance().EXTRACT_METHOD_SPECIFY_TYPE);
    }

    myCbSpecifyType.addChangeListener(new ChangeListener() {
      @Override
      public void stateChanged(ChangeEvent e) {
        myHelper.setSpecifyType(myCbSpecifyType.isSelected());
        updateSignature();
      }
    });

    myHelper.setSpecifyType(myCbSpecifyType.isSelected());
    myHelper.setVisibility(myVisibilityPanel.getVisibility());
    myNameLabel.setLabelFor(myNameField);

    final PsiType type = myHelper.getOutputType();
    if (!PsiTypes.voidType().equals(type)) {
      myForceReturnCheckBox.setSelected(GroovyApplicationSettings.getInstance().FORCE_RETURN);
    }
    else {
      myForceReturnCheckBox.setEnabled(false);
      myForceReturnCheckBox.setSelected(false);
    }
  }

  private void setUpNameField() {
    myNameLabel.setLabelFor(myNameField);
    myNameField.addDocumentListener(new DocumentListener() {
      @Override
      public void documentChanged(@NotNull DocumentEvent event) {
        fireNameDataChanged();
      }
    });

    myListenerList.add(DataChangedListener.class, new DataChangedListener());
  }

  @Override
  protected @Nullable JComponent createCenterPanel() {
    return contentPane;
  }

  @Override
  public JComponent getContentPane() {
    return contentPane;
  }

  protected @NotNull ExtractMethodInfoHelper getHelper() {
    return myHelper;
  }

  private void update() {
    String text = getEnteredName();
    myHelper.setName(text);
    updateSignature();
  }

  @Override
  protected ValidationInfo doValidate() {
    return null;
  }

  protected @Nullable String getEnteredName() {
    String text = myNameField.getText();
    if (!text.trim().isEmpty()) {
      return text.trim();
    }
    else {
      return null;
    }
  }

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

  @Override
  protected String getHelpId() {
    return HelpID.EXTRACT_METHOD;
  }

  private void createUIComponents() {
    mySignature = new GrMethodSignatureComponent("", myProject);
    mySignature.setPreferredSize(JBUI.size(500, 100));
    mySignature.setMinimumSize(JBUI.size(500, 100));
    mySignature.setBorder(
      IdeBorderFactory.createTitledBorder(GroovyRefactoringBundle.message("signature.preview.border.title"), false));
    mySignature.setFocusable(false);

    myNameField = new EditorTextField("", myProject, GroovyFileType.GROOVY_FILE_TYPE);
    myVisibilityPanel = new GroovyComboboxVisibilityPanel();

    String visibility = GroovyApplicationSettings.getInstance().EXTRACT_METHOD_VISIBILITY;
    if (visibility == null) {
      visibility = PsiModifier.PRIVATE;
    }
    myVisibilityPanel.setVisibility(visibility);
    myVisibilityPanel.addListener(new ChangeListener(){
      @Override
      public void stateChanged(ChangeEvent e) {
        myHelper.setVisibility(myVisibilityPanel.getVisibility());
        updateSignature();
      }
    });

    myParameterTablePanel = new GrParameterTablePanel() {
      @Override
      protected void updateSignature(){
        GroovyExtractMethodDialog.this.updateSignature();
      }

      @Override
      protected void doEnterAction(){
        GroovyExtractMethodDialog.this.clickDefaultButton();
      }

      @Override
      protected void doCancelAction(){
        GroovyExtractMethodDialog.this.doCancelAction();
      }
    };
  }

  private static boolean validateMethod(ExtractMethodInfoHelper helper) {
    MultiMap<PsiElement, @NlsContexts.DialogMessage String> conflicts = new MultiMap<>();
    GrMethod method = ExtractUtil.createMethod(helper);
    PsiClass owner = helper.getOwner();
    PsiMethod[] methods = ArrayUtil.mergeArrays(owner.getAllMethods(), new PsiMethod[]{method}, PsiMethod.ARRAY_FACTORY);
    final Map<PsiMethod, List<PsiMethod>> map = DuplicatesUtil.factorDuplicates(methods, new HashingStrategy<>() {
      @Override
      public int hashCode(@Nullable PsiMethod method) {
        return method == null ? 0 : method.getSignature(PsiSubstitutor.EMPTY).hashCode();
      }

      @Override
      public boolean equals(@Nullable PsiMethod method1, @Nullable PsiMethod method2) {
        return method1 == method2 ||
               (method1 != null &&
                method2 != null &&
                method1.getSignature(PsiSubstitutor.EMPTY).equals(method2.getSignature(PsiSubstitutor.EMPTY)));
      }
    });

    List<PsiMethod> list = map.get(method);
    if (list == null) return true;
    for (PsiMethod psiMethod : list) {
      if (psiMethod != method) {
        PsiClass containingClass = psiMethod.getContainingClass();
        if (containingClass == null) return true;
        String message = containingClass instanceof GroovyScriptClass
                         ? GroovyRefactoringBundle.message("method.is.already.defined.in.script",
                                                           GroovyRefactoringUtil.getMethodSignature(psiMethod),
                                                           RefactoringUIUtil.getDescription(containingClass, false))
                         : GroovyRefactoringBundle.message("method.is.already.defined.in.class",
                                                           GroovyRefactoringUtil.getMethodSignature(psiMethod),
                                                           RefactoringUIUtil.getDescription(containingClass, false));
        conflicts.putValue(psiMethod, message);
      }
    }

    return conflicts.isEmpty() || BaseRefactoringProcessor.processConflicts(helper.getProject(), conflicts);
  }

  class DataChangedListener implements EventListener {
    void dataChanged() {
      update();
    }
  }

  private void fireNameDataChanged() {
    Object[] list = myListenerList.getListenerList();
    for (Object aList : list) {
      if (aList instanceof DataChangedListener) {
        ((DataChangedListener)aList).dataChanged();
      }
    }
  }

  /*
  Update signature text area
   */
  public void updateSignature() {
    if (mySignature == null) return;
    @NonNls StringBuilder buffer = new StringBuilder();
    String modifier = ExtractUtil.getModifierString(myHelper);
    buffer.append(modifier);
    buffer.append(ExtractUtil.getTypeString(myHelper, true, modifier));

    final String _name = getEnteredName();
    String name = _name == null ? "" : _name;
    ExtractUtil.appendName(buffer, name);

    buffer.append("(");
    String[] params = ExtractUtil.getParameterString(myHelper, false);
    if (params.length > 0) {
      String INDENT = "    ";
      buffer.append("\n");
      for (String param : params) {
        buffer.append(INDENT).append(param).append("\n");
      }
    }
    buffer.append(")");
    mySignature.setSignature(buffer.toString());
  }

  public ExtractMethodSettings getSettings() {
    return new MyExtractMethodSettings(this);
  }

  private static class MyExtractMethodSettings implements ExtractMethodSettings {
    ExtractMethodInfoHelper myHelper;
    String myEnteredName;

    MyExtractMethodSettings(GroovyExtractMethodDialog dialog) {
      myHelper = dialog.getHelper();
      myEnteredName = dialog.getEnteredName();
    }

    @Override
    public @NotNull ExtractMethodInfoHelper getHelper() {
      return myHelper;
    }

    @Override
    public String getEnteredName() {
      return myEnteredName;
    }
  }
}
