/* Convention for properties.  Read from gradle.properties, use lower_case_underlines for property names.
 * For properties set within build.gradle, use camelCaseNoSpace.
 */
import org.apache.tools.ant.filters.ReplaceTokens
import org.gradle.internal.os.OperatingSystem
import org.gradle.plugins.ide.internal.generator.PropertiesPersistableConfigurationObject
import org.gradle.api.internal.PropertiesTransformer
import org.gradle.util.ConfigureUtil
import org.gradle.plugins.ide.eclipse.model.Output
import org.gradle.plugins.ide.eclipse.model.Library
import java.security.MessageDigest
import groovy.transform.ExternalizeMethods
import groovy.util.XmlParser
import groovy.xml.XmlUtil
import groovy.json.JsonBuilder
import com.vladsch.flexmark.util.ast.Node
import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser
import com.vladsch.flexmark.util.data.MutableDataSet
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.ext.tables.TablesExtension
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughExtension
import com.vladsch.flexmark.ext.autolink.AutolinkExtension
import com.vladsch.flexmark.ext.anchorlink.AnchorLinkExtension
import com.vladsch.flexmark.ext.toc.TocExtension
import com.google.common.hash.HashCode
import com.google.common.hash.Hashing
import com.google.common.io.Files
import org.jsoup.Jsoup
import org.jsoup.nodes.Element

buildscript {
  repositories {
    mavenCentral()
    mavenLocal()
  }
  dependencies {
    classpath "com.vladsch.flexmark:flexmark-all:0.62.0"
    classpath "org.jsoup:jsoup:1.14.3"
  }
}


plugins {
  id 'java'
  id 'application'
  id 'eclipse'
  id "com.diffplug.gradle.spotless" version "3.28.0"
  id 'com.github.johnrengelman.shadow' version '4.0.3'
  id 'com.install4j.gradle' version '9.0.6'
  id 'com.dorongold.task-tree' version '2.1.0' // only needed to display task dependency tree with  gradle task1 [task2 ...] taskTree
  id 'com.palantir.git-version' version '0.13.0' apply false
}

repositories {
  jcenter()
  mavenCentral()
  mavenLocal()
}



// in ext the values are cast to Object. Ensure string values are cast as String (and not GStringImpl) for later use
def string(Object o) {
  return o == null ? "" : o.toString()
}

def overrideProperties(String propsFileName, boolean output = false) {
  if (propsFileName == null) {
    return
  }
  def propsFile = file(propsFileName)
  if (propsFile != null && propsFile.exists()) {
    println("Using properties from file '${propsFileName}'")
    try {
      def p = new Properties()
      def localPropsFIS = new FileInputStream(propsFile)
      p.load(localPropsFIS)
      localPropsFIS.close()
      p.each {
        key, val -> 
          def oldval
          if (project.hasProperty(key)) {
            oldval = project.findProperty(key)
            project.setProperty(key, val)
            if (output) {
              println("Overriding property '${key}' ('${oldval}') with ${file(propsFile).getName()} value '${val}'")
            }
          } else {
            ext.setProperty(key, val)
            if (output) {
              println("Setting ext property '${key}' with ${file(propsFile).getName()}s value '${val}'")
            }
          }
      }
    } catch (Exception e) {
      println("Exception reading local.properties")
      e.printStackTrace()
    }
  }
}

ext {
  jalviewDirAbsolutePath = file(jalviewDir).getAbsolutePath()
  jalviewDirRelativePath = jalviewDir
  date = new Date()

  getdownChannelName = CHANNEL.toLowerCase()
  // default to "default". Currently only has different cosmetics for "develop", "release", "default"
  propertiesChannelName = ["develop", "release", "test-release", "jalviewjs", "jalviewjs-release" ].contains(getdownChannelName) ? getdownChannelName : "default"
  // Import channel_properties
  channelDir = string("${jalviewDir}/${channel_properties_dir}/${propertiesChannelName}")
  channelGradleProperties = string("${channelDir}/channel_gradle.properties")
  channelPropsFile = string("${channelDir}/${resource_dir}/${channel_props}")
  overrideProperties(channelGradleProperties, false)
  // local build environment properties
  // can be "projectDir/local.properties"
  overrideProperties("${projectDir}/local.properties", true)
  // or "../projectDir_local.properties"
  overrideProperties(projectDir.getParent() + "/" + projectDir.getName() + "_local.properties", true)

  ////  
  // Import releaseProps from the RELEASE file
  // or a file specified via JALVIEW_RELEASE_FILE if defined
  // Expect jalview.version and target release branch in jalview.release        
  releaseProps = new Properties();
  def releasePropFile = findProperty("JALVIEW_RELEASE_FILE");
  def defaultReleasePropFile = "${jalviewDirAbsolutePath}/RELEASE";
  try {
    (new File(releasePropFile!=null ? releasePropFile : defaultReleasePropFile)).withInputStream { 
     releaseProps.load(it)
    }
  } catch (Exception fileLoadError) {
    throw new Error("Couldn't load release properties file "+(releasePropFile==null ? defaultReleasePropFile : "from custom location: releasePropFile"),fileLoadError);
  }
  ////
  // Set JALVIEW_VERSION if it is not already set
  if (findProperty("JALVIEW_VERSION")==null || "".equals(JALVIEW_VERSION)) {
    JALVIEW_VERSION = releaseProps.get("jalview.version")
  }
  println("JALVIEW_VERSION is set to '${JALVIEW_VERSION}'")
  
  // this property set when running Eclipse headlessly
  j2sHeadlessBuildProperty = string("net.sf.j2s.core.headlessbuild")
  // this property set by Eclipse
  eclipseApplicationProperty = string("eclipse.application")
  // CHECK IF RUNNING FROM WITHIN ECLIPSE
  def eclipseApplicationPropertyVal = System.properties[eclipseApplicationProperty]
  IN_ECLIPSE = eclipseApplicationPropertyVal != null && eclipseApplicationPropertyVal.startsWith("org.eclipse.ui.")
  // BUT WITHOUT THE HEADLESS BUILD PROPERTY SET
  if (System.properties[j2sHeadlessBuildProperty].equals("true")) {
    println("Setting IN_ECLIPSE to ${IN_ECLIPSE} as System.properties['${j2sHeadlessBuildProperty}'] == '${System.properties[j2sHeadlessBuildProperty]}'")
    IN_ECLIPSE = false
  }
  if (IN_ECLIPSE) {
    println("WITHIN ECLIPSE IDE")
  } else {
    println("HEADLESS BUILD")
  }
  
  J2S_ENABLED = (project.hasProperty('j2s.compiler.status') && project['j2s.compiler.status'] != null && project['j2s.compiler.status'] == "enable")
  if (J2S_ENABLED) {
    println("J2S ENABLED")
  } 
  /* *-/
  System.properties.sort { it.key }.each {
    key, val -> println("SYSTEM PROPERTY ${key}='${val}'")
  }
  /-* *-/
  if (false && IN_ECLIPSE) {
    jalviewDir = jalviewDirAbsolutePath
  }
  */

  // datestamp
  buildDate = new Date().format("yyyyMMdd")

  // essentials
  bareSourceDir = string(source_dir)
  sourceDir = string("${jalviewDir}/${bareSourceDir}")
  resourceDir = string("${jalviewDir}/${resource_dir}")
  bareTestSourceDir = string(test_source_dir)
  testDir = string("${jalviewDir}/${bareTestSourceDir}")

  classesDir = string("${jalviewDir}/${classes_dir}")

  // clover
  useClover = clover.equals("true")
  cloverBuildDir = "${buildDir}/clover"
  cloverInstrDir = file("${cloverBuildDir}/clover-instr")
  cloverClassesDir = file("${cloverBuildDir}/clover-classes")
  cloverReportDir = file("${buildDir}/reports/clover")
  cloverTestInstrDir = file("${cloverBuildDir}/clover-test-instr")
  cloverTestClassesDir = file("${cloverBuildDir}/clover-test-classes")
  //cloverTestClassesDir = cloverClassesDir
  cloverDb = string("${cloverBuildDir}/clover.db")

  testSourceDir = useClover ? cloverTestInstrDir : testDir
  testClassesDir = useClover ? cloverTestClassesDir : "${jalviewDir}/${test_output_dir}"

  getdownChannelDir = string("${getdown_website_dir}/${propertiesChannelName}")
  getdownAppBaseDir = string("${jalviewDir}/${getdownChannelDir}/${JAVA_VERSION}")
  getdownArchiveDir = string("${jalviewDir}/${getdown_archive_dir}")
  getdownFullArchiveDir = null
  getdownTextLines = []
  getdownLaunchJvl = null
  getdownVersionLaunchJvl = null
  buildDist = true
  buildProperties = null

  // the following values might be overridden by the CHANNEL switch
  getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
  getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
  getdownArchiveAppBase = getdown_archive_base
  getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher}")
  getdownAppDistDir = getdown_app_dir_alt
  getdownImagesDir = string("${jalviewDir}/${getdown_images_dir}")
  getdownSetAppBaseProperty = false // whether to pass the appbase and appdistdir to the application
  reportRsyncCommand = false
  jvlChannelName = CHANNEL.toLowerCase()
  install4jSuffix = CHANNEL.substring(0, 1).toUpperCase() + CHANNEL.substring(1).toLowerCase(); // BUILD -> Build
  install4jDMGDSStore = "${install4j_images_dir}/${install4j_dmg_ds_store}"
  install4jDMGBackgroundImage = "${install4j_images_dir}/${install4j_dmg_background}"
  install4jInstallerName = "${jalview_name} Non-Release Installer"
  install4jExecutableName = install4j_executable_name
  install4jExtraScheme = "jalviewx"
  install4jMacIconsFile = string("${install4j_images_dir}/${install4j_mac_icons_file}")
  install4jWindowsIconsFile = string("${install4j_images_dir}/${install4j_windows_icons_file}")
  install4jPngIconFile = string("${install4j_images_dir}/${install4j_png_icon_file}")
  install4jBackground = string("${install4j_images_dir}/${install4j_background}")
  install4jBuildDir = "${install4j_build_dir}/${JAVA_VERSION}"
  install4jCheckSums = true

  applicationName = "${jalview_name}"
  switch (CHANNEL) {

    case "BUILD":
    // TODO: get bamboo build artifact URL for getdown artifacts
    getdown_channel_base = bamboo_channelbase
    getdownChannelName = string("${bamboo_planKey}/${JAVA_VERSION}")
    getdownAppBase = string("${bamboo_channelbase}/${bamboo_planKey}${bamboo_getdown_channel_suffix}/${JAVA_VERSION}")
    jvlChannelName += "_${getdownChannelName}"
    // automatically add the test group Not-bamboo for exclusion 
    if ("".equals(testng_excluded_groups)) { 
      testng_excluded_groups = "Not-bamboo"
    }
    install4jExtraScheme = "jalviewb"
    break

    case [ "RELEASE", "JALVIEWJS-RELEASE" ]:
    getdownAppDistDir = getdown_app_dir_release
    getdownSetAppBaseProperty = true
    reportRsyncCommand = true
    install4jSuffix = ""
    install4jInstallerName = "${jalview_name} Installer"
    break

    case "ARCHIVE":
    getdownChannelName = CHANNEL.toLowerCase()+"/${JALVIEW_VERSION}"
    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
    if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
    } else {
      package_dir = string("${ARCHIVEDIR}/${package_dir}")
      buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
      buildDist = false
    }
    reportRsyncCommand = true
    install4jExtraScheme = "jalviewa"
    break

    case "ARCHIVELOCAL":
    getdownChannelName = string("archive/${JALVIEW_VERSION}")
    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
    if (!file("${ARCHIVEDIR}/${package_dir}").exists()) {
      throw new GradleException("Must provide an ARCHIVEDIR value to produce an archive distribution")
    } else {
      package_dir = string("${ARCHIVEDIR}/${package_dir}")
      buildProperties = string("${ARCHIVEDIR}/${classes_dir}/${build_properties_file}")
      buildDist = false
    }
    reportRsyncCommand = true
    getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
    install4jSuffix = "Archive"
    install4jExtraScheme = "jalviewa"
    break

    case "DEVELOP":
    reportRsyncCommand = true
    getdownSetAppBaseProperty = true
    // DEVELOP-RELEASE is usually associated with a Jalview release series so set the version
    JALVIEW_VERSION=JALVIEW_VERSION+"-d${buildDate}"
    
    install4jSuffix = "Develop"
    install4jExtraScheme = "jalviewd"
    install4jInstallerName = "${jalview_name} Develop Installer"
    break

    case "TEST-RELEASE":
    reportRsyncCommand = true
    getdownSetAppBaseProperty = true
    // Don't ignore transpile errors for release build
    if (jalviewjs_ignore_transpile_errors.equals("true")) {
      jalviewjs_ignore_transpile_errors = "false"
      println("Setting jalviewjs_ignore_transpile_errors to 'false'")
    }
    JALVIEW_VERSION = JALVIEW_VERSION+"-test"
    install4jSuffix = "Test"
    install4jExtraScheme = "jalviewt"
    install4jInstallerName = "${jalview_name} Test Installer"
    break

    case ~/^SCRATCH(|-[-\w]*)$/:
    getdownChannelName = CHANNEL
    JALVIEW_VERSION = JALVIEW_VERSION+"-"+CHANNEL
    
    getdownDir = string("${getdownChannelName}/${JAVA_VERSION}")
    getdownAppBase = string("${getdown_channel_base}/${getdownDir}")
    reportRsyncCommand = true
    install4jSuffix = "Scratch"
    break

    case "TEST-LOCAL":
    if (!file("${LOCALDIR}").exists()) {
      throw new GradleException("Must provide a LOCALDIR value to produce a local distribution")
    } else {
      getdownAppBase = file(file("${LOCALDIR}").getAbsolutePath()).toURI().toString()
      getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
    }
    JALVIEW_VERSION = "TEST"
    install4jSuffix = "Test-Local"
    install4jExtraScheme = "jalviewt"
    install4jInstallerName = "${jalview_name} Test Installer"
    break

    case [ "LOCAL", "JALVIEWJS" ]:
    JALVIEW_VERSION = "TEST"
    getdownAppBase = file(getdownAppBaseDir).toURI().toString()
    getdownArchiveAppBase = file("${jalviewDir}/${getdown_archive_dir}").toURI().toString()
    getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
    install4jExtraScheme = "jalviewl"
    install4jCheckSums = false
    break

    default: // something wrong specified
    throw new GradleException("CHANNEL must be one of BUILD, RELEASE, ARCHIVE, DEVELOP, TEST-RELEASE, SCRATCH-..., LOCAL [default]")
    break

  }
  JALVIEW_VERSION_UNDERSCORES = JALVIEW_VERSION.replaceAll("\\.", "_")
  hugoDataJsonFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_data_installers_dir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
  hugoArchiveMdFile = file("${jalviewDir}/${hugo_build_dir}/${hugo_version_archive_dir}/Version-${JALVIEW_VERSION_UNDERSCORES}/_index.md")
  // override getdownAppBase if requested
  if (findProperty("getdown_appbase_override") != null) {
    // revert to LOCAL if empty string
    if (string(getdown_appbase_override) == "") {
      getdownAppBase = file(getdownAppBaseDir).toURI().toString()
      getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
    } else if (string(getdown_appbase_override).startsWith("file://")) {
      getdownAppBase = string(getdown_appbase_override)
      getdownLauncher = string("${jalviewDir}/${getdown_lib_dir}/${getdown_launcher_local}")
    } else {
      getdownAppBase = string(getdown_appbase_override)
    }
    println("Overriding getdown appbase with '${getdownAppBase}'")
  }
  // sanitise file name for jalview launcher file for this channel
  jvlChannelName = jvlChannelName.replaceAll("[^\\w\\-]+", "_")
  // install4j application and folder names
  if (install4jSuffix == "") {
    install4jBundleId = "${install4j_bundle_id}"
    install4jWinApplicationId = install4j_release_win_application_id
  } else {
    applicationName = "${jalview_name} ${install4jSuffix}"
    install4jBundleId = "${install4j_bundle_id}-" + install4jSuffix.toLowerCase()
    // add int hash of install4jSuffix to the last part of the application_id
    def id = install4j_release_win_application_id
    def idsplitreverse = id.split("-").reverse()
    idsplitreverse[0] = idsplitreverse[0].toInteger() + install4jSuffix.hashCode()
    install4jWinApplicationId = idsplitreverse.reverse().join("-")
  }
  // sanitise folder and id names
  // install4jApplicationFolder = e.g. "Jalview Build"
  install4jApplicationFolder = applicationName
                                    .replaceAll("[\"'~:/\\\\\\s]", "_") // replace all awkward filename chars " ' ~ : / \
                                    .replaceAll("_+", "_") // collapse __
  install4jInternalId = applicationName
                                    .replaceAll(" ","_")
                                    .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
                                    .replaceAll("_+", "") // collapse __
                                    //.replaceAll("_*-_*", "-") // collapse _-_
  install4jUnixApplicationFolder = applicationName
                                    .replaceAll(" ","_")
                                    .replaceAll("[^\\w\\-\\.]", "_") // replace other non [alphanumeric,_,-,.]
                                    .replaceAll("_+", "_") // collapse __
                                    .replaceAll("_*-_*", "-") // collapse _-_
                                    .toLowerCase()

  getdownWrapperLink = install4jUnixApplicationFolder // e.g. "jalview_local"
  getdownAppDir = string("${getdownAppBaseDir}/${getdownAppDistDir}")
  //getdownJ11libDir = "${getdownAppBaseDir}/${getdown_j11lib_dir}"
  getdownResourceDir = string("${getdownAppBaseDir}/${getdown_resource_dir}")
  getdownInstallDir = string("${getdownAppBaseDir}/${getdown_install_dir}")
  getdownFilesDir = string("${jalviewDir}/${getdown_files_dir}/${JAVA_VERSION}/")
  getdownFilesInstallDir = string("${getdownFilesDir}/${getdown_install_dir}")
  /* compile without modules -- using classpath libraries
  modules_compileClasspath = fileTree(dir: "${jalviewDir}/${j11modDir}", include: ["*.jar"])
  modules_runtimeClasspath = modules_compileClasspath
  */

  gitHash = "SOURCE"
  gitBranch = "Source"
  try {
    apply plugin: "com.palantir.git-version"
    def details = versionDetails()
    gitHash = details.gitHash
    gitBranch = details.branchName
  } catch(org.gradle.api.internal.plugins.PluginApplicationException e) {
    println("Not in a git repository. Using git values from RELEASE properties file.")
    gitHash = releaseProps.getProperty("git.hash")
    gitBranch = releaseProps.getProperty("git.branch")
  } catch(java.lang.RuntimeException e1) {
    throw new GradleException("Error with git-version plugin.  Directory '.git' exists but versionDetails() cannot be found.")
  }

  println("Using a ${CHANNEL} profile.")

  additional_compiler_args = []
  // configure classpath/args for j8/j11 compilation
  if (JAVA_VERSION.equals("1.8")) {
    JAVA_INTEGER_VERSION = string("8")
    //libDir = j8libDir
    libDir = j11libDir
    libDistDir = j8libDir
    compile_source_compatibility = 1.8
    compile_target_compatibility = 1.8
    // these are getdown.txt properties defined dependent on the JAVA_VERSION
    getdownAltJavaMinVersion = string(findProperty("getdown_alt_java8_min_version"))
    getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java8_max_version"))
    // this property is assigned below and expanded to multiple lines in the getdown task
    getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java8_txt_multi_java_location"))
    // this property is for the Java library used in eclipse
    eclipseJavaRuntimeName = string("JavaSE-1.8")
  } else if (JAVA_VERSION.equals("11")) {
    JAVA_INTEGER_VERSION = string("11")
    libDir = j11libDir
    libDistDir = j11libDir
    compile_source_compatibility = 11
    compile_target_compatibility = 11
    getdownAltJavaMinVersion = string(findProperty("getdown_alt_java11_min_version"))
    getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java11_max_version"))
    getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java11_txt_multi_java_location"))
    eclipseJavaRuntimeName = string("JavaSE-11")
    /* compile without modules -- using classpath libraries
    additional_compiler_args += [
    '--module-path', modules_compileClasspath.asPath,
    '--add-modules', j11modules
    ]
     */
  } else if (JAVA_VERSION.equals("17")) {
    JAVA_INTEGER_VERSION = string("17")
    libDir = j17libDir
    libDistDir = j17libDir
    compile_source_compatibility = 17
    compile_target_compatibility = 17
    getdownAltJavaMinVersion = string(findProperty("getdown_alt_java11_min_version"))
    getdownAltJavaMaxVersion = string(findProperty("getdown_alt_java11_max_version"))
    getdownAltMultiJavaLocation = string(findProperty("getdown_alt_java11_txt_multi_java_location"))
    eclipseJavaRuntimeName = string("JavaSE-17")
    /* compile without modules -- using classpath libraries
    additional_compiler_args += [
    '--module-path', modules_compileClasspath.asPath,
    '--add-modules', j11modules
    ]
     */
  } else {
    throw new GradleException("JAVA_VERSION=${JAVA_VERSION} not currently supported by Jalview")
  }


  // for install4j
  JAVA_MIN_VERSION = JAVA_VERSION
  JAVA_MAX_VERSION = JAVA_VERSION
  def jreInstallsDir = string(jre_installs_dir)
  if (jreInstallsDir.startsWith("~/")) {
    jreInstallsDir = System.getProperty("user.home") + jreInstallsDir.substring(1)
  }
  macosJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-mac-x64/jre")
  windowsJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-windows-x64/jre")
  linuxJavaVMDir = string("${jreInstallsDir}/jre-${JAVA_INTEGER_VERSION}-linux-x64/jre")
  macosJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_mac_x64.tar.gz")
  windowsJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_windows_x64.tar.gz")
  linuxJavaVMTgz = string("${jreInstallsDir}/tgz/jre_${JAVA_INTEGER_VERSION}_linux_x64.tar.gz")
  install4jDir = string("${jalviewDir}/${install4j_utils_dir}")
  install4jConfFileName = string("jalview-install4j-conf.install4j")
  install4jConfFile = file("${install4jDir}/${install4jConfFileName}")
  install4jHomeDir = install4j_home_dir
  if (install4jHomeDir.startsWith("~/")) {
    install4jHomeDir = System.getProperty("user.home") + install4jHomeDir.substring(1)
  }

  resourceBuildDir = string("${buildDir}/resources")
  resourcesBuildDir = string("${resourceBuildDir}/resources_build")
  helpBuildDir = string("${resourceBuildDir}/help_build")
  docBuildDir = string("${resourceBuildDir}/doc_build")

  if (buildProperties == null) {
    buildProperties = string("${resourcesBuildDir}/${build_properties_file}")
  }
  buildingHTML = string("${jalviewDir}/${doc_dir}/building.html")
  helpParentDir = string("${jalviewDir}/${help_parent_dir}")
  helpSourceDir = string("${helpParentDir}/${help_dir}")
  helpFile = string("${helpBuildDir}/${help_dir}/help.jhm")


  relativeBuildDir = file(jalviewDirAbsolutePath).toPath().relativize(buildDir.toPath())
  jalviewjsBuildDir = string("${relativeBuildDir}/jalviewjs")
  jalviewjsSiteDir = string("${jalviewjsBuildDir}/${jalviewjs_site_dir}")
  if (IN_ECLIPSE) {
    jalviewjsTransferSiteJsDir = string(jalviewjsSiteDir)
  } else {
    jalviewjsTransferSiteJsDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_js")
  }
  jalviewjsTransferSiteLibDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_lib")
  jalviewjsTransferSiteSwingJsDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_swingjs")
  jalviewjsTransferSiteCoreDir = string("${jalviewjsBuildDir}/tmp/${jalviewjs_site_dir}_core")
  jalviewjsJalviewCoreHtmlFile = string("")
  jalviewjsJalviewCoreName = string(jalviewjs_core_name)
  jalviewjsCoreClasslists = []
  jalviewjsJalviewTemplateName = string(jalviewjs_name)
  jalviewjsJ2sSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_settings}")
  jalviewjsJ2sAltSettingsFileName = string("${jalviewDir}/${jalviewjs_j2s_alt_settings}")
  jalviewjsJ2sProps = null
  jalviewjsJ2sPlugin = jalviewjs_j2s_plugin

  eclipseWorkspace = null
  eclipseBinary = string("")
  eclipseVersion = string("")
  eclipseDebug = false
  // ENDEXT
}


