import de.undercouch.gradle.tasks.download.Download

plugins {
  // https://plugins.gradle.org/plugin/com.gradleup.shadow
  id 'com.gradleup.shadow' version '8.3.5'
  // https://plugins.gradle.org/plugin/de.undercouch.download
  id 'de.undercouch.download' version '5.6.0'
  id 'java'
  // https://github.com/tbroyer/gradle-errorprone-plugin
  id 'net.ltgt.errorprone' version '4.1.0'
  // https://plugins.gradle.org/plugin/org.ajoberstar.grgit
  id 'org.ajoberstar.grgit' version '5.3.0' apply false
  // https://docs.gradle.org/current/userguide/eclipse_plugin.html
  id 'eclipse'

  id 'groovy' // needed for formatting Gradle files
  // Code formatting; defines targets "spotlessApply" and "spotlessCheck".
  // https://github.com/diffplug/spotless/tags ; see tags starting "gradle/"
  // Only works on JDK 11+ (even including the plugin crashes Gradle on JDK 8).
  id 'com.diffplug.spotless' version '6.25.0'
}

// There is another `repositories { ... }` block below; if you change this one, change that one as well.
repositories {
  maven { url 'https://oss.sonatype.org/content/repositories/snapshots/'}
  mavenCentral()
}

ext {
  // This call to `isCompatibleWith` causes a Gradle run-time failure: "No signature of method".
  // isJava17compatible = JavaVersion.isCompatibleWith(JavaVersion.VERSION_17)
  isJava17orHigher = JavaVersion.current() >= JavaVersion.VERSION_17
  isJava21orHigher = JavaVersion.current() >= JavaVersion.VERSION_21

  // As of 2024-08-19 (Lombok 1.18.34), delombok doesn't yet support JDK 23; see https://projectlombok.org/changelog .
  skipDelombok = JavaVersion.current() > JavaVersion.VERSION_21

  parentDir = file("${rootDir}/../").absolutePath

  annotationTools = "${parentDir}/annotation-tools"
  afu = "${annotationTools}/annotation-file-utilities"

  gitScriptsHome = "${project(':checker').projectDir}/bin-devel/.git-scripts"
  plumeScriptsHome = "${project(':checker').projectDir}/bin-devel/.plume-scripts"
  htmlToolsHome = "${project(':checker').projectDir}/bin-devel/.html-tools"
  doLikeJavacHome = "${project(':checker').projectDir}/bin/.do-like-javac"

  javadocMemberLevel = JavadocMemberLevel.PROTECTED

  // The local git repository, typically in the .git directory, but not for worktrees.
  // This value is always overwritten, but Gradle needs the variable to be initialized.
  localRepo = '.git'

  versions = [
    autoValue       : '1.11.0',
    googleJavaFormat : '1.19.2',
    lombok          : '1.18.36',
    hashmapUtil : '0.0.1',
    reflectionUtil : '1.1.4',
    plumeUtil : '1.10.0',

    // https://mvnrepository.com/artifact/com.google.errorprone/error_prone_core
    // If you update this:
    //  * Temporarily comment out "-Werror" elsewhere in this file
    //  * Repeatedly run `./gradlew clean compileJava` and fix all errors
    //  * Uncomment "-Werror"
    errorprone : '2.36.0',
  ]
}

task setLocalRepo(type:Exec) {
  commandLine 'git', 'worktree', 'list'
  standardOutput = new ByteArrayOutputStream()
  doLast {
    String worktreeList = standardOutput.toString()
    localRepo = worktreeList.substring(0, worktreeList.indexOf(' ')) + '/.git'
  }
}

// No group so it does not show up in the output of `gradlew tasks`
task installGitHooks(type: Copy, dependsOn: 'setLocalRepo') {
  description 'Copies git hooks to .git directory'
  from files('checker/bin-devel/git.post-merge', 'checker/bin-devel/git.pre-commit')
  rename('git\\.(.*)', '$1')
  into localRepo + '/hooks'
}

spotless {
  // Resolve the Spotless plugin dependencies from the buildscript repositories rather than the
  // project repositories.  That way the spotless plugin does not use the locally built version of
  // checker-qual as a dependency. Without this, errors like the follow are issued when running
  // a spotless task without a locally-built version of checker-qual.jar:
  // Could not determine the dependencies of task ':checker-qual:spotlessCheck'.
  //  > Could not create task ':checker-qual:spotlessJavaCheck'.
  //     > Could not create task ':checker-qual:spotlessJava'.
  //        > File signature can only be created for existing regular files, given:
  //          .../checker-framework/checker-qual/build/libs/checker-qual-3.25.1-SNAPSHOT.jar
  predeclareDepsFromBuildscript()
}

spotlessPredeclare {
  // Put all the formatters that have dependencies here.  Without this, errors like the following
  // will happen:
  // Could not determine the dependencies of task ':spotlessCheck'.
  //  > Could not create task ':spotlessJavaCheck'.
  //     > Could not create task ':spotlessJava'.
  //        > Add a step with [com.google.googlejavaformat:google-java-format:1.15.0] into the `spotlessPredeclare` block in the root project.
  java {
    googleJavaFormat(versions.googleJavaFormat)
  }
  groovyGradle {
    greclipse()
  }
}

