/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package org.codehaus.groovy.ant;

import org.apache.groovy.util.SystemUtil;
import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.DirectoryScanner;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.DirSet;
import org.apache.tools.ant.types.Path;
import org.apache.tools.ant.types.PatternSet;
import org.codehaus.groovy.runtime.ResourceGroovyMethods;
import org.codehaus.groovy.tools.groovydoc.ClasspathResourceManager;
import org.codehaus.groovy.tools.groovydoc.FileOutputTool;
import org.codehaus.groovy.tools.groovydoc.GroovyDocTool;
import org.codehaus.groovy.tools.groovydoc.LinkArgument;
import org.codehaus.groovy.tools.groovydoc.PreLanguageRewriter;
import org.codehaus.groovy.tools.groovydoc.gstringTemplates.GroovyDocTemplateInfo;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;

import static java.lang.System.Logger.Level.WARNING;

/**
 * Access to the GroovyDoc tool from Ant.
 */
public class Groovydoc extends Task {
    private static final System.Logger LOGGER = System.getLogger(Groovydoc.class.getName());
    private final LoggingHelper log = new LoggingHelper(this);

    private Path sourcePath;
    private File destDir;
    private final List<String> packageNames;
    private final List<String> excludePackageNames;
    private String windowTitle = "Groovy Documentation";
    private String docTitle = "Groovy Documentation";
    private String footer = "Groovy Documentation";
    private String header = "Groovy Documentation";
    private Boolean privateScope;
    private Boolean protectedScope;
    private Boolean packageScope;
    private Boolean publicScope;
    private Boolean author;
    private Boolean processScripts;
    private Boolean includeMainForScripts;
    private boolean useDefaultExcludes;
    private boolean includeNoSourcePackages;
    private Boolean noTimestamp;
    private Boolean noVersionStamp;
    private Boolean showInternal;
    private Boolean noIndex;
    private Boolean noDeprecatedList;
    private Boolean noHelp;
    private String syntaxHighlighter;
    private String theme;
    private String javaVersion;
    private String preLanguage;
    private final List<DirSet> packageSets;
    private final List<String> sourceFilesToDoc;
    private final List<LinkArgument> links = new ArrayList<>();
    private File overviewFile;
    private File styleSheetFile;
    // GROOVY-11941: additional stylesheets populated via nested <addStylesheet file="..."/>.
    private final List<File> addStylesheetFiles = new ArrayList<>();
    // dev note: update javadoc comment for #setExtensions(String) if updating below
    private String extensions = ".java:.groovy:.gv:.gvy:.gsh";

    private String charset;
    private String fileEncoding;

    /**
     * Creates a Groovydoc Ant task with the default option set.
     */
    public Groovydoc() {
        packageNames = new ArrayList<>();
        excludePackageNames = new ArrayList<>();
        packageSets = new ArrayList<DirSet>();
        sourceFilesToDoc = new ArrayList<>();
        privateScope = false;
        protectedScope = false;
        publicScope = false;
        packageScope = false;
        useDefaultExcludes = true;
        includeNoSourcePackages = false;
        author = true;
        processScripts = true;
        includeMainForScripts = true;
        noTimestamp = false;
        noVersionStamp = false;
        showInternal = false;
        noIndex = false;
        noDeprecatedList = false;
        noHelp = false;
        syntaxHighlighter = "none";
        theme = "auto";
    }

    /**
     * Specify where to find source file
     *
     * @param src a Path instance containing the various source directories.
     */
    public void setSourcepath(Path src) {
        if (sourcePath == null) {
            sourcePath = src;
        } else {
            sourcePath.append(src);
        }
    }

    /**
     * Set the directory where the Groovydoc output will be generated.
     *
     * @param dir the destination directory.
     */
    public void setDestdir(File dir) {
        destDir = dir;
        // todo: maybe tell groovydoc to use file output
    }

    /**
     * If set to false, author will not be displayed.
     * Currently not used.
     *
     * @param author new value
     */
    public void setAuthor(boolean author) {
        this.author = author;
    }

    /**
     * If set to true, hidden timestamp will not appear within generated HTML.
     *
     * @param noTimestamp new value
     */
    public void setNoTimestamp(boolean noTimestamp) {
        this.noTimestamp = noTimestamp;
    }

