// 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.codeInsight.javadoc;

import com.intellij.CommonBundle;
import com.intellij.application.options.CodeStyle;
import com.intellij.codeInsight.AnnotationUtil;
import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.codeInsight.documentation.DocumentationManagerUtil;
import com.intellij.codeInsight.javadoc.markdown.JavaDocMarkdownFlavourDescriptor;
import com.intellij.ide.highlighter.HtmlFileType;
import com.intellij.java.JavaBundle;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.javadoc.JavadocGeneratorRunProfile;
import com.intellij.lang.ASTNode;
import com.intellij.lang.Language;
import com.intellij.lang.documentation.DocumentationMarkup;
import com.intellij.lang.documentation.DocumentationSettings.InlineCodeHighlightingMode;
import com.intellij.lang.injection.InjectedLanguageManager;
import com.intellij.lang.java.JavaDocumentationProvider;
import com.intellij.lang.java.JavaLanguage;
import com.intellij.markdown.utils.MarkdownToHtmlConverter;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
import com.intellij.openapi.editor.HighlighterColors;
import com.intellij.openapi.editor.colors.CodeInsightColors;
import com.intellij.openapi.editor.colors.EditorColors;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdk;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.ProjectFileIndex;
import com.intellij.openapi.util.*;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.text.HtmlBuilder;
import com.intellij.openapi.util.text.HtmlChunk;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.impl.JavaConstantExpressionEvaluator;
import com.intellij.psi.impl.source.javadoc.PsiDocMethodOrFieldRef;
import com.intellij.psi.impl.source.javadoc.PsiSnippetDocTagImpl;
import com.intellij.psi.impl.source.tree.ElementType;
import com.intellij.psi.impl.source.tree.JavaDocElementType;
import com.intellij.psi.impl.source.tree.JavaElementType;
import com.intellij.psi.impl.source.tree.TreeElement;
import com.intellij.psi.javadoc.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.psi.util.*;
import com.intellij.psi.xml.XmlAttribute;
import com.intellij.psi.xml.XmlAttributeValue;
import com.intellij.psi.xml.XmlTag;
import com.intellij.ui.ColorUtil;
import com.intellij.util.ArrayUtil;
import com.intellij.util.ArrayUtilRt;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.ContainerUtil;
import kotlin.text.StringsKt;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jetbrains.annotations.*;

import java.awt.*;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.function.UnaryOperator;
import java.util.regex.Pattern;

import static com.intellij.codeInsight.javadoc.SnippetMarkup.*;
import static com.intellij.lang.documentation.DocumentationMarkup.BOTTOM_ELEMENT;
import static com.intellij.lang.documentation.QuickDocHighlightingHelper.appendStyledCodeBlock;
import static com.intellij.lang.documentation.QuickDocHighlightingHelper.appendStyledInlineCode;

public class JavaDocInfoGenerator {
  private static final Logger LOG = Logger.getInstance(JavaDocInfoGenerator.class);

  @ApiStatus.Internal
  public record InheritDocContext<T>(@Nullable T element, @Nullable InheritDocProvider<T> provider) {}

  @ApiStatus.Internal
  public interface InheritDocProvider<T> {
    @Nullable
    InheritDocContext<T> getInheritDoc(@Nullable PsiDocTagValue target);

    @Nullable
    PsiClass getElement();
  }

  @FunctionalInterface
  @ApiStatus.Internal
  public interface DocTagLocator<T> {
    T find(PsiDocCommentOwner owner, PsiDocComment comment);
  }

  private static final String HREF_ATTRIBUTE_NAME = "href";
  private static final String BR_TAG = "<br>";
  private static final String LINK_TAG = "link";
  private static final String LITERAL_TAG = "literal";
  private static final String CODE_TAG = "code";
  private static final String SYSTEM_PROPERTY_TAG = "systemProperty";
  private static final String LINKPLAIN_TAG = "linkplain";
  private static final String INHERIT_DOC_TAG = "inheritDoc";
  private static final String DOC_ROOT_TAG = "docRoot";
  private static final String VALUE_TAG = "value";
  private static final String INDEX_TAG = "index";
  private static final String SUMMARY_TAG = "summary";
  private static final String SNIPPET_TAG = "snippet";
  private static final String RETURN_TAG = "return";
  private static final String LT = "&lt;";
  private static final String GT = "&gt;";
  private static final String NBSP = "&nbsp;";

