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

package space.rcmd.android.sizzle.audio

import java.nio.ByteBuffer
import java.nio.ByteOrder
import java.util.concurrent.ConcurrentHashMap

/**
 * A process-wide cache of decoded audio samples used by the Sampler instrument.
 *
 * Decoding (reading a file, parsing WAV) happens on a normal thread in the UI
 * layer, which then [put]s the finished [Sample] here under a string id. The
 * audio thread only ever [get]s already-decoded samples — it never touches the
 * filesystem — so real-time playback stays allocation- and IO-free.
 *
 * The id stored in a toolbox slot's `samplePath` parameter is the key here.
 */
object SampleStore {
    /** Decoded mono PCM plus the rate it was recorded at. [loopStart]/[loopEnd] are
     *  sample-loop points in frames (from an SF2/XI file), or -1 when the source has
     *  none — the SoundFont voice uses them for seamless sustain looping. */
    class Sample(
        val data: FloatArray,
        val sampleRate: Int,
        val loopStart: Int = -1,
        val loopEnd: Int = -1,
    )

    private val samples = ConcurrentHashMap<String, Sample>()

    fun put(id: String, sample: Sample) { samples[id] = sample }
    fun get(id: String): Sample? = if (id.isEmpty()) null else samples[id]

    /** Drop a cached sample so its PCM can be GC'd. Any voice already playing it
     *  keeps its own reference, so removing a live sample never crashes playback. */
    fun remove(id: String) { if (id.isNotEmpty()) samples.remove(id) }

    /**
     * Decode a WAV file (the common uncompressed formats) into a mono [Sample].
     * Supports 8/16/24-bit PCM and 32-bit float, mono or stereo (stereo is
     * down-mixed). Returns null if the bytes are not a WAV we understand.
     */
    fun decodeWav(bytes: ByteArray): Sample? = try {
        decodeWavChecked(bytes)
    } catch (_: Exception) {
        // Any malformed/truncated file (bad chunk sizes, short buffers, …) is
        // rejected rather than crashing the caller — content validation.
        null
    }

    private fun decodeWavChecked(bytes: ByteArray): Sample? {
        if (bytes.size < 44) return null
        val bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
        if (bytes[0].toInt().toChar() != 'R' || bytes[1].toInt().toChar() != 'I') return null // "RIFF"
        bb.position(8)
        if (readTag(bb) != "WAVE") return null

        var audioFormat = 1
        var channels = 1
        var sampleRate = 44100
        var bitsPerSample = 16
        var dataOffset = -1
        var dataLength = 0

        // Walk the chunks looking for "fmt " and "data".
        while (bb.remaining() >= 8) {
            val tag = readTag(bb)
            val size = bb.int
            if (size < 0) break // corrupt chunk size — stop scanning, use what we have
            val next = bb.position() + size
            when (tag) {
                "fmt " -> {
                    audioFormat = bb.short.toInt() and 0xFFFF
                    channels = (bb.short.toInt() and 0xFFFF).coerceAtLeast(1)
                    sampleRate = bb.int
                    bb.int      // byteRate
                    bb.short    // blockAlign
                    bitsPerSample = bb.short.toInt() and 0xFFFF
                }
                "data" -> { dataOffset = bb.position(); dataLength = size }
            }
            if (next in 0..bb.limit()) bb.position(next + (size and 1)) else break // word-aligned
        }
        if (dataOffset < 0) return null
        // A header can over-report the data length; never read past the actual bytes.
        dataLength = dataLength.coerceIn(0, bytes.size - dataOffset)

        val bytesPerSample = bitsPerSample / 8
        if (bytesPerSample == 0) return null
        val frameCount = dataLength / (bytesPerSample * channels)
        if (frameCount <= 0) return null
        val out = FloatArray(frameCount)
        val data = ByteBuffer.wrap(bytes, dataOffset, dataLength).order(ByteOrder.LITTLE_ENDIAN)

        for (f in 0 until frameCount) {
            var acc = 0f
            for (c in 0 until channels) {
                acc += when {
                    audioFormat == 3 && bitsPerSample == 32 -> data.float
                    bitsPerSample == 16 -> data.short / 32768f
                    bitsPerSample == 8 -> ((data.get().toInt() and 0xFF) - 128) / 128f
                    bitsPerSample == 24 -> {
                        val b0 = data.get().toInt() and 0xFF
                        val b1 = data.get().toInt() and 0xFF
                        val b2 = data.get().toInt() // signed high byte
                        ((b2 shl 16) or (b1 shl 8) or b0) / 8388608f
                    }
                    bitsPerSample == 32 -> data.int / 2147483648f
                    else -> return null
                }
            }
            out[f] = acc / channels // down-mix to mono
        }
        return Sample(out, sampleRate)
    }

    /**
     * Encode a [Sample] to a 16-bit mono PCM WAV file (the inverse of
     * [decodeWav]). Used when a Sampler preset is saved so the audio travels with
     * the preset as a plain, portable `.wav`.
     */
    fun encodeWav(sample: Sample): ByteArray {
        val frames = sample.data.size
        val bytesPerSample = 2
        val channels = 1
        val dataSize = frames * bytesPerSample * channels
        val bb = ByteBuffer.allocate(44 + dataSize).order(ByteOrder.LITTLE_ENDIAN)
        bb.put("RIFF".toByteArray(Charsets.US_ASCII))
        bb.putInt(36 + dataSize)
        bb.put("WAVE".toByteArray(Charsets.US_ASCII))
        bb.put("fmt ".toByteArray(Charsets.US_ASCII))
        bb.putInt(16)                                   // PCM fmt chunk size
        bb.putShort(1)                                  // audioFormat = PCM
        bb.putShort(channels.toShort())
        bb.putInt(sample.sampleRate)
        bb.putInt(sample.sampleRate * channels * bytesPerSample) // byteRate
        bb.putShort((channels * bytesPerSample).toShort())       // blockAlign
        bb.putShort(16)                                 // bitsPerSample
        bb.put("data".toByteArray(Charsets.US_ASCII))
        bb.putInt(dataSize)
        for (v in sample.data) {
            bb.putShort((v.coerceIn(-1f, 1f) * 32767f).toInt().toShort())
        }
        return bb.array()
    }

    private fun readTag(bb: ByteBuffer): String {
        val b = ByteArray(4)
        bb.get(b)
        return String(b, Charsets.US_ASCII)
    }
}
