/*
 * 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.
 */
import org.ajoberstar.grgit.Grgit
import java.time.Instant
import java.time.ZoneId
import java.time.format.DateTimeFormatter

buildscript {
  repositories {
    mavenCentral()
  }

  dependencies {
    classpath "org.ajoberstar.grgit:grgit-core:5.2.0"
  }
}

plugins {
  id 'com.diffplug.spotless' version '6.19.0'
  id 'net.ltgt.errorprone' version '3.1.0'
  id 'org.springframework.boot' version '2.7.7'
  id 'io.spring.dependency-management' version '1.1.0'
  id 'org.gradle.crypto.checksum' version '1.4.0'
  id 'org.jetbrains.kotlin.jvm' version '1.8.21'
  id 'org.jetbrains.kotlin.plugin.spring' version '1.8.21'
  id 'org.jetbrains.dokka' version '1.8.10'
  id 'maven-publish'
  id 'com.jfrog.bintray' version '1.8.5'
  id 'se.patrikerdes.use-latest-versions' version '0.2.18'
  id 'com.github.ben-manes.versions' version '0.46.0'
}

description = 'A set of libraries and other tools to aid development of blockchain and other decentralized software in Java and other JVM languages'

//////
// Version numbering

def versionNumber = '2.4.0'
def buildVersion = versionNumber + buildTag(buildRelease)

static String buildTag(releaseBuild) {
  if (releaseBuild == 'true' || System.getenv('BUILD_RELEASE') == 'true') {
    return ''
  }
  if (System.getenv('BUILD_TIMESTAMP') != null) {
    return DateTimeFormatter.ofPattern("-yyyyMMddHHmmss").withLocale(Locale.US).withZone( ZoneId.systemDefault() ).format(Instant.now())
  }
  return '-SNAPSHOT'
}


//////
// Default tasks and build aliases

defaultTasks 'spotlessCheck', 'jar', 'test', ':javadoc'

def buildAliases = ['dev': [
    'spotlessApply',
    ':jar',
    'test',
    ':javadoc'
  ]]

def expandedTaskList = []
gradle.startParameter.taskNames.each {
  expandedTaskList << (buildAliases[it] ? buildAliases[it] : it)
}
gradle.startParameter.taskNames = expandedTaskList.flatten()

ext {
  gradleVersion = '7.6'
}

apply from: "${rootDir}/gradle/wrapper.gradle"

apply from: "${rootDir}/gradle/stage.gradle"

apply from: "${rootDir}/gradle/check-licenses.gradle"

//////
// RAT checks

if (file('.git').exists()) {
  apply from: "${rootDir}/gradle/rat.gradle"
  rat {
    // Exclude everything under the directory that git should be ignoring via .gitignore or that isn't checked in. These
    // restrict us only to files that are checked in or are staged.
    def repo = Grgit.open(currentDir: project.getRootDir())
    excludes = {
      def list = new ArrayList<String>(repo.clean(ignore: false, directories: true, dryRun: true))
      // And some of the files that we have checked in should also be excluded from this check
      list.addAll([
        '.*\\.asc',
        'gradle/tuweni-test.asc',
        '\\w+/out/.*',
        'eth-reference-tests/**',
        'build',
        '.*/build/**',
        '.editorconfig',
        '.idea/**',
        'gradlew.bat',
        'gradlew',
        'gradle/wrapper/gradle-wrapper.properties',
        'toml/src/test/resources/**',
        '.gitattributes',
        '.*\\.kotlin_module',
        'example-v0.4.0.toml',
        'hard_example.toml',
        'toml-v0.5.0-spec-example.toml',
        'eth2-reference-tests/**',
        'package-lock.json',
        '.github/pull_request_template.md',
        'eth-crawler/src/main/resources/mainnet.json',
        'devp2p-eth/src/test/resources/mainnet.json',
        'devp2p-eth/src/test/resources/besu-dev.json',
        'dist/netty-license/*',
        'eth/src/test/resources/astor.json',
        'eth/src/test/resources/mainnet.json',
        'eth-blockprocessor/src/test/resources/**',
        'eth-client/src/main/resources/mainnet.json',
        'eth-client/src/main/resources/default.json',
        'eth/src/test/resources/missing-difficulty.json',
        'eth/src/test/resources/missing-nonce.json',
        'eth/src/test/resources/valid-genesis.json',
        'eth/src/test/resources/besu-dev.json',
        'app-commons/src/main/resources/tuweni.txt',
        'evm/src/test/resources/**',
        'tuweni.png',
        'eth-crawler/src/main/resources/webapp/bootstrap-nightfall.css',
        'eth/src/main/resources/genesis/*.json',
        'evm-dsl/src/test/resources/contract.txt',
        'ssz/src/test/resources/org/apache/tuweni/ssz/signedBlobTransaction.hex'
      ])
      return list
    }
  }
  spotlessCheck.dependsOn rat
}

