// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.plugins.groovy.refactoring.introduce.constant;

import com.intellij.openapi.ui.Messages;
import com.intellij.openapi.util.NlsContexts.DialogMessage;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.refactoring.HelpID;
import com.intellij.refactoring.RefactoringBundle;
import com.intellij.refactoring.util.CommonRefactoringUtil;
import com.intellij.refactoring.util.RefactoringMessageUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.VisibilityUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.plugins.groovy.GroovyBundle;
import org.jetbrains.plugins.groovy.GroovyLanguage;
import org.jetbrains.plugins.groovy.codeStyle.GrReferenceAdjuster;
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory;
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.GrVariableDeclaration;
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.GrEnumTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrEnumConstantList;
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.GroovyRefactoringUtil;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceContext;
import org.jetbrains.plugins.groovy.refactoring.introduce.GrIntroduceHandlerBase;

import java.util.ArrayList;

/**
 * @author Max Medvedev
 */
public class GrIntroduceConstantProcessor {
  private final GrIntroduceContext context;
  private final GrIntroduceConstantSettings settings;

  public GrIntroduceConstantProcessor(GrIntroduceContext context, GrIntroduceConstantSettings settings) {

    this.context = context;
    this.settings = settings;
  }

  @Nullable
  public GrField run() {
    final PsiClass targetClass = settings.getTargetClass();
    if (targetClass == null) return null;

    if (checkErrors(targetClass)) {
      return null;
    }

    final GrVariableDeclaration declaration = addDeclaration(targetClass);
    final GrField field = (GrField)declaration.getVariables()[0];

    GrVariable localVar = GrIntroduceHandlerBase.resolveLocalVar(context);
    if (localVar != null) {
      assert localVar.getInitializerGroovy() != null : "initializer should exist: " + localVar.getText();
      GrIntroduceHandlerBase.deleteLocalVar(localVar);

      if (settings.replaceAllOccurrences()) {
        processOccurrences(field);
      }
      else {
        replaceOccurrence(field, localVar.getInitializerGroovy(), isEscalateVisibility());
      }
    }
    else if (context.getStringPart() != null) {
      final GrExpression ref = context.getStringPart().replaceLiteralWithConcatenation(field.getName());
      final PsiElement element = replaceOccurrence(field, ref, isEscalateVisibility());
      updateCaretPosition(element);
    }
    else if (context.getExpression() != null) {
      if (settings.replaceAllOccurrences()) {
        processOccurrences(field);
      }
      else {
        replaceOccurrence(field, context.getExpression(), isEscalateVisibility());
      }
    }
    return field;
  }

  private void processOccurrences(GrField field) {
    final PsiElement[] occurrences = context.getOccurrences();
    GroovyRefactoringUtil.sortOccurrences(occurrences);
    for (PsiElement occurrence : occurrences) {
      replaceOccurrence(field, occurrence, isEscalateVisibility());
    }
  }

  private void updateCaretPosition(PsiElement element) {
    context.getEditor().getCaretModel().moveToOffset(element.getTextRange().getEndOffset());
    context.getEditor().getSelectionModel().removeSelection();
  }

  protected GrVariableDeclaration addDeclaration(PsiClass targetClass) {
    GrVariableDeclaration declaration = createField(targetClass);
    final GrVariableDeclaration added;
    if (targetClass instanceof GrEnumTypeDefinition) {
      final GrEnumConstantList enumConstants = ((GrEnumTypeDefinition)targetClass).getEnumConstantList();
      added = (GrVariableDeclaration)targetClass.addAfter(declaration, enumConstants);
    }
    else {
      added = ((GrVariableDeclaration)targetClass.add(declaration));
    }

    JavaCodeStyleManager.getInstance(added.getProject()).shortenClassReferences(added);
    return added;
  }