    /**
     * If set to true, hidden version stamp will not appear within generated HTML.
     *
     * @param noVersionStamp new value
     */
    public void setNoVersionStamp(boolean noVersionStamp) {
        this.noVersionStamp = noVersionStamp;
    }

    /**
     * Defaults to a popular Java version defined by the JavaParser library. Otherwise, override here for a specific expected source file version.
     *
     * @param javaVersion the expected source level of any Java files that may be parsed; e.g. JAVA_11
     */
    public void setJavaVersion(String javaVersion) {
        this.javaVersion = javaVersion;
    }

    /**
     * Default language id for {@code <pre>} blocks in rendered doc comments
     * that carry no {@code class=} attribute. When set (e.g. {@code "groovy"}),
     * a post-pass rewrites such {@code <pre>} openings to
     * {@code <pre class="language-xxx">}, which enables Prism syntax
     * highlighting for legacy doc-comment code blocks without touching
     * source files. {@code <pre>} blocks that already have any {@code class}
     * attribute (including {@code class="groovyTestCase"}) are left alone.
     *
     * @param preLanguage the language id, or empty/null to disable
     *
     * @since 6.0.0
     */
    public void setPreLanguage(String preLanguage) {
        this.preLanguage = preLanguage;
    }

    /**
     * If set to true, members annotated with {@code groovy.transform.@Internal}
     * (per GEP-17) are still included in the generated documentation. Defaults
     * to false, so internal members are hidden.
     *
     * @param showInternal new value
     *
     * @since 6.0.0
     */
    public void setShowInternal(boolean showInternal) {
        this.showInternal = showInternal;
    }

    /**
     * GROOVY-11943: if set to true, suppress generation of the alphabetical
     * index page ({@code index-all.html}) and its nav-bar link.
     *
     * @since 6.0.0
     */
    public void setNoIndex(boolean noIndex) {
        this.noIndex = noIndex;
    }

    /**
     * GROOVY-11943: if set to true, suppress generation of the deprecated-list
     * page ({@code deprecated-list.html}) and its nav-bar link.
     *
     * @since 6.0.0
     */
    public void setNoDeprecatedList(boolean noDeprecatedList) {
        this.noDeprecatedList = noDeprecatedList;
    }

    /**
     * GROOVY-11943: if set to true, suppress generation of the help page
     * ({@code help-doc.html}) and its nav-bar link.
     *
     * @since 6.0.0
     */
    public void setNoHelp(boolean noHelp) {
        this.noHelp = noHelp;
    }

    /**
     * GROOVY-11938 stage 4: selects a client-side syntax highlighter for
     * {@code {@snippet}} and fenced Markdown code blocks. Valid values are
     * {@code "prism"} (bundled) or {@code "none"} (default). Any other value
     * is treated as {@code "none"}.
     *
     * @since 6.0.0
     */
    public void setSyntaxHighlighter(String syntaxHighlighter) {
        this.syntaxHighlighter = syntaxHighlighter;
    }

    /**
     * GROOVY-11947: theme lock mode for the generated docs.
     * <ul>
     *   <li>{@code "auto"} (default) — emit a {@code prefers-color-scheme}
     *       media query so each reader sees their OS preference.</li>
     *   <li>{@code "light"} — lock the palette to light regardless of OS.</li>
     *   <li>{@code "dark"} — lock the palette to dark regardless of OS.</li>
     * </ul>
     * Any other value is treated as {@code "auto"}.
     *
     * @since 6.0.0
     */
    public void setTheme(String theme) {
        this.theme = theme;
    }

    /**
     * If set to false, Scripts will not be processed.
     * Defaults to true.
     *
     * @param processScripts new value
     */
    public void setProcessScripts(boolean processScripts) {
        this.processScripts = processScripts;
    }

    /**
     * If set to false, 'public static void main' method will not be displayed.
     * Defaults to true. Ignored when not processing Scripts.
     *
     * @param includeMainForScripts new value
     */
    public void setIncludeMainForScripts(boolean includeMainForScripts) {
        this.includeMainForScripts = includeMainForScripts;
    }

    /**
     * A colon-separated list of filename extensions to look for when searching for files to process in a given directory.
     * Default value: <code>.java:.groovy:.gv:.gvy:.gsh</code>
     *
     * @param extensions new value
     */
    public void setExtensions(String extensions) {
        this.extensions = extensions;
    }

