// 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.lang.java;

import com.intellij.codeInsight.CodeInsightBundle;
import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.completion.CompletionMemory;
import com.intellij.codeInsight.daemon.impl.analysis.JavaModuleGraphUtil;
import com.intellij.codeInsight.documentation.DocumentationManagerProtocol;
import com.intellij.codeInsight.documentation.PlatformDocumentationUtil;
import com.intellij.codeInsight.documentation.QuickDocUtil;
import com.intellij.codeInsight.editorActions.CodeDocumentationUtil;
import com.intellij.codeInsight.javadoc.*;
import com.intellij.ide.actions.FqnUtil;
import com.intellij.ide.fileTemplates.FileTemplate;
import com.intellij.ide.fileTemplates.FileTemplateManager;
import com.intellij.ide.fileTemplates.JavaTemplateUtil;
import com.intellij.ide.util.PackageUtil;
import com.intellij.java.JavaBundle;
import com.intellij.java.syntax.parser.JavaKeywords;
import com.intellij.lang.CodeDocumentationAwareCommenter;
import com.intellij.lang.LanguageCommenters;
import com.intellij.lang.documentation.CodeDocumentationProvider;
import com.intellij.lang.documentation.CompositeDocumentationProvider;
import com.intellij.lang.documentation.DocumentationSettings;
import com.intellij.lang.documentation.ExternalDocumentationProvider;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Editor;
import com.intellij.openapi.editor.richcopy.HtmlSyntaxInfoUtil;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.project.IndexNotReadyException;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.JavaSdkVersion;
import com.intellij.openapi.projectRoots.JavaSdkVersionUtil;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.util.NlsSafe;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileSystem;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.psi.*;
import com.intellij.psi.impl.FakePsiElement;
import com.intellij.psi.impl.PsiImplUtil;
import com.intellij.psi.impl.beanProperties.BeanPropertyElement;
import com.intellij.psi.impl.source.javadoc.PsiDocParamRef;
import com.intellij.psi.infos.CandidateInfo;
import com.intellij.psi.javadoc.PsiDocComment;
import com.intellij.psi.javadoc.PsiDocTag;
import com.intellij.psi.scope.conflictResolvers.JavaMethodsConflictResolver;
import com.intellij.psi.util.*;
import com.intellij.util.SmartList;
import com.intellij.util.Url;
import com.intellij.util.concurrency.annotations.RequiresReadLock;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.io.HttpRequests;
import org.jetbrains.annotations.*;
import org.jetbrains.builtInWebServer.BuiltInWebBrowserUrlProviderKt;
import org.jspecify.annotations.NonNull;

import java.io.IOException;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import static com.intellij.lang.documentation.QuickDocHighlightingHelper.appendStyledSignatureFragment;
import static com.intellij.util.ObjectUtils.notNull;

/**
 * @author Maxim.Mossienko
 */
public class JavaDocumentationProvider implements CodeDocumentationProvider, ExternalDocumentationProvider {
  private static final Logger LOG = Logger.getInstance(JavaDocumentationProvider.class);

  private static final String LINE_SEPARATOR = "\n";
  private static final String PARAM_TAG = "@param";
  private static final String RETURN_TAG = "@return";
  private static final String THROWS_TAG = "@throws";

  public static final String HTML_EXTENSION = ".html";
  public static final String PACKAGE_SUMMARY_FILE = "package-summary.html";

  private static void appendStyledSpan(
    @NotNull StringBuilder buffer,
    @Nullable String value,
    String @NotNull ... properties
  ) {
    if (DocumentationSettings.isHighlightingOfQuickDocSignaturesEnabled()) {
      HtmlSyntaxInfoUtil.appendStyledSpan(buffer, value, properties);
    }
    else {
      buffer.append(value);
    }
  }

  @Override
  public @Nls String getQuickNavigateInfo(PsiElement element, PsiElement originalElement) {
    return QuickDocUtil.inferLinkFromFullDocumentation(this, element, originalElement,
                                                       getQuickNavigationInfoInner(element, originalElement));
  }