  private static final String BLOCKQUOTE_PRE_PREFIX = "<blockquote><pre>";
  private static final String BLOCKQUOTE_PRE_SUFFIX = "</pre></blockquote>";
  private static final String PRE_CODE_PREFIX = "<pre><code>";
  private static final String PRE_CODE_SUFFIX = "</code></pre>";

  private static final MarkdownToHtmlConverter ourMarkdownConverter = new MarkdownToHtmlConverter(new JavaDocMarkdownFlavourDescriptor());
  private static final @NotNull TokenSet INLINE_TAG_TOKENS = TokenSet.create(JavaDocTokenType.DOC_INLINE_TAG_END,
                                                                             JavaDocTokenType.DOC_INLINE_TAG_START,
                                                                             JavaDocTokenType.DOC_COMMENT_LEADING_ASTERISKS);

  /**
   * Tags that we know how to render.
   */
  private static final Set<String> ourKnownTags = ContainerUtil.newHashSet(
    "author",
    "version",
    "param",
    "return",
    "deprecated",
    "since",
    "throws",
    "exception",
    "see",
    "serial",
    "serialField",
    "serialData",
    "apiNote",
    "implNote",
    "implSpec"
  );

  private static final Pattern ourWhitespaces = Pattern.compile("[ \\n\\r\\t]+");

  private static final InheritDocProvider<PsiDocTag> ourEmptyProvider = new InheritDocProvider<>() {
    @Override
    public @Nullable InheritDocContext<PsiDocTag> getInheritDoc(@Nullable PsiDocTagValue target) {
      return null;
    }

    @Override
    public @Nullable PsiClass getElement() {
      return null;
    }
  };

  private static final InheritDocProvider<PsiElement[]> ourEmptyElementsProvider = mapProvider(ourEmptyProvider, false);

  private final @NotNull Project myProject;
  private final @Nullable PsiElement myElement;
  private final @NotNull JavaDocHighlightingManager myHighlightingManager;
  private final @Nullable JavaSdkVersion mySdkVersion;

  private final boolean myIsRendered;
  private final boolean myDoHighlightSignatures;
  private final boolean myDoHighlightCodeBlocks;
  private final boolean myDoSemanticHighlightingOfLinks;
  private final float myHighlightingSaturation;

  private boolean myIsSignatureGenerationInProgress;

  public JavaDocInfoGenerator(@NotNull Project project, @Nullable PsiElement element) {
    this(
      project,
      element,
      new JavaDocHighlightingManagerImpl(),
      false,
      true,
      true,
      InlineCodeHighlightingMode.AS_DEFAULT_CODE,
      false,
      1.0F);
  }

  public JavaDocInfoGenerator(
    @NotNull Project project,
    @Nullable PsiElement element,
    @NotNull JavaDocHighlightingManager highlightingManager,
    boolean isGenerationForRenderedDoc,
    boolean doHighlightSignatures,
    boolean doHighlightCodeBlocks,
    @NotNull InlineCodeHighlightingMode ignoredInlineCodeBlocksHighlightingMode,
    boolean doSemanticHighlightingOfLinks,
    float highlightingSaturationFactor
  ) {
    myProject = project;
    myElement = element;
    myIsRendered = isGenerationForRenderedDoc;
    myHighlightingManager = highlightingManager;
    myDoHighlightSignatures = doHighlightSignatures;
    myDoHighlightCodeBlocks = doHighlightCodeBlocks;
    myDoSemanticHighlightingOfLinks = doSemanticHighlightingOfLinks;
    myHighlightingSaturation = highlightingSaturationFactor;

    Sdk jdk = JavadocGeneratorRunProfile.getSdk(myProject);
    mySdkVersion = jdk == null ? null : JavaSdk.getInstance().getVersion(jdk);
  }

