package de.duffy.screencast

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.media.projection.MediaProjection
import android.media.projection.MediaProjectionManager
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.os.PowerManager
import android.util.DisplayMetrics
import android.util.Log
import android.view.WindowManager
import androidx.core.app.NotificationCompat
import de.duffy.screencast.miracast.MiracastPipeline
import de.duffy.screencast.miracast.WfdFormats
import java.net.InetAddress

/**
 * Haelt die Bildschirmaufnahme am Laufen: Aufnahme -> Encoder -> Server.
 * Laeuft als Vordergrunddienst, damit Android die Uebertragung nicht killt,
 * wenn der Nutzer die App verlaesst.
 */
class CastService : Service(), ScreenEncoder.Listener {

    private var projection: MediaProjection? = null
    private var virtualDisplay: VirtualDisplay? = null
    private var encoder: ScreenEncoder? = null
    private var server: WebServer? = null
    private var wakeLock: PowerManager.WakeLock? = null
    private val hub = StreamHub()
    private val handler = Handler(Looper.getMainLooper())

    private var quality = Quality.HD
    private var frameRate = 30
    private var portraitMode = true
    private var mode = MODE_BROWSER
    private var miracast: MiracastPipeline? = null

    private val projectionCallback = object : MediaProjection.Callback() {
        override fun onStop() {
            Log.i(TAG, "Aufnahme wurde vom System beendet")
            handler.post { stopCasting() }
        }
    }

    private val displayListener = object : DisplayManager.DisplayListener {
        override fun onDisplayAdded(displayId: Int) {}
        override fun onDisplayRemoved(displayId: Int) {}
        override fun onDisplayChanged(displayId: Int) {
            if (displayId != 0) return
            val metrics = screenMetrics()
            val nowPortrait = metrics.heightPixels >= metrics.widthPixels
            if (nowPortrait != portraitMode && projection != null) {
                portraitMode = nowPortrait
                handler.post { restartPipeline() }
            }
        }
    }

