/*
 * Copyright 2018 ConsenSys AG.
 *
 * Licensed 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 net.ltgt.gradle.errorprone.CheckSeverity

import java.text.SimpleDateFormat

plugins {
  id 'com.diffplug.gradle.spotless' version '3.23.1'
  id 'com.jfrog.bintray' version '1.8.4'
  id 'com.github.ben-manes.versions' version '0.21.0'
  id 'com.github.hierynomus.license' version '0.15.0'
  id 'io.spring.dependency-management' version '1.0.8.RELEASE'
  id 'me.champeau.gradle.jmh' version '0.4.8' apply false
  id 'net.ltgt.errorprone' version '0.8.1'
  id 'net.researchgate.release' version '2.8.1'
}

if (!JavaVersion.current().java11Compatible) {
  throw new GradleException("Java 11 or later is required to build Pantheon.\n" +
  "  Detected version ${JavaVersion.current()}")
}

group = 'tech.pegasys.pantheon'

defaultTasks 'build', 'checkLicenses', 'javadoc'

def buildAliases = ['dev': [
    'spotlessApply',
    'build',
    'checkLicenses',
    'javadoc'
  ]]

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

// Gets a integer command argument, passed with -Pname=x, or the defaut if not provided.
def _intCmdArg(name, defaultValue) {
  return project.hasProperty(name) ? project.property(name) as int : defaultValue
}

def _intCmdArg(name) {
  return _intCmdArg(name, null)
}

def _strListCmdArg(name, defaultValue) {
  if (!project.hasProperty(name))
    return defaultValue

  return ((String) project.property(name)).tokenize(',')
}

def _strListCmdArg(name) {
  return _strListCmdArg(name, null)
}

apply plugin: 'com.jfrog.bintray'

def bintrayUser = project.hasProperty('bintrayUser') ? project.property('bintrayUser') : System.getenv('BINTRAY_USER')
def bintrayKey = project.hasProperty('bintrayApiKey') ? project.property('bintrayApiKey') : System.getenv('BINTRAY_KEY')
def bintrayPackage = bintray.pkg {
  repo = 'pegasys-repo'
  name = 'pantheon'
  userOrg = 'consensys'
  licenses = ['Apache-2.0']
  websiteUrl = 'https://github.com/PegaSysEng/pantheon'
  issueTrackerUrl = 'https://github.com/PegaSysEng/pantheon/issues'
  vcsUrl = 'https://github.com/PegaSysEng/pantheon.git'

  version {
    name = project.version
    released = new Date()
  }
}

allprojects {
  apply plugin: 'java-library'
  apply plugin: 'io.spring.dependency-management'
  apply plugin: 'jacoco'
  apply plugin: 'net.ltgt.errorprone'
  apply from: "${rootDir}/gradle/versions.gradle"
  apply from: "${rootDir}/gradle/check-licenses.gradle"

  version = rootProject.version

  jacoco {
    toolVersion = '0.8.2'
    if (project.tasks.findByName('referenceTests')) {
      applyTo referenceTests
    }
  }

  task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
  }

  task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
  }

  sourceCompatibility = 11
  targetCompatibility = 11

  repositories {
    if ("$System.env.JENKINS_URL" == 'https://jenkins.pegasys.tech/') {
      maven { url "https://nexus.int.pegasys.tech/repository/consensys-pegasys/" }
      maven { url "https://nexus.int.pegasys.tech/repository/jcenter/" }
    } else {
      jcenter()
      mavenCentral()
      mavenLocal()
      maven { url "https://consensys.bintray.com/pegasys-repo" }
    }
  }

  dependencies { errorprone("com.google.errorprone:error_prone_core") }

  apply plugin: 'com.diffplug.gradle.spotless'
  spotless {
    java {
      // This path needs to be relative to each project
      target fileTree('.') {
        include '**/src/*/java/**/*.java'
        exclude '**/generalstate/GeneralStateReferenceTest*.java'
        exclude '**/generalstate/GeneralStateRegressionReferenceTest*.java'
        exclude '**/blockchain/BlockchainReferenceTest*.java'
        exclude '**/pantheon/PantheonInfo.java'
        exclude '**/.gradle/**'
      }
      removeUnusedImports()
      googleJavaFormat('1.7')
      importOrder 'tech.pegasys', 'java', ''
      trimTrailingWhitespace()
      endWithNewline()
      licenseHeaderFile "${rootDir}/gradle/spotless.java.license"
    }
    groovyGradle {
      target '*.gradle'
      greclipse().configFile(rootProject.file('gradle/formatter.properties'))
      endWithNewline()
      paddedCell()
    }

    // Below this line are currently only license header tasks
    format 'groovy', {
      target '**/src/*/grovy/**/*.groovy'
      licenseHeaderFile "${rootDir}/gradle/spotless.java.license", 'import'
    }

    // Currently disabled due to referencetest issues

    //  format 'bash', {
    //    target fileTree('.') {
    //      include '**/*.sh'
    //     exclude '**/ansible/**'
    //    }
    //   licenseHeaderFile "${rootDir}/gradle/spotless.bash.license", '#!/bin'
    //  }
    //  format 'sol', {
    //    target fileTree('.') { include '**/*.sol' }
    //   licenseHeaderFile "${rootDir}/gradle/spotless.java.license", '^(pragma|contract)'
    //  }
  }

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

    options.errorprone {
      excludedPaths = '.*/(generated/*.*|.*ReferenceTest_.*)'

      // Our equals need to be symmetric, this checker doesn't respect that.
      check('EqualsGetClass', CheckSeverity.OFF)
      // We like to use futures with no return values.
      check('FutureReturnValueIgnored', CheckSeverity.OFF)
      // We use the JSR-305 annotations instead of the Google annotations.
      check('ImmutableEnumChecker', CheckSeverity.OFF)
      // This is a style check instead of an error-prone pattern.
      check('UnnecessaryParentheses', CheckSeverity.OFF)
      // Lazy impl causes excess CPU usage O(n) of non-final fiield when it should be O(1).
      check('FieldCanBeFinal', CheckSeverity.OFF)

      // This check is broken in Java 12.  See https://github.com/google/error-prone/issues/1257
      if (JavaVersion.current().java12Compatible) {
        check('Finally', CheckSeverity.OFF)
      }

      check('InsecureCryptoUsage', CheckSeverity.WARN)
      check('WildcardImport', CheckSeverity.WARN)
    }

    options.encoding = 'UTF-8'
  }

  /*
   * Pass some system properties provided on the gradle command line to test executions for
   * convenience.
   *
   * The properties passed are:
   * - 'test.ethereum.include': allows to run a single Ethereum reference tests. For instance,
   *   running a single general state test can be done with:
   *    ./gradlew :ethereum:tech.pegasys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.include=callcodecallcallcode_101-Frontier
   *   The meaning being that will be run only the tests for which the value passed as "include"
   *   (which can be a java pattern) matches parts of the test name. Knowing that tests names for
   *   reference tests are of the form:
   *     <name>(-<milestone>([<variant>])?)?
   *   where <name> is the test name as defined in the json file (usually the name of the json file
   *   as well), <milestone> is the Ethereum milestone tested (not all test use it) and <variant>
   *   is only use in some general state tests where for the same json file and same milestone,
   *   multiple variant of that test are run. The variant is a simple number.
   * - 'test.ethereum.state.eip': for general state tests, allows to only run tests for the
   *   milestone specified by this value. So for instance,
   *    ./gradlew :ethereum:tech.pegasys.pantheon.ethereum.vm:test -Dtest.single=GeneralStateTest -Dtest.ethereum.state.eip=Frontier
   *   only run general state tests for Frontier. Note that this behavior could be achieved as well
   *   with the 'include' option above since it is a pattern, but this is a slightly more convenient
   *   option.
   * - 'root.log.level' and 'evm.log.level': allow to control the log level used during the tests.
   */
  test {
    jvmArgs = [
      '-Xmx4g',
      '-XX:-UseGCOverheadLimit',
      // Mockito and jackson-databind do some strange reflection during tests.
      // This suppresses an illegal access warning.
      '--add-opens',
      'java.base/java.util=ALL-UNNAMED',
      '--add-opens',
      'java.base/java.util.concurrent=ALL-UNNAMED'
    ]
    Set toImport = [
      'test.ethereum.include',
      'test.ethereum.state.eip',
      'root.log.level',
      'evm.log.level'
    ]
    for (String name : toImport) {
      if (System.getProperty(name) != null) {
        systemProperty name, System.getProperty(name)
      }
    }
  }

  javadoc {
    options.addStringOption('Xdoclint:all', '-quiet')
    options.addStringOption('Xwerror', '-html5')
    options.encoding = 'UTF-8'
  }

}

