// This file is part of BOINC.
// https://boinc.berkeley.edu
// Copyright (C) 2025 University of California
//
// BOINC is free software; you can redistribute it and/or modify it
// under the terms of the GNU Lesser General Public License
// as published by the Free Software Foundation,
// either version 3 of the License, or (at your option) any later version.
//
// BOINC is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with BOINC.  If not, see <http://www.gnu.org/licenses/>.

plugins {
    id 'org.ajoberstar.grgit'
    id 'jacoco'
}

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'

import org.ajoberstar.grgit.Grgit

// Use commit date as version code (valid up to 07/18/2036)
def buildVersionCode() {
    def repo = Grgit.open(currentDir: project.rootDir)
    def head = repo.head()
    def versionCode = head.time
    println "versionCode: " + versionCode
    return versionCode
}

// Derive version name from release tag and add commit SHA1
def buildVersionName() {
    def pattern = /(?<tag>.+)-(?<offset>\d+)-g(?<hash>[0-9a-f]+)/
    def version = '8.2.5'
    def isDev = true
    def offset = -1
    def tag = ""

    def repo = Grgit.open(currentDir: project.rootDir)
    def head = repo.head()
    def tagWithOffset = repo.describe(longDescr:true, tags: true)
    def match = (tagWithOffset =~ pattern)

    if(match.hasGroup() && 1L == match.size() && 4L == match[0].size()) {
        offset = match.group('offset')
        tag = match.group('tag')
    }
    if ('0' == offset) {
        pattern = /client_release\/\d+\.\d+\/(?<major>\d+)\.(?<minor>\d+)\.(?<revision>\d+)/
        match = (tag =~ pattern)

        // Sanity checks for tag format
        if (match.hasGroup() && 1L == match.size() && 4L == match[0].size()) {
            def major = match.group('major')
            def minor = match.group('minor')
            def revision = match.group('revision')
            version = "${major}.${minor}.${revision}"
            // Sanity check for tag name
            assert tag == repo.describe(tags: true): "Different tag names detected"
            assert repo.status().clean: "Dirty working tree detected! Preventing release build!"
            isDev = false
        }
    }

    def commit = head.getAbbreviatedId(10)
    def versionName = "${version} (${commit})"
    println "versionName: " + versionName
    if(isDev) {
        println "Warning! Non-release tag or offset found: $tag (offset: $offset)"
        println "Flagging as DEVELOPMENT build..."
    }
    return versionName
}

def checkAssetsFiles() {
    def configFile = android.sourceSets.main.res.sourceFiles.find { it.name.equals 'configuration.xml' }
    def clientName      = new XmlParser().parse(configFile).string.find { it.@name.equals 'client_name' }.text()
    def clientCabundle  = new XmlParser().parse(configFile).string.find { it.@name.equals 'client_cabundle' }.text()
    def allProjectsList = new XmlParser().parse(configFile).string.find { it.@name.equals 'all_projects_list' }.text()
    def nomedia         = new XmlParser().parse(configFile).string.find { it.@name.equals 'nomedia' }.text()
    def client_config   = new XmlParser().parse(configFile).string.find { it.@name.equals 'client_config' }.text()
    def assets = "src/main/assets/"
    assert file(assets + "arm64-v8a/"   + clientName).exists()
    assert file(assets + "armeabi/"     + clientName).exists()
    assert file(assets + "armeabi-v7a/" + clientName).exists()
    assert file(assets + "x86/"         + clientName).exists()
    assert file(assets + "x86_64/"      + clientName).exists()
    assert file(assets + clientCabundle).exists()
    assert file(assets + allProjectsList).exists()
    assert file(assets + nomedia).exists()
    assert file(assets + client_config).exists()
}

static def isStringXmlInValuesFolder(fileName) {
    return fileName.absolutePath.endsWith('/values/strings.xml') ||
            fileName.absolutePath.endsWith('\\values\\strings.xml')
}

static def isStringXmlInValuesFolder(fileName, language) {
    return fileName.absolutePath.endsWith('/values-' + language + '/strings.xml') ||
            fileName.absolutePath.endsWith('\\values-' + language + '\\strings.xml')
}

static def isStringXml(fileName) {
    return fileName.absolutePath.endsWith('strings.xml') && !isStringXmlInValuesFolder(fileName)
}

