#!/usr/bin/env groovy
/*
 * Copyright (c) Contributors to the Open 3D Engine Project.
 * For complete copyright and license terms please see the LICENSE at the root of this distribution.
 *
 * SPDX-License-Identifier: Apache-2.0 OR MIT
 *
 */
import groovy.json.JsonOutput
PIPELINE_CONFIG_FILE = 'scripts/build/Jenkins/lumberyard.json'
INCREMENTAL_BUILD_SCRIPT_PATH = 'scripts/build/bootstrap/incremental_build_util.py'
PIPELINE_RETRY_ATTEMPTS = 3

EMPTY_JSON = readJSON text: '{}'

ENGINE_REPOSITORY_NAME = 'o3de'

// Branches with build snapshots
BUILD_SNAPSHOTS = ['development', 'stabilization/2110']

// Build snapshots with empty snapshot (for use with 'SNAPSHOT' pipeline paramater)
BUILD_SNAPSHOTS_WITH_EMPTY = BUILD_SNAPSHOTS + ''

// The default build snapshot to be selected in the 'SNAPSHOT' pipeline paramater
DEFAULT_BUILD_SNAPSHOT = BUILD_SNAPSHOTS_WITH_EMPTY.get(0)

// Branches with build snapshots as comma separated value string
env.BUILD_SNAPSHOTS = BUILD_SNAPSHOTS.join(",")

def pipelineProperties = []

def pipelineParameters = [
    // Build/clean Parameters
    // The CLEAN_OUTPUT_DIRECTORY is used by ci_build scripts. Creating the parameter here passes it as an environment variable to jobs and is consumed that way
    booleanParam(defaultValue: false, description: 'Deletes the contents of the output directory before building. This will cause a \"clean\" build. NOTE: does not imply CLEAN_ASSETS', name: 'CLEAN_OUTPUT_DIRECTORY'),
    booleanParam(defaultValue: false, description: 'Deletes the contents of the output directories of the AssetProcessor before building.', name: 'CLEAN_ASSETS'),
    booleanParam(defaultValue: false, description: 'Deletes the contents of the workspace and forces a complete pull.', name: 'CLEAN_WORKSPACE'),
    booleanParam(defaultValue: false, description: 'Recreates the volume used for the workspace. The volume will be created out of a snapshot taken from main.', name: 'RECREATE_VOLUME')
]

def palSh(cmd, lbl = '', winSlashReplacement = true) {
    if (env.IS_UNIX) {
        sh label: lbl,
           script: cmd
    } else if (winSlashReplacement) {
        bat label: lbl,
            script: cmd.replace('/','\\')
    } else {
        bat label: lbl,
            script: cmd
    }
}

def palMkdir(path) {
    if (env.IS_UNIX) {
        sh label: "Making directories ${path}",
           script: "mkdir -p ${path}"
    } else {
        def win_path = path.replace('/','\\')
        bat label: "Making directories ${win_path}",
            script: "mkdir ${win_path}."
    }
}

def palRm(path) {
    if (env.IS_UNIX) {
        sh label: "Removing ${path}",
           script: "rm ${path}"
    } else {
        def win_path = path.replace('/','\\')
        bat label: "Removing ${win_path}",
            script: "del /Q ${win_path}"
    }
}

def palRmDir(path) {
    if (env.IS_UNIX) {
        sh label: "Removing ${path}",
           script: "rm -rf ${path}"
    } else {
        def win_path = path.replace('/','\\')
        bat label: "Removing ${win_path}",
            script: "rd /s /q ${win_path}"
    }
}

def IsPullRequest(branchName) {
    // temporarily using the name to detect if we are in a PR
    // In the future we will check with github
    return branchName.startsWith('PR-')
}

def IsJobEnabled(branchName, buildTypeMap, pipelineName, platformName) {
    if (IsPullRequest(branchName)) {
        return buildTypeMap.value.TAGS && buildTypeMap.value.TAGS.contains(pipelineName)
    }
    def job_list_override = params.JOB_LIST_OVERRIDE ? params.JOB_LIST_OVERRIDE.tokenize(',') : ''
    if (!job_list_override.isEmpty()) {
        return params[platformName] && job_list_override.contains(buildTypeMap.key);
    } else {
        return params[platformName] && buildTypeMap.value.TAGS && buildTypeMap.value.TAGS.contains(pipelineName)
    }
}

def IsAPLogUpload(branchName, jobName) {
    return !IsPullRequest(branchName) && jobName.toLowerCase().contains('asset') && env.AP_LOGS_S3_BUCKET
}

def GetRunningPipelineName(JENKINS_JOB_NAME) {
    // If the job name has an underscore
    def job_parts = JENKINS_JOB_NAME.tokenize('/')[0].tokenize('_')
    if (job_parts.size() > 1) {
        return [job_parts.take(job_parts.size() - 1).join('_'), job_parts[job_parts.size()-1]]
    }
    return [job_parts[0], 'default']
}

