/*
 * Copyright (c) 2016, 2024, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */
package jdk.internal.shellsupport.doc;

import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.Stack;
import java.util.TreeMap;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;

import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.ModuleElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.ElementFilter;
import javax.lang.model.util.Elements;
import javax.lang.model.util.Elements.DocCommentKind;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;

import com.sun.source.doctree.DocCommentTree;
import com.sun.source.doctree.DocTree;
import com.sun.source.doctree.InheritDocTree;
import com.sun.source.doctree.LinkTree;
import com.sun.source.doctree.ParamTree;
import com.sun.source.doctree.RawTextTree;
import com.sun.source.doctree.ReturnTree;
import com.sun.source.doctree.ThrowsTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.CompilationUnitTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.VariableTree;
import com.sun.source.util.DocSourcePositions;
import com.sun.source.util.DocTreePath;
import com.sun.source.util.DocTreeScanner;
import com.sun.source.util.DocTrees;
import com.sun.source.util.JavacTask;
import com.sun.source.util.TreePath;
import com.sun.source.util.TreePathScanner;
import com.sun.source.util.Trees;
import com.sun.tools.javac.api.JavacTaskImpl;
import com.sun.tools.javac.util.Assert;
import com.sun.tools.javac.util.DefinedBy;
import com.sun.tools.javac.util.DefinedBy.Api;
import com.sun.tools.javac.util.Pair;

import jdk.internal.org.commonmark.ext.gfm.tables.TablesExtension;
import jdk.internal.org.commonmark.node.Node;
import jdk.internal.org.commonmark.parser.IncludeSourceSpans;
import jdk.internal.org.commonmark.parser.Parser;
import jdk.internal.org.commonmark.renderer.html.HtmlRenderer;

/**Helper to find javadoc and resolve @inheritDoc.
 */
public abstract class JavadocHelper implements AutoCloseable {
    private static final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    /**Create the helper.
     *
     * @param mainTask JavacTask from which the further Elements originate
     * @param sourceLocations paths where source files should be searched
     * @return a JavadocHelper
     */
    public static JavadocHelper create(JavacTask mainTask, Collection<? extends Path> sourceLocations) {
        StandardJavaFileManager fm = compiler.getStandardFileManager(null, null, null);
        try {
            fm.setLocationFromPaths(StandardLocation.SOURCE_PATH, sourceLocations);
            return new OnDemandJavadocHelper(mainTask, fm);
        } catch (IOException ex) {
            try {
                fm.close();
            } catch (IOException closeEx) {
            }
            return new JavadocHelper() {
                @Override
                public String getResolvedDocComment(Element forElement) throws IOException {
                    return null;
                }
                @Override
                public Element getSourceElement(Element forElement) throws IOException {
                    return forElement;
                }
                @Override
                public void close() throws IOException {}
            };
        }
    }

    /**Returns javadoc for the given element, if it can be found, or null otherwise. The javadoc
     * will have @inheritDoc resolved.
     *
     * @param forElement element for which the javadoc should be searched
     * @return javadoc if found, null otherwise
     * @throws IOException if something goes wrong in the search
     */
    public abstract String getResolvedDocComment(Element forElement) throws IOException;

    /**Returns an element representing the same given program element, but the returned element will
     * be resolved from source, if it can be found. Returns the original element if the source for
     * the given element cannot be found.
     *
     * @param forElement element for which the source element should be searched
     * @return source element if found, the original element otherwise
     * @throws IOException if something goes wrong in the search
     */
    public abstract Element getSourceElement(Element forElement) throws IOException;

    /**Closes the helper.
     *
     * @throws IOException if something foes wrong during the close
     */
    @Override
    public abstract void close() throws IOException;

    private static final class OnDemandJavadocHelper extends JavadocHelper {
        private final JavacTask mainTask;
        private final JavaFileManager baseFileManager;
        private final StandardJavaFileManager fm;
        private final Map<String, Pair<JavacTask, TreePath>> signature2Source = new HashMap<>();

        private OnDemandJavadocHelper(JavacTask mainTask, StandardJavaFileManager fm) {
            this.mainTask = mainTask;
            this.baseFileManager = ((JavacTaskImpl) mainTask).getContext().get(JavaFileManager.class);
            this.fm = fm;
        }

        @Override
        public String getResolvedDocComment(Element forElement) throws IOException {
            Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement);

            if (sourceElement == null)
                return null;

            return getResolvedDocComment(sourceElement.fst, sourceElement.snd);
        }