  public static @Nullable @Nls String getQuickNavigationInfoInner(PsiElement element, PsiElement originalElement) {
    if (element instanceof PsiClass) {
      return generateClassInfo((PsiClass)element);
    }
    else if (element instanceof PsiMethod) {
      return generateMethodInfo((PsiMethod)element, calcSubstitutor(originalElement));
    }
    else if (element instanceof PsiField) {
      return generateFieldInfo((PsiField)element, calcSubstitutor(originalElement));
    }
    else if (element instanceof PsiVariable) {
      return generateVariableInfo((PsiVariable)element);
    }
    else if (element instanceof PsiPackage) {
      return generatePackageInfo((PsiPackage)element);
    }
    else if (element instanceof BeanPropertyElement) {
      return generateMethodInfo(((BeanPropertyElement)element).getMethod(), PsiSubstitutor.EMPTY);
    }
    else if (element instanceof PsiJavaModule) {
      return generateModuleInfo((PsiJavaModule)element);
    }
    return null;
  }

  private static PsiSubstitutor calcSubstitutor(PsiElement originalElement) {
    PsiSubstitutor substitutor = PsiSubstitutor.EMPTY;
    if (originalElement instanceof PsiReferenceExpression) {
      LOG.assertTrue(originalElement.isValid());
      substitutor = ((PsiReferenceExpression)originalElement).advancedResolve(true).getSubstitutor();
    }
    return substitutor;
  }

  @Override
  public @Unmodifiable List<String> getUrlFor(final PsiElement element, final PsiElement originalElement) {
    return getExternalJavaDocUrl(element);
  }

  private static void newLine(StringBuilder buffer) {
    // Don't know why space has to be added after newline for good text alignment...
    buffer.append("\n ");
  }

  private static void generateInitializer(StringBuilder buffer, PsiVariable variable) {
    PsiExpression initializer = variable.getInitializer();
    if (initializer != null) {
      JavaDocInfoGenerator generator = JavaDocInfoGeneratorFactory.create(variable.getProject(), null);
      generator.appendExpressionValue(buffer, initializer);
      PsiExpression constantInitializer = JavaDocInfoGenerator.calcInitializerExpression(variable);
      if (constantInitializer != null) {
        generator.appendExpressionValue(buffer, constantInitializer);
      }
    }
    else if (variable instanceof PsiEnumConstant enumConstant) {
      PsiExpressionList list = enumConstant.getArgumentList();
      if (JavaDocInfoGenerator.canComputeArguments(list)) {
        JavaDocInfoGenerator generator = JavaDocInfoGeneratorFactory.create(variable.getProject(), null);
        generator.generateExpressionText(enumConstant.getArgumentList(), buffer);
      }
    }
  }

  private static void generateModifiers(StringBuilder buffer, PsiModifierListOwner element) {
    JavaDocInfoGeneratorFactory.create(element.getProject(), null).generateModifiers(buffer, element, false);
  }

  private static @NlsSafe String generatePackageInfo(PsiPackage aPackage) {
    return aPackage.getQualifiedName();
  }

  private static void generateOrderEntryAndPackageInfo(StringBuilder buffer, @NotNull PsiElement element) {
    PsiFile file = element.getContainingFile();

    if (file != null) {
      generateOrderEntryInfo(buffer, file.getVirtualFile(), element.getProject());
    }

    if (file instanceof PsiJavaFile) {
      String packageName = ((PsiJavaFile)file).getPackageName();
      if (!packageName.isEmpty()) {
        buffer.append(packageName);
        newLine(buffer);
      }
    }
  }

  private static void generateOrderEntryInfo(StringBuilder buffer, VirtualFile file, Project project) {
    if (file != null) {
      ProjectFileIndex index = ProjectRootManager.getInstance(project).getFileIndex();
      if (index.isInLibrary(file)) {
        index.getOrderEntriesForFile(file).stream()
          .filter(LibraryOrSdkOrderEntry.class::isInstance).findFirst()
          .ifPresent(
            entry -> appendStyledSpan(buffer, "[" + StringUtil.escapeXmlEntities(entry.getPresentableName()) + "] ", "color: #909090"));
      }
      else {
        Module module = index.getModuleForFile(file);
        if (module != null) {
          appendStyledSpan(buffer, "[" + module.getName() + "] ", "color: #909090");
        }
      }
    }
  }

