import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.api.GradleException
import org.gradle.api.Project
import java.io.File

enum class OS(
    val id: String,
    val clangFlags: Array<String>
) {
    Linux("linux", arrayOf()),
    Android("android", arrayOf()),
    Windows("windows", arrayOf()),
    MacOS("macos", arrayOf("-mmacosx-version-min=11.0")),
    Wasm("wasm", arrayOf()),
    IOS("ios", arrayOf()),
    TVOS("tvos", arrayOf())
    ;

    val isWindows
        get() = this == Windows

    val isMacOs
        get() = this == MacOS

    fun idWithSuffix(isUikitSim: Boolean = false): String {
        return id + if (isUikitSim) "Sim" else ""
    }
}

val OS.isCompatibleWithHost: Boolean
    get() = when (this) {
        OS.Linux -> hostOs == OS.Linux
        OS.Windows -> hostOs == OS.Windows
        OS.MacOS, OS.IOS, OS.TVOS -> hostOs == OS.MacOS
        OS.Wasm -> true
        OS.Android -> true
    }

fun compilerForTarget(os: OS, arch: Arch): String =
    when (os) {
        // TODO: Use clang++ for all Linux targets
        OS.Linux -> when (arch) {
            Arch.X64 -> "g++"
            Arch.Arm64 -> if (hostArch == Arch.Arm64) "g++" else "aarch64-linux-gnu-g++"
            Arch.Wasm -> "Unexpected combination: $os & $arch"
        }
        OS.Android -> "clang++"
        OS.Windows -> "clang-cl.exe"
        OS.MacOS, OS.IOS, OS.TVOS -> "clang++"
        OS.Wasm -> if (Os.isFamily(Os.FAMILY_WINDOWS)) "emcc.bat" else "emcc"
    }

fun linkerForTarget(os: OS, arch: Arch): String =
    if (os.isWindows) "lld-link.exe" else compilerForTarget(os, arch)

val OS.dynamicLibExt: String
    get() = when (this) {
        OS.Linux, OS.Android -> ".so"
        OS.Windows -> ".dll"
        OS.MacOS, OS.IOS, OS.TVOS -> ".dylib"
        OS.Wasm -> ".wasm"
    }


enum class Arch(val id: String) {
    X64("x64"),
    Arm64("arm64"),
    Wasm("wasm");

    companion object {
        fun byName(name: String) = Arch.values().find { it.id == name }
    }
}

enum class SkiaBuildType(
    val id: String,
    val flags: Array<String>,
    val clangFlags: Array<String>,
    val winCompilerFlags: Array<String>,
    val winLinkerFlags: Array<String>
) {
    DEBUG(
        id = "Debug",
        flags = arrayOf("-DSK_DEBUG"),
        clangFlags = arrayOf("-std=c++17", "-g", "-DSK_TRIVIAL_ABI=[[clang::trivial_abi]]"),
        winCompilerFlags = arrayOf("/Zi", "/std:c++17"),
        winLinkerFlags = arrayOf("/DEBUG"),
    ),
    RELEASE(
        id = "Release",
        flags = arrayOf("-DNDEBUG"),
        clangFlags = arrayOf("-std=c++17", "-O3"),
        winCompilerFlags = arrayOf("/O2", "/std:c++17"),
        winLinkerFlags = arrayOf("/DEBUG"),
    );
    override fun toString() = id
}

val hostOs by lazy {
    val osName = System.getProperty("os.name")
    when {
        osName == "Mac OS X" -> OS.MacOS
        osName == "Linux" -> OS.Linux
        osName.startsWith("Win") -> OS.Windows
        else -> throw Error("Unknown OS $osName")
    }
}

val hostArch by lazy {
    val osArch = System.getProperty("os.arch")
    when (osArch) {
        "x86_64", "amd64" -> Arch.X64
        "aarch64" -> Arch.Arm64
        else -> throw Error("Unknown arch $osArch")
    }
}

fun targetId(os: OS, arch: Arch) = "${os.id}-${arch.id}"

val jdkHome = System.getProperty("java.home") ?: error("'java.home' is null")