task deploy() {}


subprojects {

  if (file('src/main/java').directory) {
    apply plugin: 'com.jfrog.bintray'
    apply plugin: 'maven-publish'

    publishing {
      publications {
        mavenJava(MavenPublication) {
          groupId "tech.pegasys.pantheon.internal"
          version "${project.version}"
          from components.java
          artifact sourcesJar
          artifact javadocJar
          versionMapping {
            usage('java-api') { fromResolutionOf('runtimeClasspath') }
            usage('java-runtime') { fromResolutionResult() }
          }
          pom {
            name = "Pantheon - ${project.name}"
            url = 'http://github.com/PegaSysEng/pantheon'
            licenses {
              license {
                name = 'The Apache License, Version 2.0'
                url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
              }
            }
            scm {
              connection = 'scm:git:git://github.com/PegaSysEng/pantheon.git'
              developerConnection = 'scm:git:ssh://github.com/PegaSysEng/pantheon.git'
              url = 'https://github.com/PegaSysEng/pantheon'
            }
          }
        }
      }
    }

    bintray {
      user = bintrayUser
      key = bintrayKey

      publications = ['mavenJava']
      override = version.endsWith('SNAPSHOT')

      publish = true

      pkg = bintrayPackage
    }
  }

  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
  }

  sourceSets {
    // test-support can be consumed as a library by other projects in their tests
    testSupport {
      java {
        compileClasspath += main.output
        runtimeClasspath += main.output
        srcDir file('src/test-support/java')
      }
      resources.srcDir file('src/test-support/resources')
    }
    integrationTest {
      java {
        compileClasspath += main.output
        runtimeClasspath += main.output
        srcDir file('src/integration-test/java')
      }
      resources.srcDir file('src/integration-test/resources')
    }
  }

  configurations {
    testSupportImplementation.extendsFrom implementation
    integrationTestImplementation.extendsFrom implementation
    testSupportArtifacts
  }

  task testSupportJar(type: Jar) {
    baseName = "${project.name}-support-test"
    from sourceSets.testSupport.output
  }

  dependencies {
    testImplementation sourceSets.testSupport.output
    integrationTestImplementation sourceSets.testSupport.output
  }

  task integrationTest(type: Test, dependsOn: ["compileTestJava"]) {
    group = "verification"
    description = "Runs the Pantheon integration tests"

    testClassesDirs = sourceSets.integrationTest.output.classesDirs
    classpath = sourceSets.integrationTest.runtimeClasspath
    outputs.upToDateWhen { false }
  }

  if (file('src/jmh').directory) {
    apply plugin: 'me.champeau.gradle.jmh'

    jmh {
      // Allows to control JMH execution directly from the command line. I typical execution may look
      // like:
      //    gradle jmh -Pf=2 -Pwi=3 -Pi=5 -Pinclude=MyBench
      // which will run 2 forks with 3 warmup iterations and 5 normal ones for each, and will only
      // run the benchmark matching 'MyBench' (a regexp).
      warmupForks = _intCmdArg('wf')
      warmupIterations = _intCmdArg('wi')
      fork = _intCmdArg('f')
      iterations = _intCmdArg('i')
      benchmarkMode = _strListCmdArg('bm')
      include = _strListCmdArg('include', [''])
      humanOutputFile = project.file("${project.buildDir}/reports/jmh/results.txt")
      resultFormat = 'JSON'
    }

    dependencies { jmh 'org.apache.logging.log4j:log4j-api' }
  }
}