  private static JavaDocInfoGenerator getDocInfoGenerator(@NotNull Project project, boolean isGenerationForRenderedDoc) {
    return JavaDocInfoGeneratorFactory.getBuilder(project).setIsGenerationForRenderedDoc(isGenerationForRenderedDoc).create();
  }

  public static @Nls String generateClassInfo(PsiClass aClass) {
    @Nls StringBuilder buffer = new StringBuilder();

    if (aClass instanceof PsiAnonymousClass) return JavaElementKind.ANONYMOUS_CLASS.subject();

    JavaDocInfoGenerator docInfoGenerator = getDocInfoGenerator(aClass.getProject(), false);

    generateOrderEntryAndPackageInfo(buffer, aClass);
    docInfoGenerator.generateTooltipAnnotations(aClass, buffer);
    generateModifiers(buffer, aClass);

    JavaDocHighlightingManagerImpl highlightingManager = JavaDocHighlightingManagerImpl.getInstance();

    final String classString = aClass.isAnnotationType() ? '@' + JavaKeywords.INTERFACE :
                               aClass.isInterface() ? JavaKeywords.INTERFACE :
                               aClass instanceof PsiTypeParameter ? JavaBundle.message("java.terms.type.parameter") :
                               aClass.isEnum() ? JavaKeywords.ENUM :
                               aClass.isRecord() ? JavaKeywords.RECORD :
                               JavaKeywords.CLASS;
    appendStyledSignatureFragment(buffer, classString, highlightingManager.getKeywordAttributes()).append(" ");

    appendStyledSignatureFragment(
      buffer,
      JavaDocUtil.getShortestClassName(aClass, aClass), highlightingManager.getClassDeclarationAttributes(aClass)
    );

    generateTypeParameters(aClass, buffer, highlightingManager);

    if (!aClass.isEnum() && !aClass.isAnnotationType()) {
      PsiReferenceList extendsList = aClass.getExtendsList();
      writeExtends(aClass, buffer, extendsList == null ? PsiClassType.EMPTY_ARRAY : extendsList.getReferencedTypes(), highlightingManager);
    }

    writeImplements(aClass, buffer, aClass.getImplementsListTypes(), highlightingManager);

    return buffer.toString();
  }

  public static void writeImplements(
    PsiClass aClass,
    StringBuilder buffer,
    PsiClassType[] refs,
    JavaDocHighlightingManager highlightingManager
  ) {
    if (refs.length > 0) {
      newLine(buffer);
      appendStyledSignatureFragment(buffer, "implements ", highlightingManager.getKeywordAttributes()); // <- Groovy keyword
      writeTypeRefs(aClass, buffer, refs, highlightingManager);
    }
  }

  public static void writeExtends(
    PsiClass aClass,
    StringBuilder buffer,
    PsiClassType[] refs,
    JavaDocHighlightingManager highlightingManager
  ) {
    if (refs.length > 0 || !aClass.isInterface() && !CommonClassNames.JAVA_LANG_OBJECT.equals(aClass.getQualifiedName())) {
      appendStyledSignatureFragment(buffer, " extends ", highlightingManager.getKeywordAttributes()); // <- Groovy keyword
      if (refs.length == 0) {
        appendStyledSignatureFragment(buffer, "Object", highlightingManager.getClassNameAttributes());
      }
      else {
        writeTypeRefs(aClass, buffer, refs, highlightingManager);
      }
    }
  }

  private static void writeTypeRefs(
    PsiClass aClass,
    StringBuilder buffer,
    PsiClassType[] refs,
    JavaDocHighlightingManager highlightingManager
  ) {
    for (int i = 0; i < refs.length; i++) {
      JavaDocInfoGeneratorFactory.getBuilder(aClass.getProject())
        .setIsGenerationForRenderedDoc(false)
        .setHighlightingManager(highlightingManager)
        .create()
        .generateType(buffer, refs[i], aClass, false, true);

      if (i < refs.length - 1) {
        appendStyledSignatureFragment(buffer, ", ", highlightingManager.getCommaAttributes());
      }
    }
  }

