apply plugin: 'com.android.application'
apply plugin: Utils

android {
    compileSdk rootProject.ext.compileSdk
    ndkVersion rootProject.ext.ndkVersion

    defaultConfig {
        minSdk rootProject.ext.minSdk
        targetSdk rootProject.ext.targetSdk
        ndk {
            abiFilters = []
        }
    }

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

    flavorDimensions 'game', 'arch'

    productFlavors {
        helloWorld {
            dimension 'game'
            applicationId 'com.kaliber.helloworld'
            resValue "string", "app_name", "Kaliber Hello World"
            manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"]
            ext {
                gnTarget = "hello_world"
            }
        }
        demo {
            dimension 'game'
            applicationId 'com.kaliber.woom'
            resValue "string", "app_name", "Kaliber Demo"
            manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"]
            ext {
                gnTarget = "demo"
            }
        }
        woom {
            dimension 'game'
            applicationId 'com.woom.game'
            resValue "string", "app_name", "woom"
            resValue "string", "interstitial_ad_unit_id", "ca-app-pub-1321063817979967/8373182022"
            resValue "string", "admob_application_id", "ca-app-pub-1321063817979967~1100949243"
            manifestPlaceholders = [appIcon: "@mipmap/ic_launcher"]
            ext {
                gnTarget = "demo"
            }
        }

        arm7 {
            dimension 'arch'
            ndk {
                abiFilters = ["armeabi-v7a"]
            }
        }
        arm8 {
            dimension 'arch'
            ndk {
                abiFilters = ["arm64-v8a"]
            }
        }
        x86 {
            dimension 'arch'
            ndk {
                abiFilters = ["x86"]
            }
        }
        x86_64 {
            dimension 'arch'
            ndk {
                abiFilters = ["x86_64"]
            }
        }

        allArchs {
            dimension 'arch'
            ndk {
                abiFilters = ["armeabi-v7a", "arm64-v8a", "x86_64", "x86"]
            }
        }
        armOnly {
            dimension 'arch'
            ndk {
                abiFilters = ["armeabi-v7a", "arm64-v8a"]
            }
        }
        x86Only {
            dimension 'arch'
            ndk {
                abiFilters = ["x86_64", "x86"]
            }
        }

        // Native library name is same as GN target name.
        android.productFlavors.each { flavor ->
            if (flavor.dimension == 'game') {
                "${flavor.name}" {
                    resValue "string", "lib_name", flavor.ext.gnTarget
                    buildConfigField 'String', 'NATIVE_LIBRARY', "\"${flavor.ext.gnTarget}\""
                }
            }
        }
    }

    sourceSets {
        main {
            java.srcDirs += ['../../../src/engine/platform/java/com/kaliber/base']
            android.buildTypes.each { buildType ->
                "${buildType.name}" {
                    assets.srcDirs = ["${utils.getGnOutDir(buildType.name)}/assets"]
                }
            }
        }
        android.buildTypes.each { buildType ->
            "${buildType.name}" {
                jniLibs.srcDirs = ["${utils.getGnOutDir(buildType.name)}/jniLibs"]
            }
        }
    }

    namespace "com.kaliber.base"
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    implementation 'com.google.android.gms:play-services-ads:22.0.0'
}

//
// Tasks for GN build
//

// Generate `args.gn` which is used by GN to take in build arguments.
utils.addTask('generateGnArgsFor') { String taskName, String buildType, String arch ->
    task(taskName, type: WriteFileTask) {
        content = utils.generateGnArgsContent(buildType, arch)
        target = project.layout.file(provider { new File("${utils.getGnOutDir(buildType)}/${arch}", 'args.gn') })
    }
}

// Run `gn gen` to generate ninja files.
utils.addTask('runGnFor') { String taskName, String buildType, String arch ->
    task(taskName, type: Exec) {
        dependsOn "generateGnArgsFor${arch}${buildType}"

        executable rootProject.ext.gn
        args '--fail-on-unused-args', 'gen', "${utils.getGnOutDir(buildType)}/${arch}"

        // Need to run gn only once unless the configuration in `args.gn` changes.
        inputs.file(new File("${utils.getGnOutDir(buildType)}/${arch}", 'args.gn'))
        outputs.file(new File("${utils.getGnOutDir(buildType)}/${arch}", 'build.ninja'))
    }
}

// Build the native library for the target game using ninja.
utils.addGameTask('runNinjaFor') { String taskName, String buildType, String arch, String game ->
    task(taskName, type: Exec) {
        dependsOn "runGnFor${arch}${buildType}"

        executable rootProject.ext.ninja
        args '-C', "${utils.getGnOutDir(buildType)}/${arch}", "src/${utils.getGnTargetFor(game)}"

        // Always run ninja and let it figure out what needs to be compiled.
        outputs.upToDateWhen { false }
    }
}

// Assets can be obtained from the output directory of any arch but it would be good to combine them
// in a single directory in case we are buildig multi-arch and some build config have different assets.
utils.addGameTask('copyAssetsFor') { String taskName, String buildType, String arch, String game ->
    task(taskName, type: Copy) {
        dependsOn "runNinjaFor${game}${arch}${buildType}"

        from "${utils.getGnOutDir(buildType)}/${arch}/assets"
        into "${utils.getGnOutDir(buildType)}/assets"
    }
}

// Copy the native library to a directory denoting its arch code as the Android Gradle plugin expects.
utils.addGameTask('copyJniLibsFor') { String taskName, String buildType, String arch, String game ->
    task(taskName, type: Copy) {
        dependsOn "runNinjaFor${game}${arch}${buildType}"

        from("${utils.getGnOutDir(buildType)}/${arch}") {
            include "lib${utils.getGnTargetFor(game)}.so"
        }
        into "${utils.getGnOutDir(buildType)}/jniLibs/${utils.getAbiCodeFor(arch)}"
    }
}

