// Copyright 2000-2019 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;

import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.refactoring.ui.ConflictsDialog;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.containers.MultiMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase;
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.typedef.GrTypeDefinition;
import org.jetbrains.plugins.groovy.lang.psi.api.statements.typedef.members.GrMethod;
import org.jetbrains.plugins.groovy.refactoring.GroovyRefactoringUtil;

import java.util.Arrays;

/**
 * @author Maxim.Medvedev
 */
public class GrIntroduceValidatorEngine implements GrIntroduceHandlerBase.Validator {
  private final GrIntroduceContext myContext;
  private final ConflictReporter myReporter;

  public GrIntroduceValidatorEngine(GrIntroduceContext context, ConflictReporter reporter) {
    myContext = context;
    myReporter = reporter;
  }

  @Override
  public boolean isOK(GrIntroduceDialog dialog) {
    final GrIntroduceSettings settings = dialog.getSettings();
    if (settings == null) return false;
    String varName = settings.getName();
    boolean allOccurrences = settings.replaceAllOccurrences();
    final MultiMap<PsiElement, String> conflicts = isOKImpl(varName, allOccurrences);
    return conflicts.size() <= 0 || reportConflicts(conflicts, getProject());
  }

  private static boolean reportConflicts(final MultiMap<PsiElement, String> conflicts, final Project project) {
    ConflictsDialog conflictsDialog = new ConflictsDialog(project, conflicts);
    return conflictsDialog.showAndGet();
  }

  private MultiMap<PsiElement, String> isOKImpl(String varName, boolean replaceAllOccurrences) {
    PsiElement firstOccurrence = getFirstOccurrence(replaceAllOccurrences);

    final MultiMap<PsiElement, String> conflicts = new MultiMap<>();
    assert varName != null;

    final int offset = firstOccurrence.getTextRange().getStartOffset();

    validateOccurrencesDown(myContext.getScope(), conflicts, varName, offset);
    if (!(myContext.getScope() instanceof GroovyFileBase)) {
      validateVariableOccurrencesUp(myContext.getScope(), conflicts, varName, offset);
    }

    if (replaceAllOccurrences) {
      for (PsiElement element : myContext.getOccurrences()) {
        if (element == firstOccurrence) continue;
        validateVariableOccurrencesUp(element, conflicts, varName, element.getTextRange().getStartOffset());
      }
    }
    return conflicts;
  }

  private PsiElement getFirstOccurrence(boolean replaceAllOccurrences) {
    if (replaceAllOccurrences) {
      if (myContext.getOccurrences().length > 0) {
        GroovyRefactoringUtil.sortOccurrences(myContext.getOccurrences());
        return myContext.getOccurrences()[0];
      }
      else {
        return myContext.getPlace();
      }
    }
    else {
      final GrExpression expression = myContext.getExpression();
      return expression != null ? expression : myContext.getStringPart().getLiteral();
    }
  }


  /**
   * Use for validator tests
   */
  public String isOKTest(String varName, boolean allOccurences) {
    MultiMap<PsiElement, String> list = isOKImpl(varName, allOccurences);
    String result = "";
    final String[] strings = ArrayUtilRt.toStringArray(list.values());
    Arrays.sort(strings);

    for (String s : strings) {
      result = result + s.replaceAll("<b><code>", "").replaceAll("</code></b>", "") + "\n";
    }
    if (!list.isEmpty()) {
      result = result.substring(0, result.length() - 1);
    }
    if (result.isEmpty()) {
      result = "ok";
    }
    return result;
  }

  /**
   * @param startElement Container to start checking conflicts from
   * @param conflicts    Conflict accumulator
   * @param varName      Variable name
   */
  private void validateOccurrencesDown(@NotNull PsiElement startElement,
                                       @NotNull MultiMap<PsiElement, String> conflicts,
                                       @NotNull String varName,
                                       double startOffset) {
    PsiElement child = startElement.getFirstChild();
    while (child != null) {
      // Do not check defined classes, methods, closures and blocks before
      if (child instanceof GrTypeDefinition ||
          child instanceof GrMethod ||
          GroovyRefactoringUtil.isAppropriateContainerForIntroduceVariable(child) &&
          child.getTextRange().getEndOffset() < startOffset) {
        myReporter.check(child, conflicts, varName);
        child = child.getNextSibling();
        continue;
      }
      if (child instanceof GrVariable) {
        myReporter.check(child, conflicts, varName);
      }
      validateOccurrencesDown(child, conflicts, varName, startOffset);
      child = child.getNextSibling();
    }
  }

  private void validateVariableOccurrencesUp(PsiElement startElement,
                                             MultiMap<PsiElement, String> conflicts,
                                             @NotNull String varName,
                                             double startOffset) {
    PsiElement prevSibling = startElement.getPrevSibling();
    while (prevSibling != null) {
      if (!(GroovyRefactoringUtil.isAppropriateContainerForIntroduceVariable(prevSibling) &&
            prevSibling.getTextRange().getEndOffset() < startOffset)) {
        validateOccurrencesDown(prevSibling, conflicts, varName, startOffset);
      }
      prevSibling = prevSibling.getPrevSibling();
    }

    PsiElement parent = startElement.getParent();
    // Do not check context out of method, type definition and directories
    if (parent == null ||
        parent instanceof GrMethod ||
        parent instanceof GrTypeDefinition ||
        parent instanceof GroovyFileBase ||
        parent instanceof PsiDirectory) {
      return;
    }

    validateVariableOccurrencesUp(parent, conflicts, varName, startOffset);
  }

  /**
   * Validates name to be suggested in context
   */
  @Override
  public String validateName(String name, boolean increaseNumber) {
    String result = name;
    if (!isOKImpl(name, true).isEmpty() && !increaseNumber || name.isEmpty()) {
      return "";
    }
    int i = 1;
    while (!isOKImpl(result, true).isEmpty()) {
      result = name + i;
      i++;
    }
    return result;
  }

  @Override
  public Project getProject() {
    return myContext.getProject();
  }
}