//////
// Gradle script formatting

spotless {
  groovyGradle {
    target '**/*.gradle'
    greclipse().configFile(rootProject.file('gradle/greclipse-gradle-tuweni-style.properties'))
    endWithNewline()
  }
}

task integrationTest(type: Test) {
}

subprojects {

  //////
  // Source formatting

  apply plugin: 'com.diffplug.spotless'
  spotless {
    java {
      target '**/*.java'
      targetExclude '**/generated-src/**/*.*'
      targetExclude '**/SECP256K1.java'
      removeUnusedImports()
      licenseHeaderFile rootProject.file('gradle/spotless.license.txt')
      eclipse().configFile(rootProject.file('gradle/eclipse-java-tuweni-style.xml'))
      importOrder 'org.apache', 'java', ''
      endWithNewline()
    }
    kotlin {
      licenseHeaderFile rootProject.file('gradle/spotless.license.txt')
      ktlint("0.47.1").editorConfigOverride(['indent_size': '2', 'continuation_indent_size' : '2'])
      endWithNewline()
    }
  }

  plugins.withId('java', { _ ->
    sourceSets {
      integrationTest {
        compileClasspath += sourceSets.main.output + sourceSets.test.output
        runtimeClasspath += sourceSets.main.output + sourceSets.test.output
      }
    }

    configurations {
      integrationTestImplementation.extendsFrom testImplementation
      integrationTestRuntime.extendsFrom testRuntime
    }
    dependencies {
      integrationTestRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine'
      integrationTestRuntimeOnly 'ch.qos.logback:logback-classic'
    }

    task integrationTest(type: Test) {
      description = 'Runs integration tests.'
      group = 'verification'

      testClassesDirs = sourceSets.integrationTest.output.classesDirs
      classpath = sourceSets.integrationTest.runtimeClasspath

      useJUnitPlatform() { includeEngines 'spek', 'junit-jupiter' }
      timeout = Duration.ofMinutes(30)
    }

    rootProject.integrationTest.dependsOn integrationTest
  })

  //////
  // Parallel build execution

  tasks.withType(Test) {
    // If GRADLE_MAX_TEST_FORKS is not set, use half the available processors
    maxParallelForks = (System.getenv('GRADLE_MAX_TEST_FORKS') ?:
      (Runtime.runtime.availableProcessors().intdiv(2) ?: 1)).toInteger()
  }

  tasks.withType(JavaCompile) {
    options.fork = true
    options.incremental = true
    options.encoding = 'UTF-8'
  }

  task allDependencies(type: DependencyReportTask) {}
}

//////
// Project defaults