sourceSets {
  main {
    java {
      srcDirs sourceDir
      outputDir = file(classesDir)
    }

    resources {
      srcDirs = [ resourcesBuildDir, docBuildDir, helpBuildDir ]
    }

    compileClasspath = files(sourceSets.main.java.outputDir)
    compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])

    runtimeClasspath = compileClasspath
    runtimeClasspath += files(sourceSets.main.resources.srcDirs)
  }

  clover {
    java {
      srcDirs cloverInstrDir
      outputDir = cloverClassesDir
    }

    resources {
      srcDirs = sourceSets.main.resources.srcDirs
    }

    compileClasspath = files( sourceSets.clover.java.outputDir )
    //compileClasspath += files( testClassesDir )
    compileClasspath += fileTree(dir: "${jalviewDir}/${libDir}", include: ["*.jar"])
    compileClasspath += fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])

    runtimeClasspath = compileClasspath
  }

  test {
    java {
      srcDirs testSourceDir
      outputDir = file(testClassesDir)
    }

    resources {
      srcDirs = useClover ? sourceSets.clover.resources.srcDirs : sourceSets.main.resources.srcDirs
    }

    compileClasspath = files( sourceSets.test.java.outputDir )
    compileClasspath += useClover ? sourceSets.clover.compileClasspath : sourceSets.main.compileClasspath
    compileClasspath += fileTree(dir: "${jalviewDir}/${utils_dir}/testnglibs", include: ["**/*.jar"])

    runtimeClasspath = compileClasspath
    runtimeClasspath += files(sourceSets.test.resources.srcDirs)
  }

}


// eclipse project and settings files creation, also used by buildship
eclipse {
  project {
    name = eclipse_project_name

    natures 'org.eclipse.jdt.core.javanature',
    'org.eclipse.jdt.groovy.core.groovyNature',
    'org.eclipse.buildship.core.gradleprojectnature'

    buildCommand 'org.eclipse.jdt.core.javabuilder'
    buildCommand 'org.eclipse.buildship.core.gradleprojectbuilder'
  }

  classpath {
    //defaultOutputDir = sourceSets.main.java.outputDir
    configurations.each{ c->
      if (c.isCanBeResolved()) {
        minusConfigurations += [c]
      }
    }

    plusConfigurations = [ ]
    file {

      whenMerged { cp ->
        def removeTheseToo = []
        HashMap<String, Boolean> alreadyAddedSrcPath = new HashMap<>();
        cp.entries.each { entry ->
          // This conditional removes all src classpathentries that a) have already been added or b) aren't "src" or "test".
          // e.g. this removes the resources dir being copied into bin/main, bin/test AND bin/clover
          // we add the resources and help/help dirs in as libs afterwards (see below)
          if (entry.kind == 'src') {
            if (alreadyAddedSrcPath.getAt(entry.path) || !(entry.path == bareSourceDir || entry.path == bareTestSourceDir)) {
              removeTheseToo += entry
            } else {
              alreadyAddedSrcPath.putAt(entry.path, true)
            }
          }

        }
        cp.entries.removeAll(removeTheseToo)

        //cp.entries += new Output("${eclipse_bin_dir}/main")
        if (file(helpParentDir).isDirectory()) {
          cp.entries += new Library(fileReference(helpParentDir))
        }
        if (file(resourceDir).isDirectory()) {
          cp.entries += new Library(fileReference(resourceDir))
        }

        HashMap<String, Boolean> alreadyAddedLibPath = new HashMap<>();

        sourceSets.main.compileClasspath.findAll { it.name.endsWith(".jar") }.any {
          //don't want to add outputDir as eclipse is using its own output dir in bin/main
          if (it.isDirectory() || ! it.exists()) {
            // don't add dirs to classpath, especially if they don't exist
            return false // groovy "continue" in .any closure
          }
          def itPath = it.toString()
          if (itPath.startsWith("${jalviewDirAbsolutePath}/")) {
            // make relative path
            itPath = itPath.substring(jalviewDirAbsolutePath.length()+1)
          }
          if (alreadyAddedLibPath.get(itPath)) {
            //println("Not adding duplicate entry "+itPath)
          } else {
            //println("Adding entry "+itPath)
            cp.entries += new Library(fileReference(itPath))
            alreadyAddedLibPath.put(itPath, true)
          }
        }

        sourceSets.test.compileClasspath.findAll { it.name.endsWith(".jar") }.any {
          //no longer want to add outputDir as eclipse is using its own output dir in bin/main
          if (it.isDirectory() || ! it.exists()) {
            // don't add dirs to classpath
            return false // groovy "continue" in .any closure
          }

          def itPath = it.toString()
          if (itPath.startsWith("${jalviewDirAbsolutePath}/")) {
            itPath = itPath.substring(jalviewDirAbsolutePath.length()+1)
          }
          if (alreadyAddedLibPath.get(itPath)) {
            // don't duplicate
          } else {
            def lib = new Library(fileReference(itPath))
            lib.entryAttributes["test"] = "true"
            cp.entries += lib
            alreadyAddedLibPath.put(itPath, true)
          }
        }

      } // whenMerged

    } // file

    containers 'org.eclipse.buildship.core.gradleclasspathcontainer'

  } // classpath

  jdt {
    // for the IDE, use java 11 compatibility
    sourceCompatibility = compile_source_compatibility
    targetCompatibility = compile_target_compatibility
    javaRuntimeName = eclipseJavaRuntimeName

    // add in jalview project specific properties/preferences into eclipse core preferences
    file {
      withProperties { props ->
        def jalview_prefs = new Properties()
        def ins = new FileInputStream("${jalviewDirAbsolutePath}/${eclipse_extra_jdt_prefs_file}")
        jalview_prefs.load(ins)
        ins.close()
        jalview_prefs.forEach { t, v ->
          if (props.getAt(t) == null) {
            props.putAt(t, v)
          }
        }
        // codestyle file -- overrides previous formatter prefs
        def csFile = file("${jalviewDirAbsolutePath}/${eclipse_codestyle_file}")
        if (csFile.exists()) {
          XmlParser parser = new XmlParser()
          def profiles = parser.parse(csFile)
          def profile = profiles.'profile'.find { p -> (p.'@kind' == "CodeFormatterProfile" && p.'@name' == "Jalview") }
          if (profile != null) {
            profile.'setting'.each { s ->
              def id = s.'@id'
              def value = s.'@value'
              if (id != null && value != null) {
                props.putAt(id, value)
              }
            }
          }
        }
      }
    }

  } // jdt

  if (IN_ECLIPSE) {
    // Don't want these to be activated if in headless build
    synchronizationTasks "eclipseSynchronizationTask"
    //autoBuildTasks "eclipseAutoBuildTask"

  }
}


/* hack to change eclipse prefs in .settings files other than org.eclipse.jdt.core.prefs */
// Class to allow updating arbitrary properties files
class PropertiesFile extends PropertiesPersistableConfigurationObject {
  public PropertiesFile(PropertiesTransformer t) { super(t); }
  @Override protected void load(Properties properties) { }
  @Override protected void store(Properties properties) { }
  @Override protected String getDefaultResourceName() { return ""; }
  // This is necessary, because PropertiesPersistableConfigurationObject fails
  // if no default properties file exists.
  @Override public void loadDefaults() { load(new StringBufferInputStream("")); }
}

// Task to update arbitrary properties files (set outputFile)
class PropertiesFileTask extends PropertiesGeneratorTask<PropertiesFile> {
  private final PropertiesFileContentMerger file;
  public PropertiesFileTask() { file = new PropertiesFileContentMerger(getTransformer()); }
  protected PropertiesFile create() { return new PropertiesFile(getTransformer()); }
  protected void configure(PropertiesFile props) {
    file.getBeforeMerged().execute(props); file.getWhenMerged().execute(props);
  }
  public void file(Closure closure) { ConfigureUtil.configure(closure, file); }
}

task eclipseUIPreferences(type: PropertiesFileTask) {
  description = "Generate Eclipse additional settings"
  def filename = "org.eclipse.jdt.ui.prefs"
  outputFile = "$projectDir/.settings/${filename}" as File
  file {
    withProperties {
      it.load new FileInputStream("$projectDir/utils/eclipse/${filename}" as String)
    }
  }
}

task eclipseGroovyCorePreferences(type: PropertiesFileTask) {
  description = "Generate Eclipse additional settings"
  def filename = "org.eclipse.jdt.groovy.core.prefs"
  outputFile = "$projectDir/.settings/${filename}" as File
  file {
    withProperties {
      it.load new FileInputStream("$projectDir/utils/eclipse/${filename}" as String)
    }
  }
}

task eclipseAllPreferences {
  dependsOn eclipseJdt
  dependsOn eclipseUIPreferences
  dependsOn eclipseGroovyCorePreferences
}

eclipseUIPreferences.mustRunAfter eclipseJdt
eclipseGroovyCorePreferences.mustRunAfter eclipseJdt

/* end of eclipse preferences hack */


// clover bits


task cleanClover {
  doFirst {
    delete cloverBuildDir
    delete cloverReportDir
  }
}


