package de.duffy.screencast.miracast

import android.util.Log
import java.io.BufferedInputStream
import java.io.OutputStream
import java.net.Socket

/**
 * Fuehrt den Miracast-Handshake als Quelle (Source) durch.
 *
 * Der Ablauf ist im Wi-Fi-Display-Standard als M1 bis M7 festgeschrieben und
 * laeuft ueber eine einzige TCP-Verbindung, auf der beide Seiten Anfragen
 * stellen: wir fragen die Faehigkeiten ab und legen das Format fest, der
 * Fernseher fordert danach Sitzung und Wiedergabe an.
 */
class RtspSession(
    private val socket: Socket,
    /** Unsere Adresse in der Wi-Fi-Direct-Gruppe, fuer die Präsentations-URL. */
    private val localAddress: String,
    private val maxHeight: Int,
    private val listener: Listener
) {
    interface Listener {
        /** Format steht fest - der Encoder kann sich darauf einstellen. */
        fun onFormatChosen(format: WfdFormats.VideoFormat)
        /** Der Fernseher will Bild sehen: ab jetzt RTP an diesen Port. */
        fun onPlay(rtpPort: Int, format: WfdFormats.VideoFormat)
        fun onStopped(reason: String)
        fun onLog(message: String)
    }

    private val input = BufferedInputStream(socket.getInputStream())
    private val output: OutputStream = socket.getOutputStream()
    private var thread: Thread? = null
    @Volatile private var running = false

    private var cseq = 1
    private var pendingM1 = 0
    private var pendingM3 = 0
    private var pendingM4 = 0
    private var pendingM5 = 0

    private var optionsAnswered = false      // M1: unsere Anfrage wurde beantwortet
    private var peerOptionsSeen = false      // M2: der Fernseher hat zurueckgefragt
    private var capabilitiesRequested = false
    private var chosenFormat: WfdFormats.VideoFormat? = null
    private var sinkRtpPort = 0
    private var sessionId = "1"
    /** Wird von aussen gesetzt: der Port, von dem aus wir RTP senden. */
    var localRtpPort: Int = 0

    fun start() {
        running = true
        thread = Thread({ loop() }, "rtsp-session").also { it.isDaemon = true; it.start() }
    }

    fun stop() {
        running = false
        try { socket.close() } catch (_: Exception) {}
    }

    /** Regelmaessiges Lebenszeichen, sonst trennt der Fernseher nach ~60 s. */
    fun keepAlive() {
        if (!running) return
        send(RtspMessage.request("GET_PARAMETER", "rtsp://$localAddress/wfd1.0", cseq++))
    }

    private fun loop() {
        try {
            // M1: Wir melden uns und fragen, was die Gegenseite kann.
            pendingM1 = cseq
            send(RtspMessage.request("OPTIONS", "*", cseq++,
                mapOf("Require" to "org.wfa.wfd1.0")))

            while (running) {
                val message = RtspMessage.read(input) ?: break
                if (message.isRequest) handleRequest(message) else handleResponse(message)
            }
            listener.onStopped("Verbindung beendet")
        } catch (e: Exception) {
            if (running) listener.onStopped("Fehler: ${e.message}")
        } finally {
            running = false
            try { socket.close() } catch (_: Exception) {}
        }
    }

    // -------------------------------------------------- Anfragen des Fernsehers

    private fun handleRequest(message: RtspMessage) {
        log("<- ${message.method} ${message.uri}")
        when (message.method.uppercase()) {
            // M2: Der Fernseher fragt seinerseits, was wir koennen.
            "OPTIONS" -> {
                send(RtspMessage.response(message.cseq, mapOf(
                    "Public" to "org.wfa.wfd1.0, GET_PARAMETER, SET_PARAMETER, SETUP, PLAY, PAUSE, TEARDOWN"
                )))
                peerOptionsSeen = true
                maybeRequestCapabilities()
            }

            // M6: Sitzung einrichten. Hier nennt er den Port fuer den Bildstrom.
            "SETUP" -> {
                val transport = message.headers["transport"].orEmpty()
                val port = Regex("client_port=(\\d+)").find(transport)?.groupValues?.get(1)?.toIntOrNull()
                if (port != null && port > 0) sinkRtpPort = port
                log("Fernseher erwartet den Bildstrom auf Port $sinkRtpPort")
                send(RtspMessage.response(message.cseq, mapOf(
                    "Session" to "$sessionId;timeout=60",
                    "Transport" to "RTP/AVP/UDP;unicast;client_port=$sinkRtpPort" +
                        if (localRtpPort > 0) ";server_port=$localRtpPort" else ""
                )))
            }

            // M7: Los geht's.
            "PLAY" -> {
                send(RtspMessage.response(message.cseq, mapOf("Session" to "$sessionId;timeout=60")))
                val format = chosenFormat
                if (format != null && sinkRtpPort > 0) {
                    log("Wiedergabe angefordert - Bild geht raus")
                    listener.onPlay(sinkRtpPort, format)
                } else {
                    log("PLAY kam, aber Format oder Port fehlen noch")
                }
            }

            "PAUSE" -> send(RtspMessage.response(message.cseq, mapOf("Session" to sessionId)))

            "TEARDOWN" -> {
                send(RtspMessage.response(message.cseq, mapOf("Session" to sessionId)))
                running = false
                listener.onStopped("Der Fernseher hat die Uebertragung beendet")
            }

            "GET_PARAMETER" -> send(RtspMessage.response(message.cseq))
            "SET_PARAMETER" -> send(RtspMessage.response(message.cseq))
            else -> send(RtspMessage.response(message.cseq, status = "501 Not Implemented"))
        }
    }

    // ------------------------------------------- Antworten auf unsere Anfragen

    private fun handleResponse(message: RtspMessage) {
        log("-> Antwort ${message.statusCode} auf CSeq ${message.cseq}")
        if (message.statusCode !in 200..299) {
            listener.onStopped("Der Fernseher hat abgelehnt (${message.statusCode} ${message.statusText})")
            running = false
            return
        }
        when (message.cseq) {
            pendingM1 -> {
                optionsAnswered = true
                // Der Standard sieht vor, dass der Fernseher jetzt selbst
                // OPTIONS schickt. Bleibt das aus, fragen wir trotzdem weiter.
                maybeRequestCapabilities()
                Thread({
                    try { Thread.sleep(1500) } catch (_: InterruptedException) { return@Thread }
                    if (running) maybeRequestCapabilities(force = true)
                }, "rtsp-m3-fallback").apply { isDaemon = true }.start()
            }
            pendingM3 -> negotiate(message)
            pendingM4 -> triggerSetup()
            pendingM5 -> log("Warte darauf, dass der Fernseher die Sitzung einrichtet")
        }
    }

    @Synchronized
    private fun maybeRequestCapabilities(force: Boolean = false) {
        if (capabilitiesRequested || !optionsAnswered) return
        if (!peerOptionsSeen && !force) return
        capabilitiesRequested = true
        requestCapabilities()
    }

    /** M3: Faehigkeiten des Fernsehers abfragen. */
    private fun requestCapabilities() {
        val body = buildString {
            append("wfd_video_formats\r\n")
            append("wfd_audio_codecs\r\n")
            append("wfd_client_rtp_ports\r\n")
            append("wfd_content_protection\r\n")
        }
        pendingM3 = cseq
        send(RtspMessage.request("GET_PARAMETER", "rtsp://$localAddress/wfd1.0", cseq++, body = body))
    }

    /** M4: Format festlegen und zurueckmelden. */
    private fun negotiate(message: RtspMessage) {
        val formats = message.param("wfd_video_formats")
        if (formats == null) {
            listener.onStopped("Der Fernseher hat keine Bildformate genannt")
            running = false
            return
        }
        val capabilities = WfdFormats.parseVideoFormats(formats)
        val format = WfdFormats.choose(capabilities, maxHeight)
        if (format == null) {
            listener.onStopped("Kein gemeinsames Bildformat gefunden")
            running = false
            return
        }
        val capability = capabilities.first()
        chosenFormat = format
        log("Gewaehltes Format: $format")
        listener.onFormatChosen(format)

        val portsValue = message.param("wfd_client_rtp_ports")
        val port = WfdFormats.parseRtpPort(portsValue)
        if (port > 0) sinkRtpPort = port

        val body = buildString {
            append("wfd_video_formats: ")
            append(WfdFormats.buildSelection(format, capability.profile, capability.level))
            append("\r\n")
            append("wfd_presentation_URL: rtsp://$localAddress/wfd1.0/streamid=0 none\r\n")
            append("wfd_client_rtp_ports: ")
            append(portsValue ?: "RTP/AVP/UDP;unicast $sinkRtpPort 0 mode=play")
            append("\r\n")
        }
        pendingM4 = cseq
        send(RtspMessage.request("SET_PARAMETER", "rtsp://$localAddress/wfd1.0", cseq++, body = body))
    }

    /** M5: Den Fernseher bitten, jetzt SETUP zu schicken. */
    private fun triggerSetup() {
        pendingM5 = cseq
        send(RtspMessage.request("SET_PARAMETER", "rtsp://$localAddress/wfd1.0", cseq++,
            body = "wfd_trigger_method: SETUP\r\n"))
    }

    private fun send(data: ByteArray) {
        try {
            synchronized(output) { output.write(data); output.flush() }
        } catch (e: Exception) {
            Log.w(TAG, "Senden fehlgeschlagen", e)
        }
    }

    private fun log(message: String) {
        Log.i(TAG, message)
        listener.onLog(message)
    }

    companion object { private const val TAG = "RtspSession" }
}