class SkikoProperties(private val myProject: Project) {
    val isTeamcityCIBuild: Boolean
        get() = myProject.hasProperty("teamcity")

    val planeDeployVersion: String = myProject.property("deploy.version") as String

    val deployVersion: String
        get() {
            val main = if (isRelease) planeDeployVersion else "$planeDeployVersion-SNAPSHOT"
            var metadata = if (buildType == SkiaBuildType.DEBUG) "+debug" else ""
            metadata += if (isWasmBuildWithProfiling) "+profiling" else ""
            return main + metadata
        }

    val isRelease: Boolean
        get() = myProject.findProperty("deploy.release") == "true"

    val buildType: SkiaBuildType
        get() = if (myProject.findProperty("skiko.debug") == "true") SkiaBuildType.DEBUG else SkiaBuildType.RELEASE

    val isWasmBuildWithProfiling: Boolean
        get() = myProject.findProperty("skiko.wasm.withProfiling") == "true"

    val targetArch: Arch
        get() = myProject.findProperty("skiko.arch")?.toString()?.let(Arch::byName) ?: hostArch

    val includeTestHelpers: Boolean
        get() = !isRelease

    val releaseGithubVersion: String
        get() = (myProject.property("release.github.version") as String)

    val releaseGithubCommit: String
        get() = (myProject.property("release.github.commit") as String)

    val visualStudioBuildToolsDir: File?
        get() = System.getenv()["SKIKO_VSBT_PATH"]?.let { File(it) }?.takeIf { it.isDirectory }

    /**
     * Skia repository root directory for building Skia from source.
     *
     * Property naming conventions:
     * - Gradle property: `-Pskia.repo.dir=...` (preferred)
     * - Kotlin accessor: `skiaRepoDir`
     * - Environment variable: `SKIA_REPO_DIR`
     *
     * Legacy `skia.pack.dir` and `SKIA_PACK_DIR` are still accepted to keep
     * existing local workflows working while the standalone repo is retired.
     *
     * Usage: `-Pskia.repo.dir=/path/to/skia`
     *
     * Note: Must point to skia repository root containing tools/skia_release/.
     */
    // todo: make compatible with the configuration cache
    val skiaRepoDir: File?
        get() = (
            System.getenv()["SKIA_REPO_DIR"]
                ?: System.getenv()["SKIA_PACK_DIR"]
                ?: System.getProperty("skia.repo.dir")
                ?: System.getProperty("skia.pack.dir")
                ?: myProject.findProperty("skia.repo.dir")?.toString()
                ?: myProject.findProperty("skia.pack.dir")?.toString()
            )?.let { skiaRepoDirProp ->
                val file = File(skiaRepoDirProp)
                if (!file.isDirectory) {
                    throw GradleException("\"skia.repo.dir\" property was explicitly set to $skiaRepoDirProp which is not resolved as a directory")
                }
                file
            }

    /**
     * Skia source directory for publishing.
     *
     * Property naming conventions:
     * - Gradle property: `-Pskia.dir=...` (kebab-case, Gradle convention)
     * - Kotlin accessor: `skiaDir` (camelCase, Kotlin convention)
     * - Environment variable: `SKIA_DIR`
     *
     * Usage: `-Pskia.dir=/path/to/skia`
     *
     * Note: Must point to directory containing built Skia source code and headers.
     * When using the integrated release scripts this is typically the same path as `skia.repo.dir`.
     */
    // todo: make compatible with the configuration cache
    val skiaDir: File?
        get() = (System.getenv()["SKIA_DIR"] ?: System.getProperty("skia.dir") ?: myProject.findProperty("skia.dir")
            ?.toString())?.let { skiaDirProp ->
                val file = File(skiaDirProp)
                if (!file.isDirectory) throw (GradleException("\"skia.dir\" property was explicitly set to ${skiaDirProp} which is not resolved as a directory"))
                file
            }

    val composeRepoUrl: String
        get() = System.getenv("COMPOSE_REPO_URL") ?: "https://packages.jetbrains.team/maven/p/cmp/dev"

