import org.gradle.internal.jvm.Jvm
import org.apache.lucene.gradle.plugins.java.RenderJavadocTaskBase
import org.apache.lucene.gradle.plugins.globals.LuceneBuildGlobalsExtension

/*
 * 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.
 */

// generate javadocs by calling javadoc tool
// see https://docs.oracle.com/en/java/javase/11/tools/javadoc.html

def resources = rootProject.file("gradle/documentation/render-javadoc")

allprojects {
  plugins.withType(JavaPlugin).configureEach {
    configurations {
      missingdoclet
    }

    dependencies {
      missingdoclet project(":build-tools:missing-doclet")
    }

    project.ext {
      relativeDocPath = project.path.replaceFirst(/:\w+:/, "").replace(':', '/')
    }

    JavaVersion minJavaVersion = buildGlobals.minJavaVersion.get()

    def renderJavadoc = tasks.register("renderJavadoc", RenderJavadocTask, {
      description = "Generates Javadoc API documentation for each module. This directly invokes javadoc tool."
      group = "documentation"

      taskResources = resources
      dependsOn sourceSets.main.compileClasspath
      classpath = sourceSets.main.compileClasspath
      srcDirSet = sourceSets.main.java
      releaseVersion = minJavaVersion

      outputDir = project.tasks.named("javadoc").get().destinationDir
    })

    // We disable the default javadoc task and have our own
    // javadoc rendering task below. The default javadoc task
    // will just invoke 'renderJavadoc' (to allow people to call
    // conventional task name).
    tasks.named("javadoc").configure {
      enabled = false
      dependsOn renderJavadoc
    }

    if (project.path == ':lucene:luke' || !(project in rootProject.ext.mavenProjects)) {
      // These projects are not part of the public API so we don't render their javadocs
      // as part of the site's creation. Linting happens via javac
    } else {
      tasks.register("renderSiteJavadoc", RenderJavadocTask, {
        description = "Generates Javadoc API documentation for the site (relative links)."
        group = "documentation"

        taskResources = resources
        dependsOn sourceSets.main.compileClasspath
        classpath = sourceSets.main.compileClasspath
        srcDirSet = sourceSets.main.java
        releaseVersion = minJavaVersion

        relativeProjectLinks = true

        enableSearch = true

        // Place the documentation under the documentation directory.
        // docroot is defined in 'documentation.gradle'
        outputDir = project.docroot.toPath().resolve(project.ext.relativeDocPath).toFile()
      })
    }
  }
}

// Set up titles and link up some offline docs for all documentation
// (they may be unused but this doesn't do any harm).

def minJava = deps.versions.minJava.get()
def javaJavadocPackages = rootProject.file("${resources}/java-${minJava}/")
if (!javaJavadocPackages.exists()) {
  throw new GradleException("Prefetched javadoc element-list is missing at " + javaJavadocPackages + ", " +
  "create this directory and fetch the element-list file from " +
  "from https://docs.oracle.com/en/java/javase/${minJava}/docs/api/element-list")
}

def junitVersion = deps.versions.junit.get()
def junitJavadocPackages = rootProject.file("${resources}/junit-${junitVersion}/")
if (!junitJavadocPackages.exists()) {
  throw new GradleException("Prefetched javadoc package-list is missing at " + junitJavadocPackages + ", " +
  "create this directory and fetch the package-list file from " +
  "from https://junit.org/junit4/javadoc/${junitVersion}/package-list")
}

allprojects {
  project.tasks.withType(RenderJavadocTask).configureEach {
    title = "Lucene ${project.version} ${project.name} API"

    offlineLinks += [
      ("https://docs.oracle.com/en/java/javase/${minJava}/docs/api/".toString()): javaJavadocPackages,
      ("https://junit.org/junit4/javadoc/${junitVersion}/".toString()): junitJavadocPackages
    ]

    luceneDocUrl = project.rootProject.buildOptions.getOption('lucene.javadoc.url').asStringProvider()

    // Set up custom doclet.
    dependsOn configurations.missingdoclet
    docletpath = configurations.missingdoclet
  }
}