  public static void generateTypeParameters(
    PsiTypeParameterListOwner typeParameterOwner,
    StringBuilder buffer,
    JavaDocHighlightingManager highlightingManager
  ) {
    if (typeParameterOwner.hasTypeParameters()) {
      PsiTypeParameter[] params = typeParameterOwner.getTypeParameters();

      appendStyledSignatureFragment(buffer, "&lt;", highlightingManager.getOperationSignAttributes());

      for (int i = 0; i < params.length; i++) {
        PsiTypeParameter p = params[i];

        appendStyledSignatureFragment(buffer, p.getName(), highlightingManager.getTypeParameterNameAttributes());
        PsiClassType[] refs = p.getExtendsList().getReferencedTypes();

        if (refs.length > 0) {
          appendStyledSignatureFragment(buffer, " extends ", highlightingManager.getKeywordAttributes()); // <- Groovy keyword

          for (int j = 0; j < refs.length; j++) {
            JavaDocInfoGeneratorFactory.create(typeParameterOwner.getProject(), null)
              .generateType(buffer, refs[j], typeParameterOwner, false, true);

            if (j < refs.length - 1) {
              appendStyledSignatureFragment(buffer, " & ", highlightingManager.getOperationSignAttributes());
            }
          }
        }

        if (i < params.length - 1) {
          appendStyledSignatureFragment(buffer, ", ", highlightingManager.getCommaAttributes());
        }
      }

      appendStyledSignatureFragment(buffer, "&gt;", highlightingManager.getOperationSignAttributes());
    }
  }

  public static @NlsSafe String generateMethodInfo(PsiMethod method, PsiSubstitutor substitutor) {
    StringBuilder buffer = new StringBuilder();

    PsiClass parentClass = method.getContainingClass();

    JavaDocInfoGenerator docInfoGenerator = getDocInfoGenerator(method.getProject(), false);
    JavaDocHighlightingManagerImpl highlightingManager = JavaDocHighlightingManagerImpl.getInstance();

    if (parentClass != null && !(parentClass instanceof PsiAnonymousClass)) {
      if (method.isConstructor()) {
        generateOrderEntryAndPackageInfo(buffer, parentClass);
      }

      appendStyledSignatureFragment(
        buffer,
        JavaDocUtil.getShortestClassName(parentClass, method), highlightingManager.getClassDeclarationAttributes(parentClass)
      );
      newLine(buffer);
    }

    docInfoGenerator.generateTooltipAnnotations(method, buffer);
    generateModifiers(buffer, method);

    generateTypeParameters(method, buffer, highlightingManager);
    if (method.hasTypeParameters()) {
      buffer.append(" ");
    }

    if (method.getReturnType() != null) {
      docInfoGenerator.generateType(buffer, substitutor.substitute(method.getReturnType()), method, false, true);
      buffer.append(" ");
    }

    appendStyledSignatureFragment(buffer, method.getName(), highlightingManager.getMethodDeclarationAttributes(method));

    appendStyledSignatureFragment(buffer, "(", highlightingManager.getParenthesesAttributes());
    PsiParameter[] params = method.getParameterList().getParameters();
    for (int i = 0; i < params.length; i++) {
      PsiParameter param = params[i];
      docInfoGenerator.generateType(buffer, substitutor.substitute(param.getType()), method, false, true);
      buffer.append(" ");
      appendStyledSignatureFragment(buffer, param.getName(), highlightingManager.getParameterAttributes());
      if (i < params.length - 1) {
        appendStyledSignatureFragment(buffer, ", ", highlightingManager.getCommaAttributes());
      }
    }

    appendStyledSignatureFragment(buffer, ")", highlightingManager.getParenthesesAttributes());

    PsiClassType[] refs = method.getThrowsList().getReferencedTypes();
    if (refs.length > 0) {
      newLine(buffer);
      appendStyledSignatureFragment(buffer, " throws ", highlightingManager.getKeywordAttributes());
      for (int i = 0; i < refs.length; i++) {
        PsiClass throwsClass = refs[i].resolve();

        if (throwsClass != null) {
          appendStyledSignatureFragment(buffer, JavaDocUtil.getShortestClassName(throwsClass, method),
                                                                       highlightingManager.getClassDeclarationAttributes(throwsClass)
          );
        }
        else {
          buffer.append(refs[i].getPresentableText());
        }

        if (i < refs.length - 1) {
          appendStyledSignatureFragment(buffer, ", ", highlightingManager.getCommaAttributes());
        }
      }
    }

    return buffer.toString();
  }