    val composeRepoUserName: String
        get() = System.getenv("COMPOSE_REPO_USERNAME") ?: ""

    val composeRepoKey: String
        get() = System.getenv("COMPOSE_REPO_KEY") ?: ""

    val signHost: String?
        get() = System.getenv("JB_SIGN_HOST")
            ?: (myProject.findProperty("sign_host") as? String)

    val signUser: String?
        get() = System.getenv("JB_SIGN_USER")
            ?: (myProject.findProperty("sign_host_user") as? String)

    val signToken: String?
        get() = System.getenv("JB_SIGN_TOKEN")
            ?: (myProject.findProperty("sign_host_token") as? String)

    val dependenciesDir: File
        get() = myProject.rootProject.projectDir.resolve("dependencies")

    val skiaTarget: SkiaTarget
        get() {
            val targetString = System.getenv("SKIA_TARGET")
                ?: myProject.findProperty("skia.target")?.toString()
                ?: hostOs.id  // Default to current OS
            return SkiaTarget.fromString(targetString)
        }

    val skiaVersionFromEnvOrProperties: String
        get() {
            // Environment variable takes precedence
            System.getenv("SKIA_VERSION")?.let { return it }

            // Fall back to gradle.properties
            return myProject.property("dependencies.skia").toString()
        }
}

object SkikoGradleProperties {
    const val AWT_ENABLED = "skiko.awt.enabled"
    const val WASM_ENABLED = "skiko.wasm.enabled"
    const val ANDROID_ENABLED = "skiko.android.enabled"
    const val NATIVE_ENABLED = "skiko.native.enabled"
    const val NATIVE_IOS = "skiko.native.ios"
    const val NATIVE_IOS_ARM64 = "skiko.native.ios.arm64.enabled"
    const val NATIVE_IOS_SIMULATOR_ARM64 = "skiko.native.ios.simulatorArm64.enabled"
    const val NATIVE_IOS_X64 = "skiko.native.ios.x64.enabled"
    const val NATIVE_TVOS = "skiko.native.tvos"
    const val NATIVE_TVOS_ARM64 = "skiko.native.tvos.arm64.enabled"
    const val NATIVE_TVOS_SIMULATOR_ARM64 = "skiko.native.tvos.simulatorArm64.enabled"
    const val NATIVE_TVOS_X64 = "skiko.native.tvos.x64.enabled"
    const val NATIVE_MAC = "skiko.native.mac.enabled"
    const val NATIVE_LINUX = "skiko.native.linux.enabled"
}

object SkikoArtifacts {
    val groupId = "org.jetbrains.skiko"
    // names are also used in samples, e.g. samples/SkijaInjectSample/build.gradle
    val commonArtifactId = "skiko"
    val jvmArtifactId = "skiko-awt"
    val jvmRuntimeArtifactId = "skiko-awt-runtime"
    // an artifact (klib) for k/js targets
    val jsArtifactId = "skiko-js"
    // an artifact (klib) for k/wasm targets
    val wasmArtifactId = "skiko-wasm-js"
    // an artifact with skiko.wasm and supporting js code - jar
    val jsWasmArtifactId = "skiko-js-wasm-runtime"
    fun jvmRuntimeArtifactIdFor(os: OS, arch: Arch) =
        if (os == OS.Android)
            "skiko-android-runtime-${arch.id}"
        else
            "skiko-awt-runtime-${targetId(os, arch)}"
    fun jvmAdditionalRuntimeArtifactIdFor(name: String, os: OS, arch: Arch) =
        "skiko-awt-runtime-$name-${os.id}-${arch.id}"
    // Using custom name like skiko-<Os>-<Arch> (with a dash)
    // does not seem possible (at least without adding a dash to a target's tasks),
    // so we're using the default naming pattern instead.
    // See https://youtrack.jetbrains.com/issue/KT-50001.
    fun nativeArtifactIdFor(os: OS, arch: Arch, isUikitSim: Boolean = false) =
        "skiko-${os.id + if (isUikitSim) "simulator" else ""}${arch.id}"
}