task cloverInstrJava(type: JavaExec) {
  group = "Verification"
  description = "Create clover instrumented source java files"

  dependsOn cleanClover

  inputs.files(sourceSets.main.allJava)
  outputs.dir(cloverInstrDir)

  //classpath = fileTree(dir: "${jalviewDir}/${clover_lib_dir}", include: ["*.jar"])
  classpath = sourceSets.clover.compileClasspath
  main = "com.atlassian.clover.CloverInstr"

  def argsList = [
    "--encoding",
    "UTF-8",
    "--initstring",
    cloverDb,
    "--destdir",
    cloverInstrDir.getPath(),
  ]
  def srcFiles = sourceSets.main.allJava.files
  argsList.addAll(
    srcFiles.collect(
      { file -> file.absolutePath }
    )
  )
  args argsList.toArray()

  doFirst {
    delete cloverInstrDir
    println("Clover: About to instrument "+srcFiles.size() +" files")
  }
}


task cloverInstrTests(type: JavaExec) {
  group = "Verification"
  description = "Create clover instrumented source test files"

  dependsOn cleanClover

  inputs.files(testDir)
  outputs.dir(cloverTestInstrDir)

  classpath = sourceSets.clover.compileClasspath
  main = "com.atlassian.clover.CloverInstr"

  def argsList = [
    "--encoding",
    "UTF-8",
    "--initstring",
    cloverDb,
    "--srcdir",
    testDir,
    "--destdir",
    cloverTestInstrDir.getPath(),
  ]
  args argsList.toArray()

  doFirst {
    delete cloverTestInstrDir
    println("Clover: About to instrument test files")
  }
}


task cloverInstr {
  group = "Verification"
  description = "Create clover instrumented all source files"

  dependsOn cloverInstrJava
  dependsOn cloverInstrTests
}


cloverClasses.dependsOn cloverInstr


task cloverConsoleReport(type: JavaExec) {
  group = "Verification"
  description = "Creates clover console report"

  onlyIf {
    file(cloverDb).exists()
  }

  inputs.dir cloverClassesDir

  classpath = sourceSets.clover.runtimeClasspath
  main = "com.atlassian.clover.reporters.console.ConsoleReporter"

  if (cloverreport_mem.length() > 0) {
    maxHeapSize = cloverreport_mem
  }
  if (cloverreport_jvmargs.length() > 0) {
    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
  }

  def argsList = [
    "--alwaysreport",
    "--initstring",
    cloverDb,
    "--unittests"
  ]

  args argsList.toArray()
}


task cloverHtmlReport(type: JavaExec) {
  group = "Verification"
  description = "Creates clover HTML report"

  onlyIf {
    file(cloverDb).exists()
  }

  def cloverHtmlDir = cloverReportDir
  inputs.dir cloverClassesDir
  outputs.dir cloverHtmlDir

  classpath = sourceSets.clover.runtimeClasspath
  main = "com.atlassian.clover.reporters.html.HtmlReporter"

  if (cloverreport_mem.length() > 0) {
    maxHeapSize = cloverreport_mem
  }
  if (cloverreport_jvmargs.length() > 0) {
    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
  }

  def argsList = [
    "--alwaysreport",
    "--initstring",
    cloverDb,
    "--outputdir",
    cloverHtmlDir
  ]

  if (cloverreport_html_options.length() > 0) {
    argsList += cloverreport_html_options.split(" ")
  }

  args argsList.toArray()
}


task cloverXmlReport(type: JavaExec) {
  group = "Verification"
  description = "Creates clover XML report"

  onlyIf {
    file(cloverDb).exists()
  }

  def cloverXmlFile = "${cloverReportDir}/clover.xml"
  inputs.dir cloverClassesDir
  outputs.file cloverXmlFile

  classpath = sourceSets.clover.runtimeClasspath
  main = "com.atlassian.clover.reporters.xml.XMLReporter"

  if (cloverreport_mem.length() > 0) {
    maxHeapSize = cloverreport_mem
  }
  if (cloverreport_jvmargs.length() > 0) {
    jvmArgs Arrays.asList(cloverreport_jvmargs.split(" "))
  }

  def argsList = [
    "--alwaysreport",
    "--initstring",
    cloverDb,
    "--outfile",
    cloverXmlFile
  ]

  if (cloverreport_xml_options.length() > 0) {
    argsList += cloverreport_xml_options.split(" ")
  }

  args argsList.toArray()
}


task cloverReport {
  group = "Verification"
  description = "Creates clover reports"

  dependsOn cloverXmlReport
  dependsOn cloverHtmlReport
}


compileCloverJava {

  doFirst {
    sourceCompatibility = compile_source_compatibility
    targetCompatibility = compile_target_compatibility
    options.compilerArgs += additional_compiler_args
    print ("Setting target compatibility to "+targetCompatibility+"\n")
  }
  //classpath += configurations.cloverRuntime
}
// end clover bits


compileJava {
  // JBP->BS should the print statement in doFirst refer to compile_target_compatibility ?
  sourceCompatibility = compile_source_compatibility
  targetCompatibility = compile_target_compatibility
  options.compilerArgs = additional_compiler_args
  options.encoding = "UTF-8"
  doFirst {
    print ("Setting target compatibility to "+compile_target_compatibility+"\n")
  }

}


compileTestJava {
  sourceCompatibility = compile_source_compatibility
  targetCompatibility = compile_target_compatibility
  options.compilerArgs = additional_compiler_args
  doFirst {
    print ("Setting target compatibility to "+targetCompatibility+"\n")
  }
}


clean {
  doFirst {
    delete sourceSets.main.java.outputDir
  }
}


cleanTest {
  dependsOn cleanClover
  doFirst {
    delete sourceSets.test.java.outputDir
  }
}


// format is a string like date.format("dd MMMM yyyy")
def getDate(format) {
  return date.format(format)
}


def convertMdToHtml (FileTree mdFiles, File cssFile) {
  MutableDataSet options = new MutableDataSet()

  def extensions = new ArrayList<>()
  extensions.add(AnchorLinkExtension.create()) 
  extensions.add(AutolinkExtension.create())
  extensions.add(StrikethroughExtension.create())
  extensions.add(TaskListExtension.create())
  extensions.add(TablesExtension.create())
  extensions.add(TocExtension.create())
  
  options.set(Parser.EXTENSIONS, extensions)

  // set GFM table parsing options
  options.set(TablesExtension.WITH_CAPTION, false)
  options.set(TablesExtension.COLUMN_SPANS, false)
  options.set(TablesExtension.MIN_HEADER_ROWS, 1)
  options.set(TablesExtension.MAX_HEADER_ROWS, 1)
  options.set(TablesExtension.APPEND_MISSING_COLUMNS, true)
  options.set(TablesExtension.DISCARD_EXTRA_COLUMNS, true)
  options.set(TablesExtension.HEADER_SEPARATOR_COLUMN_MATCH, true)
  // GFM anchor links
  options.set(AnchorLinkExtension.ANCHORLINKS_SET_ID, false)
  options.set(AnchorLinkExtension.ANCHORLINKS_ANCHOR_CLASS, "anchor")
  options.set(AnchorLinkExtension.ANCHORLINKS_SET_NAME, true)
  options.set(AnchorLinkExtension.ANCHORLINKS_TEXT_PREFIX, "<span class=\"octicon octicon-link\"></span>")

  Parser parser = Parser.builder(options).build()
  HtmlRenderer renderer = HtmlRenderer.builder(options).build()

  mdFiles.each { mdFile ->
    // add table of contents
    def mdText = "[TOC]\n"+mdFile.text

    // grab the first top-level title
    def title = null
    def titleRegex = /(?m)^#(\s+|([^#]))(.*)/
    def matcher = mdText =~ titleRegex
    if (matcher.size() > 0) {
      // matcher[0][2] is the first character of the title if there wasn't any whitespace after the #
      title = (matcher[0][2] != null ? matcher[0][2] : "")+matcher[0][3]
    }
    // or use the filename if none found
    if (title == null) {
      title = mdFile.getName()
    }

    Node document = parser.parse(mdText)
    String htmlBody = renderer.render(document)
    def htmlText = '''<html>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <meta http-equiv="Content-Style-Type" content="text/css" />
    <meta name="generator" content="flexmark" />
'''
    htmlText += ((title != null) ? "  <title>${title}</title>" : '' )
    htmlText += '''
    <style type="text/css">code{white-space: pre;}</style>
'''
    htmlText += ((cssFile != null) ? cssFile.text : '')
    htmlText += '''</head>
  <body>
'''
    htmlText += htmlBody
    htmlText += '''
  </body>
</html>
'''

    def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
    def htmlFile = file(htmlFilePath)
    println("Creating ${htmlFilePath}")
    htmlFile.text = htmlText
  }
}


task copyDocs(type: Copy) {
  def inputDir = "${jalviewDir}/${doc_dir}"
  def outputDir = "${docBuildDir}/${doc_dir}"
  from(inputDir) {
    include('**/*.txt')
    include('**/*.md')
    include('**/*.html')
    include('**/*.xml')
    filter(ReplaceTokens,
      beginToken: '$$',
      endToken: '$$',
      tokens: [
        'Version-Rel': JALVIEW_VERSION,
        'Year-Rel': getDate("yyyy")
      ]
    )
  }
  from(inputDir) {
    exclude('**/*.txt')
    exclude('**/*.md')
    exclude('**/*.html')
    exclude('**/*.xml')
  }
  into outputDir

  inputs.dir(inputDir)
  outputs.dir(outputDir)
}


task convertMdFiles {
  dependsOn copyDocs
  def mdFiles = fileTree(dir: docBuildDir, include: "**/*.md")
  def cssFile = file("${jalviewDir}/${flexmark_css}")

  doLast {
    convertMdToHtml(mdFiles, cssFile)
  }

  inputs.files(mdFiles)
  inputs.file(cssFile)

  def htmlFiles = []
  mdFiles.each { mdFile ->
    def htmlFilePath = mdFile.getPath().replaceAll(/\..*?$/, ".html")
    htmlFiles.add(file(htmlFilePath))
  }
  outputs.files(htmlFiles)
}


def hugoTemplateSubstitutions(String input, Map extras=null) {
  def replacements = [
    DATE: getDate("yyyy-MM-dd"),
    CHANNEL: propertiesChannelName,
    APPLICATION_NAME: applicationName,
    GIT_HASH: gitHash,
    GIT_BRANCH: gitBranch,
    VERSION: JALVIEW_VERSION,
    JAVA_VERSION: JAVA_VERSION,
    VERSION_UNDERSCORES: JALVIEW_VERSION_UNDERSCORES,
    DRAFT: "false",
    JVL_HEADER: ""
  ]
  def output = input
  if (extras != null) {
    extras.each{ k, v ->
      output = output.replaceAll("__${k}__", ((v == null)?"":v))
    }
  }
  replacements.each{ k, v ->
    output = output.replaceAll("__${k}__", ((v == null)?"":v))
  }
  return output
}

def mdFileComponents(File mdFile, def dateOnly=false) {
  def map = [:]
  def content = ""
  if (mdFile.exists()) {
    def inFrontMatter = false
    def firstLine = true
    mdFile.eachLine { line ->
      if (line.matches("---")) {
        def prev = inFrontMatter
        inFrontMatter = firstLine
        if (inFrontMatter != prev)
          return false
      }
      if (inFrontMatter) {
        def m = null
        if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})/) {
          map["date"] = new Date().parse("yyyy-MM-dd HH:mm:ss", m[0][1])
        } else if (m = line =~ /^date:\s*(\d{4}-\d{2}-\d{2})/) {
          map["date"] = new Date().parse("yyyy-MM-dd", m[0][1])
        } else if (m = line =~ /^channel:\s*(\S+)/) {
          map["channel"] = m[0][1]
        } else if (m = line =~ /^version:\s*(\S+)/) {
          map["version"] = m[0][1]
        } else if (m = line =~ /^\s*([^:]+)\s*:\s*(\S.*)/) {
          map[ m[0][1] ] = m[0][2]
        }
        if (dateOnly && map["date"] != null) {
          return false
        }
      } else {
        if (dateOnly)
          return false
        content += line+"\n"
      }
      firstLine = false
    }
  }
  return dateOnly ? map["date"] : [map, content]
}