        @Override
        public Element getSourceElement(Element forElement) throws IOException {
            Pair<JavacTask, TreePath> sourceElement = getSourceElement(mainTask, forElement);

            if (sourceElement == null)
                return forElement;

            Element result = Trees.instance(sourceElement.fst).getElement(sourceElement.snd);

            if (result == null)
                return forElement;

            return result;
        }

        private String getResolvedDocComment(JavacTask task, TreePath el) throws IOException {
            DocTrees trees = DocTrees.instance(task);
            Element element = trees.getElement(el);
            String docComment = trees.getDocComment(el);

            if (docComment == null && element.getKind() == ElementKind.METHOD) {
                //if a method does not have a javadoc,
                //try to use javadoc from the methods overridden by this method:
                ExecutableElement executableElement = (ExecutableElement) element;
                Iterable<Element> superTypes =
                        () -> superTypeForInheritDoc(task, element.getEnclosingElement()).iterator();
                for (Element sup : superTypes) {
                   for (ExecutableElement supMethod : ElementFilter.methodsIn(sup.getEnclosedElements())) {
                       TypeElement clazz = (TypeElement) executableElement.getEnclosingElement();
                       if (task.getElements().overrides(executableElement, supMethod, clazz)) {
                           Pair<JavacTask, TreePath> source = getSourceElement(task, supMethod);

                           if (source != null) {
                               String overriddenComment = getResolvedDocComment(source.fst, source.snd);

                               if (overriddenComment != null) {
                                   return overriddenComment;
                               }
                           }
                       }
                   }
                }
            }

            if (docComment == null)
                return null;

            DocCommentKind docCommentKind = trees.getDocCommentKind(el);
            Pair<DocCommentTree, Integer> parsed = parseDocComment(task, docComment, docCommentKind);
            DocCommentTree docCommentTree = parsed.fst;
            int offset = parsed.snd;
            IOException[] exception = new IOException[1];
            Comparator<int[]> spanComp =
                    (span1, span2) -> span1[0] != span2[0] ? span2[0] - span1[0]
                                                           : span2[1] - span1[1];
            //spans in the docComment that should be replaced with the given Strings:
            Map<int[], List<String>> replace = new TreeMap<>(spanComp);
            SyntheticAwareTreeDocSourcePositions sp =
                    new SyntheticAwareTreeDocSourcePositions(trees.getSourcePositions());

            //fill in missing elements and resolve {@inheritDoc}
            //if an element is (silently) missing in the javadoc, a synthetic {@inheritDoc}
            //is created for it.
            new DocTreeScanner<Void, Void>() {
                /* enclosing doctree that may contain {@inheritDoc} (explicit or synthetic)*/
                private Stack<DocTree> interestingParent = new Stack<>();
                /* current top-level DocCommentTree*/
                private DocCommentTree dcTree;
                /* javadoc from a super method from which we may copy elements.*/
                private String inherited;
                /* JavacTask from which inherited originates.*/
                private JavacTask inheritedJavacTask;
                /* TreePath to the super method from which inherited originates.*/
                private TreePath inheritedTreePath;
                /* Synthetic trees that contain {@inheritDoc} and
                 * texts which which they should be replaced.*/
                private Map<DocTree, String> syntheticTrees = new IdentityHashMap<>();
                /* Position on which the synthetic trees should be inserted.*/
                private long insertPos = offset;
                @Override
                public Void scan(Iterable<? extends DocTree> nodes, Void p) {
                    if (nodes != null && containsMarkdown(nodes)) {
                        JoinedMarkdown joinedMarkdowns = joinMarkdown(sp, dcTree, nodes);
                        String source = joinedMarkdowns.source();
                        Parser parser = Parser.builder()
                                .extensions(List.of(TablesExtension.create()))
                                .includeSourceSpans(IncludeSourceSpans.BLOCKS_AND_INLINES)
                                .build();
                        Node document = parser.parse(source);
                        String htmlWithPlaceHolders = stripParagraphs(HtmlRenderer.builder()
                                                                                  .build()
                                                                                  .render(document));

                        for (String part : htmlWithPlaceHolders.split(PLACEHOLDER_PATTERN, -1)) {
                            int[] replaceSpan = joinedMarkdowns.replaceSpans.remove(0);

                            replace.computeIfAbsent(replaceSpan, _ -> new ArrayList<>())
                                   .add(part);
                        }
                    }
                    return super.scan(nodes, p);
                }
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void visitDocComment(DocCommentTree node, Void p) {
                    dcTree = node;
                    interestingParent.push(node);
                    try {
                        if (node.getFullBody().isEmpty()) {
                            //there is no body in the javadoc, add synthetic {@inheritDoc}, which
                            //will be automatically filled in visitInheritDoc:
                            DocCommentTree dc = parseDocComment(task, "{@inheritDoc}", DocCommentKind.TRADITIONAL).fst;
                            syntheticTrees.put(dc, "*\n");
                            interestingParent.push(dc);
                            boolean prevInSynthetic = inSynthetic;
                            try {
                                inSynthetic = true;
                                scan(dc.getFullBody(), p);
                            } finally {
                                inSynthetic = prevInSynthetic;
                                interestingParent.pop();
                            }
                        } else {
                            scan(node.getFullBody(), p);
                        }
                        //add missing @param, @throws and @return, augmented with {@inheritDoc}
                        //which will be resolved in visitInheritDoc:
                        List<DocTree> augmentedBlockTags = new ArrayList<>(node.getBlockTags());
                        if (element.getKind() == ElementKind.METHOD) {
                            ExecutableElement executableElement = (ExecutableElement) element;
                            List<String> parameters =
                                    executableElement.getParameters()
                                                     .stream()
                                                     .map(param -> param.getSimpleName().toString())
                                                     .toList();
                            List<String> throwsList =
                                    executableElement.getThrownTypes()
                                                     .stream()
                                                     .map(TypeMirror::toString)
                                                     .toList();
                            Set<String> missingParams = new HashSet<>(parameters);
                            Set<String> missingThrows = new HashSet<>(throwsList);
                            boolean hasReturn = false;

                            for (DocTree dt : augmentedBlockTags) {
                                switch (dt.getKind()) {
                                    case PARAM:
                                        missingParams.remove(((ParamTree) dt).getName().getName().toString());
                                        break;
                                    case THROWS:
                                        missingThrows.remove(getThrownException(task, el, docCommentTree, (ThrowsTree) dt));
                                        break;
                                    case RETURN:
                                        hasReturn = true;
                                        break;
                                }
                            }

                            for (String missingParam : missingParams) {
                                DocTree syntheticTag = parseBlockTag(task, "@param " + missingParam + " {@inheritDoc}");
                                syntheticTrees.put(syntheticTag, "@param " + missingParam + " *\n");
                                insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList);
                            }

                            for (String missingThrow : missingThrows) {
                                DocTree syntheticTag = parseBlockTag(task, "@throws " + missingThrow + " {@inheritDoc}");
                                syntheticTrees.put(syntheticTag, "@throws " + missingThrow + " *\n");
                                insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList);
                            }

                            if (!hasReturn) {
                                DocTree syntheticTag = parseBlockTag(task, "@return {@inheritDoc}");
                                syntheticTrees.put(syntheticTag, "@return *\n");
                                insertTag(augmentedBlockTags, syntheticTag, parameters, throwsList);
                            }
                        }
                        scan(augmentedBlockTags, p);
                        return null;
                    } finally {
                        interestingParent.pop();
                    }
                }
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void visitParam(ParamTree node, Void p) {
                    interestingParent.push(node);
                    try {
                        return super.visitParam(node, p);
                    } finally {
                        interestingParent.pop();
                    }
                }
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void visitThrows(ThrowsTree node, Void p) {
                    interestingParent.push(node);
                    try {
                        return super.visitThrows(node, p);
                    } finally {
                        interestingParent.pop();
                    }
                }
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void visitReturn(ReturnTree node, Void p) {
                    interestingParent.push(node);
                    try {
                        return super.visitReturn(node, p);
                    } finally {
                        interestingParent.pop();
                    }
                }
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void visitInheritDoc(InheritDocTree node, Void p) {
                    //replace (schedule replacement into the replace map)
                    //{@inheritDoc} with the corresponding text from an overridden method

                    //first, fill in inherited, inheritedJavacTask and inheritedTreePath if not
                    //done yet:
                    if (inherited == null) {
                        try {
                            if (element.getKind() == ElementKind.METHOD) {
                                ExecutableElement executableElement = (ExecutableElement) element;
                                Iterable<ExecutableElement> superMethods =
                                        () -> superMethodsForInheritDoc(task, executableElement).
                                              iterator();
                                for (Element supMethod : superMethods) {
                                   Pair<JavacTask, TreePath> source =
                                           getSourceElement(task, supMethod);

                                   if (source != null) {
                                       String overriddenComment =
                                               getResolvedDocComment(source.fst,
                                                                     source.snd);

                                       if (overriddenComment != null) {
                                           inheritedJavacTask = source.fst;
                                           inheritedTreePath = source.snd;
                                           inherited = overriddenComment;
                                           break;
                                       }
                                   }
                                }
                            }
                        } catch (IOException ex) {
                            exception[0] = ex;
                            return null;
                        }
                    }
                    if (inherited == null) {
                        return null;
                    }
                    Pair<DocCommentTree, Integer> parsed =
                            parseDocComment(inheritedJavacTask, inherited, DocCommentKind.TRADITIONAL);
                    DocCommentTree inheritedDocTree = parsed.fst;
                    int offset = parsed.snd;
                    List<List<? extends DocTree>> inheritedText = new ArrayList<>();
                    //find the corresponding piece in the inherited javadoc
                    //(interesting parent keeps the enclosing tree):
                    DocTree parent = interestingParent.peek();
                    switch (parent.getKind()) {
                        case DOC_COMMENT:
                            inheritedText.add(inheritedDocTree.getFullBody());
                            break;
                        case PARAM:
                            String paramName = ((ParamTree) parent).getName().getName().toString();
                            new DocTreeScanner<Void, Void>() {
                                @Override @DefinedBy(Api.COMPILER_TREE)
                                public Void visitParam(ParamTree node, Void p) {
                                    if (node.getName().getName().contentEquals(paramName)) {
                                        inheritedText.add(node.getDescription());
                                    }
                                    return super.visitParam(node, p);
                                }
                            }.scan(inheritedDocTree, null);
                            break;
                        case THROWS:
                            String thrownName = getThrownException(task, el, docCommentTree, (ThrowsTree) parent);
                            new DocTreeScanner<Void, Void>() {
                                @Override @DefinedBy(Api.COMPILER_TREE)
                                public Void visitThrows(ThrowsTree node, Void p) {
                                    if (Objects.equals(getThrownException(inheritedJavacTask, inheritedTreePath, inheritedDocTree, node), thrownName)) {
                                        inheritedText.add(node.getDescription());
                                    }
                                    return super.visitThrows(node, p);
                                }
                            }.scan(inheritedDocTree, null);
                            break;
                        case RETURN:
                            new DocTreeScanner<Void, Void>() {
                                @Override @DefinedBy(Api.COMPILER_TREE)
                                public Void visitReturn(ReturnTree node, Void p) {
                                    inheritedText.add(node.getDescription());
                                    return super.visitReturn(node, p);
                                }
                            }.scan(inheritedDocTree, null);
                            break;
                    }
                    if (!inheritedText.isEmpty()) {
                        long start = Long.MAX_VALUE;
                        long end = Long.MIN_VALUE;

                        for (DocTree t : inheritedText.get(0)) {
                            start = Math.min(start,
                                             sp.getStartPosition(null, inheritedDocTree, t) - offset);
                            end   = Math.max(end,
                                             sp.getEndPosition(null, inheritedDocTree, t) - offset);
                        }
                        String text = end >= 0 ? inherited.substring((int) start, (int) end) : "";

                        if (syntheticTrees.containsKey(parent)) {
                            //if the {@inheritDoc} is inside a synthetic tree, don't delete anything,
                            //but insert the required text
                            //(insertPos is the position at which new stuff should be added):
                            int[] span = new int[] {(int) insertPos, (int) insertPos};
                            replace.computeIfAbsent(span, s -> new ArrayList<>())
                                    .add(syntheticTrees.get(parent).replace("*", text));
                        } else {
                            //replace the {@inheritDoc} with the full text from
                            //the overridden method:
                            long inheritedStart = sp.getStartPosition(null, dcTree, node);
                            long inheritedEnd   = sp.getEndPosition(null, dcTree, node);
                            int[] span = new int[] {(int) inheritedStart, (int) inheritedEnd};

                            replace.computeIfAbsent(span, s -> new ArrayList<>())
                                    .add(text);
                        }
                    }
                    return super.visitInheritDoc(node, p);
                }
                @Override
                public Void visitLink(LinkTree node, Void p) {
                    if (sp.isRewrittenTree(null, dcTree, node)) {
                        //this link is a synthetic rewritten link, replace
                        //the original span with the new link:
                        int start = (int) sp.getStartPosition(null, dcTree, node);
                        int end   = (int) sp.getEndPosition(null, dcTree, node);

                        replace.computeIfAbsent(new int[] {start, end}, _ -> new ArrayList<>())
                               .add(node.toString());

                        return null;
                    }
                    return super.visitLink(node, p);
                }
                private boolean inSynthetic;
                @Override @DefinedBy(Api.COMPILER_TREE)
                public Void scan(DocTree tree, Void p) {
                    if (exception[0] != null) {
                        return null;
                    }
                    boolean prevInSynthetic = inSynthetic;
                    try {
                        inSynthetic |= syntheticTrees.containsKey(tree);
                        return super.scan(tree, p);
                    } finally {
                        if (!inSynthetic && tree != null) {
                            //for nonsynthetic trees, preserve the ending position as the future
                            //insertPos (as future missing elements should be inserted behind
                            //this tree)
                            //if there is a newline immediately behind this tree, insert behind
                            //the newline:
                            long endPos = sp.getEndPosition(null, dcTree, tree);
                            if (endPos >= offset) {
                                if (endPos - offset + 1 < docComment.length() &&
                                    docComment.charAt((int) (endPos - offset + 1)) == '\n') {
                                    endPos++;
                                }
                                if (endPos - offset < docComment.length()) {
                                    insertPos = endPos + 1;
                                } else {
                                    insertPos = endPos;
                                }
                            }
                        }
                        inSynthetic = prevInSynthetic;
                    }
                }