jar { enabled = false }

apply plugin: 'application'
mainClassName = 'tech.pegasys.pantheon.Pantheon'
applicationDefaultJvmArgs = [
  '-Dvertx.disableFileCPResolving=true',
  '-Dpantheon.home=PANTHEON_HOME',
  // We shutdown log4j ourselves, as otherwise this shutdown hook runs before our own and whatever
  // happens during shutdown is not logged.
  '-Dlog4j.shutdownHookEnabled=false',
  // Bouncy Castle needs access to sun.security.provider, which is not open by default.
  // This suppresses an illegal access warning.
  '--add-opens',
  'java.base/sun.security.provider=ALL-UNNAMED'
]

run {
  args project.hasProperty("pantheon.run.args") ? project.property("pantheon.run.args").toString().split("\\s+") : []
  doFirst {
    applicationDefaultJvmArgs = applicationDefaultJvmArgs.collect {
      it.replace('PANTHEON_HOME', "$buildDir/pantheon")
    }
  }
}

startScripts {

  def shortenWindowsClasspath = { line ->
    line = line.replaceAll(/^set CLASSPATH=.*$/, "set CLASSPATH=%APP_HOME%/lib/*")
  }

  doLast {
    unixScript.text = unixScript.text.replace('PANTHEON_HOME', '\$APP_HOME')
    windowsScript.text = windowsScript.text.replace('PANTHEON_HOME', '%~dp0..')

    // Prevent the error originating from the 8191 chars limit on Windows
    windowsScript.text =
      windowsScript
      .readLines()
      .collect(shortenWindowsClasspath)
      .join('\r\n')
  }
}