task hugoTemplates {
  group "website"
  description "Create partially populated md pages for hugo website build"

  def hugoTemplatesDir = file("${jalviewDir}/${hugo_templates_dir}")
  def hugoBuildDir = "${jalviewDir}/${hugo_build_dir}"
  def templateFiles = fileTree(dir: hugoTemplatesDir)
  def releaseMdFile = file("${jalviewDir}/${releases_dir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
  def whatsnewMdFile = file("${jalviewDir}/${whatsnew_dir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")
  def oldJvlFile = file("${jalviewDir}/${hugo_old_jvl}")
  def jalviewjsFile = file("${jalviewDir}/${hugo_jalviewjs}")

  doFirst {
    // specific release template for version archive
    def changes = ""
    def whatsnew = null
    def givenDate = null
    def givenChannel = null
    def givenVersion = null
    if (CHANNEL == "RELEASE") {
      def (map, content) = mdFileComponents(releaseMdFile)
      givenDate = map.date
      givenChannel = map.channel
      givenVersion = map.version
      changes = content
      if (givenVersion != null && givenVersion != JALVIEW_VERSION) {
        throw new GradleException("'version' header (${givenVersion}) found in ${releaseMdFile} does not match JALVIEW_VERSION (${JALVIEW_VERSION})")
      }

      if (whatsnewMdFile.exists())
        whatsnew = whatsnewMdFile.text
    }

    def oldJvl = oldJvlFile.exists() ? oldJvlFile.collect{it} : []
    def jalviewjsLink = jalviewjsFile.exists() ? jalviewjsFile.collect{it} : []

    def changesHugo = null
    if (changes != null) {
      changesHugo = '<div class="release_notes">\n\n'
      def inSection = false
      changes.eachLine { line ->
        def m = null
        if (m = line =~ /^##([^#].*)$/) {
          if (inSection) {
            changesHugo += "</div>\n\n"
          }
          def section = m[0][1].trim()
          section = section.toLowerCase()
          section = section.replaceAll(/ +/, "_")
          section = section.replaceAll(/[^a-z0-9_\-]/, "")
          changesHugo += "<div class=\"${section}\">\n\n"
          inSection = true
        } else if (m = line =~ /^(\s*-\s*)<!--([^>]+)-->(.*?)(<br\/?>)?\s*$/) {
          def comment = m[0][2].trim()
          if (comment != "") {
            comment = comment.replaceAll('"', "&quot;")
            def issuekeys = []
            comment.eachMatch(/JAL-\d+/) { jal -> issuekeys += jal }
            def newline = m[0][1]
            if (comment.trim() != "")
              newline += "{{<comment>}}${comment}{{</comment>}}  "
            newline += m[0][3].trim()
            if (issuekeys.size() > 0)
              newline += "  {{< jal issue=\"${issuekeys.join(",")}\" alt=\"${comment}\" >}}"
            if (m[0][4] != null)
              newline += m[0][4]
            line = newline
          }
        }
        changesHugo += line+"\n"
      }
      if (inSection) {
        changesHugo += "\n</div>\n\n"
      }
      changesHugo += '</div>'
    }

    templateFiles.each{ templateFile ->
      def newFileName = string(hugoTemplateSubstitutions(templateFile.getName()))
      def relPath = hugoTemplatesDir.toPath().relativize(templateFile.toPath()).getParent()
      def newRelPathName = hugoTemplateSubstitutions( relPath.toString() )

      def outPathName = string("${hugoBuildDir}/$newRelPathName")

      copy {
        from templateFile
        rename(templateFile.getName(), newFileName)
        into outPathName
      }

      def newFile = file("${outPathName}/${newFileName}".toString())
      def content = newFile.text
      newFile.text = hugoTemplateSubstitutions(content,
        [
          WHATSNEW: whatsnew,
          CHANGES: changesHugo,
          DATE: givenDate == null ? "" : givenDate.format("yyyy-MM-dd"),
          DRAFT: givenDate == null ? "true" : "false",
          JALVIEWJSLINK: jalviewjsLink.contains(JALVIEW_VERSION) ? "true" : "false",
          JVL_HEADER: oldJvl.contains(JALVIEW_VERSION) ? "jvl: true" : ""
        ]
      )
    }

  }

  inputs.file(oldJvlFile)
  inputs.dir(hugoTemplatesDir)
  inputs.property("JALVIEW_VERSION", { JALVIEW_VERSION })
  inputs.property("CHANNEL", { CHANNEL })
}

def getMdDate(File mdFile) {
  return mdFileComponents(mdFile, true)
}

def getMdSections(String content) {
  def sections = [:]
  def sectionContent = ""
  def sectionName = null
  content.eachLine { line ->
    def m = null
    if (m = line =~ /^##([^#].*)$/) {
      if (sectionName != null) {
        sections[sectionName] = sectionContent
        sectionName = null
        sectionContent = ""
      }
      sectionName = m[0][1].trim()
      sectionName = sectionName.toLowerCase()
      sectionName = sectionName.replaceAll(/ +/, "_")
      sectionName = sectionName.replaceAll(/[^a-z0-9_\-]/, "")
    } else if (sectionName != null) {
      sectionContent += line+"\n"
    }
  }
  if (sectionContent != null) {
    sections[sectionName] = sectionContent
  }
  return sections
}


task copyHelp(type: Copy) {
  def inputDir = helpSourceDir
  def outputDir = "${helpBuildDir}/${help_dir}"
  from(inputDir) {
    include('**/*.txt')
    include('**/*.md')
    include('**/*.html')
    include('**/*.hs')
    include('**/*.xml')
    include('**/*.jhm')
    filter(ReplaceTokens,
      beginToken: '$$',
      endToken: '$$',
      tokens: [
        'Version-Rel': JALVIEW_VERSION,
        'Year-Rel': getDate("yyyy")
      ]
    )
  }
  from(inputDir) {
    exclude('**/*.txt')
    exclude('**/*.md')
    exclude('**/*.html')
    exclude('**/*.hs')
    exclude('**/*.xml')
    exclude('**/*.jhm')
  }
  into outputDir

  inputs.dir(inputDir)
  outputs.files(helpFile)
  outputs.dir(outputDir)
}


task releasesTemplates {
  group "help"
  description "Recreate whatsNew.html and releases.html from markdown files and templates in help"

  dependsOn copyHelp

  def releasesTemplateFile = file("${jalviewDir}/${releases_template}")
  def whatsnewTemplateFile = file("${jalviewDir}/${whatsnew_template}")
  def releasesHtmlFile = file("${helpBuildDir}/${help_dir}/${releases_html}")
  def whatsnewHtmlFile = file("${helpBuildDir}/${help_dir}/${whatsnew_html}")
  def releasesMdDir = "${jalviewDir}/${releases_dir}"
  def whatsnewMdDir = "${jalviewDir}/${whatsnew_dir}"

  doFirst {
    def releaseMdFile = file("${releasesMdDir}/release-${JALVIEW_VERSION_UNDERSCORES}.md")
    def whatsnewMdFile = file("${whatsnewMdDir}/whatsnew-${JALVIEW_VERSION_UNDERSCORES}.md")

    if (CHANNEL == "RELEASE") {
      if (!releaseMdFile.exists()) {
        throw new GradleException("File ${releaseMdFile} must be created for RELEASE")
      }
      if (!whatsnewMdFile.exists()) {
        throw new GradleException("File ${whatsnewMdFile} must be created for RELEASE")
      }
    }

    def releaseFiles = fileTree(dir: releasesMdDir, include: "release-*.md")
    def releaseFilesDates = releaseFiles.collectEntries {
      [(it): getMdDate(it)]
    }
    releaseFiles = releaseFiles.sort { a,b -> releaseFilesDates[a].compareTo(releaseFilesDates[b]) }

    def releasesTemplate = releasesTemplateFile.text
    def m = releasesTemplate =~ /(?s)__VERSION_LOOP_START__(.*)__VERSION_LOOP_END__/
    def versionTemplate = m[0][1]

    MutableDataSet options = new MutableDataSet()

    def extensions = new ArrayList<>()
    options.set(Parser.EXTENSIONS, extensions)
    options.set(Parser.HTML_BLOCK_COMMENT_ONLY_FULL_LINE, true)

    Parser parser = Parser.builder(options).build()
    HtmlRenderer renderer = HtmlRenderer.builder(options).build()

    def actualVersions = releaseFiles.collect { rf ->
      def (rfMap, rfContent) = mdFileComponents(rf)
      return rfMap.version
    }
    def versionsHtml = ""
    def linkedVersions = []
    releaseFiles.reverse().each { rFile ->
      def (rMap, rContent) = mdFileComponents(rFile)

      def versionLink = ""
      def partialVersion = ""
      def firstPart = true
      rMap.version.split("\\.").each { part ->
        def displayPart = ( firstPart ? "" : "." ) + part
        partialVersion += displayPart
        if (
            linkedVersions.contains(partialVersion)
            || ( actualVersions.contains(partialVersion) && partialVersion != rMap.version )
            ) {
          versionLink += displayPart
        } else {
          versionLink += "<a id=\"Jalview.${partialVersion}\">${displayPart}</a>"
          linkedVersions += partialVersion
        }
        firstPart = false
      }
      def displayDate = releaseFilesDates[rFile].format("dd/MM/yyyy")

      def lm = null
      def rContentProcessed = ""
      rContent.eachLine { line ->
        if (lm = line =~ /^(\s*-)(\s*<!--[^>]*?-->)(.*)$/) {
          line = "${lm[0][1]}${lm[0][3]}${lm[0][2]}"
      } else if (lm = line =~ /^###([^#]+.*)$/) {
          line = "_${lm[0][1].trim()}_"
        }
        rContentProcessed += line + "\n"
      }

      def rContentSections = getMdSections(rContentProcessed)
      def rVersion = versionTemplate
      if (rVersion != "") {
        def rNewFeatures = rContentSections["new_features"]
        def rIssuesResolved = rContentSections["issues_resolved"]
        Node newFeaturesNode = parser.parse(rNewFeatures)
        String newFeaturesHtml = renderer.render(newFeaturesNode)
        Node issuesResolvedNode = parser.parse(rIssuesResolved)
        String issuesResolvedHtml = renderer.render(issuesResolvedNode)
        rVersion = hugoTemplateSubstitutions(rVersion,
          [
            VERSION: rMap.version,
            VERSION_LINK: versionLink,
            DISPLAY_DATE: displayDate,
            NEW_FEATURES: newFeaturesHtml,
            ISSUES_RESOLVED: issuesResolvedHtml
          ]
        )
        versionsHtml += rVersion
      }
    }

    releasesTemplate = releasesTemplate.replaceAll("(?s)__VERSION_LOOP_START__.*__VERSION_LOOP_END__", versionsHtml)
    releasesTemplate = hugoTemplateSubstitutions(releasesTemplate)
    releasesHtmlFile.text = releasesTemplate

    if (whatsnewMdFile.exists()) {
      def wnDisplayDate = releaseFilesDates[releaseMdFile] != null ? releaseFilesDates[releaseMdFile].format("dd MMMM yyyy") : ""
      def whatsnewMd = hugoTemplateSubstitutions(whatsnewMdFile.text)
      Node whatsnewNode = parser.parse(whatsnewMd)
      String whatsnewHtml = renderer.render(whatsnewNode)
      whatsnewHtml = whatsnewTemplateFile.text.replaceAll("__WHATS_NEW__", whatsnewHtml)
      whatsnewHtmlFile.text = hugoTemplateSubstitutions(whatsnewHtml,
        [
            VERSION: JALVIEW_VERSION,
          DISPLAY_DATE: wnDisplayDate
        ]
      )
    } else if (gradle.taskGraph.hasTask(":linkCheck")) {
      whatsnewHtmlFile.text = "Development build " + getDate("yyyy-MM-dd HH:mm:ss")
    }

  }

  inputs.file(releasesTemplateFile)
  inputs.file(whatsnewTemplateFile)
  inputs.dir(releasesMdDir)
  inputs.dir(whatsnewMdDir)
  outputs.file(releasesHtmlFile)
  outputs.file(whatsnewHtmlFile)
}


task copyResources(type: Copy) {
  group = "build"
  description = "Copy (and make text substitutions in) the resources dir to the build area"

  def inputDir = resourceDir
  def outputDir = resourcesBuildDir
  from(inputDir) {
    include('**/*.txt')
    include('**/*.md')
    include('**/*.html')
    include('**/*.xml')
    filter(ReplaceTokens,
      beginToken: '$$',
      endToken: '$$',
      tokens: [
        'Version-Rel': JALVIEW_VERSION,
        'Year-Rel': getDate("yyyy")
      ]
    )
  }
  from(inputDir) {
    exclude('**/*.txt')
    exclude('**/*.md')
    exclude('**/*.html')
    exclude('**/*.xml')
  }
  into outputDir

  inputs.dir(inputDir)
  outputs.dir(outputDir)
}

task copyChannelResources(type: Copy) {
  dependsOn copyResources
  group = "build"
  description = "Copy the channel resources dir to the build resources area"

  def inputDir = "${channelDir}/${resource_dir}"
  def outputDir = resourcesBuildDir
  from inputDir
  into outputDir

  inputs.dir(inputDir)
  outputs.dir(outputDir)
}

task createBuildProperties(type: WriteProperties) {
  dependsOn copyResources
  group = "build"
  description = "Create the ${buildProperties} file"
  
  inputs.dir(sourceDir)
  inputs.dir(resourcesBuildDir)
  outputFile (buildProperties)
  // taking time specific comment out to allow better incremental builds
  comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd HH:mm:ss")
  //comment "--Jalview Build Details--\n"+getDate("yyyy-MM-dd")
  property "BUILD_DATE", getDate("HH:mm:ss dd MMMM yyyy")
  property "VERSION", JALVIEW_VERSION
  property "INSTALLATION", INSTALLATION+" git-commit:"+gitHash+" ["+gitBranch+"]"
  property "JAVA_COMPILE_VERSION", JAVA_INTEGER_VERSION
  if (getdownSetAppBaseProperty) {
    property "GETDOWNAPPBASE", getdownAppBase
    property "GETDOWNAPPDISTDIR", getdownAppDistDir
  }
  outputs.file(outputFile)
}


task buildIndices(type: JavaExec) {
  dependsOn copyHelp
  classpath = sourceSets.main.compileClasspath
  main = "com.sun.java.help.search.Indexer"
  workingDir = "${helpBuildDir}/${help_dir}"
  def argDir = "html"
  args = [ argDir ]
  inputs.dir("${workingDir}/${argDir}")

  outputs.dir("${classesDir}/doc")
  outputs.dir("${classesDir}/help")
  outputs.file("${workingDir}/JavaHelpSearch/DOCS")
  outputs.file("${workingDir}/JavaHelpSearch/DOCS.TAB")
  outputs.file("${workingDir}/JavaHelpSearch/OFFSETS")
  outputs.file("${workingDir}/JavaHelpSearch/POSITIONS")
  outputs.file("${workingDir}/JavaHelpSearch/SCHEMA")
  outputs.file("${workingDir}/JavaHelpSearch/TMAP")
}

task buildResources {
  dependsOn copyResources
  dependsOn copyChannelResources
  dependsOn createBuildProperties
}

task prepare {
  dependsOn buildResources
  dependsOn copyDocs
  dependsOn copyHelp
  dependsOn releasesTemplates
  dependsOn convertMdFiles
  dependsOn buildIndices
}


compileJava.dependsOn prepare
run.dependsOn compileJava
//run.dependsOn prepare


//testReportDirName = "test-reports" // note that test workingDir will be $jalviewDir
test {
  dependsOn prepare

  if (useClover) {
    dependsOn cloverClasses
   } else { //?
    dependsOn compileJava //?
  }

  useTestNG() {
    includeGroups testng_groups
    excludeGroups testng_excluded_groups
    preserveOrder true
    useDefaultListeners=true
  }

  maxHeapSize = "1024m"

  workingDir = jalviewDir
  def testLaf = project.findProperty("test_laf")
  if (testLaf != null) {
    println("Setting Test LaF to '${testLaf}'")
    systemProperty "laf", testLaf
  }
  def testHiDPIScale = project.findProperty("test_HiDPIScale")
  if (testHiDPIScale != null) {
    println("Setting Test HiDPI Scale to '${testHiDPIScale}'")
    systemProperty "sun.java2d.uiScale", testHiDPIScale
  }
  sourceCompatibility = compile_source_compatibility
  targetCompatibility = compile_target_compatibility
  jvmArgs += additional_compiler_args

  doFirst {
    if (useClover) {
      println("Running tests " + (useClover?"WITH":"WITHOUT") + " clover")
    }
  }
}


task compileLinkCheck(type: JavaCompile) {
  options.fork = true
  classpath = files("${jalviewDir}/${utils_dir}")
  destinationDir = file("${jalviewDir}/${utils_dir}")
  source = fileTree(dir: "${jalviewDir}/${utils_dir}", include: ["HelpLinksChecker.java", "BufferedLineReader.java"])

  inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
  inputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.java")
  outputs.file("${jalviewDir}/${utils_dir}/HelpLinksChecker.class")
  outputs.file("${jalviewDir}/${utils_dir}/BufferedLineReader.class")
}


task linkCheck(type: JavaExec) {
  dependsOn prepare
  dependsOn compileLinkCheck

  def helpLinksCheckerOutFile = file("${jalviewDir}/${utils_dir}/HelpLinksChecker.out")
  classpath = files("${jalviewDir}/${utils_dir}")
  main = "HelpLinksChecker"
  workingDir = "${helpBuildDir}"
  args = [ "${helpBuildDir}/${help_dir}", "-nointernet" ]

  def outFOS = new FileOutputStream(helpLinksCheckerOutFile, false) // false == don't append
  standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
    outFOS,
    System.out)
  errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
    outFOS,
    System.err)

  inputs.dir(helpBuildDir)
  outputs.file(helpLinksCheckerOutFile)
}


// import the pubhtmlhelp target
ant.properties.basedir = "${jalviewDir}"
ant.properties.helpBuildDir = "${helpBuildDir}/${help_dir}"
ant.importBuild "${utils_dir}/publishHelp.xml"


task cleanPackageDir(type: Delete) {
  doFirst {
    delete fileTree(dir: "${jalviewDir}/${package_dir}", include: "*.jar")
  }
}


jar {
  dependsOn prepare
  dependsOn linkCheck

  manifest {
    attributes "Main-Class": main_class,
    "Permissions": "all-permissions",
    "Application-Name": applicationName,
    "Codebase": application_codebase,
    "Implementation-Version": JALVIEW_VERSION
  }

  def outputDir = "${jalviewDir}/${package_dir}"
  destinationDirectory = file(outputDir)
  archiveFileName = rootProject.name+".jar"
  duplicatesStrategy "EXCLUDE"


  exclude "cache*/**"
  exclude "*.jar"
  exclude "*.jar.*"
  exclude "**/*.jar"
  exclude "**/*.jar.*"

  inputs.dir(sourceSets.main.java.outputDir)
  sourceSets.main.resources.srcDirs.each{ dir ->
    inputs.dir(dir)
  }
  outputs.file("${outputDir}/${archiveFileName}")
}


task copyJars(type: Copy) {
  from fileTree(dir: classesDir, include: "**/*.jar").files
  into "${jalviewDir}/${package_dir}"
}


// doing a Sync instead of Copy as Copy doesn't deal with "outputs" very well
task syncJars(type: Sync) {
  dependsOn jar
  from fileTree(dir: "${jalviewDir}/${libDistDir}", include: "**/*.jar").files
  into "${jalviewDir}/${package_dir}"
  preserve {
    include jar.archiveFileName.getOrNull()
  }
}


task makeDist {
  group = "build"
  description = "Put all required libraries in dist"
  // order of "cleanPackageDir", "copyJars", "jar" important!
  jar.mustRunAfter cleanPackageDir
  syncJars.mustRunAfter cleanPackageDir
  dependsOn cleanPackageDir
  dependsOn syncJars
  dependsOn jar
  outputs.dir("${jalviewDir}/${package_dir}")
}


task cleanDist {
  dependsOn cleanPackageDir
  dependsOn cleanTest
  dependsOn clean
}


shadowJar {
  group = "distribution"
  description = "Create a single jar file with all dependency libraries merged. Can be run with java -jar"
  if (buildDist) {
    dependsOn makeDist
  }
  from ("${jalviewDir}/${libDistDir}") {
    include("*.jar")
  }
  manifest {
    attributes "Implementation-Version": JALVIEW_VERSION,
    "Application-Name": applicationName
  }

  duplicatesStrategy "INCLUDE"

  mainClassName = shadow_jar_main_class
  mergeServiceFiles()
  classifier = "all-"+JALVIEW_VERSION+"-j"+JAVA_VERSION
  minimize()
}