  public boolean isRendered() {
    return myIsRendered;
  }

  public boolean doHighlightSignatures() {
    return myDoHighlightSignatures;
  }

  public boolean doHighlightCodeBlocks() {
    return myDoHighlightCodeBlocks;
  }

  public boolean doSemanticHighlightingOfLinks() {
    return myDoSemanticHighlightingOfLinks;
  }

  public float getHighlightingSaturation() {
    return myHighlightingSaturation;
  }

  public @NotNull JavaDocHighlightingManager getHighlightingManager() {
    return myHighlightingManager;
  }

  protected @NotNull StringBuilder appendStyledSpan(
    boolean doHighlighting,
    @NotNull StringBuilder buffer,
    @NotNull TextAttributes attributes,
    @Nullable String value
  ) {
    if (doHighlighting) {
      HtmlSyntaxInfoUtil.appendStyledSpan(buffer, attributes, value, getHighlightingSaturation());
    }
    else {
      buffer.append(value);
    }
    return buffer;
  }

  protected @NotNull StringBuilder appendStyledSpan(
    @NotNull StringBuilder buffer,
    @NotNull TextAttributes attributes,
    @Nullable String value
  ) {
    return appendStyledSpan(doHighlightSignatures(), buffer, attributes, value);
  }

  protected @NotNull StringBuilder appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet(
    boolean doHighlighting,
    @NotNull StringBuilder buffer,
    @NotNull Project project,
    @NotNull Language language,
    @Nullable String codeSnippet
  ) {
    if (doHighlighting) {
      HtmlSyntaxInfoUtil.appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet(
        buffer, project, language, codeSnippet, getHighlightingSaturation());
    }
    else if (codeSnippet != null) {
      codeSnippet = StringsKt.trimIndent(codeSnippet);
      codeSnippet = StringUtil.escapeXmlEntities(codeSnippet);
      codeSnippet = codeSnippet.replace("\n", BR_TAG);
      buffer.append(codeSnippet);
    }
    return buffer;
  }

  protected @NotNull String getStyledSpan(boolean doHighlighting, @NotNull TextAttributes attributes, @Nullable String value) {
    return appendStyledSpan(doHighlighting, new StringBuilder(), attributes, value).toString();
  }

  public @NotNull String getHighlightedByLexerAndEncodedAsHtmlCodeSnippet(
    @NotNull Project project,
    @NotNull Language language,
    @Nullable String codeSnippet
  ) {
    return appendHighlightedByLexerAndEncodedAsHtmlCodeSnippet(true, new StringBuilder(), project, language, codeSnippet).toString();
  }

  private static InheritDocProvider<PsiElement[]> mapProvider(InheritDocProvider<PsiDocTag> i, boolean dropFirst) {
    return new InheritDocProvider<>() {
      @Override
      public @Nullable InheritDocContext<PsiElement[]> getInheritDoc(@Nullable PsiDocTagValue target) {
        InheritDocContext<PsiDocTag> inheritDocContext = i.getInheritDoc(target);
        if (inheritDocContext == null || inheritDocContext.element == null) return null;

        PsiElement[] elements;
        PsiElement[] rawElements = inheritDocContext.element.getDataElements();
        if (dropFirst && rawElements.length > 0) {
          elements = new PsiElement[rawElements.length - 1];
          System.arraycopy(rawElements, 1, elements, 0, elements.length);
        }
        else {
          elements = rawElements;
        }

        return new InheritDocContext<>(elements, mapProvider(inheritDocContext.provider, dropFirst));
      }

      @Override
      public @Nullable PsiClass getElement() {
        return i.getElement();
      }
    };
  }

  private static DocTagLocator<PsiDocTag> parameterLocator(int parameterIndex) {
    return (owner, comment) -> {
      if (parameterIndex < 0 || comment == null || !(owner instanceof PsiMethod)) return null;

      PsiParameter[] parameters = ((PsiMethod)owner).getParameterList().getParameters();
      if (parameterIndex >= parameters.length) return null;

      String name = parameters[parameterIndex].getName();
      return getParamTagByName(comment, name);
    };
  }

