// 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.idea.devkit.actions;

import com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerEx;
import com.intellij.codeInsight.daemon.impl.HighlightInfo;
import com.intellij.lang.annotation.HighlightSeverity;
import com.intellij.openapi.actionSystem.ActionUpdateThread;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.CommonDataKeys;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.command.CommandProcessor;
import com.intellij.openapi.command.CommandProcessorEx;
import com.intellij.openapi.command.CommandToken;
import com.intellij.openapi.command.UndoConfirmationPolicy;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.ex.util.EditorUtil;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.psi.PsiFile;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

final class ToggleHighlightingMarkupAction extends DumbAwareAction {
  @Override
  public void update(@NotNull AnActionEvent e) {
    Editor editor = e.getData(CommonDataKeys.EDITOR);
    PsiFile file = e.getData(CommonDataKeys.PSI_FILE);
    e.getPresentation().setEnabled(editor != null && file != null);
  }

  @Override
  public @NotNull ActionUpdateThread getActionUpdateThread() {
    return ActionUpdateThread.BGT;
  }

  @Override
  public void actionPerformed(@NotNull AnActionEvent e) {
    final Editor editor = e.getData(CommonDataKeys.EDITOR);
    PsiFile file = e.getData(CommonDataKeys.PSI_FILE);
    if (editor == null || file == null) return;
    final Project project = file.getProject();
    CommandProcessorEx commandProcessor = (CommandProcessorEx)CommandProcessor.getInstance();
    CommandToken commandToken = commandProcessor.startCommand(project, e.getPresentation().getText(), e.getPresentation().getText(), UndoConfirmationPolicy.DEFAULT);
    try {
      WriteAction.run(() -> {
        TextRange range = editor.getSelectionModel().hasSelection()
                          ? EditorUtil.getSelectionInAnyMode(editor)
                          : TextRange.create(0, editor.getDocument().getTextLength());
        perform(project, editor.getDocument(), range);
      });
    }
    finally {
      commandProcessor.finishCommand(commandToken, null);
    }
  }

  private static void perform(Project project, final Document document, TextRange selection) {
    final CharSequence sequence = document.getCharsSequence();
    final StringBuilder sb = new StringBuilder();
    Pattern pattern = Pattern.compile("<(/?(?:error|warning|EOLError|EOLWarning|info|weak_warning))((?:\\s|=|\\w+|\"(?:[^\"]|\\\\\")*?\")*?)>(.*?)");
    Matcher matcher = pattern.matcher(sequence);
    if (matcher.find(selection.getStartOffset())) {
      boolean compactMode = false;
      int pos = 0;
      do {
        if (matcher.start(0) >= selection.getEndOffset()) break;
        String tag = matcher.group(1);
        if (!tag.startsWith("/")) {
          boolean hasDescription = matcher.start(2) < matcher.end(2);
          if (hasDescription) {
            if (!compactMode) {
              sb.setLength(pos = 0); // restart in compact mode from the first description
              compactMode = true;
            }
            sb.append(sequence, pos, matcher.start(2));
            pos = matcher.end(2);
          }
          else if (!compactMode) {
            sb.append(sequence, pos, matcher.start());
            pos = matcher.end();
          }
        }
        else if (!compactMode) {
          sb.append(sequence, pos, matcher.start());
          pos = matcher.end();
        }
      }
      while (matcher.find(matcher.end()));
      sb.append(sequence, pos, sequence.length());
    }
    else {
      final int[] offset = {0};
      final ArrayList<HighlightInfo> infos = new ArrayList<>();
      DaemonCodeAnalyzerEx.processHighlights(
        document, project, HighlightSeverity.WARNING, 0, sequence.length(),
        info -> {
          if (info.getSeverity() != HighlightSeverity.WARNING && info.getSeverity() != HighlightSeverity.ERROR) return true;
          if (info.getStartOffset() >= selection.getEndOffset()) return false;
          if (info.getEndOffset() > selection.getStartOffset()) {
            offset[0] = appendInfo(info, sb, sequence, offset[0], infos);
          }
          return true;
        });
      offset[0] = appendInfo(null, sb, sequence, offset[0], infos);
      sb.append(sequence.subSequence(offset[0], sequence.length()));
    }
    document.setText(sb);
  }

  private static int appendInfo(@Nullable HighlightInfo info,
                                StringBuilder sb,
                                CharSequence sequence,
                                int offset,
                                ArrayList<HighlightInfo> infos) {
    if (info == null || !infos.isEmpty() && getMaxEnd(infos) < info.getStartOffset()) {
      if (infos.size() == 1) {
        HighlightInfo cur = infos.remove(0);
        sb.append(sequence.subSequence(offset, cur.getStartOffset()));
        appendTag(sb, cur, true);
        sb.append(sequence.subSequence(cur.getStartOffset(), cur.getEndOffset()));
        appendTag(sb, cur, false);
        offset = cur.getEndOffset();
      }
      else {
        // process overlapped
        LinkedList<HighlightInfo> stack = new LinkedList<>();
        for (HighlightInfo cur : infos) {
          offset = processStack(stack, sb, sequence, offset, cur.getStartOffset());
          sb.append(sequence.subSequence(offset, cur.getStartOffset()));
          offset = cur.getStartOffset();
          appendTag(sb, cur, true);
          stack.addLast(cur);
        }
        offset = processStack(stack, sb, sequence, offset, sequence.length());
        infos.clear();
      }
    }
    if (info != null) {
      boolean found = false;
      for (HighlightInfo cur : infos) {
        if (cur.getStartOffset() == info.getStartOffset() && cur.getEndOffset() == info.getEndOffset() && cur.getSeverity() ==
                                                                                                          info.getSeverity()) {
          found = true;
          break;
        }
      }
      if (!found) infos.add(info);
    }
    return offset;
  }

  private static int getMaxEnd(ArrayList<HighlightInfo> infos) {
    int max = -1;
    for (HighlightInfo info : infos) {
      int endOffset = info.getEndOffset();
      if (max < endOffset) max = endOffset;
    }
    return max;
  }

  private static int processStack(LinkedList<HighlightInfo> stack,
                                  StringBuilder sb,
                                  CharSequence sequence,
                                  int offset,
                                  final int endOffset) {
    if (stack.isEmpty()) return offset;
    for (HighlightInfo cur = stack.peekLast(); cur != null && cur.getEndOffset() <= endOffset; cur = stack.peekLast()) {
      stack.removeLast();
      if (offset <= cur.getEndOffset()) {
        sb.append(sequence.subSequence(offset, cur.getEndOffset()));
      }
      offset = cur.getEndOffset();
      appendTag(sb, cur, false);
    }
    return offset;
  }

  private static void appendTag(@NonNls StringBuilder sb, HighlightInfo cur, boolean opening) {
    sb.append("<");
    if (!opening) sb.append("/");
    if (cur.isAfterEndOfLine()) {
      sb.append(cur.getSeverity() == HighlightSeverity.WARNING ? "EOLWarning" : "EOLError");
    }
    else {
      sb.append(cur.getSeverity() == HighlightSeverity.WARNING ? "warning" : "error");
    }
    if (opening) {
      sb.append(" descr=\"").append(cur.getDescription()).append("\"");
    }
    sb.append(">");
  }
}
