package launchers

import java.io.IOException
import java.lang.foreign.FunctionDescriptor
import java.lang.foreign.Linker
import java.lang.foreign.SymbolLookup
import java.lang.foreign.ValueLayout
import java.lang.invoke.MethodHandle
import java.nio.file.Path
import kotlin.math.min

internal fun javaExecutable(): String = Path.of(System.getProperty("java.home"), "bin", "java").toString()

internal fun exec(
    displayName: String,
    command: List<String>,
): Nothing = NativeExec.exec(displayName, command)

private object NativeExec {
    private const val CHUNK_BYTES = 8

    init {
        System.loadLibrary("jar_wrapper_exec")
    }

    private val reset = downcall("jar_wrapper_exec_reset", FunctionDescriptor.of(ValueLayout.JAVA_INT))
    private val append =
        downcall(
            "jar_wrapper_exec_append",
            FunctionDescriptor.of(ValueLayout.JAVA_INT, ValueLayout.JAVA_LONG, ValueLayout.JAVA_INT),
        )
    private val endArgument = downcall("jar_wrapper_exec_end_argument", FunctionDescriptor.of(ValueLayout.JAVA_INT))
    private val run = downcall("jar_wrapper_exec_run", FunctionDescriptor.of(ValueLayout.JAVA_INT))

    fun exec(
        displayName: String,
        arguments: List<String>,
    ): Nothing =
        synchronized(this) {
            requireNativeSuccess(reset.invokeExact() as Int)
            for (argument in arguments) {
                append(argument.encodeToByteArray())
                requireNativeSuccess(endArgument.invokeExact() as Int)
            }
            val errorCode = run.invokeExact() as Int
            throw IOException("${nativeErrorMessage(errorCode)} while launching $displayName")
        }

    private fun append(bytes: ByteArray) {
        var start = 0
        while (start < bytes.size) {
            val count = min(CHUNK_BYTES, bytes.size - start)
            var chunk = 0L
            for (offset in 0..<count) {
                chunk = chunk or ((bytes[start + offset].toLong() and 0xff) shl (offset * Byte.SIZE_BITS))
            }
            requireNativeSuccess(append.invokeExact(chunk, count) as Int)
            start += count
        }
    }

    private fun downcall(
        name: String,
        descriptor: FunctionDescriptor,
    ): MethodHandle {
        val symbol = SymbolLookup.loaderLookup().find(name).orElseThrow { UnsatisfiedLinkError("Missing native symbol $name") }
        return Linker.nativeLinker().downcallHandle(symbol, descriptor)
    }

    private fun requireNativeSuccess(errorCode: Int) {
        check(errorCode == 0) { nativeErrorMessage(errorCode) }
    }

    private fun nativeErrorMessage(errorCode: Int): String =
        when (errorCode) {
            2 -> "No such file or directory"
            8 -> "Exec format error"
            13 -> "Permission denied"
            -1 -> "native exec received an invalid argument chunk"
            -2 -> "native exec received invalid UTF-8"
            -3 -> "native exec requires at least one argument"
            -4 -> "native exec received an unfinished argument"
            -5 -> "native exec state is unavailable"
            else -> "native exec failed with OS error $errorCode"
        }
}