@NonCPS
def RegexMatcher(str, regex) {
    def matcher = (str =~ regex)
    return matcher ? matcher.group(1) : null
}

def LoadPipelineConfig(String pipelineName, String branchName) {
    echo 'Loading pipeline config'
    def pipelineConfig = {}
    pipelineConfig = readJSON file: PIPELINE_CONFIG_FILE
    palRm(PIPELINE_CONFIG_FILE)
    pipelineConfig.platforms = EMPTY_JSON

    // Load the pipeline configs per platform
    pipelineConfig.PIPELINE_CONFIGS.each { pipeline_config ->
        def platform_regex = pipeline_config.replace('.','\\.').replace('*', '(.*)')
        if (!env.IS_UNIX) {
            platform_regex = platform_regex.replace('/','\\\\')
        }
        echo "Searching platform pipeline configs in ${pipeline_config} using ${platform_regex}"
        for (pipeline_config_path in findFiles(glob: pipeline_config)) {
            echo "\tFound platform pipeline config ${pipeline_config_path}"
            def platform = RegexMatcher(pipeline_config_path, platform_regex)
            if(platform) {
                pipelineConfig.platforms[platform] = EMPTY_JSON
                pipelineConfig.platforms[platform].PIPELINE_ENV = readJSON file: pipeline_config_path.toString()
            }
            palRm(pipeline_config_path.toString())
        }
    }

    // Load the build configs
    pipelineConfig.BUILD_CONFIGS.each { build_config ->
        def platform_regex = build_config.replace('.','\\.').replace('*', '(.*)')
        if (!env.IS_UNIX) {
            platform_regex = platform_regex.replace('/','\\\\')
        }
        echo "Searching configs in ${build_config} using ${platform_regex}"
        for (build_config_path in findFiles(glob: build_config)) {
            echo "\tFound config ${build_config_path}"
            def platform = RegexMatcher(build_config_path, platform_regex)
            if(platform) {
                pipelineConfig.platforms[platform].build_types = readJSON file: build_config_path.toString()
            }
        }
    }
    return pipelineConfig
}

def GetBuildEnvVars(Map platformEnv, Map buildTypeEnv, String pipelineName) {
    def envVarMap = [:]
    platformPipelineEnv = platformEnv['ENV'] ?: [:]
    platformPipelineEnv.each { var ->
        envVarMap[var.key] = var.value
    }
    platformEnvOverride = platformEnv['PIPELINE_ENV_OVERRIDE'] ?: [:]
    platformPipelineEnvOverride = platformEnvOverride[pipelineName] ?: [:]
    platformPipelineEnvOverride.each { var ->
        envVarMap[var.key] = var.value
    }
    buildTypeEnv.each { var ->
        // This may override the above one if there is an entry defined by the job
        envVarMap[var.key] = var.value
    }

    // Environment that only applies to to Jenkins tweaks.
    // For 3rdParty downloads, we store them in the EBS volume so we can reuse them across node
    // instances. This allow us to scale up and down without having to re-download 3rdParty
    envVarMap['LY_PACKAGE_DOWNLOAD_CACHE_LOCATION'] = "${envVarMap['WORKSPACE']}/3rdParty/downloaded_packages"
    envVarMap['LY_PACKAGE_UNPACK_LOCATION'] = "${envVarMap['WORKSPACE']}/3rdParty/packages"

    return envVarMap
}

def GetEnvStringList(Map envVarMap) {
    def strList = []
    envVarMap.each { var ->
        strList.add("${var.key}=${var.value}")
    }
    return strList
}

def CheckoutBootstrapScripts(String branchName) {
    checkout([$class: 'GitSCM',
        branches: [[name: "*/${branchName}"]],
        doGenerateSubmoduleConfigurations: false,
        extensions: [
            [$class: 'PruneStaleBranch'],
            [$class: 'AuthorInChangelog'],
            [$class: 'SparseCheckoutPaths', sparseCheckoutPaths: [
                [ $class: 'SparseCheckoutPath', path: 'scripts/build/Jenkins/' ],
                [ $class: 'SparseCheckoutPath', path: 'scripts/build/bootstrap/' ],
                [ $class: 'SparseCheckoutPath', path: 'scripts/build/Platform' ]
            ]],
            // Shallow checkouts break changelog computation. Do not enable.
            [$class: 'CloneOption', noTags: false, reference: '', shallow: false]
        ],
        submoduleCfg: [],
        userRemoteConfigs: scm.userRemoteConfigs
    ])
}