static def buildStringsMapFromXml(fileName) {
    def stringsMap = new HashMap<String, Boolean>()
    def xmlParser = new XmlParser()
    def xml = xmlParser.parse(fileName)
    xml.string.each {
        stringsMap.put(it.@name, false)
    }
    return stringsMap
}

static def buildStringsTranslationsMapFromXml(fileName) {
    def stringsTranslationsMap = new HashMap<String, String>()
    def xmlParser = new XmlParser()
    def xml = xmlParser.parse(fileName)
    xml.string.each {
        stringsTranslationsMap.put(it.@name, it.text())
    }
    return stringsTranslationsMap
}

static def compareTwoStringsTranslationsMaps(map1, map2) {
    def mapKeys = new HashSet<String>()
    def mapValues = new HashSet<String>()
    map1.keySet().each {
        if (!map2.containsKey(it)) {
            mapKeys.add(it)
        } else {
            if (map1.get(it) != map2.get(it)) {
                mapValues.add(it)
            }
        }
    }

    if (mapKeys.size() > 0) {
        println "The following keys are missed:"
        mapKeys.each {
            println it
        }
    }
    if (mapValues.size() > 0) {
        println "The following values are different:"
        mapValues.each {
            println it
        }
    }

    return mapKeys.isEmpty() && mapValues.isEmpty()
}

static def findKeysFromTheSecondHashMapThatAreNotPresentInTheFirstHashMap(map1, map2) {
    def keys = new HashSet<String>()
    map2.keySet().each {    // iterate over the keys of map2
        if(!map1.containsKey(it)) {
            keys.add(it)
        }
    }
    return keys
}

static def doBinaryValidation(mainFile, dependentFile) {
    def mainFileContent = mainFile.getText('UTF-8')
    def dependentFileContent = dependentFile.getText('UTF-8')
    if (mainFileContent != dependentFileContent) {
        def dependentFilePath = dependentFile.getParentFile()
        println "Binary validation for " + dependentFile + " failed!"
        println "Consider copying " + mainFile + " to " + dependentFilePath
        println "or running next command:"
        println "cp " + mainFile + " " + dependentFilePath
        return false
    }
    return true
}