tasks.configureEach { task ->
    def variantPattern = /(\w+)(${utils.getArchTypesRegExp()})(${utils.getBuildTypesRegExp()})/

    def match = task.name =~ /^merge/ + variantPattern + /JniLibFolders$/
    if (match) {
        utils.project.android.productFlavors.find { arch ->
            if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) {
                // Depends on each arch type for multi-arch build flavors.
                arch.ndk.abiFilters.each { abi ->
                    task.dependsOn "copyJniLibsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}"
                }
                return true
            }
        }
        return
    }
    match = task.name =~ /^generate/ + variantPattern + /Assets$/
    if (match) {
        utils.project.android.productFlavors.find { arch ->
            if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) {
                // Depends on each arch type for multi-arch build flavors.
                arch.ndk.abiFilters.each { abi ->
                    task.dependsOn "copyAssetsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}"
                }
                return true
            }
        }
        return
    }
    match = task.name =~ /^lintVitalAnalyze/ + variantPattern + /$/
    if (match) {
        utils.project.android.productFlavors.find { arch ->
            if (arch.dimension == 'arch' && arch.name.capitalize() == match.group(2)) {
                // Depends on each arch type for multi-arch build flavors.
                arch.ndk.abiFilters.each { abi ->
                    task.dependsOn "copyAssetsFor${match.group(1)}${utils.ARCH_CODES[abi]}${match.group(3)}"
                }
                return true
            }
        }
        return
    }
}

//
// Utils plugin
//

class Utils implements Plugin<Project> {
    final def ARCH_CODES = ["armeabi-v7a": "Arm7",
                            "arm64-v8a": "Arm8",
                            "x86_64": "X86_64",
                            "x86": "X86"].asImmutable()
    final def GN_CPU_CODES = ["Arm7": "arm",
                              "Arm8": "arm64",
                              "X86_64": "x64",
                              "X86": "x86"].asImmutable()

    def project

    @Inject
    Utils(Project project) {
        this.project = project
    }

    void apply(Project project) {
        project.extensions.create('utils', Utils)
    }

    // Add a task for archs and buildTypes variants.
    void addTask(String prefix, Closure taskClosure) {
        forEachBuildVariant { String arch, String buildType ->
            def taskName = "${prefix}${arch}${buildType}"
            taskClosure(taskName, buildType, arch)
        }
    }

    // Add a task for games, archs and buildTypes variants.
    void addGameTask(String prefix, Closure taskClosure) {
        forEachGameBuildVariant { String game, String arch, String buildType ->
            def taskName = "${prefix}${game}${arch}${buildType}"
            taskClosure(taskName, buildType, arch, game)
        }
    }

    void forEachBuildVariant(Closure callback) {
        project.android.productFlavors.each { arch ->
            // Only need to add tasks for arch types which maps to a single ABI
            if (arch.dimension == 'arch' && arch.ndk.abiFilters.size() == 1) {
                project.android.buildTypes.each { buildType ->
                    callback(arch.name.capitalize(), buildType.name.capitalize())
                }
            }
        }
    }

    void forEachGameBuildVariant(Closure callback) {
        project.android.productFlavors.each { game ->
            if (game.dimension == 'game') {
                project.android.productFlavors.each { arch ->
                    // Only need to add tasks for arch types which maps to a single ABI
                    if (arch.dimension == 'arch' && arch.ndk.abiFilters.size() == 1) {
                        project.android.buildTypes.each { buildType ->
                            callback(game.name.capitalize(), arch.name.capitalize(), buildType.name.capitalize())
                        }
                    }
                }
            }
        }
    }

    def getBuildTypesRegExp() {
        def outList = []
        project.android.buildTypes.each { buildType ->
            outList += buildType.name.capitalize()
        }
        return outList.join('|')
    }

    def getArchTypesRegExp() {
        def outList = []
        project.android.productFlavors.each { flavor ->
            if (flavor.dimension == 'arch') {
                outList += flavor.name.capitalize()
            }
        }
        return outList.join('|')
    }

    def getAbiCodeFor(String arch) {
        def outStr = ''
        project.android.productFlavors.find { flavor ->
            if (flavor.name.capitalize() == arch) {
                outStr = flavor.ndk.abiFilters.first()
                return true
            }
        }
        return outStr
    }

    def getGnTargetFor(String game) {
        def outStr = ''
        project.android.productFlavors.find { flavor ->
            if (flavor.dimension == 'game' && flavor.name.capitalize() == game) {
                outStr = flavor.ext.gnTarget
                return true
            }
        }
        return outStr
    }

    def generateGnArgsContent(String buildType, String arch) {
        def content = 'target_os="android"\n'
        content += 'target_cpu="' + GN_CPU_CODES[arch] + '"\n'
        content += "is_debug=${buildType != 'Release'}\n"
        content += 'ndk="' + project.android.ndkDirectory + '"\n'
        content += "ndk_api=${project.rootProject.ext.minSdk}\n"
        return content
    }

    def getGnOutDir(String buildType) {
        return "${project.buildDir}/gn_out/${buildType.toLowerCase()}"
    }
}

abstract class WriteFileTask extends DefaultTask {
    @Input
    abstract Property<String> getContent()

    @OutputFile
    abstract RegularFileProperty getTarget()

    @TaskAction
    void run() {
        def file = target.get().asFile
        file.parentFile.mkdirs()
        def text = content.get()
        if (!file.exists() || text != file.text)
            file.text = text
    }
}
