package tasks.configuration

import Arch
import CompileSkikoCppTask
import PatchSkiaSymbolsTask
import OS
import SkiaBuildType
import SkikoProjectContext
import WriteCInteropDefFile
import compilerForTarget
import dsl.TargetEnv
import dsl.ResolvedBinaryConfiguration
import hostArch
import isCompatibleWithHost
import joinToTitleCamelCase
import mutableListOfLinkerOptions
import org.gradle.api.GradleException
import org.gradle.api.Project
import org.gradle.api.tasks.Exec
import org.gradle.api.tasks.Sync
import org.gradle.api.tasks.TaskProvider
import org.gradle.kotlin.dsl.getByName
import org.gradle.kotlin.dsl.named
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinNativeTarget
import org.jetbrains.kotlin.gradle.tasks.CInteropProcess
import projectDirs
import registerOrGetSkiaDirProvider
import registerSkikoTask
import symbols.HideSkiaSymbolsTask
import java.io.File
import kotlin.collections.plus

private fun ohosClangTarget(arch: Arch): String = when (arch) {
    Arch.Arm64 -> "aarch64-linux-ohos"
    Arch.X64 -> "x86_64-linux-ohos"
    Arch.Wasm -> error("OHOS does not support Wasm")
}

private fun Project.ohosToolchainRoot(): File? =
    (findProperty("ohos.llvm.dir")?.toString() ?: System.getenv("OHOS_LLVM"))
        ?.let(::File)
        ?.takeIf(File::isDirectory)

private fun Project.ohosSysroot(): File? =
    (findProperty("ohos.sysroot")?.toString() ?: System.getenv("OHOS_SYSROOT"))
        ?.let(::File)
        ?.takeIf(File::isDirectory)

private fun Project.ohosCompiler(): String =
    ohosToolchainRoot()?.resolve("bin/clang++")?.takeIf(File::canExecute)?.absolutePath ?: "clang++"

private fun Project.ohosToolchainFlags(): List<String> {
    val toolchain = ohosToolchainRoot() ?: return emptyList()
    val sysroot = ohosSysroot() ?: return emptyList()
    val resourceDir = toolchain.resolve("lib/clang").listFiles()
        ?.filter(File::isDirectory)
        ?.maxByOrNull(File::getName)
        ?: error("Clang resource directory not found under ${toolchain.resolve("lib/clang")}")
    return listOf(
        "--sysroot=${sysroot.absolutePath}",
        "-resource-dir=${resourceDir.absolutePath}",
        "-isystem", toolchain.resolve("include/libcxx-ohos/include/c++/v1").absolutePath,
        "-isystem", resourceDir.resolve("include").absolutePath,
        "-DOHOS",
        "-D__MUSL__",
        "-D_LIBCPP_HAS_MUSL_LIBC",
        "-D__BUILD_LINUX_WITH_CLANG",
        "-D__GNU_SOURCE",
        "-D_GNU_SOURCE",
    )
}

private fun Project.isOhosFusionRendererEnabled(): Boolean =
    findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true"

private fun Project.ohRenderRoot(): File {
    val configured = findProperty("ohrender.dir")?.toString()?.takeIf(String::isNotBlank)
        ?: System.getenv("OHRENDER_DIR")?.takeIf(String::isNotBlank)
        ?: projectDir.resolve("../../fusion-render").absolutePath
    val configuredDir = File(configured)
    val root = if (configuredDir.name == "OHRender") configuredDir else configuredDir.resolve("OHRender")
    require(root.isDirectory) {
        "OHRender directory not found: $root. Set -Pohrender.dir or OHRENDER_DIR to the OHRender repository."
    }
    return root
}

private fun ohRenderHeadersDirs(root: File): List<File> = listOf(
    root,
    root.resolve("include"),
    root.resolve("include/codec"),
    root.resolve("include/core"),
    root.resolve("include/effects"),
    root.resolve("include/pathops"),
    root.resolve("include/utils"),
    root.resolve("modules/skcms"),
    root.resolve("modules/skparagraph/include"),
    root.resolve("modules/skresources/include"),
    root.resolve("modules/skshaper/include"),
    root.resolve("modules/skunicode/include"),
    root.resolve("modules/sksg/include"),
    root.resolve("modules/svg/include"),
    root.resolve("src"),
    root.resolve("third_party/icu"),
    root.resolve("third_party/expat/expat/lib"),
    root.resolve("third_party/include"),
)