    /**
     * Set the package names to be processed.
     *
     * @param packages a comma separated list of packages specs
     *                 (may be wildcarded).
     */
    public void setPackagenames(String packages) {
        StringTokenizer tok = new StringTokenizer(packages, ",");
        while (tok.hasMoreTokens()) {
            String packageName = tok.nextToken();
            packageNames.add(packageName);
        }
    }

    /**
     * Set the packages to exclude from documentation. Mirrors the CLI
     * {@code -exclude} option; the Ant task already honoured
     * {@code excludePackageNames} internally when generating but lacked
     * the public setter.
     *
     * @param packages a comma-separated (or colon-separated, to match
     *                 the CLI form) list of package specs, possibly
     *                 wildcarded.
     *
     * @since 6.0.0
     */
    public void setExcludePackageNames(String packages) {
        StringTokenizer tok = new StringTokenizer(packages, ",:");
        while (tok.hasMoreTokens()) {
            String name = tok.nextToken().trim();
            if (!name.isEmpty()) excludePackageNames.add(name);
        }
    }

    /**
     * Accepts the historical Ant {@code use} attribute for compatibility.
     *
     * @param b ignored
     */
    public void setUse(boolean b) {
        //ignore as 'use external file' irrelevant with groovydoc :-)
    }

    /**
     * Set the title to be placed in the HTML &lt;title&gt; tag of the
     * generated documentation.
     *
     * @param title the window title to use.
     */
    public void setWindowtitle(String title) {
        windowTitle = title;
    }

    /**
     * Set the title for the overview page.
     *
     * @param htmlTitle the html to use for the title.
     */
    public void setDoctitle(String htmlTitle) {
        docTitle = htmlTitle;
    }

    /**
     * Specify the file containing the overview to be included in the generated documentation.
     *
     * @param file the overview file
     */
    public void setOverview(File file) {
        overviewFile = file;
    }

    /**
     * Indicates the access mode or scope of interest: one of public, protected, package, or private.
     * Package scoped access is ignored for fields of Groovy classes where they correspond to properties.
     *
     * @param access one of public, protected, package, or private
     */
    public void setAccess(String access) {
        if ("public".equals(access)) publicScope = true;
        else if ("protected".equals(access)) protectedScope = true;
        else if ("package".equals(access)) packageScope = true;
        else if ("private".equals(access)) privateScope = true;
    }

    /**
     * Indicate whether all classes and
     * members are to be included in the scope processed.
     *
     * @param b true if scope is to be private level.
     */
    public void setPrivate(boolean b) {
        privateScope = b;
    }

    /**
     * Indicate whether only public classes and members are to be included in the scope processed.
     *
     * @param b true if scope only includes public level classes and members
     */
    public void setPublic(boolean b) {
        publicScope = b;
    }

    /**
     * Indicate whether only protected and public classes and members are to be included in the scope processed.
     *
     * @param b true if scope includes protected level classes and members
     */
    public void setProtected(boolean b) {
        protectedScope = b;
    }

    /**
     * Indicate whether only package, protected and public classes and members are to be included in the scope processed.
     * Package scoped access is ignored for fields of Groovy classes where they correspond to properties.
     *
     * @param b true if scope includes package level classes and members
     */
    public void setPackage(boolean b) {
        packageScope = b;
    }

    /**
     * Set the footer to place at the bottom of each generated html page.
     *
     * @param footer the footer value
     */
    public void setFooter(String footer) {
        this.footer = footer;
    }

    /**
     * Specifies the header text to be placed at the top of each output file.
     * The header will be placed to the right of the upper navigation bar.
     * It may contain HTML tags and white space, though if it does, it must
     * be enclosed in quotes. Any internal quotation marks within the header
     * may have to be escaped.
     *
     * @param header the header value
     */
    public void setHeader(String header) {
        this.header = header;
    }

    /**
     * Specifies the charset to be used in the templates, i.e.&#160;the value output within:
     * &lt;meta http-equiv="Content-Type" content="text/html; charset=<em>charset</em>"&gt;.
     *
     * @param charset the charset value
     */
    public void setCharset(String charset) {
        this.charset = charset;
    }