def CheckoutRepo(boolean disableSubmodules = false) {

    if (!fileExists(ENGINE_REPOSITORY_NAME)) {
        palMkdir(ENGINE_REPOSITORY_NAME)
    }

    if(fileExists('.git')) {
        // If the repository after checkout is locked, likely we took a snapshot while git was running,
        // to leave the repo in a usable state, garbagecollect. This also helps in situations where
        def indexLockFile = '.git/index.lock'
        if(fileExists(indexLockFile)) {
            palSh('git gc', 'Git GarbageCollect')
        }
        if(fileExists(indexLockFile)) { // if it is still there, remove it
            palRm(indexLockFile)
        }
    }

    def random = new Random()
    def retryAttempt = 0
    retry(5) {
        if (retryAttempt > 0) {
            sleep random.nextInt(60 * retryAttempt)  // Stagger checkouts to prevent HTTP 429 (Too Many Requests) response from CodeCommit
        }
        retryAttempt = retryAttempt + 1
        checkout scm: [
            $class: 'GitSCM',
            branches: scm.branches,
            extensions: [
                [$class: 'PruneStaleBranch'],
                [$class: 'AuthorInChangelog'],
                [$class: 'SubmoduleOption', disableSubmodules: disableSubmodules, recursiveSubmodules: true],
                [$class: 'CheckoutOption', timeout: 60]
            ],
            userRemoteConfigs: scm.userRemoteConfigs
       ]
    }

    // CHANGE_ID is used by some scripts to identify uniquely the current change (usually metric jobs)
    palSh('git rev-parse HEAD > commitid', 'Getting commit id')
    env.CHANGE_ID = readFile file: 'commitid'
    env.CHANGE_ID = env.CHANGE_ID.trim()
    palRm('commitid')

    // CHANGE_DATE is used by the installer to provide some ability to sort tagged builds in addition to BRANCH_NAME and CHANGE_ID
    commitDateFmt = '%%cI'
    if (env.IS_UNIX) commitDateFmt = '%cI'

    palSh("git show -s --format=${commitDateFmt} ${env.CHANGE_ID} > commitdate", 'Getting commit date')
    env.CHANGE_DATE = readFile file: 'commitdate'
    env.CHANGE_DATE = env.CHANGE_DATE.trim()
    palRm('commitdate')
}

def HandleDriveMount(String snapshot, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean recreateVolume = false) {
    unstash name: 'incremental_build_script'

    def pythonCmd = ''
    if(env.IS_UNIX) pythonCmd = 'sudo -E python3 -u '
    else pythonCmd = 'python3 -u '

    if(recreateVolume) {
        palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action delete --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Deleting volume', winSlashReplacement=false)
    }
    palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action mount --snapshot ${snapshot} --repository_name ${repositoryName} --project ${projectName} --pipeline ${pipeline} --branch ${branchName} --platform ${platform} --build_type ${buildType}", 'Mounting volume', winSlashReplacement=false)

    if(env.IS_UNIX) {
        sh label: 'Setting volume\'s ownership',
            script: """
            if sudo test ! -d "${workspace}"; then
                sudo mkdir -p ${workspace}
                cd ${workspace}/..
                sudo chown -R lybuilder:root .
            fi
            """
    }
}

def PreBuildCommonSteps(Map pipelineConfig, String snapshot, String repositoryName, String projectName, String pipeline, String branchName, String platform, String buildType, String workspace, boolean mount = true, boolean disableSubmodules = false) {
    echo 'Starting pre-build common steps...'

    if (mount) {
        if(env.RECREATE_VOLUME?.toBoolean()){
            echo 'Starting to recreating drive...'
            HandleDriveMount(snapshot, repositoryName, projectName, pipeline, branchName, platform, buildType, workspace, true)
        } else {
            echo 'Starting to mounting drive...'
            HandleDriveMount(snapshot, repositoryName, projectName, pipeline, branchName, platform, buildType, workspace, false)
        }
    }

    // Cleanup previous repo location, we are currently at the root of the workspace, if we have a .git folder
    // we need to cleanup. Once all branches take this relocation, we can remove this
    if(env.CLEAN_WORKSPACE?.toBoolean() || fileExists("${workspace}/.git")) {
        if(fileExists(workspace)) {
            palRmDir(workspace)
        }
    }

    dir(workspace) {
        // Add folder where we will store the 3rdParty downloads and packages
        if(!fileExists('3rdParty')) {
            palMkdir('3rdParty')
        }
    }
    dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
        CheckoutRepo(disableSubmodules)

        // Get python
        if(env.IS_UNIX) {
            sh label: 'Getting python',
               script: 'python/get_python.sh'
        } else {
            bat label: 'Getting python',
                script: 'python/get_python.bat'
        }

        // Always run the clean step, the scripts detect what variables were set, but it also cleans if
        // the NODE_LABEL has changed
        def command = "${pipelineConfig.PYTHON_DIR}/python"
        if(env.IS_UNIX) command += '.sh'
        else command += '.cmd'
        command += " -u ${pipelineConfig.BUILD_ENTRY_POINT} --platform ${platform} --type clean"
        palSh(command, "Running ${platform} clean")

        if(fileExists('.lfsconfig')) {
            palSh("git lfs install", "LFS config exists. Installing LFS hooks to local repo")
            palSh("git lfs pull", "Pulling new LFS objects")
        }
    }
}