task getdownWebsite() {
  group = "distribution"
  description = "Create the getdown minimal app folder, and website folder for this version of jalview. Website folder also used for offline app installer"
  if (buildDist) {
    dependsOn makeDist
  }

  def getdownWebsiteResourceFilenames = []
  def getdownResourceDir = getdownResourceDir
  def getdownResourceFilenames = []

  doFirst {
    // clean the getdown website and files dir before creating getdown folders
    delete getdownAppBaseDir
    delete getdownFilesDir

    copy {
      from buildProperties
      rename(file(buildProperties).getName(), getdown_build_properties)
      into getdownAppDir
    }
    getdownWebsiteResourceFilenames += "${getdownAppDistDir}/${getdown_build_properties}"

    copy {
      from channelPropsFile
      into getdownAppBaseDir
    }
    getdownWebsiteResourceFilenames += file(channelPropsFile).getName()

    // set some getdownTxt_ properties then go through all properties looking for getdownTxt_...
    def props = project.properties.sort { it.key }
    if (getdownAltJavaMinVersion != null && getdownAltJavaMinVersion.length() > 0) {
      props.put("getdown_txt_java_min_version", getdownAltJavaMinVersion)
    }
    if (getdownAltJavaMaxVersion != null && getdownAltJavaMaxVersion.length() > 0) {
      props.put("getdown_txt_java_max_version", getdownAltJavaMaxVersion)
    }
    if (getdownAltMultiJavaLocation != null && getdownAltMultiJavaLocation.length() > 0) {
      props.put("getdown_txt_multi_java_location", getdownAltMultiJavaLocation)
    }
    if (getdownImagesDir != null && file(getdownImagesDir).exists()) {
      props.put("getdown_txt_ui.background_image", "${getdownImagesDir}/${getdown_background_image}")
      props.put("getdown_txt_ui.instant_background_image", "${getdownImagesDir}/${getdown_instant_background_image}")
      props.put("getdown_txt_ui.error_background", "${getdownImagesDir}/${getdown_error_background}")
      props.put("getdown_txt_ui.progress_image", "${getdownImagesDir}/${getdown_progress_image}")
      props.put("getdown_txt_ui.icon", "${getdownImagesDir}/${getdown_icon}")
      props.put("getdown_txt_ui.mac_dock_icon", "${getdownImagesDir}/${getdown_mac_dock_icon}")
    }

    props.put("getdown_txt_title", jalview_name)
    props.put("getdown_txt_ui.name", applicationName)

    // start with appbase
    getdownTextLines += "appbase = ${getdownAppBase}"
    props.each{ prop, val ->
      if (prop.startsWith("getdown_txt_") && val != null) {
        if (prop.startsWith("getdown_txt_multi_")) {
          def key = prop.substring(18)
          val.split(",").each{ v ->
            def line = "${key} = ${v}"
            getdownTextLines += line
          }
        } else {
          // file values rationalised
          if (val.indexOf('/') > -1 || prop.startsWith("getdown_txt_resource")) {
            def r = null
            if (val.indexOf('/') == 0) {
              // absolute path
              r = file(val)
            } else if (val.indexOf('/') > 0) {
              // relative path (relative to jalviewDir)
              r = file( "${jalviewDir}/${val}" )
            }
            if (r.exists()) {
              val = "${getdown_resource_dir}/" + r.getName()
              getdownWebsiteResourceFilenames += val
              getdownResourceFilenames += r.getPath()
            }
          }
          if (! prop.startsWith("getdown_txt_resource")) {
            def line = prop.substring(12) + " = ${val}"
            getdownTextLines += line
          }
        }
      }
    }

    getdownWebsiteResourceFilenames.each{ filename ->
      getdownTextLines += "resource = ${filename}"
    }
    getdownResourceFilenames.each{ filename ->
      copy {
        from filename
        into getdownResourceDir
      }
    }
    
    def getdownWrapperScripts = [ getdown_bash_wrapper_script, getdown_powershell_wrapper_script, getdown_batch_wrapper_script ]
    getdownWrapperScripts.each{ script ->
      def s = file( "${jalviewDir}/utils/getdown/${getdown_wrapper_script_dir}/${script}" )
      if (s.exists()) {
        copy {
          from s
          into "${getdownAppBaseDir}/${getdown_wrapper_script_dir}"
        }
        getdownTextLines += "resource = ${getdown_wrapper_script_dir}/${script}"
      }
    }

    def codeFiles = []
    fileTree(file(package_dir)).each{ f ->
      if (f.isDirectory()) {
        def files = fileTree(dir: f, include: ["*"]).getFiles()
        codeFiles += files
      } else if (f.exists()) {
        codeFiles += f
      }
    }
    def jalviewJar = jar.archiveFileName.getOrNull()
    // put jalview.jar first for CLASSPATH and .properties files reasons
    codeFiles.sort{a, b -> ( a.getName() == jalviewJar ? -1 : ( b.getName() == jalviewJar ? 1 : a <=> b ) ) }.each{f ->
      def name = f.getName()
      def line = "code = ${getdownAppDistDir}/${name}"
      getdownTextLines += line
      copy {
        from f.getPath()
        into getdownAppDir
      }
    }

    // NOT USING MODULES YET, EVERYTHING SHOULD BE IN dist
    /*
    if (JAVA_VERSION.equals("11")) {
    def j11libFiles = fileTree(dir: "${jalviewDir}/${j11libDir}", include: ["*.jar"]).getFiles()
    j11libFiles.sort().each{f ->
    def name = f.getName()
    def line = "code = ${getdown_j11lib_dir}/${name}"
    getdownTextLines += line
    copy {
    from f.getPath()
    into getdownJ11libDir
    }
    }
    }
     */

    // getdown-launcher.jar should not be in main application class path so the main application can move it when updated.  Listed as a resource so it gets updated.
    //getdownTextLines += "class = " + file(getdownLauncher).getName()
    getdownTextLines += "resource = ${getdown_launcher_new}"
    getdownTextLines += "class = ${main_class}"
    // Not setting these properties in general so that getdownappbase and getdowndistdir will default to release version in jalview.bin.Cache
    if (getdownSetAppBaseProperty) {
      getdownTextLines += "jvmarg = -Dgetdowndistdir=${getdownAppDistDir}"
      getdownTextLines += "jvmarg = -Dgetdownappbase=${getdownAppBase}"
    }

    def getdownTxt = file("${getdownAppBaseDir}/getdown.txt")
    getdownTxt.write(getdownTextLines.join("\n"))

    getdownLaunchJvl = getdown_launch_jvl_name + ( (jvlChannelName != null && jvlChannelName.length() > 0)?"-${jvlChannelName}":"" ) + ".jvl"
    def launchJvl = file("${getdownAppBaseDir}/${getdownLaunchJvl}")
    launchJvl.write("appbase=${getdownAppBase}")

    // files going into the getdown website dir: getdown-launcher.jar
    copy {
      from getdownLauncher
      rename(file(getdownLauncher).getName(), getdown_launcher_new)
      into getdownAppBaseDir
    }

    // files going into the getdown website dir: getdown-launcher(-local).jar
    copy {
      from getdownLauncher
      if (file(getdownLauncher).getName() != getdown_launcher) {
        rename(file(getdownLauncher).getName(), getdown_launcher)
      }
      into getdownAppBaseDir
    }

    // files going into the getdown website dir: ./install dir and files
    if (! (CHANNEL.startsWith("ARCHIVE") || CHANNEL.startsWith("DEVELOP"))) {
      copy {
        from getdownTxt
        from getdownLauncher
        from "${getdownAppDir}/${getdown_build_properties}"
        if (file(getdownLauncher).getName() != getdown_launcher) {
          rename(file(getdownLauncher).getName(), getdown_launcher)
        }
        into getdownInstallDir
      }

      // and make a copy in the getdown files dir (these are not downloaded by getdown)
      copy {
        from getdownInstallDir
        into getdownFilesInstallDir
      }
    }

    // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
    copy {
      from getdownTxt
      from launchJvl
      from getdownLauncher
      from "${getdownAppBaseDir}/${getdown_build_properties}"
      from "${getdownAppBaseDir}/${channel_props}"
      if (file(getdownLauncher).getName() != getdown_launcher) {
        rename(file(getdownLauncher).getName(), getdown_launcher)
      }
      into getdownFilesDir
    }

    // and ./resource (not all downloaded by getdown)
    copy {
      from getdownResourceDir
      into "${getdownFilesDir}/${getdown_resource_dir}"
    }
  }

  if (buildDist) {
    inputs.dir("${jalviewDir}/${package_dir}")
  }
  outputs.dir(getdownAppBaseDir)
  outputs.dir(getdownFilesDir)
}


// a helper task to allow getdown digest of any dir: `gradle getdownDigestDir -PDIGESTDIR=/path/to/my/random/getdown/dir
task getdownDigestDir(type: JavaExec) {
  group "Help"
  description "A task to run a getdown Digest on a dir with getdown.txt. Provide a DIGESTDIR property via -PDIGESTDIR=..."

  def digestDirPropertyName = "DIGESTDIR"
  doFirst {
    classpath = files(getdownLauncher)
    def digestDir = findProperty(digestDirPropertyName)
    if (digestDir == null) {
      throw new GradleException("Must provide a DIGESTDIR value to produce an alternative getdown digest")
    }
    args digestDir
  }
  main = "com.threerings.getdown.tools.Digester"
}


task getdownDigest(type: JavaExec) {
  group = "distribution"
  description = "Digest the getdown website folder"
  dependsOn getdownWebsite
  doFirst {
    classpath = files(getdownLauncher)
  }
  main = "com.threerings.getdown.tools.Digester"
  args getdownAppBaseDir
  inputs.dir(getdownAppBaseDir)
  outputs.file("${getdownAppBaseDir}/digest2.txt")
}


task getdown() {
  group = "distribution"
  description = "Create the minimal and full getdown app folder for installers and website and create digest file"
  dependsOn getdownDigest
  doLast {
    if (reportRsyncCommand) {
      def fromDir = getdownAppBaseDir + (getdownAppBaseDir.endsWith('/')?'':'/')
      def toDir = "${getdown_rsync_dest}/${getdownDir}" + (getdownDir.endsWith('/')?'':'/')
      println "LIKELY RSYNC COMMAND:"
      println "mkdir -p '$toDir'\nrsync -avh --delete '$fromDir' '$toDir'"
      if (RUNRSYNC == "true") {
        exec {
          commandLine "mkdir", "-p", toDir
        }
        exec {
          commandLine "rsync", "-avh", "--delete", fromDir, toDir
        }
      }
    }
  }
}


task getdownArchiveBuild() {
  group = "distribution"
  description = "Put files in the archive dir to go on the website"

  dependsOn getdownWebsite

  def v = "v${JALVIEW_VERSION_UNDERSCORES}"
  def vDir = "${getdownArchiveDir}/${v}"
  getdownFullArchiveDir = "${vDir}/getdown"
  getdownVersionLaunchJvl = "${vDir}/jalview-${v}.jvl"

  def vAltDir = "alt_${v}"
  def archiveImagesDir = "${jalviewDir}/${channel_properties_dir}/old/images"

  doFirst {
    // cleanup old "old" dir
    delete getdownArchiveDir

    def getdownArchiveTxt = file("${getdownFullArchiveDir}/getdown.txt")
    getdownArchiveTxt.getParentFile().mkdirs()
    def getdownArchiveTextLines = []
    def getdownFullArchiveAppBase = "${getdownArchiveAppBase}${getdownArchiveAppBase.endsWith("/")?"":"/"}${v}/getdown/"

    // the libdir
    copy {
      from "${getdownAppBaseDir}/${getdownAppDistDir}"
      into "${getdownFullArchiveDir}/${vAltDir}"
    }

    getdownTextLines.each { line ->
      line = line.replaceAll("^(?<s>appbase\\s*=\\s*).*", '${s}'+getdownFullArchiveAppBase)
      line = line.replaceAll("^(?<s>(resource|code)\\s*=\\s*)${getdownAppDistDir}/", '${s}'+vAltDir+"/")
      line = line.replaceAll("^(?<s>ui.background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background.png")
      line = line.replaceAll("^(?<s>ui.instant_background_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_initialising.png")
      line = line.replaceAll("^(?<s>ui.error_background\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_background_error.png")
      line = line.replaceAll("^(?<s>ui.progress_image\\s*=\\s*).*\\.png", '${s}'+"${getdown_resource_dir}/jalview_archive_getdown_progress_bar.png")
      // remove the existing resource = resource/ or bin/ lines
      if (! line.matches("resource\\s*=\\s*(resource|bin)/.*")) {
        getdownArchiveTextLines += line
      }
    }

    // the resource dir -- add these files as resource lines in getdown.txt
    copy {
      from "${archiveImagesDir}"
      into "${getdownFullArchiveDir}/${getdown_resource_dir}"
      eachFile { file ->
        getdownArchiveTextLines += "resource = ${getdown_resource_dir}/${file.getName()}"
      }
    }

    getdownArchiveTxt.write(getdownArchiveTextLines.join("\n"))

    def vLaunchJvl = file(getdownVersionLaunchJvl)
    vLaunchJvl.getParentFile().mkdirs()
    vLaunchJvl.write("appbase=${getdownFullArchiveAppBase}\n")
    def vLaunchJvlPath = vLaunchJvl.toPath().toAbsolutePath()
    def jvlLinkPath = file("${vDir}/jalview.jvl").toPath().toAbsolutePath()
    // for some reason filepath.relativize(fileInSameDirPath) gives a path to "../" which is wrong
    //java.nio.file.Files.createSymbolicLink(jvlLinkPath, jvlLinkPath.relativize(vLaunchJvlPath));
    java.nio.file.Files.createSymbolicLink(jvlLinkPath, java.nio.file.Paths.get(".",vLaunchJvl.getName()));

    // files going into the getdown files dir: getdown.txt, getdown-launcher.jar, channel-launch.jvl, build_properties
    copy {
      from getdownLauncher
      from "${getdownAppBaseDir}/${getdownLaunchJvl}"
      from "${getdownAppBaseDir}/${getdown_launcher_new}"
      from "${getdownAppBaseDir}/${channel_props}"
      if (file(getdownLauncher).getName() != getdown_launcher) {
        rename(file(getdownLauncher).getName(), getdown_launcher)
      }
      into getdownFullArchiveDir
    }

  }
}

task getdownArchiveDigest(type: JavaExec) {
  group = "distribution"
  description = "Digest the getdown archive folder"

  dependsOn getdownArchiveBuild

  doFirst {
    classpath = files(getdownLauncher)
    args getdownFullArchiveDir
  }
  main = "com.threerings.getdown.tools.Digester"
  inputs.dir(getdownFullArchiveDir)
  outputs.file("${getdownFullArchiveDir}/digest2.txt")
}

task getdownArchive() {
  group = "distribution"
  description = "Build the website archive dir with getdown digest"

  dependsOn getdownArchiveBuild
  dependsOn getdownArchiveDigest
}

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


clean {
  doFirst {
    delete getdownAppBaseDir
    delete getdownFilesDir
    delete getdownArchiveDir
  }
}


install4j {
  if (file(install4jHomeDir).exists()) {
    // good to go!
  } else if (file(System.getProperty("user.home")+"/buildtools/install4j").exists()) {
    install4jHomeDir = System.getProperty("user.home")+"/buildtools/install4j"
  } else if (file("/Applications/install4j.app/Contents/Resources/app").exists()) {
    install4jHomeDir = "/Applications/install4j.app/Contents/Resources/app"
  }
  installDir(file(install4jHomeDir))

  mediaTypes = Arrays.asList(install4j_media_types.split(","))
}


task copyInstall4jTemplate {
  def install4jTemplateFile = file("${install4jDir}/${install4j_template}")
  def install4jFileAssociationsFile = file("${install4jDir}/${install4j_installer_file_associations}")
  inputs.file(install4jTemplateFile)
  inputs.file(install4jFileAssociationsFile)
  inputs.property("CHANNEL", { CHANNEL })
  outputs.file(install4jConfFile)

  doLast {
    def install4jConfigXml = new XmlParser().parse(install4jTemplateFile)

    // turn off code signing if no OSX_KEYPASS
    if (OSX_KEYPASS == "") {
      install4jConfigXml.'**'.codeSigning.each { codeSigning ->
        codeSigning.'@macEnabled' = "false"
      }
      install4jConfigXml.'**'.windows.each { windows ->
        windows.'@runPostProcessor' = "false"
      }
    }

    // disable install screen for OSX dmg (for 2.11.2.0)
    install4jConfigXml.'**'.macosArchive.each { macosArchive -> 
      macosArchive.attributes().remove('executeSetupApp')
      macosArchive.attributes().remove('setupAppId')
    }

    // turn off checksum creation for LOCAL channel
    def e = install4jConfigXml.application[0]
    e.'@createChecksums' = string(install4jCheckSums)

    // put file association actions where placeholder action is
    def install4jFileAssociationsText = install4jFileAssociationsFile.text
    def fileAssociationActions = new XmlParser().parseText("<actions>${install4jFileAssociationsText}</actions>")
    install4jConfigXml.'**'.action.any { a -> // .any{} stops after the first one that returns true
      if (a.'@name' == 'EXTENSIONS_REPLACED_BY_GRADLE') {
        def parent = a.parent()
        parent.remove(a)
        fileAssociationActions.each { faa ->
            parent.append(faa)
        }
        // don't need to continue in .any loop once replacements have been made
        return true
      }
    }

    // use Windows Program Group with Examples folder for RELEASE, and Program Group without Examples for everything else
    // NB we're deleting the /other/ one!
    // Also remove the examples subdir from non-release versions
    def customizedIdToDelete = "PROGRAM_GROUP_RELEASE"
    // 2.11.1.0 NOT releasing with the Examples folder in the Program Group
    if (false && CHANNEL=="RELEASE") { // remove 'false && ' to include Examples folder in RELEASE channel
      customizedIdToDelete = "PROGRAM_GROUP_NON_RELEASE"
    } else {
      // remove the examples subdir from Full File Set
      def files = install4jConfigXml.files[0]
      def fileset = files.filesets.fileset.find { fs -> fs.'@customizedId' == "FULL_FILE_SET" }
      def root = files.roots.root.find { r -> r.'@fileset' == fileset.'@id' }
      def mountPoint = files.mountPoints.mountPoint.find { mp -> mp.'@root' == root.'@id' }
      def dirEntry = files.entries.dirEntry.find { de -> de.'@mountPoint' == mountPoint.'@id' && de.'@subDirectory' == "examples" }
      dirEntry.parent().remove(dirEntry)
    }
    install4jConfigXml.'**'.action.any { a ->
      if (a.'@customizedId' == customizedIdToDelete) {
        def parent = a.parent()
        parent.remove(a)
        return true
      }
    }

    // write install4j file
    install4jConfFile.text = XmlUtil.serialize(install4jConfigXml)
  }
}