allprojects { currentProj ->
  // Increment the minor version (second number) rather than just the patch
  // level (third number) if:
  //   * any new checkers have been added, or
  //   * backward-incompatible changes have been made to APIs or elsewhere.
  // To make a snapshot release, version should end in "-SNAPSHOT", then: ./gradlew publish
  version '3.48.4'

  tasks.withType(JavaCompile).configureEach {
    options.fork = true
  }

  apply plugin: 'java'
  apply plugin: 'eclipse'
  apply plugin: 'com.gradleup.shadow'
  apply plugin: 'de.undercouch.download'
  apply plugin: 'net.ltgt.errorprone'

  group 'org.checkerframework'

  // Keep in sync with "repositories { ... }" block above.
  repositories {
    maven { url 'https://oss.sonatype.org/content/repositories/snapshots/'}
    mavenCentral()
  }

  configurations {
    // This is required to run the Checker Framework on JDK 8.
    javacJar

    // Holds the combined classpath of all subprojects including the subprojects themselves.
    allProjects

    // Exclude checker-qual dependency added by Error Prone to avoid a circular dependency.
    annotationProcessor.exclude group:'org.checkerframework', module:'checker-qual'

    // This is to be used to add errorprone to the processorpath of the compile task.  The errorprone
    // configuration can't be used directly because it is marked unresolvable by the errorprone plugin.
    errorProneAnnotationProcessor.extendsFrom(errorprone)
  }

  dependencies {
    errorprone group: 'com.google.errorprone', name: 'error_prone_core', version: versions.errorprone

    javacJar group: 'com.google.errorprone', name: 'javac', version: "9+181-r4173-1"

    errorproneJavac("com.google.errorprone:javac:9+181-r4173-1")

    allProjects subprojects
  }

  eclipse.classpath {
    defaultOutputDir = file("build/default")
    file.whenMerged { cp ->
      cp.entries.forEach { cpe ->
        if (cpe instanceof org.gradle.plugins.ide.eclipse.model.SourceFolder) {
          cpe.output = cpe.output.replace "bin/", "build/classes/java/"
        }
        if (cpe instanceof org.gradle.plugins.ide.eclipse.model.Output) {
          cpe.path = cpe.path.replace "bin/", "build/"
        }
      }
    }
  }

  ext {
    // A list of add-export and add-open arguments to be used when running the Checker Framework.
    // Keep this list in sync with the lists in CheckerMain#getExecArguments,
    // the sections with labels "javac-jdk11-non-modularized", "maven", and "sbt" in the manual
    // and in the checker-framework-gradle-plugin, CheckerFrameworkPlugin#applyToProject
    compilerArgsForRunningCF = [
      // These are required in Java 16+ because the --illegal-access option is set to deny
      // by default.  None of these packages are accessed via reflection, so the module
      // only needs to be exported, but not opened.
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
      '--add-exports',
      'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
      // Required because the Checker Framework reflectively accesses private members in com.sun.tools.javac.comp.
      '--add-opens',
      'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED',
    ]
  }

  apply plugin: 'com.diffplug.spotless'
  // patterns of files to skip for individual sub-projects
  def perProjectDoNotFormat = [
    'checker': [
      'bin-devel/.git-scripts/**',
      'bin-devel/.plume-scripts/**',
      'dist/**',
      'tests/ainfer-*/annotated/*',
      'tests/build/**',
      'tests/calledmethods-delomboked/*',
      'tests/nullness-javac-errors/*',
    ],
    'dataflow': ['manual/examples/'],
    'framework': [
      'tests/build/**',
      'tests/returnsreceiverdelomboked/*',
    ],
  ]
  spotless {
    // If you add any formatters to this block that require dependencies, then you must also
    // add them to the spotlessPredeclare block.

    // Always skip formatting of files under build directory
    def doNotFormat = ['build/**']
    if (currentProj != project.rootProject) {
      if (perProjectDoNotFormat.containsKey(currentProj.name)) {
        doNotFormat += perProjectDoNotFormat[currentProj.name]
      }

      if (!isJava17orHigher) {
        doNotFormat += [
          'tests/**/java17/',
          'tests/*record*/'
        ]
      }

      if (!isJava21orHigher) {
        doNotFormat += ['tests/**/java21/']
      }
    }

    format 'misc', {
      // define the files to apply `misc` to
      target '*.md', '*.tex', '.gitignore', 'Makefile'
      targetExclude doNotFormat
      // define the steps to apply to those files
      indentWithSpaces(2)
      trimTrailingWhitespace()
      // endWithNewline() // Don't want to end empty files with a newline
    }

    java {
      if (currentProj == currentProj.rootProject) {
        // format .java files outside of Gradle sub-projects
        def targets = [
          'docs/examples',
          'docs/tutorial',
        ]
        targets = targets.collectMany {
          [
            // must call toString() to convert GString to String
            "${it}/**/*.java".toString(),
            // Not .ajava files because formatting would remove import statements.
          ]
        }
        target targets
      } else {
        // not the root project; format all .java files in the sub-project
        target '**/*.java'
      }
      targetExclude doNotFormat

      if (project.hasProperty('eisopFormatting')) {
        googleJavaFormat(versions.googleJavaFormat).aosp()
        importOrder('com', 'jdk', 'lib', 'lombok', 'org', 'java', 'javax')
      } else {
        googleJavaFormat(versions.googleJavaFormat) // the formatter to apply to Java files
      }
      formatAnnotations()
    }

    // Only define a groovyGradle task on the root project, for simplicity in setting the target pattern
    if (currentProj == currentProj.rootProject) {
      groovyGradle {
        target '**/*.gradle'
        targetExclude doNotFormat
        greclipse()  // which formatter Spotless should use to format .gradle files.
        if (project.hasProperty('eisopFormatting')) {
          indentWithSpaces(4)
        } else {
          indentWithSpaces(2)
        }
        trimTrailingWhitespace()
        // endWithNewline() // Don't want to end empty files with a newline
      }
    }

    // a useful task for debugging; prints exactly which files are getting formatted by spotless
    tasks.register('printSpotlessTaskInputs') {
      doLast {
        project.tasks.forEach { task ->
          if (task.name.contains('spotless')) {
            println "Inputs for task '${task.name}':"

            task.inputs.files.each { inputFile ->
              println "  Input: $inputFile"
            }
          }
        }
      }
    }
  }

  test {
    minHeapSize = "256m" // initial heap size
    maxHeapSize = "4g" // maximum heap size
  }

  // After all the tasks have been created, modify some of them.
  afterEvaluate {
    configurations {
      checkerFatJar {
        canBeConsumed = false
        canBeResolved = true
      }
    }

    dependencies {
      checkerFatJar(project(path: ':checker', configuration: 'fatJar'))
    }

    // Add the fat checker.jar to the classpath of every Javadoc task. This allows Javadoc in
    // any module to reference classes in any other module.
    // Also, build and use ManualTaglet as a taglet.
    tasks.withType(Javadoc) {
      dependsOn(':checker:shadowJar')
      dependsOn(":framework-test:tagletClasses")
      doFirst {
        options.encoding = 'UTF-8'
        if (!name.equals('javadocDoclintAll')) {
          options.memberLevel = javadocMemberLevel
        }
        classpath += configurations.getByName('checkerFatJar').asFileTree
        options.taglets 'org.checkerframework.taglet.ManualTaglet'
        options.tagletPath(project(':framework-test').sourceSets.taglet.output.classesDirs.getFiles() as File[])

        // This file is looked for by Javadoc.
        file("${destinationDir}/resources/fonts/").mkdirs()
        ant.touch(file: "${destinationDir}/resources/fonts/dejavu.css")

        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED', true)
        options.addBooleanOption('-add-exports=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED', true)

        // "-Xwerror" requires Javadoc everywhere.  Currently, CI jobs require Javadoc only
        // on changed lines.  Enable -Xwerror in the future when all Javadoc exists.
        // options.addBooleanOption('Xwerror', true)
        options.addStringOption('Xmaxwarns', '99999')
      }
    }

    // Add standard javac options
    tasks.withType(JavaCompile) { compilationTask ->
      dependsOn(':installGitHooks')
      boolean jdk17Compiler = project.getProperties().getOrDefault('useJdk17Compiler', false)
      if (jdk17Compiler) {
        // This uses the Java 17 compiler to compile all code.
        //  https://docs.gradle.org/current/userguide/toolchains.html
        javaCompiler = javaToolchains.compilerFor {
          languageVersion = JavaLanguageVersion.of(17)
        }
      }

      // Sorting is commented out because it disables incremental compilation.
      // Uncomment when needed.
      // // Put source files in deterministic order, for debugging.
      // compilationTask.source = compilationTask.source.sort()

      // This test is for whether the Checker Framework supports (runs under) Java 8.
      // Currently, the Checker Framework does support Java 8.
      if (true) {
        // Using `options.release.set(8)` here leads to compilation
        // errors such as "package com.sun.source.tree does not exist".
        sourceCompatibility = 8
        targetCompatibility = 8
        // Because the target is 8, all of the public compiler classes are accessible, so
        // --add-exports are not required (nor are they allowed with target 8). See
        // https://openjdk.org/jeps/247 for details on compiling for older versions.
      } else {
        // This makes the class files Java 11, and then the Checker Framework would not run under Java 8.
        options.release.set(11)
        options.compilerArgs += [
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
          '--add-exports',
          'jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
        ]
        // This is equivalent to writing "exports jdk.compiler/... to ALL-UNNAMED" in the
        // module-info.java of jdk.compiler, so corresponding --add-opens are only required for
        // reflective access to private members.
        //
        // From https://openjdk.org/jeps/261, Section titled: "Breaking encapsulation"
        // "The effect of each instance [of --add-exports] is to add a qualified export of the
        // named package from the source module to the target module. This is, essentially, a
        // command-line form of an exports clause in a module declaration[...].
        // [...]
        // The --add-exports option enables access to the public types of a specified package.
        // It is sometimes necessary to go further and enable access to all non-public elements
        // via the setAccessible method of the core reflection API. The --add-opens option can
        // be used, at run time, to do this."
      }

      options.failOnError = true
      options.deprecation = true
      // -options: To not get a warning about missing bootstrap classpath (when using Java 9 and `-source 8`).
      // -fallthrough: Don't check fallthroughs.  Instead, use Error Prone.  Its
      //   warnings are suppressible with a "// fall through" comment.
      String lint = '-Xlint:-options,-fallthrough'
      if (isJava21orHigher && !jdk17Compiler) {
        // TODO: Ignore this-escape for now, we may want to review and suppress each one later.
        lint +=',-this-escape'
      }
      options.compilerArgs += [
        '-g',
        '-Werror',
        lint,
        '-Xlint',
      ]

      options.encoding = 'UTF-8'
      options.fork = true

      // TODO: enable Error Prone on test classes.
      if (compilationTask.name.equals('compileJava')
          // Error Prone depends on checker-qual.jar, so don't run it on that project to avoid a circular dependency.
          && !project.name.startsWith('checker-qual')) {
        // Error Prone must be available in the annotation processor path
        options.annotationProcessorPath = configurations.errorProneAnnotationProcessor
        // Enable Error Prone
        options.errorprone.enabled = isJava17orHigher
        options.errorprone.disableWarningsInGeneratedCode = true
        options.errorprone.errorproneArgs = [
          // Many compiler classes are interned.
          '-Xep:ReferenceEquality:OFF',
          // Not useful to suggest Splitter; maybe clean up.
          '-Xep:StringSplitter:OFF',
          // Too broad, rejects seemingly-correct code.
          '-Xep:EqualsGetClass:OFF',
          // Not a real problem
          '-Xep:MixedMutabilityReturnType:OFF',
          // Don't want to add a dependency to ErrorProne.
          '-Xep:AnnotateFormatMethod:OFF',
          // Warns for every use of "@checker_framework.manual"
          '-Xep:InvalidBlockTag:OFF',
          // Recommends writing @InlineMe which is an Error-Prone-specific annotation
          '-Xep:InlineMeSuggester:OFF',
          // Recommends writing @CanIgnoreReturnValue which is an Error-Prone-specific annotation.
          // It would be great if Error Prone recognized the @This annotation.
          '-Xep:CanIgnoreReturnValueSuggester:OFF',
          // Should be turned off when using the Checker Framework.
          '-Xep:ExtendsObject:OFF',
          // The Checker Framework is the only nullness tool we care about.
          '-Xep:NullableWildcard:OFF',
          // In the visitor pattern, it is natural to forward a Void reference.
          '-Xep:VoidUsed:OFF',
          // -Werror halts the build if Error Prone issues a warning, which ensures that
          // the errors get fixed.  On the downside, Error Prone (or maybe the compiler?)
          // stops as soon as it issues one warning, rather than outputting them all.
          // https://github.com/google/error-prone/issues/436
          '-Werror',
        ]
      } else {
        options.errorprone.enabled = false
      }
    }
  } // end afterEvaluate
} // end allProjects