dependencies {
  compile project(':pantheon')
  errorprone 'com.google.errorprone:error_prone_core'
}

distributions {
  main {
    contents {
      from("./LICENSE") { into "." }
      from("build/reports/license/license-dependency.html") { into "." }
      from("./docs/GettingStartedBinaries.md") { into "." }
      from("./docs/DocsArchive0.8.0.html") { into "." }
    }
  }
}

installDist { dependsOn checkLicenses }

distTar {
  dependsOn checkLicenses
  doFirst {
    delete fileTree(dir: 'build/distributions', include: '*.tar.gz')
  }
  compression = Compression.GZIP
  extension = 'tar.gz'
}

distZip {
  dependsOn checkLicenses
  doFirst {
    delete fileTree(dir: 'build/distributions', include: '*.zip')
  }
}

// rename the top level dir from pantheon-<version> to pantheon and this makes it really
// simple for use in docker
tasks.register("dockerDistUntar") {
  dependsOn distTar
  dependsOn distZip
  def dockerBuildDir = "build/docker-pantheon/"
  def distTarFile = distTar.outputs.files.singleFile
  def distTarFileName = distTar.outputs.files.singleFile.name.replace(".tar.gz", "")

  doFirst {
    new File(dockerBuildDir).mkdir()
    copy {
      from tarTree(distTarFile)
      into(dockerBuildDir)
    }
    file("${dockerBuildDir}/${distTarFileName}").renameTo("${dockerBuildDir}/pantheon")
  }
}

task distDocker(type: Exec) {
  dependsOn dockerDistUntar
  def dockerBuildVersion = project.hasProperty('release.releaseVersion') ? project.property('release.releaseVersion') : "${rootProject.version}"
  def image = project.hasProperty('release.releaseVersion') ? "pegasyseng/pantheon:" + project.property('release.releaseVersion') : "pegasyseng/pantheon:develop"
  def dockerBuildDir = "build/docker-pantheon/"
  workingDir "${dockerBuildDir}"

  doFirst {
    copy {
      from file("${projectDir}/docker/Dockerfile")
      into(workingDir)
    }
  }

  executable "sh"
  args "-c", "docker build --build-arg BUILD_DATE=${buildTime()} --build-arg VERSION=${dockerBuildVersion} --build-arg VCS_REF=${getCheckedOutGitCommitHash()} -t ${image} ."
}

task jacocoRootReport(type: org.gradle.testing.jacoco.tasks.JacocoReport) {
  additionalSourceDirs.from files(subprojects.sourceSets.main.allSource.srcDirs)
  sourceDirectories.from files(subprojects.sourceSets.main.allSource.srcDirs)
  classDirectories.from files(subprojects.sourceSets.main.output)
  executionData.from fileTree(dir: '.', includes: ['**/jacoco/*.exec'])
  reports {
    xml.enabled true
    csv.enabled true
    html.destination file("build/reports/jacocoHtml")
  }
  onlyIf = { true }
}

configurations { annotationProcessor }