  private static DocTagLocator<PsiDocTag> typeParameterLocator(int parameterIndex) {
    return (owner, comment) -> {
      if (parameterIndex < 0 || comment == null || !(owner instanceof PsiTypeParameterListOwner)) return null;

      PsiTypeParameter[] parameters = ((PsiTypeParameterListOwner)owner).getTypeParameters();
      if (parameterIndex >= parameters.length) return null;

      String rawName = parameters[parameterIndex].getName();
      if (rawName == null) return null;
      String name = '<' + rawName + '>';
      return getParamTagByName(comment, name);
    };
  }

  private static PsiDocTag getParamTagByName(@NotNull PsiDocComment comment, String name) {
    return getTagByName(comment.findTagsByName("param"), name);
  }

  private static PsiDocTag getTagByName(PsiDocTag @NotNull [] tags, String name) {
    for (PsiDocTag tag : tags) {
      PsiDocTagValue value = tag.getValueElement();
      if (value != null) {
        String text = value.getText();
        if (text != null && text.equals(name)) {
          return tag;
        }
      }
    }

    return null;
  }

  private static DocTagLocator<PsiDocTag> exceptionLocator(@NotNull String name) {
    return (owner, comment) -> {
      if (comment == null) return null;

      for (PsiDocTag tag : getThrowsTags(comment)) {
        PsiDocTagValue value = tag.getValueElement();
        if (value != null) {
          String text = value.getText();
          if (text != null && areWeakEqual(text, name)) {
            return tag;
          }
        }
      }

      return null;
    };
  }

  private @Nls String sanitizeHtml(@Nls StringBuilder buffer) {
    String text = buffer.toString();
    if (text.isEmpty()) return null;
    if (myElement != null) {  // PSI element refs can't be resolved without a context
      StringBuilder result = new StringBuilder();
      int lastRef = 0;

      if (text.toUpperCase(Locale.ROOT).contains("HREF=\"")) {
        PsiFile fromText = PsiFileFactory.getInstance(myProject)
          .createFileFromText("DUMMY__.html", HtmlFileType.INSTANCE, text, System.currentTimeMillis(), false);
        Collection<XmlTag> tags = PsiTreeUtil.findChildrenOfType(fromText, XmlTag.class);
        for (XmlTag tag : tags) {
          if (!tag.getName().toLowerCase(Locale.ROOT).equals("a")) {
            continue;
          }
          final XmlAttribute hrefAttribute = tag.getAttribute(HREF_ATTRIBUTE_NAME);
          if (hrefAttribute == null) {
            continue;
          }
          XmlAttributeValue hrefAttributeValueElement = hrefAttribute.getValueElement();
          if (hrefAttributeValueElement == null) {
            continue;
          }
          int groupStart = hrefAttributeValueElement.getValueTextRange().getStartOffset();
          int groupEnd = hrefAttributeValueElement.getValueTextRange().getEndOffset();
          result.append(text, lastRef, groupStart);
          String href = text.substring(groupStart, groupEnd);
          String reference = "";
          try {
            reference = createReferenceForRelativeLink(href, myElement);
          }
          catch (IndexNotReadyException e) {
            LOG.debug(e);
            result.replace(result.length() - 6, result.length(), "wrong-href=\""); // display text instead of link
          }
          result.append(reference == null ? href : reference);
          lastRef = groupEnd;
        }
      }

      if (lastRef > 0) {  // don't copy text over if there are no matches
        result.append(text, lastRef, text.length());
        text = result.toString(); //NON-NLS
      }
    }

    if (LOG.isDebugEnabled()) {
      LOG.debug("Generated JavaDoc:");
      LOG.debug(text);
    }

    text = StringUtil.replaceIgnoreCase(text, "<p/>", "<p></p>"); //NON-NLS
    text = StringUtil.replace(text, "/>", ">");
    return text;
  }

