// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package com.intellij.codeInspection;

import com.intellij.openapi.command.undo.UndoUtil;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.PsiComment;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.codeStyle.CodeStyleManager;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.xml.*;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.HashSet;

/**
 * @author Dmitry Avdeev
 */
public class DefaultXmlSuppressionProvider extends XmlSuppressionProvider implements InspectionSuppressor {

  public static final String SUPPRESS_MARK = "suppress";

  @Override
  public boolean isProviderAvailable(@NotNull PsiFile file) {
    return true;
  }

  @Override
  public boolean isSuppressedFor(@NotNull PsiElement element, @NotNull String inspectionId) {
    final XmlTag tag = element instanceof XmlFile ? ((XmlFile)element).getRootTag() : PsiTreeUtil.getContextOfType(element, XmlTag.class, false);
    return tag != null && findSuppression(tag, inspectionId, element) != null;
  }

  @Override
  public void suppressForFile(@NotNull PsiElement element, @NotNull String inspectionId) {
    final PsiFile file = element.getContainingFile();
    if (!(file instanceof XmlFile)) return;
    final XmlDocument document = ((XmlFile)file).getDocument();
    final PsiElement anchor = document != null ? document.getRootTag() : file.findElementAt(0);
    assert anchor != null;
    suppress(file, findFileSuppression(anchor, null, element), inspectionId, anchor.getTextRange().getStartOffset());
  }

  @Override
  public void suppressForTag(@NotNull PsiElement element, @NotNull String inspectionId) {
    final XmlTag tag = PsiTreeUtil.getParentOfType(element, XmlTag.class);
    assert tag != null;
    suppress(element.getContainingFile(), findSuppressionLeaf(tag, null, 0), inspectionId, tag.getTextRange().getStartOffset());
  }

  protected @Nullable PsiElement findSuppression(final PsiElement anchor, final String id, PsiElement originalElement) {
    final PsiElement element = findSuppressionLeaf(anchor, id, 0);
    if (element != null) return element;

    return findFileSuppression(anchor, id, originalElement);
  }

  protected @Nullable PsiElement findFileSuppression(PsiElement anchor, String id, PsiElement originalElement) {
    final PsiFile file = anchor.getContainingFile();
    if (file instanceof XmlFile) {
      final XmlDocument document = ((XmlFile)file).getDocument();
      final XmlTag rootTag = document != null ? document.getRootTag() : null;
      PsiElement leaf = rootTag != null ? rootTag.getPrevSibling() : file.findElementAt(0);
      return findSuppressionLeaf(leaf, id, 0);
    }
    return null;
  }

  protected @Nullable PsiElement findSuppressionLeaf(PsiElement leaf, final @Nullable String id, int offset) {
    while (leaf != null && leaf.getTextOffset() >= offset) {
      if (isSuppressedInComment(leaf, id)) {
        return leaf;
      }
      else if (leaf instanceof XmlText || leaf instanceof XmlProlog) {
        for (PsiElement child : leaf.getChildren()) {
          if (isSuppressedInComment(child, id)) {
            return leaf;
          }
        }
      }
      leaf = leaf.getPrevSibling();
      if (leaf instanceof XmlTag) {
        return null;
      }
    }
    return null;
  }

  private boolean isSuppressedInComment(@NotNull PsiElement element, @Nullable String id) {
    if (!(element instanceof PsiComment)) return false;

    String text = element.getText();
    if (!text.contains(getPrefix())) {
      return false;
    }
    if (id == null) {
      return true;
    }
    final @NonNls HashSet<String> parts = new HashSet<>(StringUtil.getWordsIn(text));
    return parts.contains(id) || parts.contains(XmlSuppressableInspectionTool.ALL);
  }

  protected void suppress(PsiFile file, final PsiElement suppressionElement, String inspectionId, final int offset) {
    final Project project = file.getProject();
    //final Document doc = PsiDocumentManager.getInstance(project).getDocument(file);
    final Document doc = file.getViewProvider().getDocument();
    assert doc != null;

    if (suppressionElement != null) {
      final TextRange textRange = suppressionElement.getTextRange();
      String text = suppressionElement.getText();
      final String suppressionText = getSuppressionText(inspectionId, text);
      doc.replaceString(textRange.getStartOffset(), textRange.getEndOffset(), suppressionText);
    } else {
      final String suppressionText = getSuppressionText(inspectionId, null);
      doc.insertString(offset, suppressionText);
      CodeStyleManager.getInstance(project).adjustLineIndent(doc, offset + suppressionText.length());
      UndoUtil.markPsiFileForUndo(file);
    }
  }

  protected String getSuppressionText(String inspectionId, @Nullable String originalText) {
    if (originalText == null) {
      return getPrefix() + inspectionId + getSuffix() + "\n";
    } else if (inspectionId.equals(XmlSuppressableInspectionTool.ALL)) {
      final int pos = originalText.indexOf(getPrefix());
      return originalText.substring(0, pos) + getPrefix() + inspectionId + getSuffix() + "\n";
    }
    return StringUtil.replace(originalText, getSuffix(), ", " + inspectionId + getSuffix());
  }

  protected @NonNls String getPrefix() {
    return "<!--" +
           SUPPRESS_MARK +
           " ";
  }

  protected @NonNls String getSuffix() {
    return " -->";
  }
}