def Build(Map pipelineConfig, String platform, String type, String workspace) {
    timeout(time: env.TIMEOUT, unit: 'MINUTES', activity: true) {
        def command = "${pipelineConfig.PYTHON_DIR}/python"
        if(env.IS_UNIX) command += '.sh'
        else command += '.cmd'
        command += " -u ${pipelineConfig.BUILD_ENTRY_POINT} --platform ${platform} --type ${type}"
        dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
            palSh(command, "Running ${platform} ${type}")
        }
    }
}

def TestMetrics(Map pipelineConfig, String workspace, String branchName, String repoName, String buildJobName, String outputDirectory, String configuration) {
    catchError(buildResult: null, stageResult: null) {
        def cmakeBuildDir = [workspace, ENGINE_REPOSITORY_NAME, outputDirectory].join('/')
        dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
            checkout scm: [
                $class: 'GitSCM',
                branches: [[name: '*/main']],
                extensions: [
                    [$class: 'AuthorInChangelog'],
                    [$class: 'RelativeTargetDirectory', relativeTargetDir: 'mars']
                ],
                userRemoteConfigs: [[url: "${env.MARS_REPO}", name: 'mars', credentialsId: "${env.GITHUB_USER}"]]
            ]
            withCredentials([usernamePassword(credentialsId: "${env.SERVICE_USER}", passwordVariable: 'apitoken', usernameVariable: 'username')]) {
                def command = "${pipelineConfig.PYTHON_DIR}/python.cmd -u mars/scripts/python/ctest_test_metric_scraper.py " +
                              '-e jenkins.creds.user %username% -e jenkins.creds.pass %apitoken% ' +
                              "-e jenkins.base_url ${env.JENKINS_URL} " +
                              "${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} --url ${env.BUILD_URL.replace('%','%%')}"
                bat label: "Publishing ${buildJobName} Test Metrics",
                    script: command
            }
        }
    }
}

def BenchmarkMetrics(Map pipelineConfig, String workspace, String branchName, String outputDirectory) {
    catchError(buildResult: null, stageResult: null) {
        def cmakeBuildDir = [workspace, ENGINE_REPOSITORY_NAME, outputDirectory].join('/')
        dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
            checkout scm: [
                $class: 'GitSCM',
                branches: [[name: '*/main']],
                extensions: [
                    [$class: 'AuthorInChangelog'],
                    [$class: 'RelativeTargetDirectory', relativeTargetDir: 'mars']
                ],
                userRemoteConfigs: [[url: "${env.MARS_REPO}", name: 'mars', credentialsId: "${env.GITHUB_USER}"]]
            ]
            def command = "${pipelineConfig.PYTHON_DIR}/python.cmd -u mars/scripts/python/benchmark_scraper.py ${cmakeBuildDir} ${branchName}"
            palSh(command, "Publishing Benchmark Metrics")
        }
    }
}

def ExportTestResults(Map options, String platform, String type, String workspace, Map params) {
    catchError(message: "Error exporting tests results (this won't fail the build)", buildResult: 'SUCCESS', stageResult: 'FAILURE') {
        def o3deroot = "${workspace}/${ENGINE_REPOSITORY_NAME}"
        dir("${o3deroot}/${params.OUTPUT_DIRECTORY}") {
            junit testResults: "Testing/**/*.xml"
            palRmDir("Testing")
            // Recreate test runner xml directories that need to be pre generated
            palMkdir("Testing/Pytest")
            palMkdir("Testing/Gtest")
        }
    }
}

def ExportTestScreenshots(Map options, String branchName, String platformName, String jobName, String workspace, Map params) {
    catchError(message: "Error exporting test screenshots (this won't fail the build)", buildResult: 'SUCCESS', stageResult: 'FAILURE') {
        dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
            def screenshotsFolder = "AutomatedTesting/user/PythonTests/Automated/Screenshots"
            def s3Uploader = "scripts/build/tools/upload_to_s3.py"
            def command = "${options.PYTHON_DIR}/python.cmd -u ${s3Uploader} --base_dir ${screenshotsFolder} " +
                          '--file_regex \\"(.*zip\$)\\" ' +
                          "--bucket ${env.TEST_SCREENSHOT_BUCKET} " +
                          "--search_subdirectories True --key_prefix ${branchName}_${env.BUILD_NUMBER} " +
                          '--extra_args {\\"ACL\\":\\"bucket-owner-full-control\\"}'
            palSh(command, "Uploading test screenshots for ${jobName}")
        }
    }
}

