import java.nio.file.Paths

// android/build.gradle

// based on:
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/build.gradle
//   previous location:
//   - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/build.gradle
//
// * https://github.com/facebook/react-native/blob/0.60-stable/template/android/app/build.gradle
//   previous location:
//   - https://github.com/facebook/react-native/blob/0.58-stable/local-cli/templates/HelloWorld/android/app/build.gradle

// FBJNI build is based on:
// https://github.com/facebookincubator/fbjni/blob/main/docs/android_setup.md

// These defaults should reflect the SDK versions used by
// the minimum React Native version supported.
def DEFAULT_COMPILE_SDK_VERSION = 28
def DEFAULT_BUILD_TOOLS_VERSION = '28.0.3'
def DEFAULT_MIN_SDK_VERSION = 21
def DEFAULT_TARGET_SDK_VERSION = 28

def safeExtGet(prop, fallback) {
    rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

def isNewArchitectureEnabled() {
    // To opt-in for the New Architecture, you can either:
    // - Set `newArchEnabled` to true inside the `gradle.properties` file
    // - Invoke gradle with `-newArchEnabled=true`
    // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true`
    return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true"
}

apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'

def reactNativeArchitectures() {
  def value = project.getProperties().get("reactNativeArchitectures")
  return value ? value.split(",") : ["armeabi-v7a", "x86", "x86_64", "arm64-v8a"]
}

static def findNodeModules(baseDir) {
  def basePath = baseDir.toPath().normalize()
  // Node's module resolution algorithm searches up to the root directory,
  // after which the base path will be null
  while (basePath) {
    def nodeModulesPath = Paths.get(basePath.toString(), "node_modules")
    def reactNativePath = Paths.get(nodeModulesPath.toString(), "react-native")
    if (nodeModulesPath.toFile().exists() && reactNativePath.toFile().exists()) {
      // skip @babylonjs scoped packages
      if (!nodeModulesPath.toString().contains("@babylonjs")) {
        return nodeModulesPath.toString()
      }
    }
    basePath = basePath.getParent()
  }
  throw new GradleException("BabylonReactNative: Failed to find node_modules/ path!")
}

def nodeModules = findNodeModules(projectDir)
logger.warn("BabylonReactNative: node_modules/ found at: ${nodeModules}")
def graphics_api = safeExtGet('GRAPHICS_API', "OpenGL")
def rootBuildDir = "${rootProject.rootDir}/../Build/Android"
def extractedLibDir = "${rootBuildDir}/lib"

// ARCore version. Consumers can override by setting ext.arcoreVersion in their
// root build.gradle (e.g. `ext.arcoreVersion = '1.26.0'` to resolve the
// duplicate-class conflict between com.google.ar:core:1.22 and
// com.android.installreferrer:installreferrer:2.2). A consumer's
// resolutionStrategy.force on com.google.ar:core also wins over this value,
// in which case the dynamic ARCORE_LIBPATH resolution below still picks up
// the actually-extracted AAR.
def arcoreVersion = safeExtGet('arcoreVersion', '1.22.0')

def BABYLON_NATIVE_PLUGIN_NATIVECAMERA = System.getenv("BABYLON_NATIVE_PLUGIN_NATIVECAMERA") != "0"
def BABYLON_NATIVE_PLUGIN_NATIVEXR = System.getenv("BABYLON_NATIVE_PLUGIN_NATIVEXR") != "0"

def sourceBuild = false
def defaultDir

if (isNewArchitectureEnabled()) {
    apply plugin: "com.facebook.react"
}

if (rootProject.ext.has('reactNativeAndroidRoot')) {
  defaultDir = rootProject.ext.get('reactNativeAndroidRoot')
} else if (findProject(':ReactAndroid') != null) {
    sourceBuild = true
    defaultDir = project(':ReactAndroid').projectDir
} else {
  defaultDir = file("$nodeModules/react-native")
}

if (!defaultDir.exists()) {
    throw new GradleException(
      "${project.name}: React Native android directory (node_modules/react-native/android) does not exist! Resolved node_modules to: ${nodeModules}"
    )
}

def prebuiltDir = sourceBuild
    ? "$nodeModules/react-native/ReactAndroid/src/main/jni/prebuilt/lib"
    : "$buildDir/react-native-0*/jni"


def buildType = "debug"
if (gradle.startParameter.taskRequests.args[0].toString().contains("Release")) {
    buildType = "release"
} else if (gradle.startParameter.taskRequests.args[0].toString().contains("Debug")) {
    buildType = "debug"
}

def reactProperties = new Properties()
file("$nodeModules/react-native/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) }
def FULL_RN_VERSION =  (System.getenv("REACT_NATIVE_OVERRIDE_VERSION") ?: reactProperties.getProperty("VERSION_NAME"))
def REACT_NATIVE_VERSION = FULL_RN_VERSION.split("\\.")[1].toInteger()
def ENABLE_PREFAB = REACT_NATIVE_VERSION > 68

logger.warn("BabylonReactNative: RN Version: ${REACT_NATIVE_VERSION} / ${FULL_RN_VERSION}")
logger.warn("BabylonReactNative: isSourceBuild: ${sourceBuild}")
logger.warn("BabylonReactNative: PrebuiltDir: ${prebuiltDir}")
logger.warn("BabylonReactNative: buildType: ${buildType}")
logger.warn("BabylonReactNative: buildDir: ${buildDir}")
logger.warn("BabylonReactNative: node_modules: ${nodeModules}")
logger.warn("BabylonReactNative: Enable Prefab: ${ENABLE_PREFAB}")

configurations {
    //extractHeaders
    extractLibs
}

buildscript {
    // The Android Gradle plugin is only required when opening the android folder stand-alone.
    // This avoids unnecessary downloads and potential conflicts when the library is included as a
    // module dependency in an application project.
    // ref: https://docs.gradle.org/current/userguide/tutorial_using_tasks.html#sec:build_script_external_dependencies
    if (project == rootProject) {
        repositories {
            google()
        }
        dependencies {
            // This should reflect the Gradle plugin version used by
            // the minimum React Native version supported.
            classpath 'com.android.tools.build:gradle:3.4.1'
        }
    }
}

android {
    def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
    if (agpVersion.tokenize('.')[0].toInteger() >= 7) {
      namespace "com.babylonreactnative"
    }

    compileSdkVersion safeExtGet('compileSdkVersion', DEFAULT_COMPILE_SDK_VERSION)
    buildToolsVersion safeExtGet('buildToolsVersion', DEFAULT_BUILD_TOOLS_VERSION)

    // Used to override the NDK path/version on internal CI or by allowing
    // users to customize the NDK path/version from their root project.
    if (rootProject.hasProperty("ndkPath")) {
        ndkPath rootProject.ext.ndkPath
    }
    if (rootProject.hasProperty("ndkVersion")) {
        ndkVersion rootProject.ext.ndkVersion
    }

    defaultConfig {
        minSdkVersion safeExtGet('minSdkVersion', DEFAULT_MIN_SDK_VERSION)
        targetSdkVersion safeExtGet('targetSdkVersion', DEFAULT_TARGET_SDK_VERSION)
        versionCode 1
        versionName "1.0"
        buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()

        externalNativeBuild {
            cmake {
                cppFlags "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID"
                abiFilters (*reactNativeArchitectures())
                // NOTE: -DARCORE_LIBPATH below points at the AAR matching the
                // declared `arcoreVersion` (see top of this file). The actual
                // resolved ARCore version may differ (consumer
                // resolutionStrategy.force, dependency conflict resolution),
                // so the argument is rewritten further down in this script
                // (search for "arcoreAarFileName") to point at the AAR that
                // Gradle actually extracted.
                arguments "-DANDROID_STL=c++_shared",
                        "-DGRAPHICS_API=${graphics_api}",
                        "-DREACT_NATIVE_VERSION=${REACT_NATIVE_VERSION}",
                        "-DARCORE_LIBPATH=${extractedLibDir}/core-${arcoreVersion}.aar/jni",
                        "-DREACTNATIVE_DIR=${rootDir}/../node_modules/react-native/",
                        "-DBABYLON_NATIVE_PLUGIN_NATIVECAMERA=${BABYLON_NATIVE_PLUGIN_NATIVECAMERA ? '1' : '0'}",
                        "-DBABYLON_NATIVE_PLUGIN_NATIVEXR=${BABYLON_NATIVE_PLUGIN_NATIVEXR ? '1' : '0'}",
                        "-DFETCHCONTENT_FULLY_DISCONNECTED=ON"
            }
        }
    }
    lint {
        abortOnError = false
        checkReleaseBuilds = false
    }

    publishing {
        singleVariant("release") {
            withSourcesJar()
        }
    }

    sourceSets.main {
        jniLibs {
            srcDirs += ['../libs/android']
        }
        java {
            if (!isNewArchitectureEnabled()) {
                srcDirs += [
                    "src/paper/java",
                ]
            }

            if (REACT_NATIVE_VERSION == 74) {
                srcDirs += [
                    "src/reactnative74/java"
                ]
            } else {
                // the name latest here might be confusing as it applies for versions
                // below 74 as well
                srcDirs += [
                    "src/latest/java"
                ]
            }
        }
    }

    externalNativeBuild {
        cmake {
            path file('CMakeLists.txt')
        }
    }

    packaging {
        jniLibs {
            excludes += [
                 "**/libc++_shared.so",
                 "**/libfbjni.so",
                 "**/libjsi.so",
                 "**/libreact_nativemodule_core.so",
                 "**/libreactnativejni.so",
                 "**/libruntimeexecutor.so",
                 "**/libturbomodulejsijni.so",
                 "**/libreactnative.so",
            ]
        }
        resources {
            excludes += ["META-INF/**"]
        }
    }

    if (ENABLE_PREFAB) {
        // Ensure the headers directory exists at configuration time for AGP validation
        file("${project.buildDir}/headers/ReactNativeBabylon").mkdirs()

        buildFeatures {
                prefab true
                prefabPublishing true
        }
        prefab {
            ReactNativeBabylon {
                headers "${project.buildDir}/headers/ReactNativeBabylon/"
            }
        }
    }

    // Create new configurations that can be referred to in dependencies.
    // The Android Gradle Plugin 3.* does not allow hooking into existing
    // configurations like `implementation`.
    configurations {
        extractHeaders
        extractJNI
    }
}

repositories {
    maven {
        // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm
        url("$rootDir/../node_modules/react-native/android")
    }
    maven {
        // Android JSC is installed from npm
        url("$rootDir/../node_modules/jsc-android/dist")
    }
    mavenCentral {
        // We don't want to fetch react-native from Maven Central as there are
        // older versions over there.
        content {
            excludeGroup "com.facebook.react"
        }
    }
    google()
    maven { url 'https://www.jitpack.io' }
}

dependencies {
    //noinspection GradleDynamicVersion
    implementation 'com.facebook.react:react-native:+'  // From node_modules
    implementation "com.google.ar:core:${arcoreVersion}"

    extractLibs "com.google.ar:core:${arcoreVersion}"

    if (REACT_NATIVE_VERSION < 71) {
        logger.warn("BabylonReactNative: Extracting files from AAR (pre RN 0.71)")
        //noinspection GradleDynamicVersion
        extractHeaders("com.facebook.fbjni:fbjni:0.2.2:headers")
        //noinspection GradleDynamicVersion
        extractJNI("com.facebook.fbjni:fbjni:0.2.2")
    }

    if (!sourceBuild) {
        def rnAarMatcher = "**/react-native/**/*${buildType}.aar"
        if (REACT_NATIVE_VERSION < 69) {
            logger.warn("BabylonReactNative: aar state pre 69. match **/**/*.aar")
            rnAarMatcher = "**/**/*.aar"
        } else if (REACT_NATIVE_VERSION >= 71) {
            logger.warn("BabylonReactNative: aar state post 70, do nothing")
            return
        }
        def rnAAR = fileTree("${nodeModules}/react-native/android").matching({ it.include rnAarMatcher }).singleFile
        logger.warn("BabylonReactNative: Extracting JNI files (pre RN 0.71) ${rnAAR}")
        extractJNI(files(rnAAR))
    }
}

def localBuildDir = buildDir
def headersConfiguration = configurations.extractHeaders
def jniConfiguration = configurations.extractJNI

task extractAARHeaders {
    doLast {
        headersConfiguration.files.each { file ->
            def absFile = file.absoluteFile
            project.copy {
                from project.zipTree(absFile)
                into "${localBuildDir}/${absFile.name}"
                include "**/*.h"
            }
        }
    }
}

task extractJNIFiles {
    doLast {
        jniConfiguration.files.each { file ->
            def absFile = file.absoluteFile
            project.copy {
                from project.zipTree(absFile)
                into "${localBuildDir}/${absFile.name}"
                include "jni/**/*"
            }
        }
    }
}

extractJNIFiles.mustRunAfter extractAARHeaders

if (ENABLE_PREFAB) {
    // Package all the cpp code in a flattened directory structure
    task prepareHeaders(type: Copy) {
        from("../cpp")
        from("./cpp")
        into "${project.buildDir}/headers/ReactNativeBabylon/react-native-babylon"
        includeEmptyDirs = false
        include "**/*.h"
        eachFile {
            String path = it.path

            if (path.contains("api/third_party")) {
                // Skip flattening third_party dir
                path = path.substring("api/".length())
            } else if (path.contains("babylon/include") || path.contains("babylon/modules") || path.contains("babylon/src")) {
                // Skip flattening babylon dir
                path = path.substring("babylon/".length())
            } else if (path.startsWith("dawn/include/")) {
                // Flatten dawn/include/dawn and dawn/include/webgpu
                path = path.substring("dawn/include/".length())
            } else {
                // flatten anything else
                path = path.substring(path.lastIndexOf("/") + 1)
            }
            it.path = path
        }
        // Ensure the output directory exists even when no headers are found
        doFirst {
            file("${project.buildDir}/headers/ReactNativeBabylon").mkdirs()
        }
    }
    preBuild.dependsOn(prepareHeaders)
}

def nativeBuildDependsOn(dependsOnTask, variant) {
  def buildTasks = tasks.findAll({ task ->
      !task.name.contains("Clean") && (task.name.contains("externalNative") || task.name.contains("CMake")) })
  if (variant != null) {
    buildTasks = buildTasks.findAll({ task -> task.name.contains(variant) })
  }
  buildTasks.forEach { task -> task.dependsOn(dependsOnTask) }
}

// Resolve the actual ARCore AAR name. A consumer project may override
// `ext.arcoreVersion` or apply a resolutionStrategy.force to com.google.ar:core
// (e.g. to resolve a conflict with installreferrer 2.2 that requires
// com.google.ar:core 1.26+). Gradle extracts the AAR into a folder named after
// the *resolved* artifact (core-<version>.aar), so the native build's
// -DARCORE_LIBPATH argument must match that resolved name rather than the
// version literal declared in the dependencies block.
def arcoreAar = configurations.extractLibs.resolvedConfiguration.resolvedArtifacts.find { it.name == 'core' }
def arcoreAarName = arcoreAar ? "${arcoreAar.name}-${arcoreAar.moduleVersion.id.version}.aar" : "core-${arcoreVersion}.aar"
android.defaultConfig.externalNativeBuild.cmake.arguments.replaceAll {
    it.toString().startsWith('-DARCORE_LIBPATH=') ? "-DARCORE_LIBPATH=${extractedLibDir}/${arcoreAarName}/jni".toString() : it
}

configurations.extractLibs.files.each { file ->
    copy {
        from zipTree(file)
        into "$extractedLibDir/" + file.name
        include "jni/**/*"
    }
}

afterEvaluate {
  nativeBuildDependsOn(extractAARHeaders, null)
  nativeBuildDependsOn(extractJNIFiles, null)

  // Workaround for Kotlin 2.0.x / AGP 8.8.x lint incompatibility (KT-62555)
  try {
      def attrClass = Class.forName(
          "org.jetbrains.kotlin.gradle.targets.native.toolchain.KotlinNativeBundleArtifactFormat\$KotlinNativeBundleArtifactsTypes"
      )
      def attr = Attribute.of("kotlin.native.bundle.type", attrClass)
      def regularValue = attrClass.enumConstants.find { it.name() == "REGULAR" } ?: attrClass.enumConstants[0]
      configurations.configureEach { config ->
          if (!config.attributes.contains(attr)) {
              config.attributes.attribute(attr, regularValue)
          }
      }
  } catch (Exception e) {
      logger.warn("Kotlin native bundle type workaround skipped: ${e.message}")
  }
}