task version(group: 'Documentation') {
  description 'Print Checker Framework version'
  doLast {
    println version
  }
}

/**
 * Creates a task that runs the checker on the main source set of each subproject. The task is named
 * "check${taskName}", for example "checkPurity" or "checkNullness".
 *
 * @param projectName name of the project
 * @param taskName short name (often the checker name) to use as part of the task name
 * @param checker fully qualified name of the checker to run
 * @param args list of arguments to pass to the checker
 */
def createCheckTypeTask(projectName, taskName, checker, args = []) {
  project("${projectName}").tasks.create(name: "check${taskName}", type: JavaCompile, dependsOn: ':checker:shadowJar') {
    description "Run the ${taskName} Checker on the main sources."
    group 'Verification'
    // Always run the task.
    outputs.upToDateWhen { false }
    source = project("${projectName}").sourceSets.main.java
    classpath = files(project("${projectName}").compileJava.classpath,project(':checker-qual').sourceSets.main.output)
    destinationDirectory = file("${buildDir}")

    options.annotationProcessorPath = files(project(':checker').tasks.shadowJar.archiveFile)
    options.compilerArgs += [
      '-processor',
      "${checker}",
      '-proc:only',
      '-Xlint:-processing',
      '-Xmaxerrs',
      '10000',
      '-Xmaxwarns',
      '10000',
      '-ArequirePrefixInWarningSuppressions',
      '-AwarnUnneededSuppressions',
      '-AwarnRedundantAnnotations',
    ]
    options.compilerArgs += args
    options.forkOptions.jvmArgs += ['-Xmx2g']

    options.fork = true
    options.forkOptions.jvmArgs += compilerArgsForRunningCF
  }
}