allprojects {
  apply plugin: 'java-library'
  apply plugin: 'kotlin'
  apply plugin: 'io.spring.dependency-management'
  apply plugin: 'jacoco'
  apply plugin: 'org.jetbrains.dokka'
  apply plugin: 'com.jfrog.bintray'
  apply plugin: 'net.ltgt.errorprone'
  apply plugin: 'maven-publish'
  apply plugin: 'signing'
  apply from: "${rootDir}/dependency-versions.gradle"

  bootJar {
    enabled = false
  }

  jar {
    enabled = true
  }

  version = buildVersion

  repositories {
    mavenCentral()
    maven {
      url = 'https://hyperledger.jfrog.io/artifactory/besu-maven/'
    }
  }


  //////
  // Compiler arguments

  sourceCompatibility = '1.11'
  targetCompatibility = '1.11'

  dependencies {
    errorprone 'com.google.errorprone:error_prone_core'
  }

  tasks.withType(AbstractArchiveTask) {
    preserveFileTimestamps = false
    reproducibleFileOrder = true
  }

  tasks.withType(JavaCompile) {
    options.compilerArgs += [
      '-Xlint:unchecked',
      '-Xlint:cast',
      '-Xlint:rawtypes',
      '-Xlint:overloads',
      '-Xlint:divzero',
      '-Xlint:finally',
      '-Xlint:static',
      '-Werror'
    ]

    options.errorprone {
      excludedPaths = '.*/generated-src/.*'
      check('FutureReturnValueIgnored', net.ltgt.gradle.errorprone.CheckSeverity.OFF)
      check('UnnecessaryParentheses', net.ltgt.gradle.errorprone.CheckSeverity.OFF)

      disableWarningsInGeneratedCode = true
    }
  }

  tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all {
    kotlinOptions {
      jvmTarget = "11"
      allWarningsAsErrors = true
      freeCompilerArgs = [
        '-Xjsr305=strict',
        '-Xjvm-default=all'
      ]
    }
  }


  //////
  // Use JUnit5 for testing

  test {
    useJUnitPlatform() { includeEngines 'spek', 'junit-jupiter' }
    timeout = Duration.ofMinutes(30)
  }

  jacocoTestReport {
    reports {
      xml.enabled true
      html.enabled true
    }
    getExecutionData().setFrom(fileTree(buildDir).include("/jacoco/*.exec"))
  }


  //////
  // Artifact locations

  jar {
    destinationDirectory = file("${rootProject.buildDir}/libs")
  }

  task sourcesJar(type: Jar, dependsOn: classes) {
    destinationDirectory = file("${rootProject.buildDir}/src")
    classifier = 'sources'
    from sourceSets.main.allSource
  }

  //////
  // Packaging and deployment

  tasks.withType(Jar) {
    def moduleName = rootProject.name
    if (rootProject == project) {
      archiveBaseName = project.name
    } else {
      archiveBaseName = rootProject.name + '-' + project.name
      moduleName += ".${project.name}"
    }
    manifest {
      attributes('Implementation-Title': archiveBaseName,
      'Implementation-Version': project.version,
      'Automatic-Module-Name': moduleName.replaceAll("-","_"))
    }
    from(rootProject.projectDir) {
      include 'DISCLAIMER'
      include 'LICENSE'
      include 'NOTICE'
      into 'META-INF'
    }
  }

  if (project.name != 'eth-reference-tests' && project.name != 'eth2-reference-tests' && project.name != 'dist') {

    artifacts {
      archives sourcesJar
      archives jar
    }

    signing {
      useGpgCmd()
      sign configurations.archives
    }

    publishing {
      repositories {
        maven {
          def isRelease = buildVersion.endsWith('SNAPSHOT')
          def releasesRepoUrl = "https://repository.apache.org/service/local/staging/deploy/maven2"
          def snapshotsRepoUrl = "https://repository.apache.org/content/repositories/snapshots"
          url = isRelease ? snapshotsRepoUrl : releasesRepoUrl

          def settingsXml = new File(System.getProperty('user.home'), '.m2/settings.xml')

          def credentialsFound = false;
          if (settingsXml.exists()) {
            project.logger.info('Reading .m2/settings.xml')
            def serverId = (project.properties['distMgmtServerId'] ?: isRelease
              ? 'apache.releases.https' : 'apache.snapshots.https')
            def m2SettingCreds = new XmlSlurper().parse(settingsXml).servers.server.find { server -> serverId.equals(server.id.text()) }
            if (m2SettingCreds) {
              project.logger.info('Found matching credentials from .m2/settings.xml')
              credentials {
                username m2SettingCreds.username.text()
                password m2SettingCreds.password.text()
              }
              credentialsFound = true
            }
          }

          if (!credentialsFound) {
            project.logger.info('Reading credentials from environment')
            project.logger.info('Username ' + asfNexusUsername)
            credentials {
              username asfNexusUsername
              password asfNexusPassword
            }
          }
        }
      }
      publications {
        MavenDeployment(MavenPublication) { publication ->
          if (project != rootProject) {
            from components.java
            artifact sourcesJar { classifier 'sources' }
          }
          groupId 'org.apache.tuweni'
          artifactId 'tuweni-' + project.name
          version project.version

          versionMapping {
            usage('java-runtime'){
              fromResolutionResult()
            }
            usage('java-api'){
              fromResolutionResult()
            }
          }

          pom {
            name = project.name
            afterEvaluate { description = project.description }
            url = 'https://github.com/apache/incubator-tuweni'
            licenses {
              license {
                name = "The Apache License, Version 2.0"
                url = "http://www.apache.org/licenses/LICENSE-2.0.txt"
              }
            }
            scm {
              connection = 'scm:https://github.com/apache/incubator-tuweni.git'
              developerConnection = 'scm:git@github.com:apache/incubator-tuweni.git'
              url = 'https://github.com/apache/incubator-tuweni'
            }
            developers {
              developer {
                name = 'Antoine Toulme'
                email = 'antoine@lunar-ocean.com'
                organization = 'Apache'
                organizationUrl = 'https://tuweni.apache.org'
              }
              developer {
                name = 'Jonny Rhea'
                organization = 'Apache'
                organizationUrl = 'https://tuweni.apache.org'
              }
              developer {
                name = 'Steven J Schroeder'
                email = 'sjs@apache.org'
                organization = 'Apache'
                organizationUrl = 'https://tuweni.apache.org'
              }
            }
            issueManagement {
              system = "github"
              url = "https://www.github.com/apache/incubator-tuweni/issues"
            }
          }

          pom.withXml {
            // use inline versions rather than pom dependency management
            asNode().remove(asNode().dependencyManagement[0])
            if (asNode().dependencies[0]) {
              asNode().remove(asNode().dependencies[0])
            }

            def dependenciesNode = asNode().appendNode('dependencies')
            def addDependencyNode = { dep, optional, scope ->
              def dependencyNode = dependenciesNode.appendNode('dependency')
              if (dep instanceof ProjectDependency) {
                dependencyNode.appendNode('groupId', 'org.apache.tuweni')
                dependencyNode.appendNode('artifactId', rootProject.name + '-' + dep.name)
                dependencyNode.appendNode('version', dep.version)
              } else {
                dependencyNode.appendNode('groupId', dep.group)
                dependencyNode.appendNode('artifactId', dep.name)
                if (dep.version != null) {
                  dependencyNode.appendNode('version', dep.version)
                } else {
                  def version = dependencyManagement.managedVersions["$dep.group:$dep.name"]
                  dependencyNode.appendNode('version', version)
                }
              }
              if (optional) {
                dependencyNode.appendNode('optional', 'true')
              }
              if (scope != null) {
                dependencyNode.appendNode('scope', scope)
              }

              def ers = dep.excludeRules
              if (!ers.empty) {
                def exclusionsNode = dependencyNode.appendNode('exclusions')
                ers.each { er ->
                  def exclusionNode = exclusionsNode.appendNode('exclusion')
                  exclusionNode.appendNode('groupId', er.group)
                  exclusionNode.appendNode('artifactId', er.module)
                }
              }
            }
            configurations.implementation.allDependencies.each { dep ->
              addDependencyNode(dep, false, null)
            }
            configurations.compileOnly.allDependencies.each { dep ->
              addDependencyNode(dep, true, null)
            }
            configurations.runtimeOnly.allDependencies.each { dep ->
              addDependencyNode(dep, false, 'runtime')
            }

            if (System.getenv('ENABLE_SIGNING') == 'true') {
              def pomFile = file("${project.buildDir}/generated-pom.xml")
              writeTo(pomFile)
              def pomAscFile = signing.sign(pomFile).signatureFiles[0]
              artifact(pomAscFile) {
                classifier = null
                extension = 'pom.asc'
              }
            }
          }

          if (System.getenv('ENABLE_SIGNING') == 'true') {
            // create the signed artifacts
            tasks.signArchives.signatureFiles.each {
              artifact(it) {
                def matcher = it.file =~ /-(sources|javadoc)\.jar\.asc$/
                if (matcher.find()) {
                  classifier = matcher.group(1)
                } else {
                  classifier = null
                }
                extension = 'jar.asc'
              }
            }
          }
        }
      }
    }

    tasks.withType(Sign) {
      onlyIf {
        System.getenv('ENABLE_SIGNING') == 'true'
      }
    }

    tasks.withType(GenerateModuleMetadata) {
      enabled = false
    }

    model {
      tasks.generatePomFileForMavenDeploymentPublication {
        destination = file("$buildDir/generated-pom.xml")
      }
      tasks.publishMavenDeploymentPublicationToMavenLocal { dependsOn project.tasks.signArchives }
    }

    bintray {
      publications = ['MavenDeployment']
      user = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : 'invalid'
      key = project.hasProperty('bintrayKey') ? project.property('bintrayKey') : 'invalid'
      pkg {
        repo = project.hasProperty('bintrayRepo') ? project.property('bintrayRepo') : 'invalid'
        name = project.hasProperty('bintrayName') ? project.property('bintrayName') : 'invalid'
        userOrg = project.hasProperty('bintrayOrg') ? project.property('bintrayOrg') : 'invalid'
        licenses = ['Apache-2.0']
        vcsUrl = 'https://github.com/apache/incubator-tuweni.git'
      }
    }
  }
}