    /**
     * Specifies the file encoding to be used for generated files. If <em>fileEncoding</em> is missing,
     * the <em>charset</em> encoding will be used for writing the files. If <em>fileEncoding</em> and
     * <em>charset</em> are missing, the file encoding will default to <em>Charset.defaultCharset()</em>.
     *
     * @param fileEncoding the file encoding
     */
    public void setFileEncoding(String fileEncoding) {
        this.fileEncoding = fileEncoding;
    }

    /**
     * Specifies a stylesheet file to use. If not specified,
     * a default one will be generated for you.
     *
     * @param styleSheetFile the css stylesheet file to use
     */
    public void setStyleSheetFile(File styleSheetFile) {
        this.styleSheetFile = styleSheetFile;
    }

    /**
     * GROOVY-11941: Ant nested element {@code <addStylesheet file="custom.css"/>}
     * to include an additional stylesheet in the generated docs alongside the
     * default. Repeatable — specify once per extra stylesheet.
     *
     * @return a holder that Ant will populate with the nested element's file attribute
     *
     * @since 6.0.0
     */
    public AddStylesheet createAddStylesheet() {
        AddStylesheet a = new AddStylesheet();
        return a;
    }

    /**
     * Nested-element holder populated by Ant introspection for {@code <addStylesheet file="..."/>}.
     *
     * @since 6.0.0
     */
    public class AddStylesheet {
        private File file;

        /**
         * Registers an additional stylesheet to copy into the generated documentation.
         *
         * @param f the stylesheet file to add
         */
        public void setFile(File f) {
            this.file = f;
            if (f != null) addStylesheetFiles.add(f);
        }

        /**
         * Returns the stylesheet file configured for this nested element.
         *
         * @return the configured stylesheet file
         */
        public File getFile() { return file; }
    }

    /**
     * Whether {@code relativeDir} (file-separator-delimited path) has every
     * segment a valid Java/Groovy package identifier. Rejects empty segments
     * and segments containing characters illegal in identifiers, which
     * filters out resource directories like {@code snippet-files/} and
     * {@code doc-files/} whose hyphenated names can't be packages.
     */
    private static boolean isValidPackagePath(String relativeDir) {
        if (relativeDir == null || relativeDir.isEmpty()) return true;
        for (String segment : relativeDir.split(java.util.regex.Pattern.quote(File.separator))) {
            if (segment.isEmpty()) return false;
            if (!Character.isJavaIdentifierStart(segment.charAt(0))) return false;
            for (int k = 1; k < segment.length(); k++) {
                if (!Character.isJavaIdentifierPart(segment.charAt(k))) return false;
            }
        }
        return true;
    }