task htmlValidate(type: Exec, group: 'Format') {
  description 'Validate that HTML files are well-formed'
  executable 'html5validator'
  args = [
    '--ignore',
    '/api/',
    '/build/',
    '/docs/manual/manual.html',
    '/docs/manual/plume-bib/docs/index.html',
    '/checker/jdk/nullness/src/java/lang/ref/package.html'
  ]
}


// `gradle allJavadoc` builds the Javadoc for all modules in `docs/api`.
//   This is what is published to checkerframework.org.
// `gradle javadoc` builds the Javadoc for each sub-project in <subproject>/build/docs/javadoc/ .
//   It's needed to create the Javadoc jars that we release in Maven Central.
// To make javadoc for only one subproject, run `./gradlew javadoc`
//   in the subproject or `./gradlew :checker:javadoc` at the top level.
task allJavadoc(type: Javadoc, group: 'Documentation') {
  description = 'Generates API documentation that includes all the modules.'
  dependsOn(':checker:shadowJar', 'getPlumeScripts', 'getHtmlTools')
  destinationDir = file("${rootDir}/docs/api")
  source(
      project(':checker-util').sourceSets.main.allJava,
      project(':checker-qual').sourceSets.main.allJava,
      project(':checker').sourceSets.main.allJava,
      project(':framework').sourceSets.main.allJava,
      project(':dataflow').sourceSets.main.allJava,
      project(':javacutil').sourceSets.main.allJava,
      project(':framework-test').sourceSets.main.allJava,
      )

  doFirst {
    source(
        project(':framework-test').sourceSets.taglet.allJava
        )
  }

  classpath = configurations.allProjects
  // disable interpreting module-info.java files until all sub-modules support them
  modularity.inferModulePath = false

  doLast {
    copy {
      from 'docs/logo/Checkmark/CFCheckmark_favicon.png'
      rename('CFCheckmark_favicon.png', 'favicon-checkerframework.png')
      into "${rootDir}/docs/api"
    }
    exec {
      workingDir "${rootDir}/docs/api"
      executable "${htmlToolsHome}/html-add-favicon"
      args += [
        '.',
        'favicon-checkerframework.png'
      ]
    }
  }
}

