package de.duffy.screencast

import java.io.ByteArrayOutputStream

/**
 * Verpackt rohes H.264 (Annex-B, wie MediaCodec es liefert) in fragmentiertes MP4.
 *
 * Es entsteht ein Init-Segment (ftyp + moov) und danach pro Frame ein Fragment
 * (moof + mdat). Genau dieses Format nehmen die Media Source Extensions im
 * Browser direkt entgegen - deshalb braucht der Fernseher keine zusaetzliche
 * JavaScript-Bibliothek, nur seinen eingebauten Player.
 */
class Fmp4Muxer(
    private val sps: ByteArray,
    private val pps: ByteArray,
    val width: Int,
    val height: Int
) {
    private var sequence = 1L

    /** Codec-String fuer MediaSource.isTypeSupported, z.B. avc1.42E01F. */
    val codecString: String = String.format("avc1.%02X%02X%02X", sps[1].toInt() and 0xFF, sps[2].toInt() and 0xFF, sps[3].toInt() and 0xFF)

    /** ftyp + moov. Muss jeder Client als erstes bekommen. */
    fun initSegment(): ByteArray {
        val out = ByteArrayOutputStream()
        out.write(ftyp())
        out.write(moov())
        return out.toByteArray()
    }

    /**
     * Ein Fragment aus den NAL-Units eines einzelnen Frames.
     * [ptsUs] ist der Zeitstempel des Frames, [durationUs] seine geschaetzte Dauer.
     */
    fun fragment(nalUnits: List<ByteArray>, ptsUs: Long, durationUs: Long, keyframe: Boolean): ByteArray {
        // Samples liegen in MP4 als Laenge+Daten vor, nicht mit Annex-B-Startcodes.
        val payload = ByteArrayOutputStream()
        for (nal in nalUnits) {
            val type = nal[0].toInt() and 0x1F
            if (type == 7 || type == 8) continue // SPS/PPS stecken bereits in avcC
            payload.u32(nal.size.toLong())
            payload.write(nal)
        }
        val sampleData = payload.toByteArray()
        if (sampleData.isEmpty()) return ByteArray(0)

        val pts = usToScale(ptsUs)
        val duration = usToScale(durationUs).coerceAtLeast(1L)
        // moof: 8 + mfhd(16) + traf[ 8 + tfhd(16) + tfdt(20) + trun(20 + 12*1) ]
        // trun traegt pro Sample Dauer, Groesse und Flags - also 12 Byte.
        val moofSize = 88 + 12
        val dataOffset = moofSize + 8

        val moof = ByteArrayOutputStream()
        moof.u32(moofSize.toLong()); moof.str("moof")
        moof.write(mfhd())
        moof.write(traf(pts, duration, sampleData.size, keyframe, dataOffset))

        val out = ByteArrayOutputStream()
        out.write(moof.toByteArray())
        out.u32((sampleData.size + 8).toLong()); out.str("mdat")
        out.write(sampleData)
        return out.toByteArray()
    }

    // ---------------------------------------------------------------- Boxen

    private fun ftyp(): ByteArray = box("ftyp") {
        it.str("isom"); it.u32(512)
        it.str("isom"); it.str("iso2"); it.str("avc1"); it.str("mp41"); it.str("iso6")
    }

    private fun moov(): ByteArray = box("moov") {
        it.write(mvhd()); it.write(trak()); it.write(mvex())
    }

    private fun mvhd(): ByteArray = box("mvhd") {
        it.u32(0)                 // version + flags
        it.u32(0); it.u32(0)      // creation / modification time
        it.u32(1000)              // timescale
        it.u32(0)                 // duration (unbekannt, Live)
        it.u32(0x00010000)        // rate 1.0
        it.u16(0x0100); it.u16(0) // volume 1.0 + reserved
        it.u32(0); it.u32(0)
        unityMatrix(it)
        repeat(6) { _ -> it.u32(0) }
        it.u32(2)                 // next track id
    }

    private fun trak(): ByteArray = box("trak") {
        it.write(tkhd()); it.write(mdia())
    }

    private fun tkhd(): ByteArray = box("tkhd") {
        it.u32(7)                 // version 0, flags: enabled | in movie | in preview
        it.u32(0); it.u32(0)
        it.u32(1)                 // track id
        it.u32(0)
        it.u32(0)                 // duration
        it.u32(0); it.u32(0)
        it.u16(0); it.u16(0)      // layer + alternate group
        it.u16(0); it.u16(0)      // volume (0 fuer Video) + reserved
        unityMatrix(it)
        it.u32((width.toLong() shl 16))
        it.u32((height.toLong() shl 16))
    }

    private fun mdia(): ByteArray = box("mdia") {
        it.write(mdhd()); it.write(hdlr()); it.write(minf())
    }

    private fun mdhd(): ByteArray = box("mdhd") {
        it.u32(0)
        it.u32(0); it.u32(0)
        it.u32(TIMESCALE.toLong())
        it.u32(0)
        it.u16(0x55C4)            // Sprache "und"
        it.u16(0)
    }

    private fun hdlr(): ByteArray = box("hdlr") {
        it.u32(0)
        it.u32(0)
        it.str("vide")
        it.u32(0); it.u32(0); it.u32(0)
        it.write("VideoHandler".toByteArray(Charsets.US_ASCII)); it.u8(0)
    }

    private fun minf(): ByteArray = box("minf") {
        it.write(box("vmhd") { v -> v.u32(1); v.u16(0); v.u16(0); v.u16(0); v.u16(0) })
        it.write(box("dinf") { d ->
            d.write(box("dref") { r ->
                r.u32(0); r.u32(1)
                r.write(box("url ") { u -> u.u32(1) })  // Daten liegen in derselben Datei
            })
        })
        it.write(stbl())
    }

    private fun stbl(): ByteArray = box("stbl") {
        it.write(box("stsd") { s -> s.u32(0); s.u32(1); s.write(avc1()) })
        it.write(box("stts") { s -> s.u32(0); s.u32(0) })
        it.write(box("stsc") { s -> s.u32(0); s.u32(0) })
        it.write(box("stsz") { s -> s.u32(0); s.u32(0); s.u32(0) })
        it.write(box("stco") { s -> s.u32(0); s.u32(0) })
    }

    private fun avc1(): ByteArray = box("avc1") {
        repeat(6) { _ -> it.u8(0) }
        it.u16(1)                 // data reference index
        it.u16(0); it.u16(0)      // pre defined + reserved
        it.u32(0); it.u32(0); it.u32(0)
        it.u16(width); it.u16(height)
        it.u32(0x00480000)        // 72 dpi horizontal
        it.u32(0x00480000)        // 72 dpi vertikal
        it.u32(0)
        it.u16(1)                 // frame count
        repeat(32) { _ -> it.u8(0) }  // compressor name
        it.u16(0x0018)            // Farbtiefe 24
        it.u16(0xFFFF)            // pre defined = -1
        it.write(avcC())
    }

    private fun avcC(): ByteArray = box("avcC") {
        it.u8(1)                  // configuration version
        it.u8(sps[1].toInt())     // profile
        it.u8(sps[2].toInt())     // profile compatibility
        it.u8(sps[3].toInt())     // level
        it.u8(0xFF)               // 6 bit reserved + NAL-Laengenfeld = 4 Byte
        it.u8(0xE1)               // 3 bit reserved + 1 SPS
        it.u16(sps.size); it.write(sps)
        it.u8(1)                  // 1 PPS
        it.u16(pps.size); it.write(pps)
    }

    private fun mvex(): ByteArray = box("mvex") {
        it.write(box("trex") { t ->
            t.u32(0)
            t.u32(1)              // track id
            t.u32(1)              // default sample description index
            t.u32(0); t.u32(0); t.u32(0)
        })
    }

    private fun mfhd(): ByteArray = box("mfhd") {
        it.u32(0); it.u32(sequence++)
    }

    private fun traf(pts: Long, duration: Long, size: Int, keyframe: Boolean, dataOffset: Int): ByteArray =
        box("traf") {
            it.write(box("tfhd") { t ->
                t.u32(0x020000)   // default-base-is-moof
                t.u32(1)          // track id
            })
            it.write(box("tfdt") { t ->
                t.u32(0x01000000) // version 1 -> 64 Bit Zeitstempel
                t.u32(pts ushr 32); t.u32(pts and 0xFFFFFFFFL)
            })
            it.write(box("trun") { t ->
                // data-offset | sample-duration | sample-size | sample-flags
                t.u32(0x000701)
                t.u32(1)          // ein Sample pro Fragment: minimale Latenz
                t.u32(dataOffset.toLong())
                t.u32(duration)
                t.u32(size.toLong())
                t.u32(if (keyframe) 0x02000000L else 0x01010000L)
            })
        }

    private fun unityMatrix(out: ByteArrayOutputStream) {
        out.u32(0x00010000); out.u32(0); out.u32(0)
        out.u32(0); out.u32(0x00010000); out.u32(0)
        out.u32(0); out.u32(0); out.u32(0x40000000)
    }

    private fun usToScale(us: Long): Long = us * TIMESCALE / 1_000_000L

    companion object {
        const val TIMESCALE = 90000

        /** Zerlegt einen Annex-B-Puffer in seine NAL-Units (ohne Startcodes). */
        fun splitNalUnits(data: ByteArray, offset: Int, length: Int): List<ByteArray> {
            val result = ArrayList<ByteArray>(4)
            val end = offset + length
            var start = -1
            var i = offset
            while (i + 2 < end) {
                if (data[i] == 0.toByte() && data[i + 1] == 0.toByte() && data[i + 2] == 1.toByte()) {
                    val codeLen = if (i > offset && data[i - 1] == 0.toByte()) 4 else 3
                    if (start >= 0) {
                        val stop = i - (codeLen - 3)
                        if (stop > start) result.add(data.copyOfRange(start, stop))
                    }
                    start = i + 3
                    i += 3
                } else {
                    i++
                }
            }
            if (start in 0 until end) result.add(data.copyOfRange(start, end))
            return result.filter { it.isNotEmpty() }
        }
    }
}

// ------------------------------------------------------------ Schreibhelfer

private inline fun box(type: String, body: (ByteArrayOutputStream) -> Unit): ByteArray {
    val payload = ByteArrayOutputStream()
    body(payload)
    val bytes = payload.toByteArray()
    val out = ByteArrayOutputStream(bytes.size + 8)
    out.u32((bytes.size + 8).toLong())
    out.str(type)
    out.write(bytes)
    return out.toByteArray()
}

internal fun ByteArrayOutputStream.u8(v: Int) = write(v and 0xFF)

internal fun ByteArrayOutputStream.u16(v: Int) {
    write((v ushr 8) and 0xFF); write(v and 0xFF)
}

internal fun ByteArrayOutputStream.u32(v: Long) {
    write(((v ushr 24) and 0xFF).toInt()); write(((v ushr 16) and 0xFF).toInt())
    write(((v ushr 8) and 0xFF).toInt()); write((v and 0xFF).toInt())
}

internal fun ByteArrayOutputStream.u32(v: Int) = u32(v.toLong())

internal fun ByteArrayOutputStream.str(s: String) = write(s.toByteArray(Charsets.US_ASCII))
