package de.duffy.screencast.miracast

import java.io.InputStream

/**
 * Eine RTSP-Nachricht. Bei Miracast schicken beide Seiten ueber dieselbe
 * Verbindung Anfragen, deshalb muss hier beides gelesen werden koennen:
 * Anfragen des Fernsehers und Antworten auf unsere eigenen.
 */
data class RtspMessage(
    val isRequest: Boolean,
    val method: String = "",
    val uri: String = "",
    val statusCode: Int = 0,
    val statusText: String = "",
    val headers: Map<String, String> = emptyMap(),
    val body: String = ""
) {
    val cseq: Int get() = headers["cseq"]?.trim()?.toIntOrNull() ?: 0
    val session: String? get() = headers["session"]?.substringBefore(';')?.trim()

    /** Sucht einen wfd-Parameter im Nachrichtenrumpf. */
    fun param(name: String): String? = body.lineSequence()
        .firstOrNull { it.startsWith("$name:", ignoreCase = true) }
        ?.substringAfter(':')?.trim()

    companion object {
        /** Liest genau eine Nachricht. Gibt null zurueck, wenn die Gegenseite auflegt. */
        fun read(input: InputStream): RtspMessage? {
            val lines = ArrayList<String>()
            val line = StringBuilder()
            while (true) {
                val b = input.read()
                if (b < 0) return null
                if (b == '\n'.code) {
                    val text = line.toString().trimEnd('\r')
                    line.setLength(0)
                    if (text.isEmpty()) { if (lines.isEmpty()) continue else break }
                    lines.add(text)
                } else if (b != '\r'.code) {
                    line.append(b.toChar())
                }
                if (lines.size > 80) return null
            }
            if (lines.isEmpty()) return null

            val headers = HashMap<String, String>()
            for (i in 1 until lines.size) {
                val idx = lines[i].indexOf(':')
                if (idx > 0) headers[lines[i].substring(0, idx).trim().lowercase()] =
                    lines[i].substring(idx + 1).trim()
            }
            val length = headers["content-length"]?.trim()?.toIntOrNull() ?: 0
            val body = if (length > 0) {
                val buffer = ByteArray(length)
                var read = 0
                while (read < length) {
                    val n = input.read(buffer, read, length - read)
                    if (n < 0) break
                    read += n
                }
                String(buffer, 0, read, Charsets.UTF_8)
            } else ""

            val first = lines[0]
            return if (first.startsWith("RTSP/")) {
                val parts = first.split(' ', limit = 3)
                RtspMessage(
                    isRequest = false,
                    statusCode = parts.getOrNull(1)?.toIntOrNull() ?: 0,
                    statusText = parts.getOrNull(2).orEmpty(),
                    headers = headers, body = body
                )
            } else {
                val parts = first.split(' ')
                RtspMessage(
                    isRequest = true,
                    method = parts.getOrNull(0).orEmpty(),
                    uri = parts.getOrNull(1).orEmpty(),
                    headers = headers, body = body
                )
            }
        }

        fun request(method: String, uri: String, cseq: Int, headers: Map<String, String> = emptyMap(), body: String = ""): ByteArray {
            val sb = StringBuilder("$method $uri RTSP/1.0\r\n")
            sb.append("CSeq: $cseq\r\n")
            for ((k, v) in headers) sb.append("$k: $v\r\n")
            if (body.isNotEmpty()) {
                sb.append("Content-Type: text/parameters\r\n")
                sb.append("Content-Length: ${body.toByteArray(Charsets.UTF_8).size}\r\n")
            }
            sb.append("\r\n").append(body)
            return sb.toString().toByteArray(Charsets.UTF_8)
        }

        fun response(cseq: Int, headers: Map<String, String> = emptyMap(), body: String = "", status: String = "200 OK"): ByteArray {
            val sb = StringBuilder("RTSP/1.0 $status\r\n")
            sb.append("CSeq: $cseq\r\n")
            for ((k, v) in headers) sb.append("$k: $v\r\n")
            if (body.isNotEmpty()) {
                sb.append("Content-Type: text/parameters\r\n")
                sb.append("Content-Length: ${body.toByteArray(Charsets.UTF_8).size}\r\n")
            }
            sb.append("\r\n").append(body)
            return sb.toString().toByteArray(Charsets.UTF_8)
        }
    }
}