// See documentation for allJavadoc task.
javadoc.dependsOn(allJavadoc)

clean {
  delete(file("${rootDir}/docs/api"))
  doLast {
    exec {
      commandLine 'make', '-C', 'docs/manual', 'clean'
    }
  }
}

configurations {
  requireJavadoc
}
dependencies {
  requireJavadoc 'org.plumelib:require-javadoc:1.0.9'
}
task requireJavadoc(type: JavaExec, group: 'Documentation') {
  description = 'Ensures that Javadoc documentation exists in source code.'
  mainClass = 'org.plumelib.javadoc.RequireJavadoc'
  classpath = configurations.requireJavadoc
  args 'checker/src/main/java', 'checker-qual/src/main/java', 'checker-util/src/main/java', 'dataflow/src/main/java', 'framework/src/main/java', 'framework-test/src/main/java', 'javacutil/src/main/java'
}


/**
 * Creates a task named taskName that runs javadoc with the -Xdoclint:all option.
 *
 * @param taskName the name of the task to create
 * @param taskDescription description of the task
 * @param memberLevel the JavadocMemberLevel to use
 * @return the new task
 */
def createJavadocTask(taskName, taskDescription, memberLevel) {
  tasks.create(name: taskName, type: Javadoc) {
    description = taskDescription
    destinationDir = file("${rootDir}/docs/tmpapi")
    destinationDir.mkdirs()
    subprojects.forEach {
      if (!it.name.startsWith('checker-qual-android')) {
        source += it.sourceSets.main.allJava
      }
    }

    classpath = configurations.allProjects
    // disable interpreting module-info.java files until all sub-modules support them
    modularity.inferModulePath = false

    destinationDir.deleteDir()
    options.memberLevel = memberLevel
    options.addBooleanOption('Xdoclint:all', true)
    options.addStringOption('Xmaxwarns', '99999')

    // options.addStringOption('skip', 'ClassNotToCheck|OtherClass')
  }
}

createJavadocTask('javadocDoclintAll', 'Runs javadoc with -Xdoclint:all option.', JavadocMemberLevel.PRIVATE)

task manual(group: 'Documentation') {
  description 'Build the manual'
  doLast {
    exec {
      commandLine 'make', '-C', 'docs/manual', 'all'
    }
  }
}

/**
 * Quietly clones the given git repository, {@code url}, to {@directory} at a depth of 1.
 * @param url git repository to clone
 * @param directory where to clone
 * @param ignoreError whether to fail the build if the clone command fails
 * @param extraArgs any extra arguments to pass to git
 */
void clone(url, directory, ignoreError, extraArgs = []){
  exec {
    workingDir "${directory}/../"
    executable 'git'
    args = [
      'clone',
      '-q',
      '--depth=1',
      url,
      file(directory).toPath().last()
    ]
    args += extraArgs
    ignoreExitValue = ignoreError
    timeout = 60000 // 60 seconds
  }
}

/**
 * Creates a task named {@code taskName} that updates or clones the git repository at
 * {@code url} into {@code directory}.  If the clone command fails, the tasks waits
 * a minute and then trys again.
 * @param taskName name of the created task
 * @param url location of the git repository
 * @param directory where to clone the repository
 * @param extraArgs arguments to pass to the git command
 */
def createCloneTask(taskName, url, directory, extraArgs = []) {
  tasks.create(name: taskName) {
    description "Obtain or update ${url}"

    // Always run.
    outputs.upToDateWhen { false }
    doLast {
      if (file(directory).exists()) {
        exec {
          workingDir directory
          executable 'git'
          args = ['pull', '-q']
          ignoreExitValue = true
          timeout = 60000 // 60 seconds
        }
      } else {
        try {
          clone(url, directory, true, extraArgs)
        } catch (Throwable t) {
          println "Exception while cloning ${url}"
          t.printStackTrace()
        }
        if (!file(directory).exists()) {
          println "Cloning failed, will try again in 1 minute: clone(${url}, ${directory}, true, ${extraArgs})"
          sleep(60000) // wait 1 minute, then try again
          clone(url, directory, false, extraArgs)
        }
      }
    }
  }
}


createCloneTask('getGitScripts', 'https://github.com/plume-lib/git-scripts.git', gitScriptsHome)
createCloneTask('getPlumeScripts', 'https://github.com/plume-lib/plume-scripts.git', plumeScriptsHome)
createCloneTask('getHtmlTools', 'https://github.com/plume-lib/html-tools.git', htmlToolsHome)
createCloneTask('getDoLikeJavac', 'https://github.com/kelloggm/do-like-javac.git', doLikeJavacHome)