    private void parsePackages(List<String> resultantPackages, Path sourcePath) {
        List<String> addedPackages = new ArrayList<>();
        List<DirSet> dirSets = new ArrayList<DirSet>(packageSets);

        // for each sourcePath entry, add a directoryset with includes
        // taken from packagenames attribute and nested package
        // elements and excludes taken from excludepackages attribute
        // and nested excludepackage elements
        if (this.sourcePath != null) {
            PatternSet ps = new PatternSet();
            if (!packageNames.isEmpty()) {
                for (String pn : packageNames) {
                    String pkg = pn.replace('.', '/');
                    if (pkg.endsWith("*")) {
                        pkg += "*";
                    }
                    ps.createInclude().setName(pkg);
                }
            } else {
                ps.createInclude().setName("**");
            }

            for (String epn : excludePackageNames) {
                String pkg = epn.replace('.', '/');
                if (pkg.endsWith("*")) {
                    pkg += "*";
                }
                ps.createExclude().setName(pkg);
            }

            String[] pathElements = this.sourcePath.list();
            for (String pathElement : pathElements) {
                File dir = new File(pathElement);
                if (dir.isDirectory()) {
                    DirSet ds = new DirSet();
                    ds.setDefaultexcludes(useDefaultExcludes);
                    ds.setDir(dir);
                    ds.createPatternSet().addConfiguredPatternset(ps);
                    dirSets.add(ds);
                } else {
                    log.warn("Skipping " + pathElement + " since it is no directory.");
                }
            }
        }

        for (DirSet ds : dirSets) {
            File baseDir = ds.getDir(getProject());
            log.debug("scanning " + baseDir + " for packages.");
            DirectoryScanner dsc = ds.getDirectoryScanner(getProject());
            String[] dirs = dsc.getIncludedDirectories();
            boolean containsPackages = false;
            for (String dir : dirs) {
                // Skip directories whose path contains a segment that isn't a
                // valid Java/Groovy identifier — notably `snippet-files` (for
                // {@snippet file=...}) and `doc-files` (Javadoc-inherited).
                // Source files inside such directories are resources consumed
                // at render time, not package members to be documented.
                if (!isValidPackagePath(dir)) continue;
                // are there any groovy or java files in this directory?
                File pd = new File(baseDir, dir);
                String[] files = pd.list((dir1, name) -> {
                    if (!includeNoSourcePackages
                            && "package.html".equals(name)) return true;
                    final StringTokenizer tokenizer = new StringTokenizer(extensions, ":");
                    while (tokenizer.hasMoreTokens()) {
                        String ext = tokenizer.nextToken();
                        if (name.endsWith(ext)) return true;
                    }
                    return false;
                });

                if (files != null) {
                    for (String filename : files) {
                        sourceFilesToDoc.add(dir + File.separator + filename);
                    }
                    if (files.length > 0) {
                        if (dir.isEmpty()) {
                            log.warn(baseDir
                                    + " contains source files in the default package,"
                                    + " you must specify them as source files not packages.");
                        } else {
                            containsPackages = true;
                            String pn = dir.replace(File.separatorChar, '.');
                            if (!addedPackages.contains(pn)) {
                                addedPackages.add(pn);
                                resultantPackages.add(pn);
                            }
                        }
                    }
                }
            }
            if (containsPackages) {
                // We don't need to care for duplicates here,
                // Path.list does it for us.
                sourcePath.createPathElement().setLocation(baseDir);
            } else {
                log.verbose(baseDir + " doesn't contain any packages, dropping it.");
            }
        }
    }

    /**
     * Generates Groovydoc output for the configured source path.
     *
     * @throws BuildException if generation fails
     */
    @Override
    public void execute() throws BuildException {
        List<String> packagesToDoc = new ArrayList<>();
        Path sourceDirs = new Path(getProject());
        Properties properties = new Properties();
        properties.setProperty("windowTitle", windowTitle);
        properties.setProperty("docTitle", docTitle);
        properties.setProperty("footer", footer);
        properties.setProperty("header", header);
        checkScopeProperties(properties);
        properties.setProperty("publicScope", publicScope.toString());
        properties.setProperty("protectedScope", protectedScope.toString());
        properties.setProperty("packageScope", packageScope.toString());
        properties.setProperty("privateScope", privateScope.toString());
        properties.setProperty("author", author.toString());
        properties.setProperty("processScripts", processScripts.toString());
        properties.setProperty("includeMainForScripts", includeMainForScripts.toString());
        properties.setProperty("overviewFile", overviewFile != null ? overviewFile.getAbsolutePath() : "");
        properties.setProperty("charset", charset != null ? charset : "");
        properties.setProperty("fileEncoding", fileEncoding != null ? fileEncoding : "");
        properties.setProperty("timestamp", Boolean.valueOf(!noTimestamp).toString());
        properties.setProperty("versionStamp", Boolean.valueOf(!noVersionStamp).toString());
        properties.setProperty("showInternal", showInternal.toString());
        // GROOVY-11943: javadoc-parity disable flags.
        properties.setProperty("noIndex", noIndex.toString());
        properties.setProperty("noDeprecatedList", noDeprecatedList.toString());
        properties.setProperty("noHelp", noHelp.toString());
        properties.setProperty("syntaxHighlighter", syntaxHighlighter != null ? syntaxHighlighter : "none");
        properties.setProperty("theme", theme != null ? theme : "auto");
        // GROOVY-11941: expose additional stylesheet basenames to templates.
        StringBuilder extras = new StringBuilder();
        for (int k = 0; k < addStylesheetFiles.size(); k++) {
            if (k > 0) extras.append(',');
            extras.append(addStylesheetFiles.get(k).getName());
        }
        properties.setProperty("additionalStylesheets", extras.toString());
        String phaseOverride = SystemUtil.getSystemPropertySafe("groovydoc.phase.override");
        if (phaseOverride != null) properties.put("phaseOverride", phaseOverride);

        if (sourcePath != null) {
            sourceDirs.addExisting(sourcePath);
        }
        parsePackages(packagesToDoc, sourceDirs);

        GroovyDocTool htmlTool = new GroovyDocTool(
                new ClasspathResourceManager(), // we're gonna get the default templates out of the dist jar file
                sourcePath.list(),
                getDocTemplates(),
                getPackageTemplates(),
                getClassTemplates(),
                links,
                javaVersion,
                properties
        );

        try {
            htmlTool.add(sourceFilesToDoc);
            FileOutputTool output = new FileOutputTool();
            htmlTool.renderToOutput(output, destDir.getCanonicalPath()); // TODO push destDir through APIs?
        } catch (Exception e) {
            throw new BuildException("Groovydoc generation failed: " + e.getMessage(), e);
        }
        int errorCount = htmlTool.getErrorCount();
        if (errorCount > 0) {
            throw new BuildException("Groovydoc failed to parse " + errorCount + " source file(s).");
        }
        // try to override the default stylesheet with custom specified one if needed
        if (styleSheetFile != null) {
            try {
                String css = ResourceGroovyMethods.getText(styleSheetFile);
                File outfile = new File(destDir, "stylesheet.css");
                ResourceGroovyMethods.setText(outfile, css);
            } catch (IOException e) {
                LOGGER.log(WARNING, "Unable to copy specified stylesheet ''{0}''. Using default stylesheet instead. Due to: {1}",
                        styleSheetFile.getAbsolutePath(), e.getMessage());
            }
        }
        // GROOVY-11941: copy each additional stylesheet next to the default,
        // preserving basename.
        for (File f : addStylesheetFiles) {
            try {
                String css = ResourceGroovyMethods.getText(f);
                File outfile = new File(destDir, f.getName());
                ResourceGroovyMethods.setText(outfile, css);
            } catch (IOException e) {
                LOGGER.log(WARNING, "Unable to copy additional stylesheet ''{0}'': {1}",
                        f.getAbsolutePath(), e.getMessage());
            }
        }
        PreLanguageRewriter.rewriteDirectory(destDir, preLanguage);
    }