clean {
  doFirst {
    delete install4jConfFile
  }
}

task cleanInstallersDataFiles {
  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")
  def hugoDataJsonFile = file("${jalviewDir}/${install4jBuildDir}/installers-${JALVIEW_VERSION_UNDERSCORES}.json")
  doFirst {
    delete installersOutputTxt
    delete installersSha256
    delete hugoDataJsonFile
  }
}

task installerFiles(type: com.install4j.gradle.Install4jTask) {
  group = "distribution"
  description = "Create the install4j installers"
  dependsOn getdown
  dependsOn copyInstall4jTemplate
  dependsOn cleanInstallersDataFiles

  projectFile = install4jConfFile

  // create an md5 for the input files to use as version for install4j conf file
  def digest = MessageDigest.getInstance("MD5")
  digest.update(
    (file("${install4jDir}/${install4j_template}").text + 
    file("${install4jDir}/${install4j_info_plist_file_associations}").text +
    file("${install4jDir}/${install4j_installer_file_associations}").text).bytes)
  def filesMd5 = new BigInteger(1, digest.digest()).toString(16)
  if (filesMd5.length() >= 8) {
    filesMd5 = filesMd5.substring(0,8)
  }
  def install4jTemplateVersion = "${JALVIEW_VERSION}_F${filesMd5}_C${gitHash}"

  variables = [
    'JALVIEW_NAME': jalview_name,
    'JALVIEW_APPLICATION_NAME': applicationName,
    'JALVIEW_DIR': "../..",
    'OSX_KEYSTORE': OSX_KEYSTORE,
    'OSX_APPLEID': OSX_APPLEID,
    'OSX_ALTOOLPASS': OSX_ALTOOLPASS,
    'JSIGN_SH': JSIGN_SH,
    'JRE_DIR': getdown_app_dir_java,
    'INSTALLER_TEMPLATE_VERSION': install4jTemplateVersion,
    'JALVIEW_VERSION': JALVIEW_VERSION,
    'JAVA_MIN_VERSION': JAVA_MIN_VERSION,
    'JAVA_MAX_VERSION': JAVA_MAX_VERSION,
    'JAVA_VERSION': JAVA_VERSION,
    'JAVA_INTEGER_VERSION': JAVA_INTEGER_VERSION,
    'VERSION': JALVIEW_VERSION,
    'MACOS_JAVA_VM_DIR': macosJavaVMDir,
    'WINDOWS_JAVA_VM_DIR': windowsJavaVMDir,
    'LINUX_JAVA_VM_DIR': linuxJavaVMDir,
    'MACOS_JAVA_VM_TGZ': macosJavaVMTgz,
    'WINDOWS_JAVA_VM_TGZ': windowsJavaVMTgz,
    'LINUX_JAVA_VM_TGZ': linuxJavaVMTgz,
    'COPYRIGHT_MESSAGE': install4j_copyright_message,
    'BUNDLE_ID': install4jBundleId,
    'INTERNAL_ID': install4jInternalId,
    'WINDOWS_APPLICATION_ID': install4jWinApplicationId,
    'MACOS_DMG_DS_STORE': install4jDMGDSStore,
    'MACOS_DMG_BG_IMAGE': install4jDMGBackgroundImage,
    'WRAPPER_LINK': getdownWrapperLink,
    'BASH_WRAPPER_SCRIPT': getdown_bash_wrapper_script,
    'POWERSHELL_WRAPPER_SCRIPT': getdown_powershell_wrapper_script,
    'WRAPPER_SCRIPT_BIN_DIR': getdown_wrapper_script_dir,
    'INSTALLER_NAME': install4jInstallerName,
    'INSTALL4J_UTILS_DIR': install4j_utils_dir,
    'GETDOWN_CHANNEL_DIR': getdownChannelDir,
    'GETDOWN_FILES_DIR': getdown_files_dir,
    'GETDOWN_RESOURCE_DIR': getdown_resource_dir,
    'GETDOWN_DIST_DIR': getdownAppDistDir,
    'GETDOWN_ALT_DIR': getdown_app_dir_alt,
    'GETDOWN_INSTALL_DIR': getdown_install_dir,
    'INFO_PLIST_FILE_ASSOCIATIONS_FILE': install4j_info_plist_file_associations,
    'BUILD_DIR': install4jBuildDir,
    'APPLICATION_CATEGORIES': install4j_application_categories,
    'APPLICATION_FOLDER': install4jApplicationFolder,
    'UNIX_APPLICATION_FOLDER': install4jUnixApplicationFolder,
    'EXECUTABLE_NAME': install4jExecutableName,
    'EXTRA_SCHEME': install4jExtraScheme,
    'MAC_ICONS_FILE': install4jMacIconsFile,
    'WINDOWS_ICONS_FILE': install4jWindowsIconsFile,
    'PNG_ICON_FILE': install4jPngIconFile,
    'BACKGROUND': install4jBackground,

  ]

  //println("INSTALL4J VARIABLES:")
  //variables.each{k,v->println("${k}=${v}")}

  destination = "${jalviewDir}/${install4jBuildDir}"
  buildSelected = true

  if (install4j_faster.equals("true") || CHANNEL.startsWith("LOCAL")) {
    faster = true
    disableSigning = true
    disableNotarization = true
  }

  if (OSX_KEYPASS) {
    macKeystorePassword = OSX_KEYPASS
  } 
  
  if (OSX_ALTOOLPASS) {
    appleIdPassword = OSX_ALTOOLPASS
    disableNotarization = false
  } else {
    disableNotarization = true
  }

  doFirst {
    println("Using projectFile "+projectFile)
    if (!disableNotarization) { println("Will notarize OSX App DMG") }
  }
  //verbose=true

  inputs.dir(getdownAppBaseDir)
  inputs.file(install4jConfFile)
  inputs.file("${install4jDir}/${install4j_info_plist_file_associations}")
  inputs.dir(macosJavaVMDir)
  inputs.dir(windowsJavaVMDir)
  outputs.dir("${jalviewDir}/${install4j_build_dir}/${JAVA_VERSION}")
}

def getDataHash(File myFile) {
  HashCode hash = Files.asByteSource(myFile).hash(Hashing.sha256())
  return myFile.exists()
  ? [
      "file" : myFile.getName(),
      "filesize" : myFile.length(),
      "sha256" : hash.toString()
    ]
  : null
}

def writeDataJsonFile(File installersOutputTxt, File installersSha256, File dataJsonFile) {
  def hash = [
    "channel" : getdownChannelName,
    "date" : getDate("yyyy-MM-dd HH:mm:ss"),
    "git-commit" : "${gitHash} [${gitBranch}]",
    "version" : JALVIEW_VERSION
  ]
  // install4j installer files
  if (installersOutputTxt.exists()) {
    def idHash = [:]
    installersOutputTxt.readLines().each { def line ->
      if (line.startsWith("#")) {
        return;
      }
      line.replaceAll("\n","")
      def vals = line.split("\t")
      def filename = vals[3]
      def filesize = file(filename).length()
      filename = filename.replaceAll(/^.*\//, "")
      hash[vals[0]] = [ "id" : vals[0], "os" : vals[1], "name" : vals[2], "file" : filename, "filesize" : filesize ]
      idHash."${filename}" = vals[0]
    }
    if (install4jCheckSums && installersSha256.exists()) {
      installersSha256.readLines().each { def line ->
        if (line.startsWith("#")) {
          return;
        }
        line.replaceAll("\n","")
        def vals = line.split(/\s+\*?/)
        def filename = vals[1]
        def innerHash = (hash.(idHash."${filename}"))."sha256" = vals[0]
      }
    }
  }

  [
    "JAR": shadowJar.archiveFile, // executable JAR
    "JVL": getdownVersionLaunchJvl, // version JVL
    "SOURCE": sourceDist.archiveFile // source TGZ
  ].each { key, value ->
    def file = file(value)
    if (file.exists()) {
      def fileHash = getDataHash(file)
      if (fileHash != null) {
        hash."${key}" = fileHash;
      }
    }
  }
  return dataJsonFile.write(new JsonBuilder(hash).toPrettyString())
}

task staticMakeInstallersJsonFile {
  doFirst {
    def output = findProperty("i4j_output")
    def sha256 = findProperty("i4j_sha256")
    def json = findProperty("i4j_json")
    if (output == null || sha256 == null || json == null) {
      throw new GradleException("Must provide paths to all of output.txt, sha256sums, and output.json with '-Pi4j_output=... -Pi4j_sha256=... -Pi4j_json=...")
    }
    writeDataJsonFile(file(output), file(sha256), file(json))
  }
}

task installers {
  dependsOn installerFiles
}


spotless {
  java {
    eclipse().configFile(eclipse_codestyle_file)
  }
}

task createSourceReleaseProperties(type: WriteProperties) {
  group = "distribution"
  description = "Create the source RELEASE properties file"
  
  def sourceTarBuildDir = "${buildDir}/sourceTar"
  def sourceReleasePropertiesFile = "${sourceTarBuildDir}/RELEASE"
  outputFile (sourceReleasePropertiesFile)

  doFirst {
    releaseProps.each{ key, val -> property key, val }
    property "git.branch", gitBranch
    property "git.hash", gitHash
  }

  outputs.file(outputFile)
}

task sourceDist(type: Tar) {
  group "distribution"
  description "Create a source .tar.gz file for distribution"

  dependsOn createBuildProperties
  dependsOn convertMdFiles
  dependsOn eclipseAllPreferences
  dependsOn createSourceReleaseProperties


  def outputFileName = "${project.name}_${JALVIEW_VERSION_UNDERSCORES}.tar.gz"
  archiveFileName = outputFileName
  
  compression Compression.GZIP
  
  into project.name

  def EXCLUDE_FILES=[
    "build/*",
    "bin/*",
    "test-output/",
    "test-reports",
    "tests",
    "clover*/*",
    ".*",
    "benchmarking/*",
    "**/.*",
    "*.class",
    "**/*.class","$j11modDir/**/*.jar","appletlib","**/*locales",
    "*locales/**",
    "utils/InstallAnywhere",
    "**/*.log",
    "RELEASE",
  ] 
  def PROCESS_FILES=[
    "AUTHORS",
    "CITATION",
    "FEATURETODO",
    "JAVA-11-README",
    "FEATURETODO",
    "LICENSE",
    "**/README",
    "THIRDPARTYLIBS",
    "TESTNG",
    "build.gradle",
    "gradle.properties",
    "**/*.java",
    "**/*.html",
    "**/*.xml",
    "**/*.gradle",
    "**/*.groovy",
    "**/*.properties",
    "**/*.perl",
    "**/*.sh",
  ]
  def INCLUDE_FILES=[
    ".classpath",
    ".settings/org.eclipse.buildship.core.prefs",
    ".settings/org.eclipse.jdt.core.prefs"
  ]

  from(jalviewDir) {
    exclude (EXCLUDE_FILES)
    include (PROCESS_FILES)
    filter(ReplaceTokens,
      beginToken: '$$',
      endToken: '$$',
      tokens: [
        'Version-Rel': JALVIEW_VERSION,
        'Year-Rel': getDate("yyyy")
      ]
    )
  }
  from(jalviewDir) {
    exclude (EXCLUDE_FILES)
    exclude (PROCESS_FILES)
    exclude ("appletlib")
    exclude ("**/*locales")
    exclude ("*locales/**")
    exclude ("utils/InstallAnywhere")

    exclude (getdown_files_dir)
    // getdown_website_dir and getdown_archive_dir moved to build/website/docroot/getdown
    //exclude (getdown_website_dir)
    //exclude (getdown_archive_dir)

    // exluding these as not using jars as modules yet
    exclude ("${j11modDir}/**/*.jar")
  }
  from(jalviewDir) {
    include(INCLUDE_FILES)
  }
//  from (jalviewDir) {
//    // explicit includes for stuff that seemed to not get included
//    include(fileTree("test/**/*."))
//    exclude(EXCLUDE_FILES)
//    exclude(PROCESS_FILES)
//  }

  from(file(buildProperties).getParent()) {
    include(file(buildProperties).getName())
    rename(file(buildProperties).getName(), "build_properties")
    filter({ line ->
      line.replaceAll("^INSTALLATION=.*\$","INSTALLATION=Source Release"+" git-commit\\\\:"+gitHash+" ["+gitBranch+"]")
    })
  }

  def sourceTarBuildDir = "${buildDir}/sourceTar"
  from(sourceTarBuildDir) {
    // this includes the appended RELEASE properties file
  }
}

task dataInstallersJson {
  group "website"
  description "Create the installers-VERSION.json data file for installer files created"

  mustRunAfter installers
  mustRunAfter shadowJar
  mustRunAfter sourceDist
  mustRunAfter getdownArchive

  def installersOutputTxt = file("${jalviewDir}/${install4jBuildDir}/output.txt")
  def installersSha256 = file("${jalviewDir}/${install4jBuildDir}/sha256sums")

  if (installersOutputTxt.exists()) {
    inputs.file(installersOutputTxt)
  }
  if (install4jCheckSums && installersSha256.exists()) {
    inputs.file(installersSha256)
  }
  [
    shadowJar.archiveFile, // executable JAR
    getdownVersionLaunchJvl, // version JVL
    sourceDist.archiveFile // source TGZ
  ].each { fileName ->
    if (file(fileName).exists()) {
      inputs.file(fileName)
    }
  }

  outputs.file(hugoDataJsonFile)

  doFirst {
    writeDataJsonFile(installersOutputTxt, installersSha256, hugoDataJsonFile)
  }
}

task helppages {
  group "help"
  description "Copies all help pages to build dir. Runs ant task 'pubhtmlhelp'."

  dependsOn copyHelp
  dependsOn pubhtmlhelp
  
  inputs.dir("${helpBuildDir}/${help_dir}")
  outputs.dir("${buildDir}/distributions/${help_dir}")
}


task j2sSetHeadlessBuild {
  doFirst {
    IN_ECLIPSE = false
  }
}


task jalviewjsEnableAltFileProperty(type: WriteProperties) {
  group "jalviewjs"
  description "Enable the alternative J2S Config file for headless build"

  outputFile = jalviewjsJ2sSettingsFileName
  def j2sPropsFile = file(jalviewjsJ2sSettingsFileName)
  def j2sProps = new Properties()
  if (j2sPropsFile.exists()) {
    try {
      def j2sPropsFileFIS = new FileInputStream(j2sPropsFile)
      j2sProps.load(j2sPropsFileFIS)
      j2sPropsFileFIS.close()

      j2sProps.each { prop, val ->
        property(prop, val)
      }
    } catch (Exception e) {
      println("Exception reading ${jalviewjsJ2sSettingsFileName}")
      e.printStackTrace()
    }
  }
  if (! j2sProps.stringPropertyNames().contains(jalviewjs_j2s_alt_file_property_config)) {
    property(jalviewjs_j2s_alt_file_property_config, jalviewjs_j2s_alt_file_property)
  }
}


task jalviewjsSetEclipseWorkspace {
  def propKey = "jalviewjs_eclipse_workspace"
  def propVal = null
  if (project.hasProperty(propKey)) {
    propVal = project.getProperty(propKey)
    if (propVal.startsWith("~/")) {
      propVal = System.getProperty("user.home") + propVal.substring(1)
    }
  }
  def propsFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_workspace_location_file}"
  def propsFile = file(propsFileName)
  def eclipseWsDir = propVal
  def props = new Properties()

  def writeProps = true
  if (( eclipseWsDir == null || !file(eclipseWsDir).exists() ) && propsFile.exists()) {
    def ins = new FileInputStream(propsFileName)
    props.load(ins)
    ins.close()
    if (props.getProperty(propKey, null) != null) {
      eclipseWsDir = props.getProperty(propKey)
      writeProps = false
    }
  }

  if (eclipseWsDir == null || !file(eclipseWsDir).exists()) {
    def tempDir = File.createTempDir()
    eclipseWsDir = tempDir.getAbsolutePath()
    writeProps = true
  }
  eclipseWorkspace = file(eclipseWsDir)

  doFirst {
    // do not run a headless transpile when we claim to be in Eclipse
    if (IN_ECLIPSE) {
      println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
      throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
    } else {
      println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
    }

    if (writeProps) {
      props.setProperty(propKey, eclipseWsDir)
      propsFile.parentFile.mkdirs()
      def bytes = new ByteArrayOutputStream()
      props.store(bytes, null)
      def propertiesString = bytes.toString()
      propsFile.text = propertiesString
      print("NEW ")
    } else {
      print("EXISTING ")
    }

    println("ECLIPSE WORKSPACE: "+eclipseWorkspace.getPath())
  }

  //inputs.property(propKey, eclipseWsDir) // eclipseWsDir only gets set once this task runs, so will be out-of-date
  outputs.file(propsFileName)
  outputs.upToDateWhen { eclipseWorkspace.exists() && propsFile.exists() }
}