// No group so it does not show up in the output of `gradlew tasks`
task pythonIsInstalled(type: Exec) {
  description 'Check that the python3 executable is installed.'
  executable = 'python3'
  args '--version'
}

task tags {
  group 'Emacs'
  description 'Create Emacs TAGS table'
  doLast {
    exec {
      commandLine 'etags', '-i', 'checker/TAGS', '-i', 'checker-qual/TAGS', '-i', 'checker-util/TAGS', '-i', 'dataflow/TAGS', '-i', 'framework/TAGS', '-i', 'framework-test/TAGS', '-i', 'javacutil/TAGS', '-i', 'docs/manual/TAGS'
    }
    exec {
      commandLine 'make', '-C', 'docs/manual', 'tags'
    }
  }
}

subprojects {
  configurations {
    annotatedGuava
  }

  dependencies {
    // TODO: it's a bug that annotatedlib:guava requires the error_prone_annotations dependency.
    annotatedGuava "com.google.errorprone:error_prone_annotations:${versions.errorprone}"
    annotatedGuava ('org.checkerframework.annotatedlib:guava:33.1.0.2-jre') {
      // So long as Guava only uses annotations from checker-qual, excluding it should not cause problems.
      exclude group: 'org.checkerframework'
    }
  }

  shadowJar {
    // If you add an external dependency, then do the following:
    //  * On the master branch and on the modified branch, run:
    //    ./gradlew assembleForJavac && jar tf checker/dist/checker.jar | grep -v '^annotated-jdk/' | sort > checker-jar-contents.txt
    //  * Compare the files, and add relocate lines below.
    //  * Repeat until no new classes appear (all are under org/checkerframework/).

    // Relocate packages that might conflict with user's classpath.
    relocate 'com.github.javaparser', 'org.checkerframework.com.github.javaparser'
    relocate 'org.apache', 'org.checkerframework.org.apache'
    relocate 'org.relaxng', 'org.checkerframework.org.relaxng'
    relocate 'org.plumelib', 'org.checkerframework.org.plumelib'
    relocate 'org.codehaus', 'org.checkerframework.org.codehaus'
    relocate 'org.objectweb.asm', 'org.checkerframework.org.objectweb.asm'
    relocate 'io.github.classgraph', 'org.checkerframework.io.github.classgraph'
    relocate 'nonapi.io.github.classgraph', 'org.checkerframework.nonapi.io.github.classgraph'
    // relocate 'sun', 'org.checkerframework.sun'
    relocate 'com.google', 'org.checkerframework.com.google'
    relocate 'plume', 'org.checkerframework.plume'

    exclude '**/module-info.class'
  }

  if (!project.name.startsWith('checker-qual-android')) {
    task tags(type: Exec) {
      description 'Create Emacs TAGS table'
      commandLine 'bash', '-c', "find . \\( -name build -o -name jtreg -o -name tests \\) -prune -o -name '*.java' -print | sort-directory-order | xargs ctags -e -f TAGS"
    }
  }

  java {
    withJavadocJar()
    withSourcesJar()
  }

  // Things in this block reference definitions in the subproject that do not exist,
  // until the project is evaluated.
  afterEvaluate {
    // Adds manifest to all Jar files
    tasks.withType(Jar) {
      includeEmptyDirs = false
      if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) {
        metaInf {
          from './LICENSE.txt'
        }
      } else {
        metaInf {
          from "${rootDir}/LICENSE.txt"
        }
      }
      manifest {
        attributes('Implementation-Version': "${project.version}")
        attributes('Implementation-URL': 'https://checkerframework.org')
        if (! archiveFileName.get().endsWith('source.jar') && ! archiveFileName.get().startsWith('checker-qual')) {
          attributes('Automatic-Module-Name': 'org.checkerframework.' + project.name.replaceAll('-', '.'))
        }
        if (archiveFileName.get().startsWith('checker-qual') || archiveFileName.get().startsWith('checker-util')) {
          attributes('Bundle-License': 'MIT')
        } else {
          attributes('Bundle-License': '(GPL-2.0-only WITH Classpath-exception-2.0)')
        }
      }
    }

    // Tasks such as `checkResourceLeak` to run various checkers on all the main source sets.
    // These pass and are run by the `typecheck` task.
    // When you add one here, also update a dependsOn item for the 'typecheck' task.
    createCheckTypeTask(project.name, 'Formatter',
        'org.checkerframework.checker.formatter.FormatterChecker')
    createCheckTypeTask(project.name, 'Interning',
        'org.checkerframework.checker.interning.InterningChecker',
        [
          '-Astubs=javax-lang-model-element-name.astub'
        ])
    createCheckTypeTask(project.name, 'Optional',
        'org.checkerframework.checker.optional.OptionalChecker',
        [
          // to avoid having to annotate JavaParser
          '-AassumePureGetters',
          '-AassumeAssertionsAreEnabled',
        ])
    createCheckTypeTask(project.name, 'Purity',
        'org.checkerframework.framework.util.PurityChecker')
    createCheckTypeTask(project.name, 'ResourceLeak',
        'org.checkerframework.checker.resourceleak.ResourceLeakChecker')
    createCheckTypeTask(project.name, 'Signature',
        'org.checkerframework.checker.signature.SignatureChecker')

    // The checkNullness task runs on all code, but it only *checks* the following code:
    //  * All files outside the 'framework' and 'checker' subprojects.
    //  * In the 'framework' and 'checker' subprojects, files with `@AnnotatedFor("nullness")`.
    if (project.name.is('framework') || project.name.is('checker')) {
      createCheckTypeTask(project.name, 'Nullness',
          'org.checkerframework.checker.nullness.NullnessChecker',
          [
            '-AskipUses=com\\.sun\\.*',
            // If a file does not contain @AnnotatedFor("nullness"), all its routines are assumed to return @Nullable.
            '-AuseConservativeDefaultsForUncheckedCode=source'
          ])
    } else {
      createCheckTypeTask(project.name, 'Nullness',
          'org.checkerframework.checker.nullness.NullnessChecker',
          ['-AskipUses=com\\.sun\\.*'])
    }


    // Add jtregTests to framework and checker modules
    if (project.name.is('framework') || project.name.is('checker')) {
      tasks.create(name: 'jtregTests', group: 'Verification') {
        description 'Run the jtreg tests.'
        dependsOn('compileJava')
        dependsOn('compileTestJava')
        dependsOn('shadowJar')

        String jtregOutput = "${buildDir}/jtreg"
        String name = 'all'
        String tests = '.'
        doLast {
          try {
            exec {
              executable 'jtreg'
              args = [
                "-dir:${projectDir}/jtreg",
                "-workDir:${jtregOutput}/${name}/work",
                "-reportDir:${jtregOutput}/${name}/report",
                '-verbose:error,fail,nopass',
                // Don't add debugging information
                //  '-javacoptions:-g',
                '-keywords:!ignore',
                '-samevm',
                "-javacoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}",
                // Required for checker/jtreg/nullness/PersistUtil.java and other tests
                "-vmoptions:-classpath ${tasks.shadowJar.archiveFile.get()}:${sourceSets.test.output.asPath}",
              ]
              args += [
                // checker/jtreg/nullness/defaultsPersist/ReferenceInfoUtil.java
                // uses the jdk.jdeps module.
                '-javacoptions:--add-modules jdk.jdeps',
                '-javacoptions:--add-exports=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.jdeps/com.sun.tools.classfile=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED',
                '-vmoptions:--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED',
              ]
              if (project.name.is('framework')) {
                // Do not check for the annotated JDK
                args += [
                  '-javacoptions:-ApermitMissingJdk'
                ]
              } else if (project.name.is('checker')) {
                args += [
                  "-javacoptions:-classpath ${sourceSets.testannotations.output.asPath}",
                ]
              }

              // Location of jtreg tests
              args += "${tests}"
            }
          } catch (Exception ex) {
            if (ex.getCause() != null && ex.getCause().getCause()!= null) {
              String msg = String.join(System.lineSeparator(),
                  ex.getCause().getLocalizedMessage() + ':',
                  ex.getCause().getCause().getLocalizedMessage(),
                  'Have you installed jtreg?')
              println msg
            }
            throw ex
          }
        }
      }
    }

    // Create a task for each JUnit test class whose name is the same as the JUnit class name.
    sourceSets.test.allJava.filter { it.path.contains('test/junit') }.forEach { file ->
      String junitClassName = file.name.replaceAll('.java', '')
      tasks.create(name: "${junitClassName}", type: Test) {
        description "Run ${junitClassName} tests."
        include "**/${name}.class"
        testClassesDirs = testing.suites.test.sources.output.classesDirs
        classpath = testing.suites.test.sources.runtimeClasspath
      }
    }

    // Configure JUnit tests
    tasks.withType(Test) {
      jvmArgs += compilerArgsForRunningCF

      // maxParallelForks controls the parallelism of a single Test
      // task. --parallel controls parallelism for the entire build.

      // Run tests in parallel, except on CI where it seems to lead to flaky failures.
      // The TF_BUILD environment variable is set to 'True' for jobs running on Azure Pipelines.
      if (!System.getenv('TF_BUILD')?.equals('True')) {
        // Not running under Azure Pipelines CI.

        maxParallelForks = Runtime.runtime.availableProcessors() ?: 1
      } else {
        // Running under Azure Pipelines CI.

        // Per Manu's comment above, on CI, parallelism seems to lead to flaky failures.
        maxParallelForks = 1

        // Fork the test to try to improve performance.
        // https://docs.gradle.org/current/userguide/performance.html#fork_tests_into_multiple_processes
        tasks.withType(Test).configureEach {
          forkEvery = 25
        }
      }

      if (project.name.is('checker')) {
        dependsOn('assembleForJavac')
      }

      if (project.hasProperty('emit.test.debug')) {
        systemProperties += ['emit.test.debug': 'true']
      }

      testLogging {
        showStandardStreams = true
        // Always run the tests
        outputs.upToDateWhen { false }

        // Show the found unexpected diagnostics and expected diagnostics not found.
        exceptionFormat 'full'
        events 'failed'

        // Don't show the uninteresting stack traces from the exceptions.
        showStackTraces = false
      }
    }

    // Create a nonJunitTests task per project
    tasks.create(name: 'nonJunitTests', group: 'Verification') {
      description 'Run all Checker Framework tests except for the Junit tests and inference tests.'
      if (project.name.is('framework') || project.name.is('checker')) {
        dependsOn('jtregTests')
      }
      if (project.name.is('framework')) {
        dependsOn('loaderTests')
      }

      if (project.name.is('checker')) {
        dependsOn('jtregJdk11Tests', 'nullnessExtraTests', 'commandLineTests', 'tutorialTests')
      }

      if (project.name.is('dataflow')) {
        dependsOn('allDataflowTests')
      }
    }

    // Create an inferenceTests task per project
    tasks.create(name: 'inferenceTests', group: 'Verification') {
      description 'Run inference tests.'
      if (project.name.is('checker')) {
        dependsOn('inferenceTests-part1', 'inferenceTests-part1')
      }
    }
    tasks.create(name: 'inferenceTests-part1', group: 'Verification') {
      description 'Run inference tests (part 1).'
      if (project.name.is('checker')) {
        dependsOn('ainferTest', 'wpiManyTest')
      }
    }
    tasks.create(name: 'inferenceTests-part2', group: 'Verification') {
      description 'Run inference tests (part 2).'
      if (project.name.is('checker')) {
        dependsOn('wpiPlumeLibTest')
      }
    }

    // Create a typecheck task per project (dogfooding the Checker Framework on itself).
    // This isn't a test of the Checker Framework as the test and nonJunitTests tasks are.
    // Tasks such as 'checkInterning' are constructed by createCheckTypeTask.
    tasks.create(name: 'typecheck', group: 'Verification') {
      description 'Run the Checker Framework on itself'
      dependsOn('typecheck-part1', 'typecheck-part2')
    }
    tasks.create(name: 'typecheck-part1', group: 'Verification') {
      description 'Run the Checker Framework on itself (part 1)'
      dependsOn('checkFormatter', 'checkInterning', 'checkOptional', 'checkPurity')
    }
    tasks.create(name: 'typecheck-part2', group: 'Verification') {
      description 'Run the Checker Framework on itself (part 2)'
      dependsOn('checkResourceLeak', 'checkSignature')
      if (project.name.is('framework') || project.name.is('checker')) {
        dependsOn('checkCompilerMessages')
      } else {
        dependsOn('checkNullness')
      }
    }

    // Create an allTests task per project.
    // allTests = test + nonJunitTests + inferenceTests + typecheck
    tasks.create(name: 'allTests', group: 'Verification') {
      description 'Run all Checker Framework tests'
      // The 'test' target is just the JUnit tests.
      dependsOn('test', 'nonJunitTests', 'inferenceTests', 'typecheck')
    }

    task javadocPrivate(dependsOn: javadoc) {
      doFirst {
        javadocMemberLevel = JavadocMemberLevel.PRIVATE
      }
      doLast {
        javadocMemberLevel = JavadocMemberLevel.PROTECTED
      }
    }
  }
}