  private static @Nls String generateFieldInfo(PsiField field, PsiSubstitutor substitutor) {
    @Nls StringBuilder buffer = new StringBuilder();
    PsiClass parentClass = field.getContainingClass();

    if (parentClass != null && !(parentClass instanceof PsiAnonymousClass)) {
      appendStyledSignatureFragment(
        buffer,
        JavaDocUtil.getShortestClassName(parentClass, field),
        JavaDocHighlightingManagerImpl.getInstance().getClassDeclarationAttributes(parentClass)
      );
      newLine(buffer);
    }

    JavaDocInfoGenerator docInfoGenerator = getDocInfoGenerator(field.getProject(), false);

    docInfoGenerator.generateTooltipAnnotations(field, buffer);
    generateModifiers(buffer, field);

    docInfoGenerator.generateType(buffer, substitutor.substitute(field.getType()), field, false, true);
    buffer.append(" ");
    appendStyledSignatureFragment(buffer, field.getName(),
                                                                 JavaDocHighlightingManagerImpl.getInstance().getFieldDeclarationAttributes(field));

    generateInitializer(buffer, field);

    JavaDocInfoGenerator.enumConstantOrdinal(buffer, field, parentClass, "\n");
    return buffer.toString();
  }

  private static @Nls String generateVariableInfo(PsiVariable variable) {
    @Nls StringBuilder buffer = new StringBuilder();

    generateModifiers(buffer, variable);

    JavaDocInfoGeneratorFactory.create(variable.getProject(), null)
      .generateType(buffer, variable.getType(), variable, false, true);

    buffer.append(" ");

    appendStyledSignatureFragment(buffer, variable.getName(), JavaDocHighlightingManagerImpl.getInstance().getLocalVariableAttributes());
    generateInitializer(buffer, variable);

    return buffer.toString();
  }

  private static @Nls String generateModuleInfo(PsiJavaModule module) {
    @Nls StringBuilder sb = new StringBuilder();

    VirtualFile file = PsiImplUtil.getModuleVirtualFile(module);
    generateOrderEntryInfo(sb, file, module.getProject());

    appendStyledSignatureFragment(sb, JavaKeywords.MODULE, JavaDocHighlightingManagerImpl.getInstance().getKeywordAttributes());
    sb.append(' ');
    appendStyledSignatureFragment(sb, module.getName(), JavaDocHighlightingManagerImpl.getInstance().getClassNameAttributes());

    return sb.toString();
  }

  @Override
  public PsiComment findExistingDocComment(final PsiComment comment) {
    if (comment instanceof PsiDocComment) {
      final PsiJavaDocumentedElement owner = ((PsiDocComment)comment).getOwner();
      if (owner != null) {
        return owner.getDocComment();
      }
    }
    return null;
  }

  @Override
  public @Nullable Pair<PsiElement, PsiComment> parseContext(@NotNull PsiElement startPoint) {
    PsiElement current = startPoint;
    while (current != null) {
      if (current instanceof PsiJavaDocumentedElement &&
          !(current instanceof PsiTypeParameter) &&
          !(current instanceof PsiAnonymousClass)) {
        PsiDocComment comment = ((PsiJavaDocumentedElement)current).getDocComment();
        return Pair.create(current, comment);
      }
      else if (PackageUtil.isPackageInfoFile(current)) {
        return Pair.create(current, getPackageInfoComment(current));
      }
      current = current.getParent();
    }
    return null;
  }