task jalviewjsEclipsePaths {
  def eclipseProduct

  def eclipseRoot = jalviewjs_eclipse_root
  if (eclipseRoot.startsWith("~/")) {
    eclipseRoot = System.getProperty("user.home") + eclipseRoot.substring(1)
  }
  if (OperatingSystem.current().isMacOsX()) {
    eclipseRoot += "/Eclipse.app"
    eclipseBinary = "${eclipseRoot}/Contents/MacOS/eclipse"
    eclipseProduct = "${eclipseRoot}/Contents/Eclipse/.eclipseproduct"
  } else if (OperatingSystem.current().isWindows()) { // check these paths!!
    if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
      eclipseRoot += "/eclipse"
    }
    eclipseBinary = "${eclipseRoot}/eclipse.exe"
    eclipseProduct = "${eclipseRoot}/.eclipseproduct"
  } else { // linux or unix
    if (file("${eclipseRoot}/eclipse").isDirectory() && file("${eclipseRoot}/eclipse/.eclipseproduct").exists()) {
      eclipseRoot += "/eclipse"
println("eclipseDir exists")
    }
    eclipseBinary = "${eclipseRoot}/eclipse"
    eclipseProduct = "${eclipseRoot}/.eclipseproduct"
  }

  eclipseVersion = "4.13" // default
  def assumedVersion = true
  if (file(eclipseProduct).exists()) {
    def fis = new FileInputStream(eclipseProduct)
    def props = new Properties()
    props.load(fis)
    eclipseVersion = props.getProperty("version")
    fis.close()
    assumedVersion = false
  }
  
  def propKey = "eclipse_debug"
  eclipseDebug = (project.hasProperty(propKey) && project.getProperty(propKey).equals("true"))

  doFirst {
    // do not run a headless transpile when we claim to be in Eclipse
    if (IN_ECLIPSE) {
      println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
      throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
    } else {
      println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
    }

    if (!assumedVersion) {
      println("ECLIPSE VERSION=${eclipseVersion}")
    }
  }
}


task printProperties {
  group "Debug"
  description "Output to console all System.properties"
  doFirst {
    System.properties.each { key, val -> System.out.println("Property: ${key}=${val}") }
  }
}


task eclipseSetup {
  dependsOn eclipseProject
  dependsOn eclipseClasspath
  dependsOn eclipseJdt
}


// this version (type: Copy) will delete anything in the eclipse dropins folder that isn't in fromDropinsDir
task jalviewjsEclipseCopyDropins(type: Copy) {
  dependsOn jalviewjsEclipsePaths

  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_eclipse_dropins_dir}", include: "*.jar")
  inputFiles += file("${jalviewDir}/${jalviewjsJ2sPlugin}")
  def outputDir = "${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}"

  from inputFiles
  into outputDir
}


// this eclipse -clean doesn't actually work
task jalviewjsCleanEclipse(type: Exec) {
  dependsOn eclipseSetup
  dependsOn jalviewjsEclipsePaths
  dependsOn jalviewjsEclipseCopyDropins

  executable(eclipseBinary)
  args(["-nosplash", "--launcher.suppressErrors", "-data", eclipseWorkspace.getPath(), "-clean", "-console", "-consoleLog"])
  if (eclipseDebug) {
    args += "-debug"
  }
  args += "-l"

  def inputString = """exit
y
"""
  def inputByteStream = new ByteArrayInputStream(inputString.getBytes())
  standardInput = inputByteStream
}

/* not really working yet
jalviewjsEclipseCopyDropins.finalizedBy jalviewjsCleanEclipse
*/


task jalviewjsTransferUnzipSwingJs {
  def file_zip = "${jalviewDir}/${jalviewjs_swingjs_zip}"

  doLast {
    copy {
      from zipTree(file_zip)
      into "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
    }
  }

  inputs.file file_zip
  outputs.dir "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
}


task jalviewjsTransferUnzipLib {
  def zipFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_libjs_dir}", include: "*.zip")

  doLast {
    zipFiles.each { file_zip -> 
      copy {
        from zipTree(file_zip)
        into "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
      }
    }
  }

  inputs.files zipFiles
  outputs.dir "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
}


task jalviewjsTransferUnzipAllLibs {
  dependsOn jalviewjsTransferUnzipSwingJs
  dependsOn jalviewjsTransferUnzipLib
}


task jalviewjsCreateJ2sSettings(type: WriteProperties) {
  group "JalviewJS"
  description "Create the alternative j2s file from the j2s.* properties"

  jalviewjsJ2sProps = project.properties.findAll { it.key.startsWith("j2s.") }.sort { it.key }
  def siteDirProperty = "j2s.site.directory"
  def setSiteDir = false
  jalviewjsJ2sProps.each { prop, val ->
    if (val != null) {
      if (prop == siteDirProperty) {
        if (!(val.startsWith('/') || val.startsWith("file://") )) {
          val = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${val}"
        }
        setSiteDir = true
      }
      property(prop,val)
    }
    if (!setSiteDir) { // default site location, don't override specifically set property
      property(siteDirProperty,"${jalviewDirRelativePath}/${jalviewjsTransferSiteJsDir}")
    }
  }
  outputFile = jalviewjsJ2sAltSettingsFileName

  if (! IN_ECLIPSE) {
    inputs.properties(jalviewjsJ2sProps)
    outputs.file(jalviewjsJ2sAltSettingsFileName)
  }
}


task jalviewjsEclipseSetup {
  dependsOn jalviewjsEclipseCopyDropins
  dependsOn jalviewjsSetEclipseWorkspace
  dependsOn jalviewjsCreateJ2sSettings
}


task jalviewjsSyncAllLibs (type: Sync) {
  dependsOn jalviewjsTransferUnzipAllLibs
  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteLibDir}")
  inputFiles += fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}")
  def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"

  from inputFiles
  into outputDir
  def outputFiles = []
  rename { filename ->
    outputFiles += "${outputDir}/${filename}"
    null
  }
  preserve {
    include "**"
  }

  // should this be exclude really ?
  duplicatesStrategy "INCLUDE"

  outputs.files outputFiles
  inputs.files inputFiles
}


task jalviewjsSyncResources (type: Sync) {
  dependsOn buildResources

  def inputFiles = fileTree(dir: resourcesBuildDir)
  def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"

  from inputFiles
  into outputDir
  def outputFiles = []
  rename { filename ->
    outputFiles += "${outputDir}/${filename}"
    null
  }
  preserve {
    include "**"
  }
  outputs.files outputFiles
  inputs.files inputFiles
}


task jalviewjsSyncSiteResources (type: Sync) {
  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjs_site_resource_dir}")
  def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"

  from inputFiles
  into outputDir
  def outputFiles = []
  rename { filename ->
    outputFiles += "${outputDir}/${filename}"
    null
  }
  preserve {
    include "**"
  }
  outputs.files outputFiles
  inputs.files inputFiles
}


task jalviewjsSyncBuildProperties (type: Sync) {
  dependsOn createBuildProperties
  def inputFiles = [file(buildProperties)]
  def outputDir = "${jalviewDir}/${jalviewjsSiteDir}/${jalviewjs_j2s_subdir}"

  from inputFiles
  into outputDir
  def outputFiles = []
  rename { filename ->
    outputFiles += "${outputDir}/${filename}"
    null
  }
  preserve {
    include "**"
  }
  outputs.files outputFiles
  inputs.files inputFiles
}


task jalviewjsProjectImport(type: Exec) {
  dependsOn eclipseSetup
  dependsOn jalviewjsEclipsePaths
  dependsOn jalviewjsEclipseSetup

  doFirst {
    // do not run a headless import when we claim to be in Eclipse
    if (IN_ECLIPSE) {
      println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
      throw new StopExecutionException("Not running headless import whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
    } else {
      println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
    }
  }

  //def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview/org.eclipse.jdt.core"
  def projdir = eclipseWorkspace.getPath()+"/.metadata/.plugins/org.eclipse.core.resources/.projects/jalview"
  executable(eclipseBinary)
  args(["-nosplash", "--launcher.suppressErrors", "-application", "com.seeq.eclipse.importprojects.headlessimport", "-data", eclipseWorkspace.getPath(), "-import", jalviewDirAbsolutePath])
  if (eclipseDebug) {
    args += "-debug"
  }
  args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
  if (!IN_ECLIPSE) {
    args += [ "-D${j2sHeadlessBuildProperty}=true" ]
    args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
  }

  inputs.file("${jalviewDir}/.project")
  outputs.upToDateWhen { 
    file(projdir).exists()
  }
}


task jalviewjsTranspile(type: Exec) {
  dependsOn jalviewjsEclipseSetup 
  dependsOn jalviewjsProjectImport
  dependsOn jalviewjsEclipsePaths
  if (!IN_ECLIPSE) {
    dependsOn jalviewjsEnableAltFileProperty
  }

  doFirst {
    // do not run a headless transpile when we claim to be in Eclipse
    if (IN_ECLIPSE) {
      println("Skipping task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
      throw new StopExecutionException("Not running headless transpile whilst IN_ECLIPSE is '${IN_ECLIPSE}'")
    } else {
      println("Running task ${name} as IN_ECLIPSE=${IN_ECLIPSE}")
    }
  }

  executable(eclipseBinary)
  args(["-nosplash", "--launcher.suppressErrors", "-application", "org.eclipse.jdt.apt.core.aptBuild", "-data", eclipseWorkspace, "-${jalviewjs_eclipse_build_arg}", eclipse_project_name ])
  if (eclipseDebug) {
    args += "-debug"
  }
  args += [ "--launcher.appendVmargs", "-vmargs", "-Dorg.eclipse.equinox.p2.reconciler.dropins.directory=${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_eclipse_tmp_dropins_dir}" ]
  if (!IN_ECLIPSE) {
    args += [ "-D${j2sHeadlessBuildProperty}=true" ]
    args += [ "-D${jalviewjs_j2s_alt_file_property}=${jalviewjsJ2sAltSettingsFileName}" ]
  }

  def stdout
  def stderr
  doFirst {
    stdout = new ByteArrayOutputStream()
    stderr = new ByteArrayOutputStream()

    def logOutFileName = "${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}"
    def logOutFile = file(logOutFileName)
    logOutFile.createNewFile()
    logOutFile.text = """ROOT: ${jalviewjs_eclipse_root}
BINARY: ${eclipseBinary}
VERSION: ${eclipseVersion}
WORKSPACE: ${eclipseWorkspace}
DEBUG: ${eclipseDebug}
----
"""
    def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
    // combine stdout and stderr
    def logErrFOS = logOutFOS

    if (jalviewjs_j2s_to_console.equals("true")) {
      standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
        new org.apache.tools.ant.util.TeeOutputStream(
          logOutFOS,
          stdout),
        System.out)
      errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
        new org.apache.tools.ant.util.TeeOutputStream(
          logErrFOS,
          stderr),
        System.err)
    } else {
      standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
        logOutFOS,
        stdout)
      errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
        logErrFOS,
        stderr)
    }
  }

  doLast {
    if (stdout.toString().contains("Error processing ")) {
      // j2s did not complete transpile
      //throw new TaskExecutionException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
      if (jalviewjs_ignore_transpile_errors.equals("true")) {
        println("IGNORING TRANSPILE ERRORS")
        println("See eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
      } else {
        throw new GradleException("Error during transpilation:\n${stderr}\nSee eclipse transpile log file '${jalviewDir}/${jalviewjsBuildDir}/${jalviewjs_j2s_transpile_stdout}'")
      }
    }
  }

  inputs.dir("${jalviewDir}/${sourceDir}")
  outputs.dir("${jalviewDir}/${jalviewjsTransferSiteJsDir}")
  outputs.upToDateWhen( { file("${jalviewDir}/${jalviewjsTransferSiteJsDir}${jalviewjs_server_resource}").exists() } )
}


def jalviewjsCallCore(String name, FileCollection list, String prefixFile, String suffixFile, String jsfile, String zjsfile, File logOutFile, Boolean logOutConsole) {

  def stdout = new ByteArrayOutputStream()
  def stderr = new ByteArrayOutputStream()

  def coreFile = file(jsfile)
  def msg = ""
  msg = "Creating core for ${name}...\nGenerating ${jsfile}"
  println(msg)
  logOutFile.createNewFile()
  logOutFile.append(msg+"\n")

  def coreTop = file(prefixFile)
  def coreBottom = file(suffixFile)
  coreFile.getParentFile().mkdirs()
  coreFile.createNewFile()
  coreFile.write( coreTop.getText("UTF-8") )
  list.each {
    f ->
    if (f.exists()) {
      def t = f.getText("UTF-8")
      t.replaceAll("Clazz\\.([^_])","Clazz_${1}")
      coreFile.append( t )
    } else {
      msg = "...file '"+f.getPath()+"' does not exist, skipping"
      println(msg)
      logOutFile.append(msg+"\n")
    }
  }
  coreFile.append( coreBottom.getText("UTF-8") )

  msg = "Generating ${zjsfile}"
  println(msg)
  logOutFile.append(msg+"\n")
  def logOutFOS = new FileOutputStream(logOutFile, true) // true == append
  def logErrFOS = logOutFOS

  javaexec {
    classpath = files(["${jalviewDir}/${jalviewjs_closure_compiler}"])
    main = "com.google.javascript.jscomp.CommandLineRunner"
    jvmArgs = [ "-Dfile.encoding=UTF-8" ]
    args = [ "--compilation_level", "SIMPLE_OPTIMIZATIONS", "--warning_level", "QUIET", "--charset", "UTF-8", "--js", jsfile, "--js_output_file", zjsfile ]
    maxHeapSize = "2g"

    msg = "\nRunning '"+commandLine.join(' ')+"'\n"
    println(msg)
    logOutFile.append(msg+"\n")

    if (logOutConsole) {
      standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
        new org.apache.tools.ant.util.TeeOutputStream(
          logOutFOS,
          stdout),
        standardOutput)
        errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
          new org.apache.tools.ant.util.TeeOutputStream(
            logErrFOS,
            stderr),
          System.err)
    } else {
      standardOutput = new org.apache.tools.ant.util.TeeOutputStream(
        logOutFOS,
        stdout)
        errorOutput = new org.apache.tools.ant.util.TeeOutputStream(
          logErrFOS,
          stderr)
    }
  }
  msg = "--"
  println(msg)
  logOutFile.append(msg+"\n")
}