  protected boolean checkErrors(@NotNull PsiClass targetClass) {
    String fieldName = settings.getName();
    String errorString = check(targetClass, fieldName);

    if (errorString != null) {
      String message = RefactoringBundle.getCannotRefactorMessage(errorString);
      CommonRefactoringUtil
        .showErrorMessage(GroovyBundle.message("introduce.constant.title"), message, HelpID.INTRODUCE_CONSTANT, context.getProject());
      return true;
    }

    PsiField oldField = targetClass.findFieldByName(fieldName, true);
    if (oldField != null) {
      String message = RefactoringBundle.message("field.exists", fieldName, oldField.getContainingClass().getQualifiedName());
      int answer = Messages
        .showYesNoDialog(context.getProject(), message, GroovyBundle.message("introduce.constant.title"), Messages.getWarningIcon());
      if (answer != Messages.YES) {
        return true;
      }
    }
    return false;
  }

  @Nullable
  private @DialogMessage String check(@NotNull PsiClass targetClass, @Nullable final String fieldName) {
    if (!GroovyLanguage.INSTANCE.equals(targetClass.getLanguage())) {
      return GroovyRefactoringBundle.message("class.language.is.not.groovy");
    }

    if (fieldName == null || fieldName.isEmpty()) {
      return RefactoringBundle.message("no.field.name.specified");
    }

    else if (!PsiNameHelper.getInstance(context.getProject()).isIdentifier(fieldName)) {
      return RefactoringMessageUtil.getIncorrectIdentifierMessage(fieldName);
    }

    if (targetClass instanceof GroovyScriptClass) {
      return GroovyRefactoringBundle.message("target.class.must.not.be.script");
    }

    return null;
  }


  private PsiElement replaceOccurrence(@NotNull GrField field, @NotNull PsiElement occurrence, boolean escalateVisibility) {
    boolean isOriginal = occurrence == context.getExpression();
    final GrReferenceExpression newExpr = createRefExpression(field, occurrence);
    final PsiElement replaced = occurrence instanceof GrExpression
                                ? ((GrExpression)occurrence).replaceWithExpression(newExpr, false)
                                : occurrence.replace(newExpr);
    if (escalateVisibility) {
      PsiUtil.escalateVisibility(field, replaced);
    }
    if (replaced instanceof GrReferenceExpression) {
      GrReferenceAdjuster.shortenReference((GrReferenceExpression)replaced);
    }
    if (isOriginal) {
      updateCaretPosition(replaced);
    }
    return replaced;
  }

  @NotNull
  private static GrReferenceExpression createRefExpression(@NotNull GrField field, @NotNull PsiElement place) {
    final PsiClass containingClass = field.getContainingClass();
    assert containingClass != null;
    final String qname = containingClass.getQualifiedName();
    final String fieldName = field.getName();
    final String refText = qname != null && !qname.equals(fieldName) ? qname + "." + fieldName : fieldName;
    return GroovyPsiElementFactory.getInstance(place.getProject()).createReferenceExpressionFromText(refText, place);
  }

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

    String[] modifiers = collectModifiers(targetClass);

    final GroovyPsiElementFactory factory = GroovyPsiElementFactory.getInstance(context.getProject());
    return factory.createFieldDeclaration(modifiers, name, getInitializer(), type);
  }

  @NotNull
  protected GrExpression getInitializer() {
    GrVariable var = GrIntroduceHandlerBase.resolveLocalVar(context);
    GrExpression expression = context.getExpression();

    if (var != null) {
      return var.getInitializerGroovy();
    }
    else if (expression != null) {
      return expression;
    }
    else {
      return context.getStringPart().createLiteralFromSelected();
    }
  }

  private String @NotNull [] collectModifiers(PsiClass targetClass) {
    String modifier = isEscalateVisibility() ? PsiModifier.PRIVATE : settings.getVisibilityModifier();
    ArrayList<String> modifiers = new ArrayList<>();
    if (modifier!= null && !PsiModifier.PACKAGE_LOCAL.equals(modifier)) {
      modifiers.add(modifier);
    }
    if (!targetClass.isInterface()) {
      modifiers.add(PsiModifier.STATIC);
      modifiers.add(PsiModifier.FINAL);
    }
    return ArrayUtilRt.toStringArray(modifiers);
  }

  private boolean isEscalateVisibility() {
    return VisibilityUtil.ESCALATE_VISIBILITY.equals(settings.getVisibilityModifier());
  }

}