private fun SkikoProjectContext.prepareFusionBridgeSourcesTask(): TaskProvider<Exec> = with(project) {
    val output = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
    val syncTask = if ("syncFusionBridgeSources" in tasks.names) {
        tasks.named<Sync>("syncFusionBridgeSources")
    } else {
        tasks.register("syncFusionBridgeSources", Sync::class.java) {
            from(projectDir) {
                include("src/commonMain/cpp/common/**")
                include("src/nativeJsMain/cpp/**")
            }
            into(output)
        }
    }
    return if ("prepareFusionBridgeSources" in tasks.names) {
        tasks.named<Exec>("prepareFusionBridgeSources")
    } else {
        tasks.register("prepareFusionBridgeSources", Exec::class.java) {
            dependsOn(syncTask)
            //region ohos
            // The bridge sources and exported ABI come from Skiko m150. This patch only
            // translates m150 implementation details to the older OHRender C++ API.
            val patch = projectDir.resolve("src/ohosMain/fusionRenderer/patches/m150-to-ohrender.patch")
            //endregion
            inputs.file(patch)
            outputs.dir(output)
            workingDir(output)
            commandLine("/usr/bin/patch", "-p1", "--forward", "--input=${patch.absolutePath}")
        }
    }
}

fun SkikoProjectContext.compileOhRenderTask(os: OS, arch: Arch): TaskProvider<CompileSkikoCppTask> =
    with(project) {
        require(os == OS.OHOS)
        val root = ohRenderRoot()
        registerSkikoTask<CompileSkikoCppTask>("compileOhRender", os, arch) {
            compiler.set(ohosCompiler())
            buildTargetOS.set(os)
            buildTargetArch.set(arch)
            buildVariant.set(buildType)
            buildSuffix.set("fusion-ohrender")
            flags.set(listOf(
                "-target", ohosClangTarget(arch),
                *ohosToolchainFlags().toTypedArray(),
                *buildType.clangFlags,
                //region ohos
                // Keep SkRefCnt's inline ABI consistent with the Fusion bridge. In particular,
                // Release must define NDEBUG; otherwise OHRender emits debug destructors while
                // the bridge emits release-mode internal_dispose(), causing a ref-count abort.
                *buildType.flags,
                //endregion
                "-fPIC", "-frtti", "-fno-exceptions",
                "-DSK_BUILD_FOR_OHOS", "-DSK_TRACE",
            ))
            sourceRoots.set(listOf(
                root.resolve("src"),
                root.resolve("modules/skcms"),
                root.resolve("modules/skparagraph"),
                root.resolve("modules/skresources"),
                root.resolve("modules/svg/src"),
                root.resolve("modules/skshaper/src"),
            ).map { layout.dir(provider { it }).get() })
            excludePatterns.set(listOf(
                "**/SkOHAvifCodec.cpp",
                "**/SkShaper_harfbuzz.cpp",
            ))
            includeHeadersNonRecursive(ohRenderHeadersDirs(root))
        }
    }

fun SkikoProjectContext.archiveOhRenderTask(
    os: OS,
    arch: Arch,
    compileTask: TaskProvider<CompileSkikoCppTask>,
): TaskProvider<Exec> = with(project) {
    require(os == OS.OHOS)
    val targetString = "${os.id}-${arch.id}"
    registerSkikoTask<Exec>("archiveOhRender", os, arch) {
        dependsOn(compileTask)
        val objectFiles = fileTree(compileTask.flatMap { it.outDir }) { include("**/*.o") }
        inputs.files(objectFiles)
        val outDir = layout.buildDirectory.dir("nativeFusion/static/$targetString")
        val output = outDir.map { it.file("ohrender-$targetString.a") }
        outputs.file(output)
        doFirst { outDir.get().asFile.mkdirs() }
        workingDir = outDir.get().asFile
        executable = System.getenv("OHOS_LLVM_AR")
            ?: ohosToolchainRoot()?.resolve("bin/llvm-ar")?.absolutePath
            ?: "llvm-ar"
        argumentProviders.add {
            listOf("-crs", output.get().asFile.absolutePath) + objectFiles.files.map(File::getAbsolutePath)
        }
    }
}