//////
// Configure root project as a virtual package that depends on all components

dependencies {
  subprojects.findAll {
    !it.name.contains('reference-tests')
  }.each { p ->
    switch (p.name) {
      case 'crypto':
        implementation(p) {
          exclude group: 'com.github.jnr', module: 'jnr-ffi'
        }
        break
      default:
        implementation p
        break
    }
  }
}

apply plugin: 'kotlin'
apply plugin: 'org.jetbrains.dokka'

tasks.dokkaHtmlMultiModule.configure {
  outputDirectory.set(file("$rootProject.buildDir/docs"))
}

tasks.register("docs") {
  dependsOn dokkaHtmlMultiModule
}

dokkaHtml {
  moduleName = 'tuweni'

  dokkaSourceSets {
    configureEach {

      // Use to include or exclude non public members.
      includeNonPublic.set(false)

      // Do not output deprecated members. Applies globally, can be overridden by packageOptions
      skipDeprecated.set(false)

      // Emit warnings about not documented members. Applies globally, also can be overridden by packageOptions
      reportUndocumented.set(true)

      // Do not create index pages for empty packages
      skipEmptyPackages.set(true)

      // Platform used for code analysis. See the "Platforms" section of this readme
      platform.set(org.jetbrains.dokka.Platform.jvm)

      // List of files with module and package documentation
      // https://kotlinlang.org/docs/reference/kotlin-doc.html#module-and-package-documentation
      includes.from('docs.md')
    }
  }
}

project.task("checkNotice") {
  def lines = file("NOTICE").readLines()
  def expected = "Copyright 2019-${LocalDate.now().getYear()} The Apache Software Foundation"
  for (line in lines) {
    if (expected == line) {
      return
    }
  }
  throw new GradleException('NOTICE file is not up-to-date')
}