  /**
   * Converts a relative link into {@link DocumentationManagerProtocol#PSI_ELEMENT_PROTOCOL PSI_ELEMENT_PROTOCOL}-type link if possible.
   */
  public static @Nullable String createReferenceForRelativeLink(@NotNull String relativeLink, @NotNull PsiElement contextElement) {
    String fragment = null;
    int hashPosition = relativeLink.indexOf('#');
    if (hashPosition >= 0) {
      fragment = relativeLink.substring(hashPosition + 1);
      relativeLink = relativeLink.substring(0, hashPosition);
    }

    PsiElement targetElement;
    if (relativeLink.isEmpty()) {
      targetElement = contextElement instanceof PsiField || contextElement instanceof PsiMethod ?
                      ((PsiMember)contextElement).getContainingClass() : contextElement;
    }
    else {
      if (!(StringUtil.endsWithIgnoreCase(relativeLink, ".htm") || StringUtil.endsWithIgnoreCase(relativeLink, ".html"))) {
        return null;
      }
      relativeLink = relativeLink.substring(0, relativeLink.lastIndexOf('.'));

      String packageName = getPackageName(contextElement);
      if (packageName == null) return null;

      Couple<String> pathWithPackage = removeParentReferences(relativeLink, packageName);
      if (pathWithPackage == null) return null;
      relativeLink = pathWithPackage.first;
      packageName = pathWithPackage.second;

      relativeLink = relativeLink.replace('/', '.');

      String qualifiedTargetName = packageName.isEmpty() ? relativeLink : packageName + '.' + relativeLink;
      JavaPsiFacade javaPsiFacade = JavaPsiFacade.getInstance(contextElement.getProject());
      targetElement = "package-summary".equals(StringUtil.getShortName(qualifiedTargetName))
                      ? javaPsiFacade.findPackage(StringUtil.getPackageName(qualifiedTargetName))
                      : javaPsiFacade.findClass(qualifiedTargetName, contextElement.getResolveScope());
    }
    if (targetElement == null) return null;

    if (fragment != null && targetElement instanceof PsiClass) {
      if (fragment.indexOf('-') >= 0 || fragment.indexOf('(') >= 0) {
        for (PsiMethod method : ((PsiClass)targetElement).getMethods()) {
          Set<String> signatures = JavaDocumentationProvider.getHtmlMethodSignatures(method, null);
          if (signatures.contains(fragment)) {
            targetElement = method;
            fragment = null;
            break;
          }
        }
      }
      else {
        for (PsiField field : ((PsiClass)targetElement).getFields()) {
          if (fragment.equals(field.getName())) {
            targetElement = field;
            fragment = null;
            break;
          }
        }
      }
    }

    StringBuilder builder = new StringBuilder();
    builder.append(DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL);
    builder.append(JavaDocUtil.getReferenceText(targetElement.getProject(), targetElement));
    if (fragment != null) {
      builder.append(DocumentationManagerProtocol.PSI_ELEMENT_PROTOCOL_REF_SEPARATOR).append(fragment);
    }
    return builder.toString();
  }

  /**
   * Takes a pair of strings representing a relative path and a package name, and returns corresponding pair, where path is stripped of
   * leading ../ elements, and package name adjusted correspondingly. Returns {@code null} if there are more ../ elements than package
   * components.
   */
  private static @Nullable Couple<String> removeParentReferences(String path, String packageName) {
    while (path.startsWith("../")) {
      if (packageName.isEmpty()) return null;
      int dotPos = packageName.lastIndexOf('.');
      packageName = dotPos < 0 ? "" : packageName.substring(0, dotPos);
      path = path.substring(3);
    }
    return Couple.of(path, packageName);
  }

  private static String getPackageName(PsiElement element) {
    String packageName = null;
    if (element instanceof PsiPackage pkg) {
      packageName = pkg.getQualifiedName();
    }
    else {
      PsiFile file = element.getContainingFile();
      if (file instanceof PsiClassOwner classOwner) {
        packageName = classOwner.getPackageName();
      }
    }
    return packageName;
  }