task jalviewjsBuildAllCores {
  group "JalviewJS"
  description "Build the core js lib closures listed in the classlists dir"
  dependsOn jalviewjsTranspile
  dependsOn jalviewjsTransferUnzipSwingJs

  def j2sDir = "${jalviewDir}/${jalviewjsTransferSiteJsDir}/${jalviewjs_j2s_subdir}"
  def swingJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_j2s_subdir}"
  def libJ2sDir = "${jalviewDir}/${jalviewjsTransferSiteLibDir}/${jalviewjs_j2s_subdir}"
  def jsDir = "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}/${jalviewjs_js_subdir}"
  def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}/${jalviewjs_j2s_subdir}/core"
  def prefixFile = "${jsDir}/core/coretop2.js"
  def suffixFile = "${jsDir}/core/corebottom2.js"

  inputs.file prefixFile
  inputs.file suffixFile

  def classlistFiles = []
  // add the classlists found int the jalviewjs_classlists_dir
  fileTree(dir: "${jalviewDir}/${jalviewjs_classlists_dir}", include: "*.txt").each {
    file ->
    def name = file.getName() - ".txt"
    classlistFiles += [
      'file': file,
      'name': name
    ]
  }

  // _jmol and _jalview cores. Add any other peculiar classlist.txt files here
  //classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jmol}"), 'name': "_jvjmol" ]
  classlistFiles += [ 'file': file("${jalviewDir}/${jalviewjs_classlist_jalview}"), 'name': jalviewjsJalviewCoreName ]

  jalviewjsCoreClasslists = []

  classlistFiles.each {
    hash ->

    def file = hash['file']
    if (! file.exists()) {
      //println("...classlist file '"+file.getPath()+"' does not exist, skipping")
      return false // this is a "continue" in groovy .each closure
    }
    def name = hash['name']
    if (name == null) {
      name = file.getName() - ".txt"
    }

    def filelist = []
    file.eachLine {
      line ->
        filelist += line
    }
    def list = fileTree(dir: j2sDir, includes: filelist)

    def jsfile = "${outputDir}/core${name}.js"
    def zjsfile = "${outputDir}/core${name}.z.js"

    jalviewjsCoreClasslists += [
      'jsfile': jsfile,
      'zjsfile': zjsfile,
      'list': list,
      'name': name
    ]

    inputs.file(file)
    inputs.files(list)
    outputs.file(jsfile)
    outputs.file(zjsfile)
  }
  
  // _stevesoft core. add any cores without a classlist here (and the inputs and outputs)
  def stevesoftClasslistName = "_stevesoft"
  def stevesoftClasslist = [
    'jsfile': "${outputDir}/core${stevesoftClasslistName}.js",
    'zjsfile': "${outputDir}/core${stevesoftClasslistName}.z.js",
    'list': fileTree(dir: j2sDir, include: "com/stevesoft/pat/**/*.js"),
    'name': stevesoftClasslistName
  ]
  jalviewjsCoreClasslists += stevesoftClasslist
  inputs.files(stevesoftClasslist['list'])
  outputs.file(stevesoftClasslist['jsfile'])
  outputs.file(stevesoftClasslist['zjsfile'])

  // _all core
  def allClasslistName = "_all"
  def allJsFiles = fileTree(dir: j2sDir, include: "**/*.js")
  allJsFiles += fileTree(
    dir: libJ2sDir,
    include: "**/*.js",
    excludes: [
      // these exlusions are files that the closure-compiler produces errors for. Should fix them
      "**/org/jmol/jvxl/readers/IsoIntersectFileReader.js",
      "**/org/jmol/export/JSExporter.js"
    ]
  )
  allJsFiles += fileTree(
    dir: swingJ2sDir,
    include: "**/*.js",
    excludes: [
      // these exlusions are files that the closure-compiler produces errors for. Should fix them
      "**/sun/misc/Unsafe.js",
      "**/swingjs/jquery/jquery-editable-select.js",
      "**/swingjs/jquery/j2sComboBox.js",
      "**/sun/misc/FloatingDecimal.js"
    ]
  )
  def allClasslist = [
    'jsfile': "${outputDir}/core${allClasslistName}.js",
    'zjsfile': "${outputDir}/core${allClasslistName}.z.js",
    'list': allJsFiles,
    'name': allClasslistName
  ]
  // not including this version of "all" core at the moment
  //jalviewjsCoreClasslists += allClasslist
  inputs.files(allClasslist['list'])
  outputs.file(allClasslist['jsfile'])
  outputs.file(allClasslist['zjsfile'])

  doFirst {
    def logOutFile = file("${jalviewDirAbsolutePath}/${jalviewjsBuildDir}/${jalviewjs_j2s_closure_stdout}")
    logOutFile.getParentFile().mkdirs()
    logOutFile.createNewFile()
    logOutFile.write(getDate("yyyy-MM-dd HH:mm:ss")+" jalviewjsBuildAllCores\n----\n")

    jalviewjsCoreClasslists.each {
      jalviewjsCallCore(it.name, it.list, prefixFile, suffixFile, it.jsfile, it.zjsfile, logOutFile, jalviewjs_j2s_to_console.equals("true"))
    }
  }

}


def jalviewjsPublishCoreTemplate(String coreName, String templateName, File inputFile, String outputFile) {
  copy {
    from inputFile
    into file(outputFile).getParentFile()
    rename { filename ->
      if (filename.equals(inputFile.getName())) {
        return file(outputFile).getName()
      }
      return null
    }
    filter(ReplaceTokens,
      beginToken: '_',
      endToken: '_',
      tokens: [
        'MAIN': '"'+main_class+'"',
        'CODE': "null",
        'NAME': jalviewjsJalviewTemplateName+" [core ${coreName}]",
        'COREKEY': jalviewjs_core_key,
        'CORENAME': coreName
      ]
    )
  }
}


task jalviewjsPublishCoreTemplates {
  dependsOn jalviewjsBuildAllCores
  def inputFileName = "${jalviewDir}/${j2s_coretemplate_html}"
  def inputFile = file(inputFileName)
  def outputDir = "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"

  def outputFiles = []
  jalviewjsCoreClasslists.each { cl ->
    def outputFile = "${outputDir}/${jalviewjsJalviewTemplateName}_${cl.name}.html"
    cl['outputfile'] = outputFile
    outputFiles += outputFile
  }

  doFirst {
    jalviewjsCoreClasslists.each { cl ->
      jalviewjsPublishCoreTemplate(cl.name, jalviewjsJalviewTemplateName, inputFile, cl.outputfile)
    }
  }
  inputs.file(inputFile)
  outputs.files(outputFiles)
}


task jalviewjsSyncCore (type: Sync) {
  dependsOn jalviewjsBuildAllCores
  dependsOn jalviewjsPublishCoreTemplates
  def inputFiles = fileTree(dir: "${jalviewDir}/${jalviewjsTransferSiteCoreDir}")
  def outputDir = "${jalviewDir}/${jalviewjsSiteDir}"

  from inputFiles
  into outputDir
  def outputFiles = []
  rename { filename ->
    outputFiles += "${outputDir}/${filename}"
    null
  }
  preserve {
    include "**"
  }
  outputs.files outputFiles
  inputs.files inputFiles
}


// this Copy version of TransferSiteJs will delete anything else in the target dir
task jalviewjsCopyTransferSiteJs(type: Copy) {
  dependsOn jalviewjsTranspile
  from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
  into "${jalviewDir}/${jalviewjsSiteDir}"
}


// this Sync version of TransferSite is used by buildship to keep the website automatically up to date when a file changes
task jalviewjsSyncTransferSiteJs(type: Sync) {
  from "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
  include "**/*.*"
  into "${jalviewDir}/${jalviewjsSiteDir}"
  preserve {
    include "**"
  }
}


jalviewjsSyncAllLibs.mustRunAfter jalviewjsCopyTransferSiteJs
jalviewjsSyncResources.mustRunAfter jalviewjsCopyTransferSiteJs
jalviewjsSyncSiteResources.mustRunAfter jalviewjsCopyTransferSiteJs
jalviewjsSyncBuildProperties.mustRunAfter jalviewjsCopyTransferSiteJs

jalviewjsSyncAllLibs.mustRunAfter jalviewjsSyncTransferSiteJs
jalviewjsSyncResources.mustRunAfter jalviewjsSyncTransferSiteJs
jalviewjsSyncSiteResources.mustRunAfter jalviewjsSyncTransferSiteJs
jalviewjsSyncBuildProperties.mustRunAfter jalviewjsSyncTransferSiteJs


task jalviewjsPrepareSite {
  group "JalviewJS"
  description "Prepares the website folder including unzipping files and copying resources"
  dependsOn jalviewjsSyncAllLibs
  dependsOn jalviewjsSyncResources
  dependsOn jalviewjsSyncSiteResources
  dependsOn jalviewjsSyncBuildProperties
  dependsOn jalviewjsSyncCore
}


task jalviewjsBuildSite {
  group "JalviewJS"
  description "Builds the whole website including transpiled code"
  dependsOn jalviewjsCopyTransferSiteJs
  dependsOn jalviewjsPrepareSite
}


task cleanJalviewjsTransferSite {
  doFirst {
    delete "${jalviewDir}/${jalviewjsTransferSiteJsDir}"
    delete "${jalviewDir}/${jalviewjsTransferSiteLibDir}"
    delete "${jalviewDir}/${jalviewjsTransferSiteSwingJsDir}"
    delete "${jalviewDir}/${jalviewjsTransferSiteCoreDir}"
  }
}


task cleanJalviewjsSite {
  dependsOn cleanJalviewjsTransferSite
  doFirst {
    delete "${jalviewDir}/${jalviewjsSiteDir}"
  }
}


task jalviewjsSiteTar(type: Tar) {
  group "JalviewJS"
  description "Creates a tar.gz file for the website"
  dependsOn jalviewjsBuildSite
  def outputFilename = "jalviewjs-site-${JALVIEW_VERSION}.tar.gz"
  archiveFileName = outputFilename

  compression Compression.GZIP

  from "${jalviewDir}/${jalviewjsSiteDir}"
  into jalviewjs_site_dir // this is inside the tar file

  inputs.dir("${jalviewDir}/${jalviewjsSiteDir}")
}


task jalviewjsServer {
  group "JalviewJS"
  def filename = "jalviewjsTest.html"
  description "Starts a webserver on localhost to test the website. See ${filename} to access local site on most recently used port."
  def htmlFile = "${jalviewDirAbsolutePath}/${filename}"
  doLast {

    def factory
    try {
      def f = Class.forName("org.gradle.plugins.javascript.envjs.http.simple.SimpleHttpFileServerFactory")
      factory = f.newInstance()
    } catch (ClassNotFoundException e) {
      throw new GradleException("Unable to create SimpleHttpFileServerFactory")
    }
    def port = Integer.valueOf(jalviewjs_server_port)
    def start = port
    def running = false
    def url
    def jalviewjsServer
    while(port < start+1000 && !running) {
      try {
        def doc_root = new File("${jalviewDirAbsolutePath}/${jalviewjsSiteDir}")
        jalviewjsServer = factory.start(doc_root, port)
        running = true
        url = jalviewjsServer.getResourceUrl(jalviewjs_server_resource)
        println("SERVER STARTED with document root ${doc_root}.")
        println("Go to "+url+" . Run  gradle --stop  to stop (kills all gradle daemons).")
        println("For debug: "+url+"?j2sdebug")
        println("For verbose: "+url+"?j2sverbose")
      } catch (Exception e) {
        port++;
      }
    }
    def htmlText = """
      <p><a href="${url}">JalviewJS Test. &lt;${url}&gt;</a></p>
      <p><a href="${url}?j2sdebug">JalviewJS Test with debug. &lt;${url}?j2sdebug&gt;</a></p>
      <p><a href="${url}?j2sverbose">JalviewJS Test with verbose. &lt;${url}?j2sdebug&gt;</a></p>
      """
    jalviewjsCoreClasslists.each { cl ->
      def urlcore = jalviewjsServer.getResourceUrl(file(cl.outputfile).getName())
      htmlText += """
      <p><a href="${urlcore}">${jalviewjsJalviewTemplateName} [core ${cl.name}]. &lt;${urlcore}&gt;</a></p>
      """
      println("For core ${cl.name}: "+urlcore)
    }

    file(htmlFile).text = htmlText
  }

  outputs.file(htmlFile)
  outputs.upToDateWhen({false})
}


task cleanJalviewjsAll {
  group "JalviewJS"
  description "Delete all configuration and build artifacts to do with JalviewJS build"
  dependsOn cleanJalviewjsSite
  dependsOn jalviewjsEclipsePaths
  
  doFirst {
    delete "${jalviewDir}/${jalviewjsBuildDir}"
    delete "${jalviewDir}/${eclipse_bin_dir}"
    if (eclipseWorkspace != null && file(eclipseWorkspace.getAbsolutePath()+"/.metadata").exists()) {
      delete file(eclipseWorkspace.getAbsolutePath()+"/.metadata")
    }
    delete jalviewjsJ2sAltSettingsFileName
  }

  outputs.upToDateWhen( { false } )
}


task jalviewjsIDE_checkJ2sPlugin {
  group "00 JalviewJS in Eclipse"
  description "Compare the swingjs/net.sf.j2s.core(-j11)?.jar file with the Eclipse IDE's plugin version (found in the 'dropins' dir)"

  doFirst {
    def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
    def j2sPluginFile = file(j2sPlugin)
    def eclipseHome = System.properties["eclipse.home.location"]
    if (eclipseHome == null || ! IN_ECLIPSE) {
      throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. Skipping J2S Plugin Check.")
    }
    def eclipseJ2sPluginDirs = [ "${eclipseHome}/dropins" ]
    def altPluginsDir = System.properties["org.eclipse.equinox.p2.reconciler.dropins.directory"]
    if (altPluginsDir != null && file(altPluginsDir).exists()) {
      eclipseJ2sPluginDirs += altPluginsDir
    }
    def foundPlugin = false
    def j2sPluginFileName = j2sPluginFile.getName()
    def eclipseJ2sPlugin
    def eclipseJ2sPluginFile
    eclipseJ2sPluginDirs.any { dir ->
      eclipseJ2sPlugin = "${dir}/${j2sPluginFileName}"
      eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
      if (eclipseJ2sPluginFile.exists()) {
        foundPlugin = true
        return true
      }
    }
    if (!foundPlugin) {
      def msg = "Eclipse J2S Plugin is not installed (could not find '${j2sPluginFileName}' in\n"+eclipseJ2sPluginDirs.join("\n")+"\n)\nTry running task jalviewjsIDE_copyJ2sPlugin"
      System.err.println(msg)
      throw new StopExecutionException(msg)
    }

    def digest = MessageDigest.getInstance("MD5")

    digest.update(j2sPluginFile.text.bytes)
    def j2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')

    digest.update(eclipseJ2sPluginFile.text.bytes)
    def eclipseJ2sPluginMd5 = new BigInteger(1, digest.digest()).toString(16).padLeft(32, '0')
     
    if (j2sPluginMd5 != eclipseJ2sPluginMd5) {
      def msg = "WARNING! Eclipse J2S Plugin '${eclipseJ2sPlugin}' is different to this commit's version '${j2sPlugin}'"
      System.err.println(msg)
      throw new StopExecutionException(msg)
    } else {
      def msg = "Eclipse J2S Plugin '${eclipseJ2sPlugin}' is the same as '${j2sPlugin}' (this is good)"
      println(msg)
    }
  }
}

task jalviewjsIDE_copyJ2sPlugin {
  group "00 JalviewJS in Eclipse"
  description "Copy the swingjs/net.sf.j2s.core(-j11)?.jar file into the Eclipse IDE's 'dropins' dir"

  doFirst {
    def j2sPlugin = string("${jalviewDir}/${jalviewjsJ2sPlugin}")
    def j2sPluginFile = file(j2sPlugin)
    def eclipseHome = System.properties["eclipse.home.location"]
    if (eclipseHome == null || ! IN_ECLIPSE) {
      throw new StopExecutionException("Cannot find running Eclipse home from System.properties['eclipse.home.location']. NOT copying J2S Plugin.")
    }
    def eclipseJ2sPlugin = "${eclipseHome}/dropins/${j2sPluginFile.getName()}"
    def eclipseJ2sPluginFile = file(eclipseJ2sPlugin)
    def msg = "WARNING! Copying this commit's j2s plugin '${j2sPlugin}' to Eclipse J2S Plugin '${eclipseJ2sPlugin}'\n* May require an Eclipse restart"
    System.err.println(msg)
    copy {
      from j2sPlugin
      eclipseJ2sPluginFile.getParentFile().mkdirs()
      into eclipseJ2sPluginFile.getParent()
    }
  }
}


task jalviewjsIDE_j2sFile {
  group "00 JalviewJS in Eclipse"
  description "Creates the .j2s file"
  dependsOn jalviewjsCreateJ2sSettings
}


task jalviewjsIDE_SyncCore {
  group "00 JalviewJS in Eclipse"
  description "Build the core js lib closures listed in the classlists dir and publish core html from template"
  dependsOn jalviewjsSyncCore
}


task jalviewjsIDE_SyncSiteAll {
  dependsOn jalviewjsSyncAllLibs
  dependsOn jalviewjsSyncResources
  dependsOn jalviewjsSyncSiteResources
  dependsOn jalviewjsSyncBuildProperties
}


cleanJalviewjsTransferSite.mustRunAfter jalviewjsIDE_SyncSiteAll


task jalviewjsIDE_PrepareSite {
  group "00 JalviewJS in Eclipse"
  description "Sync libs and resources to site dir, but not closure cores"

  dependsOn jalviewjsIDE_SyncSiteAll
  //dependsOn cleanJalviewjsTransferSite // not sure why this clean is here -- will slow down a re-run of this task
}


task jalviewjsIDE_AssembleSite {
  group "00 JalviewJS in Eclipse"
  description "Assembles unzipped supporting zipfiles, resources, site resources and closure cores into the Eclipse transpiled site"
  dependsOn jalviewjsPrepareSite
}


task jalviewjsIDE_SiteClean {
  group "00 JalviewJS in Eclipse"
  description "Deletes the Eclipse transpiled site"
  dependsOn cleanJalviewjsSite
}


task jalviewjsIDE_Server {
  group "00 JalviewJS in Eclipse"
  description "Starts a webserver on localhost to test the website"
  dependsOn jalviewjsServer
}


// buildship runs this at import or gradle refresh
task eclipseSynchronizationTask {
  //dependsOn eclipseSetup
  dependsOn createBuildProperties
  if (J2S_ENABLED) {
    dependsOn jalviewjsIDE_j2sFile
    dependsOn jalviewjsIDE_checkJ2sPlugin
    dependsOn jalviewjsIDE_PrepareSite
  }
}


// buildship runs this at build time or project refresh
task eclipseAutoBuildTask {
  //dependsOn jalviewjsIDE_checkJ2sPlugin
  //dependsOn jalviewjsIDE_PrepareSite
}


task jalviewjs {
  group "JalviewJS"
  description "Build the site"
  dependsOn jalviewjsBuildSite
}
