package invalid.lena.rsend

import android.content.Context
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.InputStream
import java.util.concurrent.TimeUnit

// Native locates and runs the executables shipped inside the APK as lib*.so.
// Android only allows exec of native code from the app's native library dir,
// so the rsync and rsh binaries are packaged there and run from there.
object Native {

    fun rsync(ctx: Context): File = File(ctx.applicationInfo.nativeLibraryDir, "libxrsync.so")

    fun rsh(ctx: Context): File = File(ctx.applicationInfo.nativeLibraryDir, "libxrsh.so")

    data class Result(val code: Int, val output: String)

    // Blocking; call off the main thread. stderr is merged into stdout.
    //
    // The output is drained on its own thread while the process runs. Waiting
    // first would deadlock as soon as the child filled the pipe buffer, and the
    // only symptom would be the 45s timeout.
    fun run(bin: File, args: List<String>, env: Map<String, String> = emptyMap()): Result {
        val pb = ProcessBuilder(listOf(bin.absolutePath) + args).redirectErrorStream(true)
        pb.environment().putAll(env)
        val p = pb.start()
        var output = ""
        val drain = Thread {
            try {
                p.inputStream.use { output = boundedOutput(it) }
            } catch (_: Exception) {
                // The pipe closes when the process is killed; whatever was read
                // before that is still worth returning.
            }
        }
        drain.start()
        if (!p.waitFor(45, TimeUnit.SECONDS)) {
            p.destroyForcibly()
            p.waitFor()
            p.inputStream.close()
            drain.join(5_000)
            throw IllegalStateException("${bin.name} timed out")
        }
        drain.join(5_000)
        if (drain.isAlive) {
            p.inputStream.close()
            drain.join(5_000)
        }
        if (drain.isAlive) throw IllegalStateException("${bin.name} output reader did not stop")
        return Result(p.exitValue(), output)
    }

    internal fun boundedOutput(input: InputStream): String {
        val out = ByteArrayOutputStream()
        val buf = ByteArray(4096)
        var truncated = false
        while (true) {
            val n = input.read(buf)
            if (n < 0) break
            val room = MAX_OUTPUT_BYTES - out.size()
            if (room > 0) out.write(buf, 0, minOf(room, n))
            if (n > room) truncated = true
        }
        return String(out.toByteArray(), Charsets.UTF_8) + if (truncated) "\n[output truncated]\n" else ""
    }

    private const val MAX_OUTPUT_BYTES = 64 * 1024
}