                /* Insert a synthetic tag (toInsert) into the list of tags at
                 * an appropriate position.*/
                private void insertTag(List<DocTree> tags, DocTree toInsert, List<String> parameters, List<String> throwsTypes) {
                    Comparator<DocTree> comp = (tag1, tag2) -> {
                        if (tag1.getKind() == tag2.getKind()) {
                            switch (toInsert.getKind()) {
                                case PARAM: {
                                    ParamTree p1 = (ParamTree) tag1;
                                    ParamTree p2 = (ParamTree) tag2;
                                    int i1 = parameters.indexOf(p1.getName().getName().toString());
                                    int i2 = parameters.indexOf(p2.getName().getName().toString());

                                    return i1 - i2;
                                }
                                case THROWS: {
                                    ThrowsTree t1 = (ThrowsTree) tag1;
                                    ThrowsTree t2 = (ThrowsTree) tag2;
                                    int i1 = throwsTypes.indexOf(getThrownException(task, el, docCommentTree, t1));
                                    int i2 = throwsTypes.indexOf(getThrownException(task, el, docCommentTree, t2));

                                    return i1 - i2;
                                }
                            }
                        }

                        int i1 = tagOrder.indexOf(tag1.getKind());
                        int i2 = tagOrder.indexOf(tag2.getKind());

                        return i1 - i2;
                    };

                    for (int i = 0; i < tags.size(); i++) {
                        if (comp.compare(tags.get(i), toInsert) >= 0) {
                            tags.add(i, toInsert);
                            return ;
                        }
                    }
                    tags.add(toInsert);
                }