    private void checkScopeProperties(Properties properties) {
        // make protected the default scope and check for invalid duplication
        int scopeCount = 0;
        if (packageScope) scopeCount++;
        if (privateScope) scopeCount++;
        if (protectedScope) scopeCount++;
        if (publicScope) scopeCount++;
        if (scopeCount == 0) {
            protectedScope = true;
        } else if (scopeCount > 1) {
            throw new BuildException("More than one of public, private, package, or protected scopes specified.");
        }
    }

    /**
     * Create link to Javadoc/GroovyDoc output at the given URL.
     *
     * @return link argument to configure
     */
    public LinkArgument createLink() {
        LinkArgument result = new LinkArgument();
        links.add(result);
        return result;
    }

    /**
     * Creates and returns an array of package template classpath entries.
     * <p>
     * This method is meant to be overridden by custom GroovyDoc implementations, using custom package templates.
     *
     * @return an array of package templates, whereas each entry is resolved as classpath entry, defaults to
     * {@link GroovyDocTemplateInfo#DEFAULT_PACKAGE_TEMPLATES}.
     */
    protected String[] getPackageTemplates() {
        return GroovyDocTemplateInfo.DEFAULT_PACKAGE_TEMPLATES;
    }

    /**
     * Creates and returns an array of doc template classpath entries.
     * <p>
     * This method is meant to be overridden by custom GroovyDoc implementations, using custom doc templates.
     *
     * @return an array of doc templates, whereas each entry is resolved as classpath entry, defaults to
     * {@link GroovyDocTemplateInfo#DEFAULT_DOC_TEMPLATES}.
     */
    protected String[] getDocTemplates() {
        return GroovyDocTemplateInfo.DEFAULT_DOC_TEMPLATES;
    }

    /**
     * Creates and returns an array of class template classpath entries.
     * <p>
     * This method is meant to be overridden by custom GroovyDoc implementations, using custom class templates.
     *
     * @return an array of class templates, whereas each entry is resolved as classpath entry, defaults to
     * {@link GroovyDocTemplateInfo#DEFAULT_CLASS_TEMPLATES}.
     */
    protected String[] getClassTemplates() {
        return GroovyDocTemplateInfo.DEFAULT_CLASS_TEMPLATES;
    }
}