// The `assembleForJavac` task is faster than the `assemble` task, if you only
// care about running `javac`.
// The assemble task also produces Javadoc jars, because its purpose is to build
// all artifacts produced by the Gradle project:
// https://docs.gradle.org/current/userguide/base_plugin.html#sec:base_tasks
assemble.dependsOn(':checker:assembleForJavac')

assemble.mustRunAfter(clean)

task buildAll(group: 'Build') {
  description 'Build all jar files, including source and javadoc jars'
  dependsOn(allJavadoc)
  subprojects { Project subproject ->
    dependsOn("${subproject.name}:assemble")
    dependsOn("${subproject.name}:javadocJar")
    dependsOn("${subproject.name}:sourcesJar")
  }
  dependsOn('framework:allJavadocJar', 'framework:allSourcesJar', 'checker:allJavadocJar', 'checker:allSourcesJar', 'checker-qual:jar', 'checker-util:jar')
  dependsOn('checker:assembleForJavac')
}

task releaseBuild(group: 'Build') {
  description 'Cleans, then builds everything required for a release'
  dependsOn(clean)
  dependsOn(buildAll)
}

// No group so it does not show up in the output of `gradlew tasks`
task releaseAndTest {
  description 'Build everything required for a release and run allTests'
  dependsOn(releaseBuild)
  subprojects { Project subproject ->
    dependsOn("${subproject.name}:allTests")
  }
}

// Don't create an empty checker-framework-VERSION.jar
jar.onlyIf {false}

/**
 * Adds the shared pom information to the given publication.
 * @param publication the MavenPublication
 */
final sharedPublicationConfiguration(publication) {
  publication.pom {
    url = 'https://checkerframework.org/'
    developers {
      // These are the lead developers/maintainers, not all the developers or contributors.
      developer {
        id = 'mernst'
        name = 'Michael Ernst'
        email = 'mernst@cs.washington.edu'
        url = 'https://homes.cs.washington.edu/~mernst/'
        organization = 'University of Washington'
        organizationUrl = 'https://www.cs.washington.edu/'
      }
      developer {
        id = 'smillst'
        name = 'Suzanne Millstein'
        email = 'smillst@cs.washington.edu'
        organization = 'University of Washington'
        organizationUrl = 'https://www.cs.washington.edu/'
      }
    }

    scm {
      url = 'https://github.com/typetools/checker-framework.git'
      connection = 'scm:git:https://github.com/typetools/checker-framework.git'
      developerConnection = 'scm:git:ssh://git@github.com/typetools/checker-framework.git'
    }
  }
}