def UploadAPLogs(Map options, String branchName, String platformName, String jobName, String workspace, Map params) {
    dir("${workspace}/${ENGINE_REPOSITORY_NAME}") {
        projects = params.CMAKE_LY_PROJECTS.split(",")
        projects.each{ project ->
            def apLogsPath = "${project}/user/log"
            def s3UploadScriptPath = "scripts/build/tools/upload_to_s3.py"
            if(env.IS_UNIX) {
                pythonPath = "${options.PYTHON_DIR}/python.sh"
            }
            else {
                pythonPath = "${options.PYTHON_DIR}/python.cmd"
            }
            def command = "${pythonPath} -u ${s3UploadScriptPath} --base_dir ${apLogsPath} " +
                          "--file_regex \".*\" --bucket ${env.AP_LOGS_S3_BUCKET} " +
                          "--search_subdirectories True --key_prefix ${env.JENKINS_JOB_NAME}/${branchName}/${env.BUILD_NUMBER}/${platformName}/${jobName} " +
                          '--extra_args {\\"ACL\\":\\"bucket-owner-full-control\\"}'
            palSh(command, "Uploading AP logs for job ${jobName} for branch ${branchName}", false)
            }
        }
    }

def PostBuildCommonSteps(String workspace, boolean mount = true) {
    echo 'Starting post-build common steps...'

    if (mount) {
        def pythonCmd = ''
        if(env.IS_UNIX) pythonCmd = 'sudo -E python3 -u '
        else pythonCmd = 'python3 -u '

        try {
            timeout(5) {
                palSh("${pythonCmd} ${INCREMENTAL_BUILD_SCRIPT_PATH} --action unmount", 'Unmounting volume')
            }
        } catch (Exception e) {
            echo "Unmount script error ${e}"
        }
    }
}