def validateTranslations() {
    def mainStringsFile = android.sourceSets.main.res.sourceFiles.find {
            isStringXmlInValuesFolder(it)
    }
    assert mainStringsFile != null, "No strings.xml file found in values folder"

    def translationStringsFiles = android.sourceSets.main.res.sourceFiles.filter {
            isStringXml(it)
    }.files.toList()
    assert translationStringsFiles.size() > 0, "No translation strings files found"

    def mainStringsMap = buildStringsMapFromXml(mainStringsFile)

    android.sourceSets.main.res.sourceFiles.filter {
        !isStringXml(it) && !isStringXmlInValuesFolder(it)
    }.files.toList().each {
        new File(it.absolutePath).each {
            def source = it
            mainStringsMap.each {
                if (!it.value) {
                    def string = it
                    source.eachLine {
                        if (it.contains(string.key)) {
                            string.setValue(true)
                            return
                        }
                    }
                }
            }
        }
    }

    android.sourceSets.main.java.srcDirs.toList().each {
        new File(it.absolutePath).eachFileRecurse() {
            if (it.name.endsWith('.java') || it.name.endsWith('.kt')) {
                def source = it
                mainStringsMap.each {
                    if (!it.value) {
                        def string = it
                        source.eachLine {
                            if (it.contains('R.string.' + string.key)) {   // if the key is present in the file
                                string.setValue(true)
                                return
                            }
                        }
                    }
                }
            }
        }
    }

    android.sourceSets.main.manifest.srcFile.each { source ->
        mainStringsMap.each {
            if (!it.value) {
                def string = it
                source.eachLine {
                    if (it.contains(string.key)) {
                        string.setValue(true)
                        return
                    }
                }
            }
        }
    }

    def mainStringsContainsRedundantKeys = false
    mainStringsMap.each {
        if (!it.value) {
            mainStringsContainsRedundantKeys = true
            println it.key + " is not used in main strings file"
        }
    }

    assert !mainStringsContainsRedundantKeys, "Redundant keys found in main strings file"

    def translationFilesContainRedundantKeys = false
    translationStringsFiles.each { file ->
        def stringsMap = buildStringsMapFromXml(file)
        def diff = findKeysFromTheSecondHashMapThatAreNotPresentInTheFirstHashMap(mainStringsMap, stringsMap)
        if (diff.size() > 0) {   // if there are keys that are not present in the main strings file
            println file.absolutePath + ": the following keys are not present in the main strings file: " + diff.toString()
            translationFilesContainRedundantKeys = true
        }
    }
    assert !translationFilesContainRedundantKeys, "The translation files contain redundant keys"

    def mainStringsTranslationsMap = buildStringsTranslationsMapFromXml(mainStringsFile)
    def enStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'en')
    }
    assert enStringsTranslationFile != null, "No strings.xml file found in values-en folder"

    def enStringsTranslationsMap = buildStringsTranslationsMapFromXml(enStringsTranslationFile)
    assert compareTwoStringsTranslationsMaps(mainStringsTranslationsMap, enStringsTranslationsMap), "The main strings and en translations are not equal"

    def heStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'he')
    }
    assert heStringsTranslationFile != null, "No strings.xml file found in values-he folder"

    def heStringsTranslationsMap = buildStringsTranslationsMapFromXml(heStringsTranslationFile)
    def iwStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'iw')
    }
    assert iwStringsTranslationFile != null, "No strings.xml file found in values-iw folder"

    def iwStringsTranslationsMap = buildStringsTranslationsMapFromXml(iwStringsTranslationFile)
    assert compareTwoStringsTranslationsMaps(heStringsTranslationsMap, iwStringsTranslationsMap), "The Hebrew strings (he nad iw) are not equal"

    assert doBinaryValidation(heStringsTranslationFile, iwStringsTranslationFile), "The Hebrew strings (he and iw) contain equal data but files are different"

    def inStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'in')
    }
    assert inStringsTranslationFile != null, "No strings.xml file found in values-in folder"

    def idStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'id')
    }
    assert idStringsTranslationFile != null, "No strings.xml file found in values-id folder"

    def inStringsTranslationsMap = buildStringsTranslationsMapFromXml(inStringsTranslationFile)
    def idStringsTranslationsMap = buildStringsTranslationsMapFromXml(idStringsTranslationFile)
    assert compareTwoStringsTranslationsMaps(inStringsTranslationsMap, idStringsTranslationsMap), "The Indonesian strings (in and id) are not equal"

    assert doBinaryValidation(inStringsTranslationFile, idStringsTranslationFile), "The Indonesian strings (in and id) contain equal data but files are different"

    def yiStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'yi')
    }
    def jiStringsTranslationFile = translationStringsFiles.find {
        isStringXmlInValuesFolder(it, 'ji')
    }

    if (yiStringsTranslationFile != null || jiStringsTranslationFile != null) {
        assert yiStringsTranslationFile != null, "No strings.xml file found in values-yi folder"
        assert jiStringsTranslationFile != null, "No strings.xml file found in values-ji folder"

        def yiStringsTranslationsMap = buildStringsTranslationsMapFromXml(yiStringsTranslationFile)
        def jiStringsTranslationsMap = buildStringsTranslationsMapFromXml(jiStringsTranslationFile)
        assert compareTwoStringsTranslationsMaps(yiStringsTranslationsMap, jiStringsTranslationsMap), "The Yiddish strings (yi and ji) are not equal"

        assert doBinaryValidation(yiStringsTranslationFile, jiStringsTranslationFile), "The Yiddish strings (yi and ji) contain equal data but files are different"
    }
}

preBuild.doFirst {
    checkAssetsFiles()
    validateTranslations()
}