    override fun onBind(intent: Intent?): IBinder? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        when (intent?.action) {
            ACTION_STOP -> { stopCasting(); return START_NOT_STICKY }
            ACTION_SINK_READY -> {
                // Die Wi-Fi-Direct-Gruppe steht: zusaetzlich in Gegenrichtung probieren.
                val address = intent.getStringExtra(EXTRA_SINK_ADDRESS)
                val port = intent.getIntExtra(EXTRA_SINK_PORT, 7236)
                if (!address.isNullOrEmpty()) {
                    CastState.appendLog("Fernseher erreichbar unter $address")
                    miracast?.tryOutgoing(InetAddress.getByName(address), port)
                }
            }
            ACTION_START -> startCasting(intent)
        }
        return START_NOT_STICKY
    }

    private fun startCasting(intent: Intent) {
        if (projection != null) return

        quality = Quality.entries.getOrElse(intent.getIntExtra(EXTRA_QUALITY, 1)) { Quality.HD }
        frameRate = intent.getIntExtra(EXTRA_FPS, 30)
        mode = intent.getStringExtra(EXTRA_MODE) ?: MODE_BROWSER

        // Auf Android 14+ muss der Dienst im Vordergrund sein, bevor die
        // MediaProjection geholt werden darf.
        startForegroundNotification("Wird gestartet ...")

        val resultCode = intent.getIntExtra(EXTRA_RESULT_CODE, 0)
        val data: Intent? = if (Build.VERSION.SDK_INT >= 33) {
            intent.getParcelableExtra(EXTRA_RESULT_DATA, Intent::class.java)
        } else {
            @Suppress("DEPRECATION") intent.getParcelableExtra(EXTRA_RESULT_DATA)
        }
        if (data == null) { stopCasting(); return }

        val manager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager
        val mediaProjection = manager.getMediaProjection(resultCode, data)
        if (mediaProjection == null) {
            CastState.error.postValue("Bildschirmaufnahme wurde nicht erlaubt")
            stopCasting(); return
        }
        mediaProjection.registerCallback(projectionCallback, handler)
        projection = mediaProjection

        val power = getSystemService(Context.POWER_SERVICE) as PowerManager
        wakeLock = power.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "screencast:stream").apply {
            setReferenceCounted(false); acquire(4 * 60 * 60 * 1000L)
        }

        if (mode == MODE_MIRACAST) {
            startMiracast(intent)
            return
        }

        try {
            server = WebServer({ name -> assets.open(name).use { it.readBytes() } }, hub)
                .also { it.start() }
        } catch (e: Exception) {
            Log.e(TAG, "Server-Start fehlgeschlagen", e)
            CastState.error.postValue("Server konnte nicht starten: ${e.message}")
            stopCasting(); return
        }
        hub.onKeyFrameNeeded = { encoder?.requestKeyFrame() }

        val metrics = screenMetrics()
        portraitMode = metrics.heightPixels >= metrics.widthPixels
        (getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
            .registerDisplayListener(displayListener, handler)
        startPipeline()

        val url = "http://${NetworkUtil.localAddress(this)}:${server?.port ?: 8080}"
        CastState.url.postValue(url)
        CastState.running.postValue(true)
        CastState.error.postValue(null)
        startForegroundNotification(url)
        scheduleViewerUpdates()
    }

    /** Direktweg: Bild ueber Wi-Fi Direct an den wartenden Fernseher schicken. */
    private fun startMiracast(intent: Intent) {
        val address = intent.getStringExtra(EXTRA_SINK_ADDRESS)
        val port = intent.getIntExtra(EXTRA_SINK_PORT, 7236)
        val projection = this.projection ?: run { stopCasting(); return }

        val pipeline = MiracastPipeline(
            projection = projection,
            densityDpi = screenMetrics().densityDpi,
            handler = handler,
            maxHeight = quality.longSide.coerceAtMost(1080),
            bitrate = quality.bitrate,
            listener = object : MiracastPipeline.Listener {
                override fun onStatus(text: String) {
                    Log.i(TAG, "Miracast: $text")
                    CastState.appendLog(text)
                }

                override fun onStreaming(format: WfdFormats.VideoFormat) {
                    CastState.running.postValue(true)
                    CastState.resolution.postValue("$format über Wi-Fi Direct")
                    CastState.viewers.postValue(1)
                    CastState.appendLog("Übertragung läuft: $format")
                    startForegroundNotification("Direkt am Fernseher · $format")
                }

                override fun onFinished(reason: String) {
                    CastState.appendLog(reason)
                    CastState.error.postValue(reason)
                    handler.post { stopCasting() }
                }
            }
        )
        miracast = pipeline
        CastState.running.postValue(true)
        CastState.url.postValue("")
        CastState.error.postValue(null)
        startForegroundNotification("Warte auf den Fernseher ...")

        // Zuerst zuhoeren: Beim Wi-Fi-Display-Standard ruft der Fernseher an.
        pipeline.startListening()
        if (!address.isNullOrEmpty()) {
            pipeline.tryOutgoing(InetAddress.getByName(address), port)
        }

        // Falls sich gar nichts tut, nach einer Weile Bescheid sagen.
        handler.postDelayed({
            if (miracast === pipeline && CastState.viewers.value == 0) {
                CastState.appendLog(
                    "Der Fernseher hat sich nicht gemeldet. Manche Geräte verlangen, " +
                    "dass sich das Handy als Miracast-Quelle ausweist - das ist " +
                    "Systemapps vorbehalten. Dann hilft nur der Weg über den " +
                    "Übertragungsdienst von Android oder der Browser."
                )
            }
        }, 30_000)
    }

    /** Encoder und VirtualDisplay aufsetzen - passend zur aktuellen Ausrichtung. */
    private fun startPipeline() {
        val metrics = screenMetrics()
        val (width, height) = fitSize(metrics.widthPixels, metrics.heightPixels, quality.longSide)

        val enc = ScreenEncoder(width, height, quality.bitrate, frameRate, this)
        enc.start()
        encoder = enc

        virtualDisplay = projection?.createVirtualDisplay(
            "SmartScreenCast",
            width, height, metrics.densityDpi,
            DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC,
            enc.surface, null, handler
        )

        CastState.resolution.postValue("${width}x$height @ ${frameRate}fps")
        Log.i(TAG, "Uebertragung laeuft mit ${width}x$height")
    }

    /** Nach dem Drehen des Geraets neu aufsetzen, damit das Bild passt. */
    private fun restartPipeline() {
        if (projection == null) return
        Log.i(TAG, "Ausrichtung geaendert - Encoder wird neu gestartet")
        virtualDisplay?.release(); virtualDisplay = null
        encoder?.stop(); encoder = null
        startPipeline()
    }

    private fun stopCasting() {
        try {
            (getSystemService(Context.DISPLAY_SERVICE) as DisplayManager)
                .unregisterDisplayListener(displayListener)
        } catch (_: Exception) {}
        miracast?.stop(); miracast = null
        hub.closeAll()
        server?.stop(); server = null
        virtualDisplay?.release(); virtualDisplay = null
        encoder?.stop(); encoder = null
        projection?.unregisterCallback(projectionCallback)
        projection?.stop(); projection = null
        wakeLock?.let { if (it.isHeld) it.release() }
        wakeLock = null
        CastState.running.postValue(false)
        CastState.viewers.postValue(0)
        CastState.url.postValue("")
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            stopForeground(STOP_FOREGROUND_REMOVE)
        } else {
            @Suppress("DEPRECATION") stopForeground(true)
        }
        stopSelf()
    }

    override fun onDestroy() {
        super.onDestroy()
        if (projection != null) stopCasting()
    }

    // ------------------------------------------------------- Encoder-Rueckruf

    override fun onConfig(sps: ByteArray, pps: ByteArray, width: Int, height: Int) {
        hub.onEncoderConfig(sps, pps, width, height)
    }

    override fun onFrame(nalUnits: List<ByteArray>, ptsUs: Long, keyframe: Boolean) {
        hub.onFrame(nalUnits, ptsUs, keyframe)
    }

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

    private fun scheduleViewerUpdates() {
        handler.post(object : Runnable {
            override fun run() {
                if (projection == null) return
                val count = hub.viewerCount
                if (CastState.viewers.value != count) CastState.viewers.postValue(count)
                handler.postDelayed(this, 1000)
            }
        })
    }

    @Suppress("DEPRECATION")
    private fun screenMetrics(): DisplayMetrics {
        val metrics = DisplayMetrics()
        val wm = getSystemService(Context.WINDOW_SERVICE) as WindowManager
        wm.defaultDisplay.getRealMetrics(metrics)
        return metrics
    }

    private fun startForegroundNotification(text: String) {
        val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            manager.createNotificationChannel(
                NotificationChannel(CHANNEL_ID, "Bildschirmuebertragung", NotificationManager.IMPORTANCE_LOW)
                    .apply { setShowBadge(false) }
            )
        }
        val openApp = PendingIntent.getActivity(
            this, 0, Intent(this, MainActivity::class.java),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val stop = PendingIntent.getService(
            this, 1, Intent(this, CastService::class.java).setAction(ACTION_STOP),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )
        val notification: Notification = NotificationCompat.Builder(this, CHANNEL_ID)
            .setContentTitle("Bildschirm wird uebertragen")
            .setContentText(text)
            .setSmallIcon(R.drawable.ic_cast)
            .setOngoing(true)
            .setContentIntent(openApp)
            .addAction(R.drawable.ic_cast, "Beenden", stop)
            .setPriority(NotificationCompat.PRIORITY_LOW)
            .build()

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            startForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION)
        } else {
            startForeground(NOTIFICATION_ID, notification)
        }
    }

    /** Passt die Aufloesung an die Zielgroesse an, auf Vielfache von 16 gerundet. */
    private fun fitSize(width: Int, height: Int, longSide: Int): Pair<Int, Int> {
        val scale = longSide.toFloat() / maxOf(width, height)
        val w = if (scale < 1f) (width * scale).toInt() else width
        val h = if (scale < 1f) (height * scale).toInt() else height
        return Pair(align(w), align(h))
    }

    private fun align(value: Int): Int = ((value + 8) / 16) * 16

    enum class Quality(val longSide: Int, val bitrate: Int, val label: String) {
        SD(854, 2_500_000, "Sparsam - 480p"),
        HD(1280, 6_000_000, "Ausgewogen - 720p"),
        FHD(1920, 12_000_000, "Scharf - 1080p")
    }

    companion object {
        private const val TAG = "CastService"
        private const val CHANNEL_ID = "screencast"
        private const val NOTIFICATION_ID = 42

        const val ACTION_START = "de.duffy.screencast.START"
        const val ACTION_STOP = "de.duffy.screencast.STOP"
        const val EXTRA_RESULT_CODE = "resultCode"
        const val EXTRA_RESULT_DATA = "resultData"
        const val EXTRA_QUALITY = "quality"
        const val EXTRA_FPS = "fps"
        const val EXTRA_MODE = "mode"
        const val EXTRA_SINK_ADDRESS = "sinkAddress"
        const val EXTRA_SINK_PORT = "sinkPort"
        const val ACTION_SINK_READY = "de.duffy.screencast.SINK_READY"
        const val MODE_BROWSER = "browser"
        const val MODE_MIRACAST = "miracast"
    }
}