// Configure project-specific tweaks and to-dos.
configure(project(":lucene:analysis:common")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure([
  project(":lucene:analysis:kuromoji"),
  project(":lucene:analysis:nori"),
  project(":lucene:analysis:opennlp"),
  project(":lucene:analysis:smartcn"),
  project(":lucene:benchmark"),
  project(":lucene:codecs"),
  project(":lucene:grouping"),
  project(":lucene:highlighter"),
  project(":lucene:luke"),
  project(":lucene:monitor"),
  project(":lucene:queries"),
  project(":lucene:queryparser"),
  project(":lucene:replicator"),
  project(":lucene:spatial-extras"),
]) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure([
  project(":lucene:analysis:icu"),
  project(":lucene:analysis:morfologik"),
  project(":lucene:analysis:phonetic"),
  project(":lucene:analysis:stempel"),
  project(":lucene:classification"),
  project(":lucene:demo"),
  project(":lucene:expressions"),
  project(":lucene:facet"),
  project(":lucene:join"),
  project(":lucene:spatial3d"),
  project(":lucene:suggest"),
]) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing @param tags
    javadocMissingLevel = "method"
  }
}

configure(project(":lucene:backward-codecs")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing @param tags
    javadocMissingLevel = "method"
  }
}

configure(project(":lucene:test-framework")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure(project(":lucene:sandbox")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure(project(":lucene:spatial-test-fixtures")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure(project(":lucene:misc")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
  }
}

configure(project(":lucene:core")) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // TODO: fix missing javadocs
    javadocMissingLevel = "class"
    // some packages are fixed already
    javadocMissingMethod = [
      "org.apache.lucene.util.automaton",
      "org.apache.lucene.analysis.standard",
      "org.apache.lucene.analysis.tokenattributes",
      "org.apache.lucene.document",
      "org.apache.lucene.search.similarities",
      "org.apache.lucene.index",
      "org.apache.lucene.codecs",
      "org.apache.lucene.codecs.lucene50",
      "org.apache.lucene.codecs.lucene60",
      "org.apache.lucene.codecs.lucene80",
      "org.apache.lucene.codecs.lucene84",
      "org.apache.lucene.codecs.lucene86",
      "org.apache.lucene.codecs.lucene87",
      "org.apache.lucene.codecs.perfield"
    ]
  }
}

configure(project(':lucene:demo')) {
  project.tasks.withType(RenderJavadocTask).configureEach {
    // For the demo, we link the example source in the javadocs, as it's ref'ed elsewhere
    linksource = true
  }
}

// Add cross-project documentation task dependencies:
// - each RenderJavaDocs task gets a dependency to all tasks with the same name in its dependencies
// - the dependency is using dependsOn with a closure to enable lazy evaluation
configure(subprojects) {
  project.tasks.withType(RenderJavadocTask).configureEach { task ->
    task.dependsOn {
      task.project.configurations.implementation.allDependencies.withType(ProjectDependency).collect { dep ->
        return dep.path + ":" + task.name
      }
    }
  }
}

class OfflineLink implements Serializable {
  @Input
  String url

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  @IgnoreEmptyDirectories
  File location

  OfflineLink(String url, File location) {
    this.url = url
    this.location = location
  }
}

@CacheableTask
abstract class RenderJavadocTask extends RenderJavadocTaskBase {
  @InputFiles
  @PathSensitive(PathSensitivity.RELATIVE)
  @IgnoreEmptyDirectories
  @SkipWhenEmpty
  SourceDirectorySet srcDirSet;

  @OutputDirectory
  File outputDir

  @CompileClasspath
  FileCollection classpath

  @CompileClasspath
  FileCollection docletpath

  @Input
  String title

  @Input
  boolean linksource = false

  @Input
  boolean enableSearch = false

  @Input
  boolean relativeProjectLinks = false

  @Input
  JavaVersion releaseVersion

  @Internal
  Map<String, File> offlineLinks = [:]

  // Computes cacheable inputs from the map in offlineLinks.
  @Nested
  List<OfflineLink> getCacheableOfflineLinks() {
    return offlineLinks.collect { url, location -> new OfflineLink(url, location) }
  }

