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'

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()) {
      return nodeModulesPath.toString()
    }
    basePath = basePath.getParent()
  }
  throw new GradleException("React-Native-Skia: Failed to find node_modules/ path!")
}

def nodeModules = findNodeModules(projectDir)

// Resolve a node package directory using Node's own module resolution, so we
// pick up the prebuilt Skia binaries straight from node_modules (no postinstall
// copy step needed). Mirrors `require.resolve(pkg/package.json)`.
static def resolveNodePackage(packageName, baseDir) {
  def script = "process.stdout.write(require('path').dirname(require.resolve('${packageName}/package.json')))"
  def proc = ["node", "-e", script].execute(null, baseDir)
  proc.waitFor()
  if (proc.exitValue() != 0) {
    throw new GradleException(
      "react-native-skia: Could not resolve the '${packageName}' package. " +
      "Make sure dependencies are installed (yarn install / npm install).\n" + proc.err.text
    )
  }
  return proc.text.trim()
}

// Graphite is detected via a marker file (created by install-skia-graphite for
// in-repo development, or shipped in the npm tarball for next-channel releases).
// The prebuilt binaries live in the react-native-skia-android npm package for the
// default (Ganesh) build and in react-native-skia-graphite-android for Graphite.
// In-repo Graphite development downloads them directly into libs/android instead,
// which takes precedence over the npm package when present.
def useGraphite = file("${projectDir}/../libs/.graphite").exists()
def localGraphiteLibs = file("${projectDir}/../libs/android")
def skiaLibsPath = useGraphite
    ? (localGraphiteLibs.exists()
        ? "${projectDir}/../libs/android"
        : "${resolveNodePackage('react-native-skia-graphite-android', projectDir)}/libs")
    : "${resolveNodePackage('react-native-skia-android', projectDir)}/libs"

// Both packages must link the exact same Dawn artifact: compare this
// Graphite build's Dawn release tag (libs/.dawn-version) with the tag
// react-native-webgpu declares in its package.json `dawn` field.
if (useGraphite) {
    def dawnMarker = file("${projectDir}/../libs/.dawn-version")
    def webgpuDir = null
    try {
        webgpuDir = resolveNodePackage('react-native-webgpu', projectDir)
    } catch (Exception ignored) {
        // react-native-webgpu not installed: nothing to check
    }
    if (dawnMarker.exists() && webgpuDir != null) {
        def skiaDawn = dawnMarker.text.trim()
        def webgpuJson = new groovy.json.JsonSlurper().parse(file("${webgpuDir}/package.json"))
        if (webgpuJson.dawn == null) {
            throw new GradleException(
                "react-native-skia: the installed react-native-webgpu " +
                "(${webgpuJson.version}) does not declare a `dawn` field in its " +
                "package.json, so it predates shared-Dawn support and cannot be " +
                "paired with this Graphite build (which links ${skiaDawn}). " +
                "Upgrade react-native-webgpu.")
        }
        def webgpuDawn = "dawn-" + webgpuJson.dawn.toString().replace('/', '-')
        if (skiaDawn != webgpuDawn) {
            throw new GradleException(
                "react-native-skia: Dawn version mismatch. This Graphite build links " +
                "${skiaDawn} but react-native-webgpu links ${webgpuDawn}. " +
                "Align the two packages so the app contains exactly one Dawn.")
        }
        logger.warn("react-native-skia: Dawn versions match (${skiaDawn})")
    }
}

logger.warn("react-native-skia: SK_GRAPHITE: ${useGraphite}")
logger.warn("react-native-skia: Skia libs: ${skiaLibsPath}")
logger.warn("react-native-skia: node_modules/ found at: ${nodeModules}")

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("react-native-skia: RN Version: ${REACT_NATIVE_VERSION} / ${FULL_RN_VERSION}")
logger.warn("react-native-skia: isSourceBuild: ${sourceBuild}")
logger.warn("react-native-skia: PrebuiltDir: ${prebuiltDir}")
logger.warn("react-native-skia: buildType: ${buildType}")
logger.warn("react-native-skia: buildDir: ${buildDir}")
logger.warn("react-native-skia: node_modules: ${nodeModules}")
logger.warn("react-native-skia: Enable Prefab: ${ENABLE_PREFAB}")

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.shopify.reactnative.skia"
    }

    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())
                arguments '-DANDROID_STL=c++_shared',
                          "-DREACT_NATIVE_VERSION=${REACT_NATIVE_VERSION}",
                          "-DREACT_NATIVE_DIR=${defaultDir}",
                          "-DNODE_MODULES_DIR=${nodeModules}",
                          "-DPREBUILT_DIR=${prebuiltDir}",
                          "-DSKIA_LIBS_PATH=${skiaLibsPath}",
                          "-DSK_GRAPHITE=${useGraphite ? 'ON' : 'OFF'}",
                          "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"

            }
        }
    }
    lintOptions {
        abortOnError false
    }

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

    sourceSets.main {
        jniLibs {
            srcDirs += [skiaLibsPath]
        }
        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')
        }
    }

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

    if (ENABLE_PREFAB) {
        buildFeatures {
                prefab true
                prefabPublishing true
        }
        prefab {
            rnskia {
                headers "${project.buildDir}/headers/rnskia/"
            }
        }
    }

    // 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()
}

dependencies {
    //noinspection GradleDynamicVersion
    implementation 'com.facebook.react:react-native:+'  // From node_modules

    if (REACT_NATIVE_VERSION < 71) {
        logger.warn("react-native-skia: 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("react-native-skia: aar state pre 69. match **/**/*.aar")
            rnAarMatcher = "**/**/*.aar"
        } else if (REACT_NATIVE_VERSION >= 71) {
            logger.warn("react-native-skia: aar state post 70, do nothing")
            return
        }
        def rnAAR = fileTree("${nodeModules}/react-native/android").matching({ it.include rnAarMatcher }).singleFile
        logger.warn("react-native-skia: 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/rnskia/react-native-skia"
        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("skia/include") || path.contains("skia/modules") || path.contains("skia/src")) {
                // Skip flattening Skia dir
                path = path.substring("skia/".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
        }
    }
    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) }
}

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