fun String.withSuffix(isUikitSim: Boolean = false) =
    this + if (isUikitSim) "Sim" else ""

fun KotlinTarget.isUikitSimulator() =
    name.contains("Simulator", ignoreCase = true) || name == "tvosX64" // x64 tvOS is implicitly a simulator

fun Project.findXcodeSdkRoot(): String {
    val defaultPath = "/Applications/Xcode.app/Contents/Developer/Platforms"
    if (File(defaultPath).exists()) {
        return defaultPath.also {
            println("findXcodeSdkRoot = $it")
        }
    }

    return (project.property("skiko.ci.xcodehome") as? String)?.let {
        val sdkPath = it + "/Platforms"
        println("findXcodeSdkRoot = $sdkPath")
        sdkPath
    } ?: error("gradle property `skiko.ci.xcodehome` is not set")
}

fun SkikoProjectContext.compileNativeBridgesTask(
    os: OS, arch: Arch, isUikitSim: Boolean
): TaskProvider<CompileSkikoCppTask> = with (this.project) {
    val isFusion = os == OS.OHOS && isOhosFusionRendererEnabled()
    val skiaNativeDir = if (isFusion) null else registerOrGetSkiaDirProvider(os, arch, isUikitSim = isUikitSim)

    val actionName = (if (isFusion) "compileFusionNativeBridges" else "compileNativeBridges")
        .withSuffix(isUikitSim = isUikitSim)
    val fusionBridgeSources = if (isFusion) prepareFusionBridgeSourcesTask() else null

    return project.registerSkikoTask<CompileSkikoCppTask>(actionName, os, arch) {
        skiaNativeDir?.let { dependsOn(it) }
        fusionBridgeSources?.let { dependsOn(it) }
        val nativeHeadersRoot = if (isFusion) ohRenderRoot() else skiaNativeDir!!.get()

        compiler.set(if (os == OS.OHOS) ohosCompiler() else compilerForTarget(os, arch))
        buildTargetOS.set(os)
        if (isUikitSim) {
            buildSuffix.set("sim")
        }
        if (isFusion) {
            buildSuffix.set("fusion")
        }
        buildTargetArch.set(arch)
        buildVariant.set(buildType)

        when (os) {
            OS.IOS -> {
                val sdkRoot = findXcodeSdkRoot()
                val iphoneOsSdk = "$sdkRoot/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"
                val iphoneSimSdk = "$sdkRoot/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
                val iosArchFlags = when (arch) {
                    Arch.Arm64 -> arrayOf(
                        "-target", if (isUikitSim) "arm64-apple-ios-simulator" else "arm64-apple-ios",
                        "-isysroot", if (isUikitSim) iphoneSimSdk else iphoneOsSdk,
                        if (isUikitSim) "-mios-simulator-version-min=12.0" else "-mios-version-min=12.0"
                    )
                    Arch.X64 -> arrayOf(
                        "-target", "x86_64-apple-ios-simulator",
                        "-mios-version-min=12.0",
                        "-isysroot", iphoneSimSdk
                    )
                    else -> throw GradleException("Unsupported arch: $arch")
                }
                flags.set(listOf(
                    *iosArchFlags,
                    *buildType.clangFlags,
                    "-stdlib=libc++",
                    *skiaPreprocessorFlags(OS.IOS, buildType),
                ))
            }
            OS.TVOS -> {
                val sdkRoot = findXcodeSdkRoot()
                val tvOsSdk = "$sdkRoot/AppleTVOS.platform/Developer/SDKs/AppleTVOS.sdk"
                val tvSimSdk = "$sdkRoot/AppleTVSimulator.platform/Developer/SDKs/AppleTVSimulator.sdk"
                val tvosArchFlags = when (arch) {
                    Arch.Arm64 -> arrayOf(
                        "-target", if (isUikitSim) "arm64-apple-tvos-simulator" else "arm64-apple-tvos",
                        if (isUikitSim) "-mappletvsimulator-version-min=12.0" else "-mappletvos-version-min=12.0" ,
                        "-isysroot", if (isUikitSim) tvSimSdk else tvOsSdk,
                    )
                    Arch.X64 -> arrayOf(
                        "-target", "x86_64-apple-tvos-simulator",
                        "-mappletvsimulator-version-min=12.0",
                        "-isysroot", tvSimSdk
                    )
                    else -> throw GradleException("Unsupported arch: $arch")
                }
                flags.set(listOf(
                    *tvosArchFlags,
                    *buildType.clangFlags,
                    "-stdlib=libc++",
                    *skiaPreprocessorFlags(OS.TVOS, buildType),
                ))
            }
            OS.MacOS -> {
                compiler.set(project.appleToolchainExecutableOrDefault("clang++", compiler.get()))
                val archFlags = when (arch) {
                    Arch.Arm64 -> arrayOf("-arch", "arm64")
                    Arch.X64 -> arrayOf("-arch", "x86_64")
                    else -> error("Unexpected arch: $arch for $os")
                }
                flags.set(listOf(
                    *project.appleMacOsSdkFlags().toTypedArray(),
                    *buildType.clangFlags,
                    *skiaPreprocessorFlags(OS.MacOS, buildType),
                    *archFlags
                ))
            }
            OS.Linux -> {
                val archFlags = if (arch == Arch.Arm64) arrayOf(
                    // Always inline atomics for ARM64 to prevent linking incompatibility issues after updating GCC to 10
                    "-mno-outline-atomics",
                ) else arrayOf()
                val linuxFlags = mutableListOf(
                    *buildType.clangFlags,
                    "-fPIC",
                    "-fno-rtti",
                    "-fno-exceptions",
                    "-fvisibility=hidden",
                    "-fvisibility-inlines-hidden",
                    *archFlags,
                    *skiaPreprocessorFlags(OS.Linux, buildType)
                )
                // Add sysroot for ARM64 cross-compilation
                if (arch == Arch.Arm64 && hostArch != Arch.Arm64) {
                    linuxFlags.add(0, "--sysroot=/opt/arm-gnu-toolchain/aarch64-none-linux-gnu/libc")
                }
                flags.set(linuxFlags)
            }
            OS.OHOS -> flags.set(listOf(
                "-target", ohosClangTarget(arch),
                *ohosToolchainFlags().toTypedArray(),
                *buildType.clangFlags,
                "-fPIC", if (isFusion) "-frtti" else "-fno-rtti", "-fno-exceptions",
                "-DSK_BUILD_FOR_OHOS",
                *skiaPreprocessorFlags(OS.OHOS, buildType),
                *(if (isFusion) arrayOf(
                    "-DUSE_OHRENDER", "-USK_GANESH", "-USK_GL", "-USK_UNICODE_AVAILABLE"
                ) else emptyArray())
            ))
            else -> throw GradleException("$os not yet supported")
        }

        val sharedSrcDirs = if (isFusion) {
            val compatRoot = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
            listOf(
                compatRoot.resolve("src/commonMain/cpp/common"),
                compatRoot.resolve("src/nativeJsMain/cpp"),
            ).map { layout.dir(provider { it }).get() }
        } else {
            projectDirs("src/commonMain/cpp/common", "src/nativeNativeJs/cpp", "src/nativeJsMain/cpp")
        }
        val srcDirs = sharedSrcDirs +
                (if (os == OS.OHOS) projectDirs("src/ohosMain/cpp") else emptyList()) +
                (if (os == OS.OHOS) projectDirs(
                    "src/ohosMain/" +
                        (if (project.findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true")
                            "fusionRenderer" else "skia") + "/cpp"
                ) else emptyList()) +
                (if (skiko.includeTestHelpers) projectDirs("src/nativeJsTest/cpp") else emptyList())
        sourceRoots.set(srcDirs)

        if (isFusion) {
            excludePatterns.set(listOf(
                "**/DirectContext.cc",
                "**/BackendRenderTarget.cc",
                "**/BackendTexture.cc",
                "**/RuntimeEffect.cc",
                "**/RuntimeShaderBuilder.cc",
                "**/render.cc",
                "**/skottie/**",
                "**/sksg/**",
                "**/SVGCanvas.cc",
                "**/GrGLMakeNativeInterface_ohos.cpp",
            ))
        }

        if (isFusion) {
            val compatRoot = layout.buildDirectory.dir("fusionBridgeCompat").get().asFile
            includeHeadersNonRecursive(compatRoot.resolve("src/nativeJsMain/cpp"))
            includeHeadersNonRecursive(compatRoot.resolve("src/commonMain/cpp/common/include"))
        } else {
            includeHeadersNonRecursive(projectDir.resolve("src/nativeJsMain/cpp"))
            includeHeadersNonRecursive(projectDir.resolve("src/commonMain/cpp/common/include"))
        }
        if (os == OS.OHOS) includeHeadersNonRecursive(
            projectDir.resolve(
                "src/ohosMain/" +
                    (if (project.findProperty("skiko.native.ohos.fusionRenderer.enabled") == "true")
                        "fusionRenderer" else "skia") + "/cpp"
            )
        )
        includeHeadersNonRecursive(
            if (isFusion) ohRenderHeadersDirs(nativeHeadersRoot) else skiaHeadersDirs(nativeHeadersRoot)
        )
    }
}


fun configureCinterop(
    cinteropName: String,
    os: OS,
    arch: Arch,
    target: KotlinNativeTarget,
    targetString: String,
    linkerOpts: List<String>,
) {
    val tasks = target.project.tasks
    val taskNameSuffix = joinToTitleCamelCase(os.idWithSuffix(isUikitSim = target.isUikitSimulator()), arch.id)
    val writeCInteropDef = tasks.register("writeCInteropDef$taskNameSuffix", WriteCInteropDefFile::class.java) {
        this.linkerOpts.set(linkerOpts)
        outputFile.set(project.layout.buildDirectory.file("cinterop/$targetString/$cinteropName.def"))
    }
    tasks.withType(CInteropProcess::class.java).configureEach {
        if (konanTarget == target.konanTarget) {
            dependsOn(writeCInteropDef)
        }
    }
    target.compilations.getByName("main") {
        cinterops.create(cinteropName).apply {
            definitionFile.set(writeCInteropDef.flatMap { it.outputFile })
        }
    }
}

private fun ohosSkiaStaticLibraries(
    skiaDir: String,
    targetString: String,
    buildType: SkiaBuildType,
): List<String> {
    val skiaBinSubdir = "$skiaDir/out/${buildType.id}-$targetString"
    val libraries = if (targetString.startsWith("ohos-")) listOf(
        "libskresources.a",
        "libskparagraph.a",
        "libskia.a",
        "libjsonreader.a",
        "libskottie.a",
        "libsksg.a",
        "libskunicode_core.a",
        "libskunicode_icu.a",
        "libskshaper.a",
        "libskcms.a",
        "libwuffs.a",
    ) else listOf(
        "libskresources.a",
        "libskparagraph.a",
        "libskia.a",
        "libskia_ganesh_ext.a",
        "libicu.a",
        "libjsonreader.a",
        "libskottie.a",
        "libsvg.a",
        "libpng.a",
        "libwebp_sse41.a",
        "libsksg.a",
        "libskunicode_core.a",
        "libskunicode_icu.a",
        "libwebp.a",
        "libdng_sdk.a",
        "libpiex.a",
        "libharfbuzz.a",
        "libexpat.a",
        "libzlib.a",
        "libjpeg.a",
        "libskshaper.a"
    )
    return libraries.map {
        "$skiaBinSubdir/$it"
    }
}

fun SkikoProjectContext.configureNativeTarget(os: OS, arch: Arch, target: KotlinNativeTarget) = with(this.project) {
    if (!os.isCompatibleWithHost) return

    target.generateVersion(os, arch, skiko)
    val isUikitSim = target.isUikitSimulator()
    val isFusion = os == OS.OHOS && isOhosFusionRendererEnabled()

    val targetString = "${os.idWithSuffix(isUikitSim = isUikitSim)}-${arch.id}"
    val bridgeArtifactKey = if (isFusion) "fusion-$targetString" else targetString
    val unzipper = if (isFusion) null else registerOrGetSkiaDirProvider(os, arch, isUikitSim)
    val skiaDir = unzipper?.get()?.absolutePath

    val bridgesLibrary = layout.buildDirectory.file(
        "nativeBridges/static/$bridgeArtifactKey/$nativeBridgesLibPrefix-$bridgeArtifactKey.a"
    )
    val bridgesLibraryPath = bridgesLibrary.get().asFile.absolutePath

    // For iOS/tvOS we patch every library so that public Skia symbols are
    // renamed, preventing conflicts when multiple Skia copies are present in
    // the same app binary. Many C++ Itanium-mangled symbols are rewritten by
    // inserting the `skiko` namespace into the mangled name; C symbols and
    // unsupported shapes fall back to a "_skiko" suffix.
    val requiresSymbolPatching = os == OS.IOS || os == OS.TVOS
    val patchedLibsDir = layout.buildDirectory.dir("nativeBridges/patched/$targetString").get().asFile

    val ohRenderCompile = if (isFusion) compileOhRenderTask(os, arch) else null
    val ohRenderArchive = ohRenderCompile?.let { archiveOhRenderTask(os, arch, it) }
    val skiaBinDir = skiaDir?.let { "$it/out/${buildType.id}-$targetString" }
    val resolvedBinaryInputs = if (isFusion || os == OS.OHOS) {
        ResolvedBinaryConfiguration()
    } else {
        resolveBinaryInputs(os, arch, TargetEnv.NATIVE, skiaBinDir!!)
    }
    val nativeArchives = if (os == OS.OHOS && !isFusion) {
        ohosSkiaStaticLibraries(skiaDir!!, targetString, buildType)
    } else {
        resolvedBinaryInputs.staticArchivePaths.distinct()
    }
    val allLibraries = if (isFusion) {
        val ohRenderRoot = ohRenderRoot()
        val archPrefix = if (arch == Arch.Arm64) "aarch64" else "x86_64"
        listOf(
            bridgesLibraryPath,
            layout.buildDirectory.file("nativeFusion/static/$targetString/ohrender-$targetString.a")
                .get().asFile.absolutePath,
            ohRenderRoot.resolve("third_party/libs/${archPrefix}_libexpat.a").absolutePath,
        )
    } else if (requiresSymbolPatching) {
        nativeArchives.map { lib ->
            "${patchedLibsDir.absolutePath}/${File(lib).name}"
        } + "${patchedLibsDir.absolutePath}/$nativeBridgesLibPrefix-$targetString.a"
    } else {
        nativeArchives + bridgesLibraryPath
    }

    val hiddenSymbolsFile = layout.buildDirectory.file(
        "nativeHiddenSymbols/$targetString/${if (os.isLinux) "symbols.map" else "symbols.txt"}"
    )
    val hiddenSymbolSources = if (requiresSymbolPatching) {
        val patchedBaseLibraries = nativeArchives.map { lib -> "${patchedLibsDir.absolutePath}/${File(lib).name}" }
        val patchedBridgeLibrary = "${patchedLibsDir.absolutePath}/${nativeBridgesLibPrefix}-$targetString.a"
        patchedBaseLibraries + patchedBridgeLibrary
    } else {
        nativeArchives
    }
    val hideSkiaSymbols = project.registerSkikoTask<HideSkiaSymbolsTask>(
        "hideSkiaSymbols".withSuffix(isUikitSim = isUikitSim),
        os,
        arch
    ) {
        targetOs.set(os)
        symbolExtractorCommand.set(if (os == OS.IOS || os == OS.TVOS) listOf("xcrun", "nm") else listOf("nm"))
        symbolSourceLibraries.from(hiddenSymbolSources.map { File(it) })
        outputFile.set(hiddenSymbolsFile)
    }

    val linkerFlags = when (os) {
        OS.MacOS -> {
            configureCinterop(cinteropName, os, arch, target, targetString, resolvedBinaryInputs.frameworks)
            mutableListOfLinkerOptions(resolvedBinaryInputs.frameworks + resolvedBinaryInputs.linkFlags + listOf(
                "-unexported_symbols_list",
                hiddenSymbolsFile.get().asFile.absolutePath
            ))
        }
        OS.IOS -> {
            // list of linker options to be included into klib, which are needed for skiko consumers
            // https://github.com/JetBrains/compose-multiplatform/issues/3178
            // Important! Removing or renaming cinterop-uikit publication might cause compile error
            // for projects depending on older Compose/Skiko transitively https://youtrack.jetbrains.com/issue/KT-60399
            configureCinterop("uikit", os, arch, target, targetString, resolvedBinaryInputs.frameworks)
            mutableListOfLinkerOptions(resolvedBinaryInputs.frameworks + resolvedBinaryInputs.linkFlags + listOf(
                "-unexported_symbols_list",
                hiddenSymbolsFile.get().asFile.absolutePath
            ))
        }
        OS.TVOS -> {
            configureCinterop("uikit", os, arch, target, targetString, resolvedBinaryInputs.frameworks)
            mutableListOfLinkerOptions(resolvedBinaryInputs.frameworks + resolvedBinaryInputs.linkFlags + listOf(
                "-unexported_symbols_list",
                hiddenSymbolsFile.get().asFile.absolutePath
            ))
        }
        OS.Linux -> {
            val options = mutableListOf(
                "-L/usr/lib64",
                "-L/usr/lib/${if (arch == Arch.Arm64) "aarch64" else "x86_64"}-linux-gnu",
            )
            options.addAll(resolvedBinaryInputs.directStaticArchivePaths)
            options.addAll(resolvedBinaryInputs.dynamicLibNames.map { "-l$it" })
            options.addAll(resolvedBinaryInputs.linkFlags)
            // When cross-compiling for ARM64 from x64, use the ARM toolchain sysroot
            if (arch == Arch.Arm64 && hostArch != Arch.Arm64) {
                // ARM GNU toolchain sysroot paths
                options.add(0, "-L/opt/arm-gnu-toolchain/aarch64-none-linux-gnu/libc/lib64")
                options.add(1, "-L/opt/arm-gnu-toolchain/aarch64-none-linux-gnu/libc/usr/lib64")
            }
            options.add("--version-script=${hiddenSymbolsFile.get().asFile.absolutePath}")
            mutableListOfLinkerOptions(options)
        }
        OS.OHOS -> mutableListOfLinkerOptions(if (isFusion) listOf(
            "-target", ohosClangTarget(arch),
            "-lace_napi.z", "-lhilog_ndk.z", "-lnative_drawing",
            "-lpixelmap", "-lpixelmap_ndk.z", "-limage_source",
            "-lhitrace_ndk.z", "-luv", "-limage_packer",
            "-ldeviceinfo_ndk.z", "-lace_ndk.z", "-larkui_ndk", "-lz",
            "-lrawfile.z", "-lohresmgr",
        ) else listOf(
            "-target", ohosClangTarget(arch),
            "-L$skiaBinDir", "-lGLESv3", "-lEGL", "-ldl", "-lhilog",
            "-lrawfile.z", "-lohresmgr",
            "$skiaBinDir/libskia.a", "$skiaBinDir/libskparagraph.a", "$skiaBinDir/libskshaper.a"
        ))
        else -> mutableListOf()
    }
    if (skiko.includeTestHelpers) {
        linkerFlags.addAll(when (os) {
            OS.Linux -> listOf(
                "-linker-option", "-lX11",
                "-linker-option", "-lGLX",
            )
            else -> emptyList()
        })
    }

    // For some reason since 1.8.0 we need to set freeCompilerArgs for binaries AND for compilations
    target.binaries.all {
        freeCompilerArgs += allLibraries.map { listOf("-include-binary", it) }.flatten() + linkerFlags
    }


    target.compilations.all {
        compilerOptions.configure {
            freeCompilerArgs.addAll(
                allLibraries.flatMap { listOf("-include-binary", it) } + linkerFlags
            )
        }
    }

    val crossCompileTask = compileNativeBridgesTask(os, arch, isUikitSim = isUikitSim)

    // TODO: move to LinkSkikoTask.
    val actionName = (if (isFusion) "linkFusionNativeBridges" else "linkNativeBridges")
        .withSuffix(isUikitSim = isUikitSim)
    val linkTask = project.registerSkikoTask<Exec>(actionName, os, arch) {
        dependsOn(crossCompileTask)
        val objectFilesDir = crossCompileTask.map { it.outDir.get() }
        val objectFiles = project.fileTree(objectFilesDir) {
            include("**/*.o")
        }
        inputs.files(objectFiles)
        val outDir = layout.buildDirectory.dir("nativeBridges/static/$bridgeArtifactKey").get().asFile
        val staticLib = "$nativeBridgesLibPrefix-$bridgeArtifactKey.a"
        workingDir = outDir
        when (os) {
            OS.Linux, OS.OHOS -> {
                executable = if (arch == Arch.Arm64 && hostArch != Arch.Arm64) "aarch64-linux-gnu-ar" else "ar"
                if (os == OS.OHOS) executable = System.getenv("OHOS_LLVM_AR")
                    ?: ohosToolchainRoot()?.resolve("bin/llvm-ar")?.absolutePath
                    ?: "llvm-ar"
                argumentProviders.add { listOf("-crs", staticLib) }
            }
            OS.MacOS, OS.IOS, OS.TVOS -> {
                executable = "libtool"
                argumentProviders.add { listOf("-static", "-o", staticLib) }
            }
            else -> error("Unexpected OS for native bridges linking: $os")
        }
        argumentProviders.add { objectFiles.files.map { it.absolutePath } }
        file(outDir).mkdirs()
        outputs.dir(outDir)
    }

    // For iOS/tvOS: patch all Skia + skiko-bridge symbols after linking.
    val compilationDependency = if (requiresSymbolPatching) {
        val patchActionName = "patchSkikoSymbols".withSuffix(isUikitSim = isUikitSim)
        project.registerSkikoTask<PatchSkiaSymbolsTask>(patchActionName, os, arch) {
            dependsOn(unzipper!!)
            dependsOn(linkTask)
            skiaLibs.set(nativeArchives.map { File(it) })
            symbolSourceLibs.set(emptyList())
            skikoBridge.set(File(bridgesLibraryPath))
            outputDir.set(patchedLibsDir)
        }
    } else {
        linkTask
    }

    hideSkiaSymbols.configure {
        unzipper?.let { dependsOn(it) }
        dependsOn(compilationDependency)
    }

    target.compilations.all {
        compileTaskProvider.configure {
            dependsOn(hideSkiaSymbols)
            ohRenderArchive?.let { dependsOn(it) }
        }
    }
}


fun KotlinMultiplatformExtension.configureIOSTestsWithMetal(project: Project) {
    val metalTestTargets = listOf("iosX64", "iosSimulatorArm64")
    metalTestTargets.forEach { target: String ->
        if (targets.names.contains(target)) {
            val testBinary = targets.getByName<KotlinNativeTarget>(target).binaries.getTest("DEBUG")
            project.tasks.register(target + "TestWithMetal") {
                dependsOn(testBinary.linkTaskProvider)
                doLast {
                    val simulatorIdPropertyKey = "skiko.iosSimulatorUUID"
                    val simulatorId = project.findProperty(simulatorIdPropertyKey)?.toString()
                        ?: error("Property '$simulatorIdPropertyKey' not found. Pass it with -P$simulatorIdPropertyKey=...")

                    project.providers.exec { commandLine("xcrun", "simctl", "boot", simulatorId) }
                    try {
                        project.providers.exec { commandLine("xcrun", "simctl", "spawn", simulatorId, testBinary.outputFile) }
                    } finally {
                        project.providers.exec { commandLine("xcrun", "simctl", "shutdown", simulatorId) }
                    }
                }
            }
        }
    }
}