  public boolean generateDocInfoCore(StringBuilder buffer, boolean generatePrologue) {
    if (myElement instanceof PsiTypeParameter parameter) {
      generateTypeParameterJavaDoc(buffer, parameter, generatePrologue);
    }
    else if (myElement instanceof PsiClass cls) {
      generateClassJavaDoc(buffer, cls, generatePrologue);
    }
    else if (myElement instanceof PsiMethod method) {
      generateMethodJavaDoc(buffer, method, generatePrologue);
    }
    else if (myElement instanceof PsiPatternVariable var) {
      generatePatternVariableJavaDoc(buffer, generatePrologue, var);
    }
    else if (myElement instanceof PsiParameter parameter) {
      generateMethodParameterJavaDoc(buffer, parameter, generatePrologue);
    }
    else if (myElement instanceof PsiField field) {
      generateFieldJavaDoc(buffer, field, generatePrologue);
    }
    else if (myElement instanceof PsiRecordComponent component) {
      generateRecordComponentJavaDoc(buffer, generatePrologue, component);
    }
    else if (myElement instanceof PsiVariable var) {
      generateVariableJavaDoc(buffer, var, generatePrologue);
    }
    else if (myElement instanceof PsiDirectory dir) {
      PsiPackage aPackage = JavaDirectoryService.getInstance().getPackage(dir);
      if (aPackage == null) return false;
      generatePackageJavaDoc(buffer, aPackage, generatePrologue);
    }
    else if (myElement instanceof PsiPackage pkg) {
      generatePackageJavaDoc(buffer, pkg, generatePrologue);
    }
    else if (myElement instanceof PsiJavaModule module) {
      generateModuleJavaDoc(buffer, module, generatePrologue);
    }
    else {
      return false;
    }

    return true;
  }

  private void generateRecordComponentJavaDoc(StringBuilder buffer, boolean generatePrologue, @NotNull PsiRecordComponent recordComponent) {
    if (generatePrologue) generatePrologue(buffer);
    generateVariableDefinition(buffer, recordComponent, true);

    if (!(recordComponent.getParent() instanceof PsiRecordHeader recordHeader)) return;
    int recordIndex = ArrayUtil.indexOf(recordHeader.getRecordComponents(), recordComponent);
    PsiClass recordClass = recordComponent.getContainingClass();
    if (recordClass == null) return;
    String recordComponentJavadoc = getRecordComponentJavadocFromParameterTag(recordIndex, recordClass);
    if (recordComponentJavadoc != null) {
      buffer.append(DocumentationMarkup.CONTENT_START).append(recordComponentJavadoc).append(DocumentationMarkup.CONTENT_END);
    }
  }

  private void generatePatternVariableJavaDoc(StringBuilder buffer, boolean generatePrologue, @NotNull PsiPatternVariable variable) {
    if (generatePrologue) generatePrologue(buffer);
    generateVariableDefinition(buffer, variable, true);

    String docForPattern = getDocForPattern(variable);
    if (docForPattern != null) {
      buffer.append(DocumentationMarkup.CONTENT_START).append(docForPattern).append(DocumentationMarkup.CONTENT_END);
    }
  }

  private @Nullable String getDocForPattern(@NotNull PsiPatternVariable variable) {
    PsiPattern pattern = variable.getPattern();
    PsiElement parent = pattern.getParent();
    if (!(parent instanceof PsiDeconstructionList deconstructionList)) return null;
    PsiPattern[] components = deconstructionList.getDeconstructionComponents();
    int index = ArrayUtil.indexOf(components, pattern);
    PsiDeconstructionPattern deconstructionPattern = (PsiDeconstructionPattern)parent.getParent();
    PsiTypeElement typeElement = deconstructionPattern.getTypeElement();
    PsiType deconstructionType = typeElement.getType();
    PsiClass recordClass = PsiUtil.resolveClassInClassTypeOnly(deconstructionType);
    if (recordClass == null) return null;
    return getRecordComponentJavadocFromParameterTag(index, recordClass);
  }

