// SPDX-FileCopyrightText: 2026 Reactorcoremeltdown
// SPDX-License-Identifier: GPL-3.0-or-later

package space.rcmd.android.sizzle.audio

import java.io.File
import java.io.RandomAccessFile
import kotlin.concurrent.thread

/**
 * Records the engine's master mono output to a 16-bit PCM WAV file.
 *
 * The audio thread pushes one float per output sample via [write] into a
 * preallocated ring buffer (single producer). A dedicated writer thread drains the
 * ring to a temp WAV file (single consumer), so NO file I/O or allocation ever
 * happens on the audio thread — the whole point, given how sensitive this engine is
 * to audio-thread stalls.
 *
 * Lifecycle: [begin] opens the temp file and starts the writer. [endCapture] tells
 * the writer to drain whatever is left, patch the WAV header sizes (via a
 * RandomAccessFile seek — a `content://` stream isn't seekable, which is why we
 * stage to a local temp file first) and then invoke the completion callback with the
 * finished file. The caller copies that temp file to its final destination.
 *
 * The ring is single-producer / single-consumer with monotonically increasing
 * positions, so plain @Volatile longs give correct visibility without locks:
 * the audio thread only writes [writePos] and reads [readPos]; the writer thread
 * only writes [readPos] and reads [writePos].
 */
class MasterRecorder(private val sampleRate: Int) {

    // Interleaved stereo (L,R,L,R…); ~1 s of headroom. Positions count samples.
    private val ring = FloatArray((sampleRate * 2).coerceAtLeast(2))
    @Volatile private var writePos = 0L   // samples produced by the audio thread
    @Volatile private var readPos = 0L    // samples consumed by the writer thread
    @Volatile private var capturing = false
    @Volatile private var active = false  // begin()..completion; guards re-entry
    private var writer: Thread? = null

    /** True from [begin] until the writer thread has fully finalized the file. */
    val isActive: Boolean get() = active

    /**
     * Start a new recording into [temp]. Returns false if one is already running
     * (so a fast replay during a previous take's tail can't clobber it).
     * [onComplete] runs on the writer thread with the finished file (or null on
     * failure).
     */
    fun begin(temp: File, onComplete: (File?) -> Unit): Boolean {
        if (active) return false
        writePos = 0L
        readPos = 0L
        active = true
        capturing = true
        writer = thread(name = "sizzle-master-rec", isDaemon = true) {
            val file = runCatching { writeLoop(temp) }.getOrNull()
            active = false
            onComplete(file)
        }
        return true
    }

    /** Audio thread: append one stereo frame (L,R). Never allocates; drops the frame
     *  if the ring is full (only possible if the writer thread starved). */
    fun write(left: Float, right: Float) {
        if (!capturing) return
        val w = writePos
        if (w - readPos > ring.size - 2) return
        ring[(w % ring.size).toInt()] = left
        ring[((w + 1) % ring.size).toInt()] = right
        writePos = w + 2
    }

    /** Audio thread: stop accepting samples; the writer drains the rest and finalizes. */
    fun endCapture() { capturing = false }

    private fun writeLoop(temp: File): File {
        val raf = RandomAccessFile(temp, "rw")
        try {
            raf.setLength(0)
            writeHeader(raf, 0) // placeholder sizes; patched at the end
            var samples = 0L    // total interleaved 16-bit samples written
            val chunk = 4096
            val bytes = ByteArray(chunk * 2)
            while (true) {
                val w = writePos
                val r = readPos
                if (r < w) {
                    val n = minOf(w - r, chunk.toLong()).toInt()
                    for (i in 0 until n) {
                        val s = ring[((r + i) % ring.size).toInt()].coerceIn(-1f, 1f)
                        val v = (s * 32767f).toInt()
                        bytes[i * 2] = (v and 0xFF).toByte()
                        bytes[i * 2 + 1] = ((v shr 8) and 0xFF).toByte()
                    }
                    raf.write(bytes, 0, n * 2)
                    samples += n
                    readPos = r + n
                } else if (!capturing) {
                    break // capture ended and the ring is fully drained
                } else {
                    Thread.sleep(5) // wait for the audio thread to produce more
                }
            }
            writeHeader(raf, samples) // rewrite the 44-byte header with the real sizes
        } finally {
            raf.close()
        }
        return temp
    }

    // ---- WAV (stereo, 16-bit PCM) header. [sampleCount] = total interleaved samples. ----
    private fun writeHeader(raf: RandomAccessFile, sampleCount: Long) {
        val dataSize = sampleCount * 2 // 2 bytes per sample
        raf.seek(0)
        raf.writeBytes("RIFF")
        writeIntLE(raf, (36 + dataSize).toInt())
        raf.writeBytes("WAVE")
        raf.writeBytes("fmt ")
        writeIntLE(raf, 16)             // PCM fmt chunk size
        writeShortLE(raf, 1)            // format = PCM
        writeShortLE(raf, 2)            // channels = stereo
        writeIntLE(raf, sampleRate)
        writeIntLE(raf, sampleRate * 4) // byte rate = sampleRate * channels * bytesPerSample
        writeShortLE(raf, 4)            // block align = channels * bytesPerSample
        writeShortLE(raf, 16)           // bits per sample
        raf.writeBytes("data")
        writeIntLE(raf, dataSize.toInt())
    }

    private fun writeIntLE(raf: RandomAccessFile, v: Int) {
        raf.write(v and 0xFF); raf.write((v shr 8) and 0xFF)
        raf.write((v shr 16) and 0xFF); raf.write((v shr 24) and 0xFF)
    }

    private fun writeShortLE(raf: RandomAccessFile, v: Int) {
        raf.write(v and 0xFF); raf.write((v shr 8) and 0xFF)
    }
}