// Prevent errorprone-checks being dependent upon errorprone-checks!
// However, ensure all subprojects comply with the custom rules.
configure(subprojects.findAll { it.name != 'errorprone-checks' }) {
  dependencies { annotationProcessor project(":errorprone-checks") }

  tasks.withType(JavaCompile) {
    options.annotationProcessorPath = configurations.annotationProcessor
  }
}

// http://label-schema.org/rc1/
// using the RFC3339 format "2016-04-12T23:20:50.52Z"
def buildTime() {
  def df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
  df.setTimeZone(TimeZone.getTimeZone("UTC"))
  return df.format(new Date())
}

// Takes the version, and if -SNAPSHOT is part of it replaces SNAPSHOT
// with the git commit version.
def calculateVersion() {
  String version = rootProject.version
  if (version.endsWith("-SNAPSHOT")) {
    version = version.replace("-SNAPSHOT", "-dev-" + getCheckedOutGitCommitHash())
  }
  return version
}

def getCheckedOutGitCommitHash() {
  def gitFolder = "$projectDir/.git/"
  if (!file(gitFolder).isDirectory()) {
    // We are in a submodule.  The file's contents are `gitdir: <gitFolder>\n`.
    // Read the file, cut off the front, and trim the whitespace.
    gitFolder = file(gitFolder).text.substring(8).trim() + "/"
  }
  def takeFromHash = 8
  /*
   * '.git/HEAD' contains either
   *      in case of detached head: the currently checked out commit hash
   *      otherwise: a reference to a file containing the current commit hash
   */
  def head = new File(gitFolder + "HEAD").text.split(":") // .git/HEAD
  def isCommit = head.length == 1 // e5a7c79edabbf7dd39888442df081b1c9d8e88fd

  if (isCommit) return head[0].trim().take(takeFromHash) // e5a7c79edabb

  def refHead = new File(gitFolder + head[1].trim()) // .git/refs/heads/master
  refHead.text.trim().take takeFromHash
}

apply plugin: 'net.researchgate.release'

task releaseIntegrationTest(type: Test) {
  for (TaskContainer taskList : subprojects.tasks) {
    def subProjectIntegrationTask = taskList.findByName('integrationTest')

    if (subProjectIntegrationTask != null) {
      dependsOn subProjectIntegrationTask
    }
  }
}

task releaseReferenceTest(type: Test, dependsOn: [
  ':ethereum:core:referenceTests',
  ':ethereum:rlp:referenceTests',
  ':ethereum:trie:referenceTests'
]) {
}

task releaseAcceptanceTest(type: Test, dependsOn: ':acceptance-tests:acceptanceTest') {}

tasks.register("verifyDistributions") {
  dependsOn distTar
  dependsOn distZip
  def distTarFile = distTar.outputs.files.singleFile
  def distZipFile = distZip.outputs.files.singleFile
  def minDistributionSize = 20000000

  // Sanity check the distributions by checking they are at least a reasonable size
  doFirst {
    if (distTarFile.length() < minDistributionSize) {
      throw new GradleException("Distribution tar is suspiciously small: " + distTarFile.length() + " bytes")
    }
    if (distZipFile.length() < minDistributionSize) {
      throw new GradleException("Distribution zip is suspiciously small: " + distZipFile.length() + " bytes")
    }
  }
}


release {
  preTagCommitMessage = '[Gradle Release Plugin] - pre tag commit: '
  tagCommitMessage = '[Gradle Release Plugin] - creating tag: '
  newVersionCommitMessage = '[Gradle Release Plugin] - new version commit: '
  buildTasks = [
    'build',
    'releaseIntegrationTest',
    'releaseAcceptanceTest',
    'releaseReferenceTest',
    'checkLicenses',
    'javadoc'
  ]

  git {
    requireBranch = project.hasProperty('release.branch') ? project.property('release.branch') : 'master'
  }
}

bintray {
  user = bintrayUser
  key = bintrayKey

  filesSpec {
    from distTar.destinationDir.path
    from distZip.destinationDir.path
    into '.'
  }

  publish = true
  override = version.endsWith('SNAPSHOT')

  pkg = bintrayPackage
}

build.dependsOn verifyDistributions
bintrayUpload.dependsOn verifyDistributions
afterReleaseBuild.dependsOn bintrayUpload
bintrayUpload.mustRunAfter(distTar)
bintrayUpload.mustRunAfter(distZip)