android {
    compileSdkVersion 33
    buildToolsVersion '33.0.1'

    buildFeatures {
        viewBinding true
        buildConfig true
        aidl true
    }

    defaultConfig {
        applicationId "edu.berkeley.boinc"
        minSdkVersion 16
        targetSdkVersion 28
        versionCode buildVersionCode().toInteger()
        versionName buildVersionName()

        // Required when setting minSdkVersion to 20 or lower
        multiDexEnabled true
        vectorDrawables.useSupportLibrary = true
    }

    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
        }

        debug {
            minifyEnabled false
            debuggable true
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules-debug.txt'
            enableUnitTestCoverage true
            enableAndroidTestCoverage true
        }

        armv6_release {
            initWith release
        }

        armv6_debug {
            initWith debug
            minifyEnabled true
            enableUnitTestCoverage true
            enableAndroidTestCoverage true
        }
    }

    applicationVariants.all { variant ->
        if (variant.buildType.name.startsWith('armv6')) {
            variant.mergeAssetsProvider.configure {
                doLast {
                    delete(fileTree(dir: outputDir, includes: ['**/arm64-v8a/*', '**/armeabi-v7a/*', '**/x86/*', '**/x86_64/*']))
                }
            }
        }
    }

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17

        // Flag to enable support for the new language APIs
        coreLibraryDesugaringEnabled true
    }

    kotlinOptions {
        jvmTarget = '17'
    }

    testOptions {
        unitTests {
            includeAndroidResources = true
            all {
                useJUnitPlatform()
                jacoco {
                    includeNoLocationClasses = true
                    excludes = ['jdk.internal.*']
                }
                jvmArgs = ['--add-opens=java.base/java.lang=ALL-UNNAMED', '--add-opens=java.base/java.util=ALL-UNNAMED']
            }
        }
    }
    testCoverage {
        jacocoVersion
    }
    namespace 'edu.berkeley.boinc'

    tasks.withType(JavaCompile) {
        options.compilerArgs << '-Xlint:unchecked'
        options.deprecation = true
    }

    tasks.withType(Test) {
        testLogging {
            events "passed", "skipped", "failed"
        }
    }
}

dependencies {
    coreLibraryDesugaring Android.tools.desugarJdkLibs

    // androidx dependencies
    implementation AndroidX.multidex
    implementation AndroidX.appCompat
    implementation AndroidX.core.ktx
    implementation AndroidX.fragment.ktx
    implementation AndroidX.preference.ktx
    implementation AndroidX.recyclerView
    implementation AndroidX.swipeRefreshLayout
    implementation AndroidX.viewPager2
    implementation("androidx.biometric:biometric:1.1.0")

    implementation 'javax.annotation:javax.annotation-api:_'
    implementation 'com.github.bumptech.glide:glide:_'
    implementation Google.android.material
    implementation "com.google.guava:guava:_"
    implementation Square.okio
    implementation 'org.apache.commons:commons-lang3:_'
    implementation Kotlin.stdlib.jdk8

    // lifecycle dependencies
    implementation AndroidX.lifecycle.runtime.ktx
    implementation AndroidX.lifecycle.service

    // coroutine dependencies
    implementation KotlinX.coroutines.core
    implementation KotlinX.coroutines.android

    // dagger dependencies
    implementation Google.dagger
    annotationProcessor Google.dagger.compiler
    kapt Google.dagger.compiler

    // Testing dependencies
    testImplementation AndroidX.test.core
    testImplementation "com.google.guava:guava-testlib:_"
    testImplementation Testing.junit4
    testImplementation Testing.robolectric
    testImplementation Testing.mockK
    debugImplementation AndroidX.fragment.testing

    // powermock dependencies
    testImplementation "org.powermock:powermock-module-junit4:_"
    testImplementation "org.powermock:powermock-api-mockito2:_"

    // junit dependencies
    testImplementation Testing.junit.jupiter.api
    testImplementation Testing.junit.jupiter.params
    testRuntimeOnly Testing.junit.jupiter.engine
    testRuntimeOnly "org.junit.vintage:junit-vintage-engine:_"
}

repositories {
    mavenCentral()
}

//from https://about.codecov.io/blog/code-coverage-for-android-development-using-kotlin-jacoco-github-actions-and-codecov/
task jacocoTestReport(type: JacocoReport, dependsOn: ['testDebugUnitTest']) {

    reports {
        csv.getRequired().set(true)
        xml.getRequired().set(true)
        html.getRequired().set(true)
    }

    def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/META-INF*.*', '**/*Test*.*', 'android/**/*.*']
    def debugTree = fileTree(dir: "${buildDir}/intermediates/javac/debug/classes", excludes: fileFilter) +
            fileTree(dir: "${buildDir}/tmp/kotlin-classes/debug", excludes: fileFilter)
    def mainSrc = "${project.projectDir}/src/main/java"

    sourceDirectories.setFrom(files([mainSrc]))
    classDirectories.setFrom(files([debugTree]))
    executionData.setFrom(fileTree(dir: "$buildDir", includes: [
            "outputs/unit_test_code_coverage/debugUnitTest/testDebugUnitTest.exec",
            "outputs/code-coverage/debugAndroidTest/connected/coverage.ec"
    ]))
}

jacoco {
    toolVersion
}