def CreateSetupStage(Map pipelineConfig, String snapshot, String repositoryName, String projectName, String pipelineName, String branchName, String platformName, String jobName, Map environmentVars, boolean onlyMountEBSVolume = false) {
    return {
        stage('Setup') {
            if(onlyMountEBSVolume) {
                HandleDriveMount(snapshot, repositoryName, projectName, pipelineName, branchName, platformName, jobName, environmentVars['WORKSPACE'], false)
            } else {
                PreBuildCommonSteps(pipelineConfig, snapshot, repositoryName, projectName, pipelineName, branchName, platformName, jobName,  environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME'])
            }
        }
    }
}

def CreateBuildStage(Map pipelineConfig, String platformName, String jobName, Map environmentVars) {
    return {
        stage("${jobName}") {
            Build(pipelineConfig, platformName, jobName, environmentVars['WORKSPACE'])
        }
    }
}

def CreateTestMetricsStage(Map pipelineConfig, String branchName, Map environmentVars, String buildJobName, String outputDirectory, String configuration) {
    return {
        stage("${buildJobName}_metrics") {
            TestMetrics(pipelineConfig, environmentVars['WORKSPACE'], branchName, env.DEFAULT_REPOSITORY_NAME, buildJobName, outputDirectory, configuration)
            BenchmarkMetrics(pipelineConfig, environmentVars['WORKSPACE'], branchName, outputDirectory)
        }
    }
}

def CreateExportTestResultsStage(Map pipelineConfig, String platformName, String jobName, Map environmentVars, Map params) {
    return {
        stage("${jobName}_results") {
            ExportTestResults(pipelineConfig, platformName, jobName, environmentVars['WORKSPACE'], params)
        }
    }
}

def CreateExportTestScreenshotsStage(Map pipelineConfig, String branchName, String platformName, String jobName, Map environmentVars, Map params) {
    return {
        stage("${jobName}_screenshots") {
            ExportTestScreenshots(pipelineConfig, branchName, platformName, jobName, environmentVars['WORKSPACE'], params)
        }
    }
}

def CreateUploadAPLogsStage(Map pipelineConfig, String branchName, String platformName, String jobName, String workspace, Map params) {
    return {
        stage("${jobName}_upload_ap_logs") {
            UploadAPLogs(pipelineConfig, branchName, platformName, jobName, workspace, params)
        }
    }
}

def CreateTeardownStage(Map environmentVars) {
    return {
        stage('Teardown') {
            PostBuildCommonSteps(environmentVars['WORKSPACE'], environmentVars['MOUNT_VOLUME'])
        }
    }
}

def CreateSingleNode(Map pipelineConfig, def platform, def build_job, Map envVars, String branchName, String pipelineName, String repositoryName, String projectName, boolean onlyMountEBSVolume = false) {
    def nodeLabel = envVars['NODE_LABEL']
    return {
        def currentResult = ''
        def currentException = ''
        retry(PIPELINE_RETRY_ATTEMPTS) {
            node("${nodeLabel}") {
                if(isUnix()) { // Has to happen inside a node
                    envVars['IS_UNIX'] = 1
                }
                withEnv(GetEnvStringList(envVars)) {
                    def build_job_name = build_job.key
                    try {
                        CreateSetupStage(pipelineConfig, snapshot, repositoryName, projectName, pipelineName, branchName, platform.key, build_job.key, envVars, onlyMountEBSVolume).call()

                        if(build_job.value.steps) { //this is a pipe with many steps so create all the build stages
                            pipelineEnvVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
                            build_job.value.steps.each { build_step ->
                                build_job_name = build_step
                                // This addition of maps makes it that the right operand will override entries if they overlap with the left operand
                                envVars = pipelineEnvVars + GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, platform.value.build_types[build_step].PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
                                try {
                                    CreateBuildStage(pipelineConfig,  platform.key, build_step, envVars).call()
                                }
                                catch (Exception e) {
                                    if (envVars['NONBLOCKING_STEP']?.toBoolean()) {
                                        unstable(message: "Build step ${build_step} failed but it's a non-blocking step in build job ${build_job.key}")
                                    } else {
                                        throw e
                                    }
                                }
                            }
                        } else {
                            CreateBuildStage(pipelineConfig,  platform.key, build_job.key, envVars).call()
                        }
                    }
                    catch(Exception e) {
                        if (e instanceof org.jenkinsci.plugins.workflow.steps.FlowInterruptedException) {
                            def causes = e.getCauses().toString()
                            if (causes.contains('RemovedNodeCause')) {
                                error "Node disconnected during build: ${e}"  // Error raised to retry stage on a new node
                            }
                        }
                        if (IsAPLogUpload(branchName, build_job_name)) {
                            CreateUploadAPLogsStage(pipelineConfig, branchName, platform.key, build_job_name, envVars['WORKSPACE'], platform.value.build_types[build_job_name].PARAMETERS).call()
                        }
                        // All other errors will be raised outside the retry block
                        currentResult = envVars['ON_FAILURE_MARK'] ?: 'FAILURE'
                        currentException = e.toString()
                    }
                    finally {
                        def params = platform.value.build_types[build_job_name].PARAMETERS
                        if (env.MARS_REPO && params && params.containsKey('TEST_METRICS') && params.TEST_METRICS == 'True') {
                            def output_directory = params.OUTPUT_DIRECTORY
                            def configuration = params.CONFIGURATION
                            CreateTestMetricsStage(pipelineConfig, branchName, envVars, build_job_name, output_directory, configuration).call()
                        }
                        if (params && params.containsKey('TEST_RESULTS') && params.TEST_RESULTS == 'True') {
                            CreateExportTestResultsStage(pipelineConfig, platform.key, build_job_name, envVars, params).call()
                        }
                        if (params && params.containsKey('TEST_SCREENSHOTS') && params.TEST_SCREENSHOTS == 'True' && currentResult == 'FAILURE') {
                            CreateExportTestScreenshotsStage(pipelineConfig, branchName, platform.key, build_job_name, envVars, params).call()
                        }
                        CreateTeardownStage(envVars).call()
                    }
                }
            }
        }
        // https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Result.java
        // {SUCCESS,UNSTABLE,FAILURE,NOT_BUILT,ABORTED}
        if (currentResult == 'FAILURE') {
            currentBuild.result = 'FAILURE'
            error "FAILURE: ${currentException}"
        } else if (currentResult == 'UNSTABLE') {
            currentBuild.result = 'UNSTABLE'
            unstable(message: "UNSTABLE: ${currentException}")
        }
    }
}

// Used in CreateBuildJobs() to preprocess the build_job steps to programically create
// Node sections with a set of steps that can run on that node.
class PipeStepJobData {
    String m_nodeLabel = ""
    def m_steps = []

    PipeStepJobData(String label) {
        this.m_nodeLabel = label
    }

    def addStep(def step) {
        this.m_steps.add(step)
    }
}

def CreateBuildJobs(Map pipelineConfig, def platform, def build_job, Map envVars, String branchName, String pipelineName, String repositoryName, String projectName) {
    
    // if this is a pipeline, split jobs based on the NODE_LABEL 
    if(build_job.value.steps) {
        def defaultLabel = envVars['NODE_LABEL']
        def lastNodeLable = ""
        def jobList = []
        def currentIdx = -1;

        // iterate the steps to build the order of node label + steps sets.
        // Order matters, as it is executed from first to last.
        // example layout.
        // node A
        //  step 1
        //  step 2
        // node B
        //  step 3
        // node C
        //  step 4
        build_job.value.steps.each { build_step ->
            //if node label defined
            if(platform.value.build_types[build_step] && platform.value.build_types[build_step].PIPELINE_ENV &&
               platform.value.build_types[build_step].PIPELINE_ENV['NODE_LABEL']) {
               
               //if the last node label doen't match the new one, append it.
               if(platform.value.build_types[build_step].PIPELINE_ENV['NODE_LABEL'] != lastNodeLable) {
                    lastNodeLable = platform.value.build_types[build_step].PIPELINE_ENV['NODE_LABEL']
                    jobList.add(new PipeStepJobData(lastNodeLable))
                    currentIdx++
                } 
            }
            //no label define, so it needs to run on the default node label
            else if(lastNodeLable != defaultLabel) { //if the last node is not the default, append default
                lastNodeLable = defaultLabel
                jobList.add(new PipeStepJobData(lastNodeLable))
                currentIdx++
            }
            //add the build_step to the current node
            jobList[currentIdx].addStep(build_step)
        }
        
        return {
            jobList.eachWithIndex{ element, idx ->
                //update the node label + steps to the discovered data
                envVars['NODE_LABEL'] = element.m_nodeLabel
                build_job.value.steps = element.m_steps
                //no any additional nodes just mount the drive, do not handle clean parameters as that will be done by the first node.
                boolean onlyMountEBSVolume = idx != 0;
                //add this node
                CreateSingleNode(pipelineConfig, platform, build_job, envVars, branchName, pipelineName, repositoryName, projectName, onlyMountEBSVolume).call()
            }
        }
    } else {
        return CreateSingleNode(pipelineConfig, platform, build_job, envVars, branchName, pipelineName, repositoryName, projectName)
    }
}

def projectName = ''
def pipelineName = ''
def branchName = ''
def pipelineConfig = {}

// Start Pipeline
try {
    stage('Setup Pipeline') {
        node('controller') {
            def envVarList = []
            if(isUnix()) {
                envVarList.add('IS_UNIX=1')
            }
            withEnv(envVarList) {
                timestamps {
                    repositoryUrl = scm.getUserRemoteConfigs()[0].getUrl()
                    // repositoryName is the full repository name
                    repositoryName = (repositoryUrl =~ /https:\/\/github.com\/(.*)\.git/)[0][1]
                    env.REPOSITORY_NAME = repositoryName
                    (projectName, pipelineName) = GetRunningPipelineName(env.JOB_NAME) // env.JOB_NAME is the name of the job given by Jenkins
                    env.PIPELINE_NAME = pipelineName
                    if(env.BRANCH_NAME) {
                        branchName = env.BRANCH_NAME
                    } else {
                        branchName = scm.branches[0].name // for non-multibranch pipelines
                        env.BRANCH_NAME = branchName // so scripts that read this environment have it (e.g. incremental_build_util.py)                        
                    }
                    if(env.CHANGE_TARGET) {
                        // PR builds
                        if(BUILD_SNAPSHOTS.contains(env.CHANGE_TARGET)) {
                            snapshot = env.CHANGE_TARGET
                            echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" found."
                        } else { 
                            snapshot = DEFAULT_BUILD_SNAPSHOT
                            echo "Snapshot for destination branch \"${env.CHANGE_TARGET}\" does not exist, defaulting to snapshot \"${snapshot}\""
                        }
                    } else {
                        // Non-PR builds
                        pipelineParameters.add(choice(defaultValue: DEFAULT_BUILD_SNAPSHOT, name: 'SNAPSHOT', choices: BUILD_SNAPSHOTS_WITH_EMPTY, description: 'Selects the build snapshot to use. A more diverted snapshot will cause longer build times, but will not cause build failures.'))
                        snapshot = env.SNAPSHOT
                        echo "Snapshot \"${snapshot}\" selected."
                    }
                    pipelineProperties.add(disableConcurrentBuilds())

                    echo "Running repository: \"${repositoryName}\", pipeline: \"${pipelineName}\", branch: \"${branchName}\", CHANGE_ID: \"${env.CHANGE_ID}\", GIT_COMMMIT: \"${scm.GIT_COMMIT}\"..."

                    CheckoutBootstrapScripts(branchName)

                    // Load configs
                    pipelineConfig = LoadPipelineConfig(pipelineName, branchName)

                    // Add each platform as a parameter that the user can disable if needed
                    if (!IsPullRequest(branchName)) {
                        pipelineParameters.add(stringParam(defaultValue: '', description: 'Filters and overrides the list of jobs to run for each of the below platforms (comma-separated). Can\'t be used during a pull request.', name: 'JOB_LIST_OVERRIDE'))

                        pipelineConfig.platforms.each { platform ->
                            pipelineParameters.add(booleanParam(defaultValue: true, description: '', name: platform.key))
                        }
                    }
                    // Add additional Jenkins parameters
                    pipelineConfig.platforms.each { platform ->
                        platformEnv = platform.value.PIPELINE_ENV
                        pipelineJenkinsParameters = platformEnv['PIPELINE_JENKINS_PARAMETERS'] ?: [:]
                        jenkinsParametersToAdd = pipelineJenkinsParameters[pipelineName] ?: [:]
                        jenkinsParametersToAdd.each{ jenkinsParameter ->
                            defaultValue = jenkinsParameter['default_value']
                            // Use last run's value as default value so we can save values in different Jenkins environment
                            if (jenkinsParameter['use_last_run_value']?.toBoolean()) {
                                defaultValue = params."${jenkinsParameter['parameter_name']}" ?: jenkinsParameter['default_value']
                            }
                            switch (jenkinsParameter['parameter_type']) {
                                case 'string':
                                    pipelineParameters.add(stringParam(defaultValue: defaultValue,
                                                                   description: jenkinsParameter['description'],
                                                                   name: jenkinsParameter['parameter_name']
                                                                   ))
                                    break
                                case 'boolean':
                                    pipelineParameters.add(booleanParam(defaultValue: defaultValue,
                                                                   description: jenkinsParameter['description'],
                                                                   name: jenkinsParameter['parameter_name']
                                                                   ))
                                    break
                                case 'password':
                                    pipelineParameters.add(password(defaultValue: defaultValue,
                                                                   description: jenkinsParameter['description'],
                                                                   name: jenkinsParameter['parameter_name']
                                                                   ))
                                    break
                            }
                        }
                    }

                    pipelineProperties.add(parameters(pipelineParameters))
                    properties(pipelineProperties)

                    // Stash the INCREMENTAL_BUILD_SCRIPT_PATH since all nodes will use it
                    stash name: 'incremental_build_script',
                          includes: INCREMENTAL_BUILD_SCRIPT_PATH
                 }
            }
        }
    }

    if(env.BUILD_NUMBER == '1' && !IsPullRequest(branchName)) {
        // Exit pipeline early on the intial build. This allows Jenkins to load the pipeline for the branch and enables users
        // to select build parameters on their first actual build. See https://issues.jenkins.io/browse/JENKINS-41929
        currentBuild.result = 'SUCCESS'
        return
    }

    def someBuildHappened = false

    // Build and Post-Build Testing Stage
    def buildConfigs = [:]

    // Platform Builds run on EC2
    pipelineConfig.platforms.each { platform ->
        platform.value.build_types.each { build_job ->
            if (IsJobEnabled(branchName, build_job, pipelineName, platform.key)) {   // User can filter jobs, jobs are tagged by pipeline
                def envVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName)
                envVars['JENKINS_JOB_NAME'] = env.JOB_NAME // Save original Jenkins job name to JENKINS_JOB_NAME
                envVars['JOB_NAME'] = "${branchName}_${platform.key}_${build_job.key}" // backwards compatibility, some scripts rely on this
                someBuildHappened = true

                buildConfigs["${platform.key} [${build_job.key}]"] = CreateBuildJobs(pipelineConfig, platform, build_job, envVars, branchName, pipelineName, repositoryName, projectName)
            }
        }
    }

    timestamps {

        stage('Build') {
            parallel buildConfigs // Run parallel builds
        }

        echo 'All builds successful'
    }
    if (!someBuildHappened) {
        currentBuild.result = 'NOT_BUILT'
    }
}
catch(Exception e) {
    error "Exception: ${e}"
}
finally {
    try {
        node('controller') {
            if("${currentBuild.currentResult}" == "SUCCESS") {
                buildFailure = ""
                emailBody = "${BUILD_URL}\nSuccess!"
            } else {
                buildFailure = tm('${BUILD_FAILURE_ANALYZER}')
                emailBody = "${BUILD_URL}\n${buildFailure}!"

            }
            if(env.POST_AR_BUILD_SNS_TOPIC) {
                message_json = [
                    "build_url": env.BUILD_URL,
                    "build_number": env.BUILD_NUMBER,
                    "repository_name": env.REPOSITORY_NAME,
                    "branch_name": env.BRANCH_NAME,
                    "build_result": "${currentBuild.currentResult}",
                    "build_failure": buildFailure,
                    "recreate_volume": env.RECREATE_VOLUME,
                    "clean_output_directory": env.CLEAN_OUTPUT_DIRECTORY,
                    "clean_assets": env.CLEAN_ASSETS
                ]
                snsPublish(
                    topicArn: env.POST_AR_BUILD_SNS_TOPIC,
                    subject:'Build Result',
                    message:JsonOutput.toJson(message_json)
                )
            }
            emailext (
                body: "${emailBody}",
                subject: "${currentBuild.currentResult}: ${JOB_NAME} - Build # ${BUILD_NUMBER}",
                recipientProviders: [
                    [$class: 'RequesterRecipientProvider']
                ]
            )
        }
    } catch(Exception e) {
    }
}