  private static Map<String, Object> collectContextAttributes(PsiJavaDocumentedElement commentOwner){
    HashMap<String, Object> attributes = new HashMap<>();
    if (commentOwner instanceof PsiMember member){
      String name = member.getName();
      if (name != null) {
        attributes.put("ELEMENT_NAME", name);
      }
      PsiClass containingClass = member.getContainingClass();
      String className = containingClass != null ? containingClass.getName() : null;
      if (className != null) {
        attributes.put("CONTAINING_CLASS", className);
      }
    }
    if (commentOwner instanceof PsiMethod method) {
      PsiParameter[] parameters = method.getParameterList().getParameters();
      attributes.put("PARAMS", ContainerUtil.map(parameters, param -> param.getName()));
      attributes.put("TYPE_PARAMS", ContainerUtil.map(method.getTypeParameters(), param -> param.getName()));
      attributes.put("THROWS", ContainerUtil.map(method.getThrowsList().getReferenceElements(), reference -> reference.getText()));
      PsiTypeElement returnElement = method.getReturnTypeElement();
      if (returnElement != null) {
        attributes.put("RETURN_TYPE", String.valueOf(returnElement.getText()));
      }

      PsiMethod[] superMethods = method.findSuperMethods();
      if (superMethods.length > 0) {
        PsiMethod superMethod = superMethods[0];
        String qualifiedSuperMethod = FqnUtil.getQualifiedNameFromProviders(superMethod);
        if (qualifiedSuperMethod != null) {
          attributes.put("SUPER_METHOD", qualifiedSuperMethod);
        }
        Map<Integer, String> paramToParentDescription = collectParentParameterDescriptions(method, parameters);
        if (! paramToParentDescription.isEmpty()) {
          List<String> inheritedParams = IntStream
            .range(0, parameters.length)
            .mapToObj(i -> parameters[i].getName() + StringUtil.notNullize(paramToParentDescription.get(i)))
            .collect(Collectors.toList());
          attributes.put("PARAMS_INHERITED", inheritedParams);
        }
      }
    }
    if (commentOwner instanceof PsiClass psiClass) {
      attributes.put("TYPE_PARAMS", ContainerUtil.map(psiClass.getTypeParameters(), param -> param.getName()));
      if (psiClass.isInterface()){
        attributes.put("INTERFACE", String.valueOf(true));
      }
      if (psiClass.isEnum()){
        attributes.put("ENUM", String.valueOf(true));
      }
      if (psiClass.isRecord()){
        attributes.put("RECORD", String.valueOf(true));
        attributes.put("RECORD_COMPONENTS", ContainerUtil.map(psiClass.getRecordComponents(), param -> param.getName()));
      }
      String className = psiClass.getName();
      if (className != null) {
        attributes.put("CONTAINING_CLASS", psiClass.getName());
      }
    }
    return attributes;
  }

  private static @Nullable String generateStubFromTemplate(@Nullable PsiJavaDocumentedElement commentOwner) {
    if (commentOwner == null) return null;
    FileTemplate template = findTemplateByElement(commentOwner);
    if (template == null) return null;

    Map<String, Object> attributes = collectContextAttributes(commentOwner);
    Properties properties = FileTemplateManager.getInstance(commentOwner.getProject()).getDefaultProperties();
    for (String property : properties.stringPropertyNames()) {
      attributes.put(property, properties.getProperty(property));
    }
    try {
      return template.getText(attributes);
    }
    catch (IOException e) {
      LOG.trace(e);
      return null;
    }
  }

  private static @Nullable FileTemplate findTemplateByElement(PsiJavaDocumentedElement commentOwner) {
    String templateName = findTemplateNameByElement(commentOwner);
    if (templateName == null) return null;
    try {
      return FileTemplateManager.getInstance(commentOwner.getProject()).getCodeTemplate(templateName);
    }
    catch (IllegalStateException e) {
      return null;
    }
  }