  private @Nullable String getRecordComponentJavadocFromParameterTag(int recordComponentIndex, @NotNull PsiClass recordClass) {
    PsiRecordComponent[] recordComponents = recordClass.getRecordComponents();
    if (recordComponents.length <= recordComponentIndex) return null;
    PsiRecordComponent recordComponent = recordComponents[recordComponentIndex];
    PsiDocComment classComment = recordClass.getDocComment();
    String recordComponentName = recordComponent.getName();
    if (classComment == null) return null;
    PsiDocTag tag = getParamTagByName(classComment, recordComponentName);
    if (tag == null) return null;
    PsiElement[] elements = tag.getDataElements();
    if (elements.length == 0) return null;
    String text = elements[0].getText();
    StringBuilder buffer = new StringBuilder();
    generateValue(buffer, new ParamInfo(recordComponentName, tag, ourEmptyProvider), elements, text);
    return buffer.toString();
  }

  public @NlsSafe String generateSignature(PsiElement element) {
    StringBuilder buf = new StringBuilder();
    if (element instanceof PsiClass cls) {
      if (generateClassSignature(buf, cls, SignaturePlace.ToolTip)) return null;
    }
    else if (element instanceof PsiField field) {
      generateFieldSignature(buf, field, SignaturePlace.ToolTip);
    }
    else if (element instanceof PsiMethod method) {
      generateMethodSignature(buf, method, SignaturePlace.ToolTip);
    }
    return buf.toString();
  }

  public @Nls @Nullable String generateDocInfo(List<@NlsSafe String> docURLs) {
    @Nls StringBuilder buffer = new StringBuilder();

    HtmlChunk containerInfo = generateContainerInfo(myElement);
    generatePrologue(buffer);

    if (containerInfo != null) {
      containerInfo.appendTo(buffer);
    }

    if (!generateDocInfoCore(buffer, false)) {
      return null;
    }

    if (docURLs != null) {
      if (!buffer.isEmpty() && elementHasSourceCode()) {
        LOG.debug("Documentation for " + myElement + " was generated from source code, it wasn't found at following URLs: ", docURLs);
      }
      else {
        HtmlChunk urlList = DocumentationMarkup.GRAYED_ELEMENT
          .addText(JavaBundle.message("javadoc.documentation.url.checked", docURLs.size()))
          .children(ContainerUtil.map(docURLs, url -> new HtmlBuilder().br().nbsp().append(url).toFragment()));
        HtmlChunk settingsLink = HtmlChunk.link("open://Project Settings", JavaBundle.message("javadoc.edit.api.docs.paths"));
        buffer.append(DocumentationMarkup.CONTENT_ELEMENT.child(
          DocumentationMarkup.CENTERED_ELEMENT.children(urlList, HtmlChunk.br(), settingsLink)));
      }
    }

    return sanitizeHtml(buffer);
  }

  public @Nls @Nullable String generateRenderedDocInfo() {
    StringBuilder buffer = new StringBuilder();

    switch (myElement) {
      case PsiClass aClass -> generateClassJavaDoc(buffer, aClass, true);
      case PsiMethod method -> generateMethodJavaDoc(buffer, method, true);
      case PsiField field -> generateFieldJavaDoc(buffer, field, true);
      case PsiJavaModule module -> generateModuleJavaDoc(buffer, module, true);
      // package-info case
      case PsiDocComment comment -> generatePackageJavaDoc(buffer, comment, true);
      case null, default -> {
        return null;
      }
    }

    return sanitizeHtml(buffer);
  }