  @Input
  @Optional
  final Property<String> luceneDocUrl = project.objects.property(String)

  // default is to require full javadocs
  @Input
  String javadocMissingLevel = "parameter"

  // anything in these packages is checked with level=method. This allows iteratively fixing one package at a time.
  @Input
  List<String> javadocMissingMethod = []

  // default is not to ignore any elements, should only be used to workaround split packages
  @Input
  List<String> javadocMissingIgnore = []

  @InputDirectory
  @PathSensitive(PathSensitivity.RELATIVE)
  @IgnoreEmptyDirectories
  File taskResources

  /** Utility method to recursively collect all tasks with same name like this one that we depend on */
  private Set findRenderTasksInDependencies() {
    Set found = []
    def collectDeps
    collectDeps = { task ->
      project.gradle.taskGraph.getDependencies(task).findAll{ it.name == this.name && it.enabled && !found.contains(it) }.each{
        found << it
        collectDeps(it)
      }
    }
    collectDeps(this)
    return found
  }

  @TaskAction
  public void render() {
    def srcDirs = srcDirSet.sourceDirectories.filter { dir -> dir.exists() }

    def optionsFile = project.file("${getTemporaryDir()}/javadoc-options.txt")

    // create the directory, so relative link calculation knows that it's a directory:
    outputDir.mkdirs();

    def opts = []

    def overviewFiles = srcDirs
        .collect {  dir -> project.file("${dir}/overview.html") }
        .findAll { overviewFile -> overviewFile.exists() }

    assert overviewFiles.size() == 1 : "Must be exactly one overview.html file: " + overviewFiles
    opts << [
      '-overview',
      project.file(overviewFiles.getFirst())
    ]

    opts << ['-d', outputDir]
    opts << '-protected'
    opts << ['-encoding', 'UTF-8']
    opts << ['-charset', 'UTF-8']
    opts << ['-docencoding', 'UTF-8']
    if (!enableSearch) {
      opts << '-noindex'
    }
    opts << '-author'
    opts << '-version'
    if (linksource) {
      opts << '-linksource'
    }
    opts << '-use'
    opts << ['-locale', 'en_US']
    opts << ['-windowtitle', title]
    opts << ['-doctitle', title]
    if (!classpath.isEmpty()) {
      opts << [
        '-classpath',
        classpath.asPath
      ]
    }

    LuceneBuildGlobalsExtension buildGlobals = project.rootProject.extensions.getByType(LuceneBuildGlobalsExtension)
    opts << [
      '-bottom',
      "<i>Copyright &copy; 2000-${buildGlobals.buildYear} Apache Software Foundation. All Rights Reserved.</i>"
    ]

    opts << [
      '-tag',
      'lucene.experimental:a:WARNING: This API is experimental and might change in incompatible ways in the next release.'
    ]
    opts << [
      '-tag',
      'lucene.internal:a:NOTE: This API is for internal purposes only and might change in incompatible ways in the next release.'
    ]
    opts << [
      '-tag',
      "lucene.spi:t:SPI Name (case-insensitive: if the name is 'htmlStrip', 'htmlstrip' can be used when looking up the service)."
    ]

    opts << [
      '-doclet',
      "org.apache.lucene.missingdoclet.MissingDoclet"
    ]
    opts << [
      '-docletpath',
      docletpath.asPath
    ]
    opts << [
      '--missing-level',
      javadocMissingLevel
    ]
    if (javadocMissingIgnore) {
      opts << [
        '--missing-ignore',
        String.join(',', javadocMissingIgnore)
      ]
    }
    if (javadocMissingMethod) {
      opts << [
        '--missing-method',
        String.join(',', javadocMissingMethod)
      ]
    }

    opts << ['-quiet']

    // Add all extra options, if any.
    opts.addAll(extraOpts.orElse([]).get())

    def allOfflineLinks = [:]
    allOfflineLinks.putAll(offlineLinks)

    // Resolve inter-project links:
    // - find all (enabled) tasks this tasks depends on (with same name), calling findRenderTasksInDependencies()
    // - sort the tasks preferring those whose project name equals 'core', then lexigraphical by path
    // - for each task get output dir to create relative or absolute link
    findRenderTasksInDependencies()
        .sort(false, Comparator.comparing { (it.project.name != 'core') as Boolean }.thenComparing(Comparator.comparing { it.path }))
        .each { otherTask ->
          def otherProject = otherTask.project
          // For relative links we compute the actual relative link between projects.
          if (relativeProjectLinks) {
            def pathTo = otherTask.outputDir.toPath().toAbsolutePath()
            def pathFrom = outputDir.toPath().toAbsolutePath()
            def relative = pathFrom.relativize(pathTo).toString().replace(File.separator, '/')
            opts << ['-link', relative]
          } else {
            // For absolute links, we determine the target URL by assembling the full URL (if base is available).
            def value = luceneDocUrl.getOrElse(null)
            if (value) {
              allOfflineLinks.put("${value}/${otherProject.relativeDocPath}/".toString(), otherTask.outputDir)
            }
          }
        }

    // Add offline links.
    allOfflineLinks.each { url, dir ->
      // Some sanity check/ validation here to ensure dir/package-list or dir/element-list is present.
      if (!project.file("$dir/package-list").exists() &&
          !project.file("$dir/element-list").exists()) {
        throw new GradleException("Expected pre-rendered package-list or element-list at ${dir}.")
      }
      logger.info("Linking ${url} to ${dir}")
      opts << ['-linkoffline', url, dir]
    }

    opts << [
      '--release',
      releaseVersion.toString()
    ]
    opts << '-Xdoclint:all,-missing'

    // Increase Javadoc's heap.
    opts += ["-J-Xmx512m"]
    // Force locale to be "en_US" (fix for: https://bugs.openjdk.java.net/browse/JDK-8222793)
    opts += [
      "-J-Duser.language=en",
      "-J-Duser.country=US"
    ]

    // -J options have to be passed on command line, they are not interpreted if passed via args file.
    def jOpts = opts.findAll { opt -> opt instanceof String && opt.startsWith("-J") }
    opts.removeAll(jOpts)

    // Collect all source files, for now excluding module descriptors.
    opts.addAll(
        srcDirs.collectMany { dir ->
          project.fileTree(dir: dir, include: "**/*.java", exclude: "**/module-info.java").files
        }.collect {
          it.toString()
        }
        )

    // handle doc-files manually since in explicit source file mode javadoc does not copy them.
    srcDirs.each { File dir ->
      project.copy {
        into outputDir

        from(dir, {
          include "**/doc-files/**"
        })
      }
    }

    // Temporary file that holds all javadoc options for the current task (except jOpts)
    optionsFile.withWriter("UTF-8", { writer ->
      // escapes an option with single quotes or whitespace to be passed in the options.txt file for
      def escapeJavadocOption = { String s -> (s =~ /[ '"]/) ? ("'" + s.replaceAll(/[\\'"]/, /\\$0/) + "'") : s }

      opts.each { entry ->
        if (entry instanceof List) {
          writer.write(entry.collect { escapeJavadocOption(it as String) }.join(" "))
        } else {
          writer.write(escapeJavadocOption(entry as String))
        }
        writer.write('\n')
      }
    })

    def javadocCmd = project.file(executable.get())
    logger.info("Javadoc executable used: ${javadocCmd}")

    buildGlobals.quietExec(this, {
      executable javadocCmd

      args += ["@${optionsFile}"]
      args += jOpts
    })

    // append some special table css, prettify css
    ant.concat(destfile: "${outputDir}/stylesheet.css", append: "true", fixlastline: "true", encoding: "UTF-8") {
      filelist(dir: taskResources, files:
      [
        "table_padding.css",
        "custom_styles.css",
        "prettify/prettify.css"
      ].join(" ")
      )
    }

    // append prettify to scripts
    ant.concat(destfile: "${outputDir}/script.js", append: "true", fixlastline: "true", encoding: "UTF-8") {
      filelist(dir: project.file("${taskResources}/prettify"), files: "prettify.js inject-javadocs.js")
    }

    ant.fixcrlf(srcdir: outputDir, includes: "stylesheet.css script.js", eol: "lf", fixlast: "true", encoding: "UTF-8")
  }
}