  private static @Nullable @NonNls String findTemplateNameByElement(PsiJavaDocumentedElement commentOwner){
    return switch (commentOwner) {
      case PsiMethod method -> method.isConstructor() ? JavaTemplateUtil.TEMPLATE_JAVADOC_CONSTRUCTOR :
                               method.findSuperMethods().length > 0 ? JavaTemplateUtil.TEMPLATE_JAVADOC_OVERRIDING_METHOD
                                                                    : JavaTemplateUtil.TEMPLATE_JAVADOC_METHOD;
      case PsiField ignored -> JavaTemplateUtil.TEMPLATE_JAVADOC_FIELD;
      case PsiClass ignored -> JavaTemplateUtil.TEMPLATE_JAVADOC_CLASS;
      case null, default -> null;
    };
  }

  @Override
  public String generateDocumentationContentStub(PsiComment _comment) {
    final PsiJavaDocumentedElement commentOwner = ((PsiDocComment)_comment).getOwner();
    if (Registry.is("java.javadoc.use.templates")) {
      String javaDocStub = generateStubFromTemplate(commentOwner);
      if (javaDocStub != null) return javaDocStub;
    }

    final StringBuilder builder = new StringBuilder();
    final CodeDocumentationAwareCommenter commenter =
      (CodeDocumentationAwareCommenter)LanguageCommenters.INSTANCE.forLanguage(_comment.getLanguage());
    if (commentOwner instanceof PsiMethod psiMethod) {
      generateParametersTakingDocFromSuperMethods(builder, commenter, psiMethod);

      final PsiTypeParameterList typeParameterList = psiMethod.getTypeParameterList();
      if (typeParameterList != null) {
        createTypeParamsListComment(builder, commenter, typeParameterList);
      }
      if (psiMethod.getReturnType() != null && !PsiTypes.voidType().equals(psiMethod.getReturnType())) {
        builder.append(CodeDocumentationUtil.createDocCommentLine(RETURN_TAG, _comment.getContainingFile(), commenter));
        builder.append(LINE_SEPARATOR);
      }

      final PsiJavaCodeReferenceElement[] references = psiMethod.getThrowsList().getReferenceElements();
      for (PsiJavaCodeReferenceElement reference : references) {
        builder.append(CodeDocumentationUtil.createDocCommentLine(THROWS_TAG, _comment.getContainingFile(), commenter));
        builder.append(reference.getText());
        builder.append(LINE_SEPARATOR);
      }
    }
    else if (commentOwner instanceof PsiClass) {
      if (((PsiClass)commentOwner).isRecord()) {
        for (PsiRecordComponent component : ((PsiClass)commentOwner).getRecordComponents()) {
          builder.append(CodeDocumentationUtil.createDocCommentLine(PARAM_TAG, commentOwner.getContainingFile(), commenter));
          builder.append(component.getName());
          builder.append(LINE_SEPARATOR);
        }
      }
      final PsiTypeParameterList typeParameterList = ((PsiClass)commentOwner).getTypeParameterList();
      if (typeParameterList != null) {
        createTypeParamsListComment(builder, commenter, typeParameterList);
      }
    }
    return !builder.isEmpty() ? builder.toString() : null;
  }

  public static void generateParametersTakingDocFromSuperMethods(StringBuilder builder,
                                                                 CodeDocumentationAwareCommenter commenter,
                                                                 PsiMethod psiMethod) {
    PsiParameterList parameterList = psiMethod.getParameterList();
    final PsiParameter[] parameters = parameterList.getParameters();
    final Map<Integer, String> index2Description = collectParentParameterDescriptions(psiMethod, parameters);

    for (int i = 0; i < parameters.length; i++) {
      builder.append(CodeDocumentationUtil.createDocCommentLine(PARAM_TAG, psiMethod.getContainingFile(), commenter));
      builder.append(parameters[i].getName());
      String description = index2Description.get(i);
      if (description != null) {
        builder.append(description);
      }
      builder.append(LINE_SEPARATOR);
    }
  }