                private static final List<DocTree.Kind> tagOrder =
                        Arrays.asList(DocTree.Kind.PARAM, DocTree.Kind.THROWS,
                                      DocTree.Kind.RETURN, DocTree.Kind.SEE,
                                      DocTree.Kind.SINCE);
            }.scan(docCommentTree, null);

            if (replace.isEmpty())
                return docComment;

            //do actually replace {@inheritDoc} with the new text (as scheduled by the visitor
            //above):
            StringBuilder replacedInheritDoc = new StringBuilder(docComment);

            for (Entry<int[], List<String>> e : replace.entrySet()) {
                replacedInheritDoc.delete(e.getKey()[0] - offset, e.getKey()[1] - offset);
                replacedInheritDoc.insert(e.getKey()[0] - offset,
                                          e.getValue().stream().collect(Collectors.joining("")));
            }

            return replacedInheritDoc.toString();
        }

        /* Find methods from which the given method may inherit javadoc, in the proper order.*/
        private Stream<ExecutableElement> superMethodsForInheritDoc(JavacTask task,
                                                                     ExecutableElement method) {
            TypeElement type = (TypeElement) method.getEnclosingElement();

            return this.superTypeForInheritDoc(task, type)
                       .flatMap(sup -> ElementFilter.methodsIn(sup.getEnclosedElements()).stream())
                       .filter(supMethod -> task.getElements().overrides(method, supMethod, type));
        }

        /* Find types from which methods in type may inherit javadoc, in the proper order.*/
        private Stream<Element> superTypeForInheritDoc(JavacTask task, Element type) {
            TypeElement clazz = (TypeElement) type;
            Stream<Element> result = interfaces(clazz);
            result = Stream.concat(result, interfaces(clazz).flatMap(el -> superTypeForInheritDoc(task, el)));

            if (clazz.getSuperclass().getKind() == TypeKind.DECLARED) {
                Element superClass = ((DeclaredType) clazz.getSuperclass()).asElement();
                result = Stream.concat(result, Stream.of(superClass));
                result = Stream.concat(result, superTypeForInheritDoc(task, superClass));
            }

            return result;
        }
        //where:
            private Stream<Element> interfaces(TypeElement clazz) {
                return clazz.getInterfaces()
                            .stream()
                            .filter(tm -> tm.getKind() == TypeKind.DECLARED)
                            .map(tm -> ((DeclaredType) tm).asElement());
            }

         private DocTree parseBlockTag(JavacTask task, String blockTag) {
            DocCommentTree dc = parseDocComment(task, blockTag, DocCommentKind.TRADITIONAL).fst;

            return dc.getBlockTags().get(0);
        }

        private Pair<DocCommentTree, Integer> parseDocComment(JavacTask task, String javadoc, DocCommentKind docCommentKind) {
            DocTrees trees = DocTrees.instance(task);
            try {
                URI uri;
                Kind kind;
                String content;
                int offset;
                if (docCommentKind == DocCommentKind.TRADITIONAL) {
                    uri = new URI("mem:///doc.html");
                    kind = Kind.HTML;
                    content = "<body>" + javadoc + "</body>";
                    offset = "<body>".length();
                } else {
                    uri = new URI("mem:///doc.md");
                    kind = Kind.OTHER;
                    content = javadoc;
                    offset = 0;
                }
                SimpleJavaFileObject fo = new SimpleJavaFileObject(uri, kind) {
                        @Override @DefinedBy(Api.COMPILER)
                        public CharSequence getCharContent(boolean ignoreEncodingErrors)
                                throws IOException {
                            return content;
                        }
                    };
                DocCommentTree tree = trees.getDocCommentTree(fo);
                offset += (int) trees.getSourcePositions().getStartPosition(null, tree, tree);
                return Pair.of(tree, offset);
            } catch (URISyntaxException ex) {
                throw new IllegalStateException(ex);
            }
        }

        private String getThrownException(JavacTask task, TreePath rootOn, DocCommentTree comment, ThrowsTree tt) {
            DocTrees trees = DocTrees.instance(task);
            Element exc = trees.getElement(new DocTreePath(new DocTreePath(rootOn, comment), tt.getExceptionName()));
            return exc != null ? exc.toString() : null;
        }

        private Pair<JavacTask, TreePath> getSourceElement(JavacTask origin, Element el) throws IOException {
            String handle = elementSignature(el);
            Pair<JavacTask, TreePath> cached = signature2Source.get(handle);

            if (cached != null) {
                return cached.fst != null ? cached : null;
            }

            TypeElement type = topLevelType(el);

            if (type == null)
                return null;

            Elements elements = origin.getElements();
            String binaryName = elements.getBinaryName(type).toString();
            ModuleElement module = elements.getModuleOf(type);
            String moduleName = module == null || module.isUnnamed()
                    ? null
                    : module.getQualifiedName().toString();
            Pair<JavacTask, CompilationUnitTree> source = findSource(moduleName, binaryName);

            if (source == null)
                return null;

            fillElementCache(source.fst, source.snd);

            cached = signature2Source.get(handle);

            if (cached != null) {
                return cached;
            } else {
                signature2Source.put(handle, Pair.of(null, null));
                return null;
            }
        }
        //where:
            private String elementSignature(Element el) {
                switch (el.getKind()) {
                    case ANNOTATION_TYPE: case CLASS: case ENUM: case INTERFACE: case RECORD:
                        return ((TypeElement) el).getQualifiedName().toString();
                    case FIELD:
                        return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName() + ":" + el.asType();
                    case ENUM_CONSTANT:
                        return elementSignature(el.getEnclosingElement()) + "." + el.getSimpleName();
                    case EXCEPTION_PARAMETER: case LOCAL_VARIABLE: case PARAMETER: case RESOURCE_VARIABLE:
                        return el.getSimpleName() + ":" + el.asType();
                    case CONSTRUCTOR: case METHOD:
                        StringBuilder header = new StringBuilder();
                        header.append(elementSignature(el.getEnclosingElement()));
                        if (el.getKind() == ElementKind.METHOD) {
                            header.append(".");
                            header.append(el.getSimpleName());
                        }
                        header.append("(");
                        String sep = "";
                        ExecutableElement method = (ExecutableElement) el;
                        for (Iterator<? extends VariableElement> i = method.getParameters().iterator(); i.hasNext();) {
                            VariableElement p = i.next();
                            header.append(sep);
                            header.append(p.asType());
                            sep = ", ";
                        }
                        header.append(")");
                        return header.toString();
                    case PACKAGE, STATIC_INIT, INSTANCE_INIT, TYPE_PARAMETER,
                         OTHER, MODULE, RECORD_COMPONENT, BINDING_VARIABLE:
                        return el.toString();
                    default:
                        throw Assert.error(el.getKind().name());
                }
            }

            private TypeElement topLevelType(Element el) {
                if (el.getKind() == ElementKind.PACKAGE)
                    return null;

                while (el != null && el.getEnclosingElement().getKind() != ElementKind.PACKAGE) {
                    el = el.getEnclosingElement();
                }

                return el != null && (el.getKind().isClass() || el.getKind().isInterface()) ? (TypeElement) el : null;
            }

            private void fillElementCache(JavacTask task, CompilationUnitTree cut) throws IOException {
                Trees trees = Trees.instance(task);

                new TreePathScanner<Void, Void>() {
                    @Override @DefinedBy(Api.COMPILER_TREE)
                    public Void visitMethod(MethodTree node, Void p) {
                        handleDeclaration();
                        return null;
                    }

                    @Override @DefinedBy(Api.COMPILER_TREE)
                    public Void visitClass(ClassTree node, Void p) {
                        handleDeclaration();
                        return super.visitClass(node, p);
                    }

                    @Override @DefinedBy(Api.COMPILER_TREE)
                    public Void visitVariable(VariableTree node, Void p) {
                        handleDeclaration();
                        return super.visitVariable(node, p);
                    }

                    private void handleDeclaration() {
                        Element currentElement = trees.getElement(getCurrentPath());

                        if (currentElement != null) {
                            signature2Source.put(elementSignature(currentElement), Pair.of(task, getCurrentPath()));
                        }
                    }
                }.scan(cut, null);
            }

        private Pair<JavacTask, CompilationUnitTree> findSource(String moduleName,
                                                                String binaryName) throws IOException {
            JavaFileObject jfo = fm.getJavaFileForInput(StandardLocation.SOURCE_PATH,
                                                        binaryName,
                                                        JavaFileObject.Kind.SOURCE);

            if (jfo == null)
                return null;

            List<JavaFileObject> jfos = Arrays.asList(jfo);
            JavaFileManager patchFM = moduleName != null
                    ? new PatchModuleFileManager(baseFileManager, jfo, moduleName)
                    : baseFileManager;
            JavacTaskImpl task = (JavacTaskImpl) compiler.getTask(null, patchFM, d -> {}, null, null, jfos);
            Iterable<? extends CompilationUnitTree> cuts = task.parse();

            task.enter();

            return Pair.of(task, cuts.iterator().next());
        }

        @Override
        public void close() throws IOException {
            fm.close();
        }

        private static boolean containsMarkdown(Iterable<? extends DocTree> trees) {
            return StreamSupport.stream(trees.spliterator(), false)
                                .anyMatch(t -> t.getKind() == DocTree.Kind.MARKDOWN);
        }

        private static final char PLACEHOLDER = '\uFFFC'; // Unicode Object Replacement Character

        private static JoinedMarkdown joinMarkdown(SyntheticAwareTreeDocSourcePositions sp,
                                                   DocCommentTree comment,
                                                   Iterable<? extends DocTree> trees) {
            StringBuilder sourceBuilder = new StringBuilder();
            List<int[]> replaceSpans = new ArrayList<>();
            int currentSpanStart = (int) sp.getStartPosition(null, comment, trees.iterator().next());
            DocTree lastTree = null;

            for (DocTree tree : trees) {
                if (tree instanceof RawTextTree t) {
                    if (t.getKind() != DocTree.Kind.MARKDOWN) {
                        throw new IllegalStateException(t.getKind().toString());
                    }
                    String code = t.getContent();
                    // handle the (unlikely) case of any U+FFFC characters existing in the code
                    int start = 0;
                    int pos;
                    while ((pos = code.indexOf(PLACEHOLDER, start)) != -1) {
                        replaceSpans.add(new int[] {currentSpanStart, currentSpanStart + pos - start});
                        currentSpanStart += pos - start + 1;
                        start = pos + 1;
                    }
                    sourceBuilder.append(code);
                } else {
                    int treeStart = (int) sp.getStartPosition(null, comment, tree);
                    int treeEnd = (int) sp.getEndPosition(null, comment, tree);
                    replaceSpans.add(new int[] {currentSpanStart, treeStart});
                    currentSpanStart = treeEnd;
                    sourceBuilder.append(PLACEHOLDER);
                }
                lastTree = tree;
            }

            int end = (int) sp.getEndPosition(null, comment, lastTree);

            replaceSpans.add(new int[] {currentSpanStart, end});

            return new JoinedMarkdown(sourceBuilder.toString(), replaceSpans);
        }

        private static String stripParagraphs(String input) {
            input = input.replace("</p>", "");

            if (input.startsWith("<p>")) {
                input = input.substring(3);
            }

            if (input.endsWith("\n")) {
                input = input.substring(0, input.length() - 1);
            }

            return input.replace("<p>", "\n<p>");
        }

        private static final String PLACEHOLDER_PATTERN = Pattern.quote("" + PLACEHOLDER);

        private record JoinedMarkdown(String source, List<int[]> replaceSpans) {}

        //embedded transformers may produce rewritten trees for link,
        //there re-written trees has start position -1, the DocSourcePositions
        //will provide an adjusted span based on the link nested nodes:
        private static final class SyntheticAwareTreeDocSourcePositions implements DocSourcePositions {

            private final DocSourcePositions delegate;
            private final Map<DocTree, long[]> adjustedSpan = new HashMap<>();
            private final Set<DocTree> rewrittenTrees = new HashSet<>();

            public SyntheticAwareTreeDocSourcePositions(DocSourcePositions delegate) {
                this.delegate = delegate;
            }

            @Override
            public long getStartPosition(CompilationUnitTree file, DocCommentTree comment, DocTree tree) {
                ensureAdjustedSpansFilled(file, comment, tree);

                long[] adjusted = adjustedSpan.get(tree);

                if (adjusted != null) {
                    return adjusted[0];
                }

                return delegate.getStartPosition(file, comment, tree);
            }

            @Override
            public long getEndPosition(CompilationUnitTree file, DocCommentTree comment, DocTree tree) {
                ensureAdjustedSpansFilled(file, comment, tree);

                long[] adjusted = adjustedSpan.get(tree);

                if (adjusted != null) {
                    return adjusted[1];
                }

                return delegate.getEndPosition(file, comment, tree);
            }

            @Override
            public long getStartPosition(CompilationUnitTree file, Tree tree) {
                return delegate.getStartPosition(file, tree);
            }

            @Override
            public long getEndPosition(CompilationUnitTree file, Tree tree) {
                return delegate.getEndPosition(file, tree);
            }

            boolean isRewrittenTree(CompilationUnitTree file,
                                    DocCommentTree comment,
                                    DocTree tree) {
                ensureAdjustedSpansFilled(file, comment, tree);
                return rewrittenTrees.contains(tree);
            }

            private void ensureAdjustedSpansFilled(CompilationUnitTree file,
                                                   DocCommentTree comment,
                                                   DocTree tree) {
                if (tree.getKind() != DocTree.Kind.LINK &&
                    tree.getKind() != DocTree.Kind.LINK_PLAIN) {
                    return ;
                }

                long[] span;
                long treeStart = delegate.getStartPosition(file, comment, tree);

                if (treeStart == (-1)) {
                    LinkTree link = (LinkTree) tree;
                    Iterable<? extends DocTree> nested = () -> Stream.concat(link.getLabel().stream(),
                                                                             Stream.of(link.getReference()))
                                                                     .iterator();
                    long start = Long.MAX_VALUE;
                    long end = Long.MIN_VALUE;

                    for (DocTree t : nested) {
                        start = Math.min(start,
                                         delegate.getStartPosition(file, comment, t));
                        end   = Math.max(end,
                                         delegate.getEndPosition(file, comment, t));
                    }

                    span = new long[] {(int) start - 1, (int) end + 1};
                    rewrittenTrees.add(tree);
                } else {
                    long treeEnd = delegate.getEndPosition(file, comment, tree);
                    span = new long[] {treeStart, treeEnd};
                }

                adjustedSpan.put(tree, span);
            }
        }

        private static final class PatchModuleFileManager
                extends ForwardingJavaFileManager<JavaFileManager> {

            private final JavaFileObject file;
            private final String moduleName;

            public PatchModuleFileManager(JavaFileManager fileManager,
                                          JavaFileObject file,
                                          String moduleName) {
                super(fileManager);
                this.file = file;
                this.moduleName = moduleName;
            }

            @Override @DefinedBy(Api.COMPILER)
            public Location getLocationForModule(Location location,
                                                 JavaFileObject fo) throws IOException {
                return fo == file
                        ? PATCH_LOCATION
                        : super.getLocationForModule(location, fo);
            }

            @Override @DefinedBy(Api.COMPILER)
            public String inferModuleName(Location location) throws IOException {
                return location == PATCH_LOCATION
                        ? moduleName
                        : super.inferModuleName(location);
            }

            @Override @DefinedBy(Api.COMPILER)
            public boolean hasLocation(Location location) {
                return location == StandardLocation.PATCH_MODULE_PATH ||
                       super.hasLocation(location);
            }

            private static final Location PATCH_LOCATION = new Location() {
                @Override @DefinedBy(Api.COMPILER)
                public String getName() {
                    return "PATCH_LOCATION";
                }

                @Override @DefinedBy(Api.COMPILER)
                public boolean isOutputLocation() {
                    return false;
                }

                @Override @DefinedBy(Api.COMPILER)
                public boolean isModuleOrientedLocation() {
                    return false;
                }

            };
        }
    }

}
