package de.duffy.screencast.miracast

import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.projection.MediaProjection
import android.os.Handler
import android.util.Log
import de.duffy.screencast.ScreenEncoder
import de.duffy.screencast.TsMuxer
import java.net.InetAddress
import java.net.Inet4Address
import java.net.NetworkInterface
import java.net.ServerSocket
import java.net.Socket

/**
 * Der komplette Weg zum Fernseher, wenn dieser im Screen-Mirroring-Wartemodus
 * steht: Handshake aushandeln, Encoder auf das vereinbarte Format einstellen,
 * Bild als MPEG-TS in RTP-Paketen hinueberschicken.
 */
class MiracastPipeline(
    /** Null nur im Test - ohne Aufnahme laesst sich der Handshake trotzdem pruefen. */
    private val projection: MediaProjection?,
    private val densityDpi: Int,
    private val handler: Handler,
    private val maxHeight: Int,
    private val bitrate: Int,
    private val listener: Listener
) : ScreenEncoder.Listener, RtspSession.Listener {

    interface Listener {
        fun onStatus(text: String)
        fun onStreaming(format: WfdFormats.VideoFormat)
        fun onFinished(reason: String)
    }

    private var session: RtspSession? = null
    private var encoder: ScreenEncoder? = null
    private var virtualDisplay: VirtualDisplay? = null
    private var sender: RtpTsSender? = null
    private var tsMuxer: TsMuxer? = null
    private var target: InetAddress? = null
    private var keepAlive: Runnable? = null
    @Volatile private var streaming = false
    private val serverSockets = ArrayList<ServerSocket>()
    @Volatile private var adopted = false

    /**
     * Oeffnet die Steuerkanaele und wartet auf den Fernseher.
     *
     * Beim Wi-Fi-Display-Standard baut der Fernseher die Steuerverbindung zu
     * uns auf, nicht umgekehrt - genauso macht es Android intern. Wir hoeren
     * deshalb auf den ueblichen Ports zu. Zusaetzlich probieren wir es spaeter
     * auch andersherum, denn manche Geraete erwarten es so.
     */
    fun startListening(ports: List<Int> = listOf(7236, 8554)) {
        for (port in ports) {
            try {
                val server = ServerSocket()
                server.reuseAddress = true
                server.bind(java.net.InetSocketAddress(port))
                serverSockets.add(server)
                Thread({ acceptLoop(server, port) }, "miracast-listen-$port")
                    .apply { isDaemon = true }.start()
                listener.onStatus("Warte auf den Fernseher (Port $port)")
            } catch (e: Exception) {
                Log.w(TAG, "Port $port liess sich nicht oeffnen: ${e.message}")
            }
        }
        if (serverSockets.isEmpty()) {
            listener.onFinished("Kein Steuerkanal moeglich - Port 7236 ist belegt")
        }
    }

    private fun acceptLoop(server: ServerSocket, port: Int) {
        try {
            while (!adopted) {
                val socket = server.accept()
                listener.onStatus("Der Fernseher hat sich verbunden (${socket.inetAddress?.hostAddress})")
                if (!adopt(socket)) socket.close()
            }
        } catch (e: Exception) {
            if (!adopted) Log.d(TAG, "Lauschen auf $port beendet: ${e.message}")
        }
    }

    /** Zusaetzlicher Versuch in die Gegenrichtung, falls der Fernseher lauscht. */
    fun tryOutgoing(sink: InetAddress, rtspPort: Int) {
        if (target == null) target = sink
        Thread({
            if (adopted) return@Thread
            try {
                val socket = Socket()
                socket.connect(java.net.InetSocketAddress(sink, rtspPort), 8000)
                listener.onStatus("Steuerkanal zum Fernseher aufgebaut (Port $rtspPort)")
                if (!adopt(socket)) socket.close()
            } catch (e: Exception) {
                // Normalfall: der Fernseher lauscht nicht, sondern ruft uns an.
                Log.d(TAG, "Ausgehender Versuch auf $rtspPort erfolglos: ${e.message}")
            }
        }, "miracast-outgoing").start()
    }

    /** Die erste zustande gekommene Verbindung gewinnt, egal aus welcher Richtung. */
    @Synchronized
    private fun adopt(socket: Socket): Boolean {
        if (adopted) return false
        adopted = true
        for (server in serverSockets) try { server.close() } catch (_: Exception) {}
        serverSockets.clear()
        return try {
            socket.tcpNoDelay = true
            target = socket.inetAddress
            val local = localP2pAddress() ?: socket.localAddress.hostAddress ?: "0.0.0.0"
            Log.i(TAG, "RTSP-Sitzung mit ${socket.inetAddress?.hostAddress}, eigene Adresse $local")
            val rtsp = RtspSession(socket, local, maxHeight, this)
            session = rtsp
            rtsp.start()
            startKeepAlive()
            true
        } catch (e: Exception) {
            listener.onFinished("Steuerverbindung fehlgeschlagen: ${e.message}")
            false
        }
    }

    fun stop() {
        streaming = false
        for (server in serverSockets) try { server.close() } catch (_: Exception) {}
        serverSockets.clear()
        keepAlive?.let { handler.removeCallbacks(it) }
        keepAlive = null
        session?.stop(); session = null
        virtualDisplay?.release(); virtualDisplay = null
        encoder?.stop(); encoder = null
        sender?.close(); sender = null
        tsMuxer = null
    }

    // ------------------------------------------------------- RTSP-Rueckmeldungen

    override fun onFormatChosen(format: WfdFormats.VideoFormat) {
        listener.onStatus("Format ausgehandelt: $format")
    }

    override fun onPlay(rtpPort: Int, format: WfdFormats.VideoFormat) {
        val sink = target ?: return
        val projection = this.projection ?: run {
            listener.onStatus("Handshake fertig, aber keine Bildschirmaufnahme vorhanden")
            return
        }
        handler.post {
            if (streaming) return@post
            try {
                val rtp = RtpTsSender(sink, rtpPort)
                sender = rtp
                session?.localRtpPort = rtp.localPort

                val enc = ScreenEncoder(format.width, format.height, bitrate, format.fps, this)
                enc.start()
                encoder = enc

                virtualDisplay = projection.createVirtualDisplay(
                    "Miracast", format.width, format.height, densityDpi,
                    DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
                    enc.surface, null, handler
                )
                streaming = true
                listener.onStreaming(format)
            } catch (e: Exception) {
                Log.e(TAG, "Bildstrom liess sich nicht starten", e)
                listener.onFinished("Encoder-Start fehlgeschlagen: ${e.message}")
            }
        }
    }

    override fun onStopped(reason: String) {
        handler.post { listener.onFinished(reason) }
    }

    override fun onLog(message: String) {
        listener.onStatus(message)
    }

    // ---------------------------------------------------- Encoder-Rueckmeldungen

    override fun onConfig(sps: ByteArray, pps: ByteArray, width: Int, height: Int) {
        tsMuxer = TsMuxer(sps, pps)
    }

    override fun onFrame(nalUnits: List<ByteArray>, ptsUs: Long, keyframe: Boolean) {
        val muxer = tsMuxer ?: return
        val rtp = sender ?: return
        rtp.send(muxer.frame(nalUnits, ptsUs, keyframe), ptsUs)
    }

    // ----------------------------------------------------------------- Hilfen

    private fun startKeepAlive() {
        val task = object : Runnable {
            override fun run() {
                session?.keepAlive()
                handler.postDelayed(this, 25_000)
            }
        }
        keepAlive = task
        handler.postDelayed(task, 25_000)
    }

    /** Unsere Adresse innerhalb der Wi-Fi-Direct-Gruppe. */
    private fun localP2pAddress(): String? = try {
        NetworkInterface.getNetworkInterfaces().toList()
            .filter { it.isUp && it.name.startsWith("p2p") }
            .flatMap { it.inetAddresses.toList() }
            .filterIsInstance<Inet4Address>()
            .firstOrNull()?.hostAddress
    } catch (_: Exception) { null }

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