  private static @NotNull Map<Integer, String> collectParentParameterDescriptions(PsiMethod psiMethod, PsiParameter[] parameters) {
    final Map<Integer, String> index2Description = new HashMap<>();

    for (int i = 0; i < parameters.length; i++) {
      PsiDocTag param = JavaDocInfoGenerator.findInheritDocTag(psiMethod, i);
      if (param == null) continue;
      final PsiElement[] dataElements = param.getDataElements();
      String paramName = null;
      int endOffset = 0;
      for (PsiElement dataElement : dataElements) {
        if (dataElement instanceof PsiDocParamRef) {
          //noinspection ConstantConditions
          paramName = dataElement.getReference().getCanonicalText();
          endOffset = dataElement.getTextRangeInParent().getEndOffset();
          break;
        }
      }
      if (paramName != null) {
        String description = param.getText().substring(endOffset).replaceFirst("(\\s*\\*)?\\s*$", "");
        index2Description.put(i, description);
      }
    }
    return index2Description;
  }

  public static void createTypeParamsListComment(final StringBuilder buffer,
                                                 final CodeDocumentationAwareCommenter commenter,
                                                 final PsiTypeParameterList typeParameterList) {
    final PsiTypeParameter[] typeParameters = typeParameterList.getTypeParameters();
    for (PsiTypeParameter typeParameter : typeParameters) {
      buffer.append(CodeDocumentationUtil.createDocCommentLine(PARAM_TAG, typeParameterList.getContainingFile(), commenter));
      buffer.append("<").append(typeParameter.getName()).append(">");
      buffer.append(LINE_SEPARATOR);
    }
  }

  @Override
  public @Nls String generateDoc(PsiElement element, PsiElement originalElement) {
    return generateDocStatic(element, originalElement);
  }

  @RequiresReadLock
  public static @Nullable @Nls String generateDocStatic(PsiElement element, PsiElement originalElement) {
    // for new Class(<caret>) or methodCall(<caret>) proceed from method call or new expression
    // same for new Cl<caret>ass() or method<caret>Call()
    if (element instanceof PsiExpressionList ||
        element instanceof PsiReferenceExpression && element.getParent() instanceof PsiMethodCallExpression) {
      element = element.getParent();
      originalElement = null;
    }
    if (element instanceof PsiCallExpression && CodeInsightSettings.getInstance().SHOW_PARAMETER_NAME_HINTS_ON_COMPLETION) {
      PsiMethod method = CompletionMemory.getChosenMethod((PsiCallExpression)element);
      if (method != null) element = method;
    }
    if (element instanceof PsiMethodCallExpression) {
      return getMethodCandidateInfo((PsiMethodCallExpression)element);
    }

    if (element instanceof FakePsiElement) {
      PsiDocCommentBase docCommentBase = PsiTreeUtil.getParentOfType(originalElement, PsiDocCommentBase.class);
      if (docCommentBase != null) {
        element = docCommentBase.getOwner();
      }
    }

    // Try hard for documentation of incomplete new Class instantiation
    PsiElement elt = element;
    if (originalElement != null && !(originalElement instanceof PsiPackage || originalElement instanceof PsiFileSystemItem)) {
      elt = PsiTreeUtil.prevLeaf(originalElement);
    }
    if (elt instanceof PsiErrorElement) {
      elt = elt.getPrevSibling();
    }
    else if (elt != null && !(elt instanceof PsiNewExpression)) {
      elt = elt.getParent();
    }
    if (elt instanceof PsiNewExpression) {
      PsiClass targetClass = null;

      if (element instanceof PsiJavaCodeReferenceElement) {     // new Class<caret>
        PsiElement resolve = ((PsiJavaCodeReferenceElement)element).resolve();
        if (resolve instanceof PsiClass) targetClass = (PsiClass)resolve;
      }
      else if (element instanceof PsiClass) { //Class in completion
        targetClass = (PsiClass)element;
      }
      else if (element instanceof PsiNewExpression) { // new Class(<caret>)
        PsiJavaCodeReferenceElement reference = ((PsiNewExpression)element).getClassReference();
        if (reference != null) {
          PsiElement resolve = reference.resolve();
          if (resolve instanceof PsiClass) targetClass = (PsiClass)resolve;
        }
      }

      if (targetClass != null) {
        PsiMethod[] constructors = targetClass.getConstructors();
