package de.arxsilex.maptools.converter.core.pbf

import java.io.ByteArrayInputStream
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
import java.util.ServiceLoader
import java.util.zip.InflaterInputStream

/**
 * Erweiterungspunkt für optionale native Dekompressionsmodule, beispielsweise CUDA/nvCOMP.
 * Ein Zusatzmodul wird per ServiceLoader gefunden; die portable Grundausgabe benötigt es nicht.
 */
interface PbfDecompressionProvider {
    val id: String
    val priority: Int
    val accelerated: Boolean

    fun isAvailable(): Boolean
    fun supports(compression: PbfCompression): Boolean
    fun decompress(compressed: ByteArray, expectedSize: Int, maximumSize: Int): ByteArray
}

data class PbfAccelerationStatus(
    val activeProvider: String,
    val accelerated: Boolean,
    val cudaRuntimeCandidateFound: Boolean,
    val discoveredProviders: List<String>,
)

object PbfDecompression {
    private val discoveredProviders: List<PbfDecompressionProvider> by lazy {
        runCatching {
            ServiceLoader.load(PbfDecompressionProvider::class.java)
                .iterator()
                .asSequence()
                .toList()
        }.getOrDefault(emptyList())
    }

    fun accelerationStatus(): PbfAccelerationStatus {
        val provider = select(PbfCompression.ZLIB, discoveredProviders)
        return PbfAccelerationStatus(
            activeProvider = provider.id,
            accelerated = provider.accelerated,
            cudaRuntimeCandidateFound = CudaRuntimeProbe.candidateFound(),
            discoveredProviders = discoveredProviders.map(PbfDecompressionProvider::id),
        )
    }

    internal fun decompress(
        compression: PbfCompression,
        compressed: ByteArray,
        expectedSize: Int,
        maximumSize: Int,
    ): ByteArray = select(compression, discoveredProviders)
        .decompress(compressed, expectedSize, maximumSize)

    internal fun select(
        compression: PbfCompression,
        additionalProviders: List<PbfDecompressionProvider>,
    ): PbfDecompressionProvider = (additionalProviders + CpuZlibDecompressionProvider)
        .asSequence()
        .filter { provider ->
            runCatching { provider.isAvailable() && provider.supports(compression) }.getOrDefault(false)
        }
        .maxByOrNull(PbfDecompressionProvider::priority)
        ?: throw IOException("Für $compression ist kein Dekompressionsmodul verfügbar.")
}

private object CpuZlibDecompressionProvider : PbfDecompressionProvider {
    override val id = "cpu-zlib"
    override val priority = 0
    override val accelerated = false

    override fun isAvailable(): Boolean = true

    override fun supports(compression: PbfCompression): Boolean = compression == PbfCompression.ZLIB

    override fun decompress(compressed: ByteArray, expectedSize: Int, maximumSize: Int): ByteArray {
        require(expectedSize in 0..maximumSize) { "Ungültige erwartete PBF-Blockgröße." }
        val unpacked = InflaterInputStream(ByteArrayInputStream(compressed)).use {
            it.readBounded(maximumSize)
        }
        if (unpacked.size != expectedSize) {
            throw IOException("Die entpackte PBF-Blockgröße stimmt nicht mit raw_size überein.")
        }
        return unpacked
    }
}

internal object CudaRuntimeProbe {
    fun candidateFound(
        environment: Map<String, String> = System.getenv(),
        osName: String = System.getProperty("os.name", ""),
    ): Boolean {
        if (!environment["CUDA_PATH"].isNullOrBlank() || !environment["CUDA_HOME"].isNullOrBlank()) {
            return true
        }
        if (osName.contains("windows", ignoreCase = true)) {
            val windowsRoot = environment["SystemRoot"] ?: environment["WINDIR"]
            return windowsRoot != null && Files.isRegularFile(Path.of(windowsRoot, "System32", "nvcuda.dll"))
        }
        return listOf(
            Path.of("/usr/lib/x86_64-linux-gnu/libcuda.so.1"),
            Path.of("/usr/lib64/libcuda.so.1"),
        ).any(Files::isRegularFile)
    }
}