  private @Nullable HtmlChunk generateContainerInfo(@Nullable PsiElement element) {
    JavaDocHighlightingManager highlightingManager = JavaDocHighlightingManagerImpl.getInstance();

    @NlsSafe String ownerLink = null;
    String ownerIcon = null;

    if (element instanceof PsiPackage pkg) {
      return BOTTOM_ELEMENT
        .children(
          HtmlChunk.tag("icon").attr("src", "AllIcons.Nodes.Package"),
          HtmlChunk.nbsp(),
          HtmlChunk.tag("code").addText(pkg.getQualifiedName())
        );
    }
    else if (element instanceof PsiClass) {
      PsiFile file = element.getContainingFile();
      if (file instanceof PsiJavaFile javaFile) {
        String packageName = javaFile.getPackageName();
        if (!packageName.isEmpty()) {
          PsiPackage aPackage = JavaPsiFacade.getInstance(file.getProject()).findPackage(packageName);
          StringBuilder packageFqnBuilder = new StringBuilder();
          if (myDoSemanticHighlightingOfLinks) {
            appendStyledSpan(packageFqnBuilder, highlightingManager.getClassNameAttributes(), packageName);
          }
          else {
            packageFqnBuilder.append(packageName);
          }
          ownerLink = aPackage != null
                      ? generateLink(aPackage, packageFqnBuilder.toString())
                      : "<code>" + packageFqnBuilder + "</code>";
          ownerIcon = "AllIcons.Nodes.Package";
        }
      }
    }
    else if (element instanceof PsiMember member) {
      PsiClass parentClass = member.getContainingClass();
      if (parentClass != null && !PsiUtil.isArrayClass(parentClass)) {
        String qName = parentClass.getQualifiedName();
        if (qName != null) {
          StringBuilder classFqnBuilder = new StringBuilder();
          if (myDoSemanticHighlightingOfLinks) {
            appendStyledSpan(classFqnBuilder, highlightingManager.getClassNameAttributes(), qName);
          }
          else {
            classFqnBuilder.append(qName);
          }
          classFqnBuilder.append(generateTypeParameters(parentClass, false));
          ownerLink = generateLink(parentClass, classFqnBuilder.toString());
          ownerIcon = "AllIcons.Nodes.Class";
        }
      }
    }

    if (ownerLink != null) {
      return BOTTOM_ELEMENT
        .children(
          HtmlChunk.tag("icon").attr("src", ownerIcon),
          HtmlChunk.nbsp(),
          HtmlChunk.raw(ownerLink)
        );
    }
    return null;
  }

  private boolean elementHasSourceCode() {
    PsiFileSystemItem[] items;
    if (myElement instanceof PsiDirectory) {
      final PsiPackage aPackage = JavaDirectoryService.getInstance().getPackage((PsiDirectory)myElement);
      if (aPackage == null) return false;
      items = aPackage.getDirectories(GlobalSearchScope.everythingScope(myProject));
    }
    else if (myElement instanceof PsiPackage pkg) {
      items = pkg.getDirectories(GlobalSearchScope.everythingScope(myProject));
    }
    else {
      if (myElement == null || myElement.getNavigationElement() == null) return false;
      PsiFile containingFile = myElement.getNavigationElement().getContainingFile();
      if (containingFile == null) return false;
      items = new PsiFileSystemItem[]{containingFile};
    }
    ProjectFileIndex projectFileIndex = ProjectFileIndex.getInstance(myProject);
    for (PsiFileSystemItem item : items) {
      VirtualFile file = item.getVirtualFile();
      if (file != null && projectFileIndex.isInSource(file)) return true;
    }
    return false;
  }

  private void generateClassJavaDoc(StringBuilder buffer, PsiClass aClass, boolean generatePrologue) {
    if (aClass instanceof PsiAnonymousClass) return;

    if (generatePrologue) generatePrologue(buffer);

    if (!isRendered()) {
      buffer.append(DocumentationMarkup.DEFINITION_START);
      myIsSignatureGenerationInProgress = true;
      if (generateClassSignature(buffer, aClass, SignaturePlace.Javadoc)) {
        myIsSignatureGenerationInProgress = false;
        return;
      }
      myIsSignatureGenerationInProgress = false;
      buffer.append(DocumentationMarkup.DEFINITION_END);
    }

    PsiDocComment comment = getDocComment(aClass);
    if (comment != null) {
      generateCommonSection(buffer, comment);
      generateAuthorAndVersionSections(buffer, comment);
      generateRecordParametersSection(buffer, aClass, comment);
      generateTypeParametersSection(buffer, aClass);
