// 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

/**
 * Loads a SoundFont (`.sf2`) or FastTracker II instrument (`.xi`) file and extracts
 * a single playable PCM sample plus the MIDI note at which it plays natural pitch.
 * The result feeds the existing [SampleStore] / [SampleVoice] pitched-playback path
 * (the same one the Sampler uses), so a SoundFont slot plays like a pitched
 * single-sample instrument across the keyboard.
 *
 * This is intentionally pragmatic rather than a full synthesiser: SF2 preset/zone
 * layering, per-key sample maps and loop points are not honoured — we take the
 * file's first sample (SF2 `shdr` / XI first sample) and pitch it from its root.
 * A plain WAV is also accepted as a convenience. Returns null if the bytes are not
 * a format we understand (the engine then keeps its synth fallback).
 */
object SoundFontLoader {

    class Loaded(val sample: SampleStore.Sample, val rootNote: Int)

    /** Frames cap so a pathologically large sample region can't exhaust memory. */
    private const val MAX_FRAMES = 48_000 * 30

    fun load(bytes: ByteArray): Loaded? = when {
        looksLikeSf2(bytes) -> runCatching { loadSf2(bytes) }.getOrNull()
        looksLikeXi(bytes) -> runCatching { loadXi(bytes) }.getOrNull()
        else -> SampleStore.decodeWav(bytes)?.let { Loaded(it, 60) }
    }

    // ------------------------------------------------------------------- SF2

    private fun looksLikeSf2(b: ByteArray): Boolean =
        b.size > 12 && b.tagAt(0) == "RIFF" && b.tagAt(8) == "sfbk"

    private fun loadSf2(bytes: ByteArray): Loaded? {
        val bb = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
        if (readTag(bb) != "RIFF") return null
        bb.int // riff size
        if (readTag(bb) != "sfbk") return null

        var smplOff = -1; var shdrOff = -1; var shdrBytes = 0
        while (bb.remaining() >= 8) {
            val id = readTag(bb)
            val size = bb.int
            val chunkEnd = (bb.position() + size).coerceAtMost(bb.limit())
            if (id == "LIST" && bb.remaining() >= 4) {
                val listType = readTag(bb)
                while (bb.position() + 8 <= chunkEnd) {
                    val sid = readTag(bb)
                    val ssize = bb.int
                    val dataPos = bb.position()
                    if (listType == "sdta" && sid == "smpl") smplOff = dataPos
                    if (listType == "pdta" && sid == "shdr") { shdrOff = dataPos; shdrBytes = ssize }
                    bb.position((dataPos + ssize + (ssize and 1)).coerceAtMost(bb.limit()))
                }
            }
            bb.position((chunkEnd + (size and 1)).coerceAtMost(bb.limit()))
        }
        if (smplOff < 0 || shdrOff < 0 || shdrBytes < SHDR_REC) return null

        // First sample header record (46 bytes): 20-byte name, then the fields.
        val h = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN).apply { position(shdrOff + 20) }
        val start = h.int.toLong() and 0xFFFFFFFFL
        val end = h.int.toLong() and 0xFFFFFFFFL
        val startLoop = h.int.toLong() and 0xFFFFFFFFL
        val endLoop = h.int.toLong() and 0xFFFFFFFFL
        val sampleRate = h.int
        val originalKey = h.get().toInt() and 0xFF
        if (end <= start) return null

        // Coerce the span as a Long *before* narrowing to Int: a corrupt/huge header
        // could otherwise overflow to a negative Int and crash FloatArray().
        val frames = (end - start).coerceIn(0L, MAX_FRAMES.toLong()).toInt()
        if (frames <= 0) return null
        val out = FloatArray(frames)
        val db = ByteBuffer.wrap(bytes).order(ByteOrder.LITTLE_ENDIAN)
            .apply { position((smplOff + start.toInt() * 2).coerceIn(0, bytes.size)) }
        var i = 0
        while (i < frames && db.remaining() >= 2) { out[i] = db.short / 32768f; i++ }

        val root = if (originalKey in 1..127) originalKey else 60
        val rate = if (sampleRate in 8000..192000) sampleRate else 44100
        // Loop points are absolute (same base as start/end); make them relative to the
        // extracted region and keep only if they fall inside it.
        val lsRel = startLoop - start
        val leRel = endLoop - start
        val loopS = if (leRel > lsRel && lsRel >= 0 && leRel <= frames) lsRel.toInt() else -1
        val loopE = if (loopS >= 0) leRel.toInt() else -1
        return Loaded(SampleStore.Sample(out, rate, loopS, loopE), root)
    }

    // -------------------------------------------------------------------- XI

    private fun looksLikeXi(b: ByteArray): Boolean =
        b.size > 300 && String(b, 0, 15, Charsets.US_ASCII) == "Extended Instru"

    private fun loadXi(bytes: ByteArray): Loaded? {
        // Fixed XI header layout: number-of-samples word at 296, first 40-byte
        // sample header at 298, then delta-encoded sample data after all headers.
        val numSamples = u16(bytes, 296)
        if (numSamples < 1) return null
        val hp = 298
        if (hp + 40 > bytes.size) return null
        val length = u32(bytes, hp).toInt()          // sample length in bytes
        val type = bytes[hp + 14].toInt() and 0xFF
        val relativeNote = bytes[hp + 16].toInt()     // signed
        val is16 = (type and 0x10) != 0

        val dataStart = hp + numSamples * 40
        if (length <= 0 || dataStart >= bytes.size) return null
        val byteLen = minOf(length, bytes.size - dataStart)

        val div = if (is16) 2 else 1
        val n = (byteLen / div).coerceAtMost(MAX_FRAMES)
        if (n <= 0) return null
        val out = FloatArray(n)
        if (is16) {
            val db = ByteBuffer.wrap(bytes, dataStart, byteLen).order(ByteOrder.LITTLE_ENDIAN)
            var old = 0
            for (k in 0 until n) {
                old = (old + db.short.toInt()) and 0xFFFF
                out[k] = old.toShort() / 32768f
            }
        } else {
            var old = 0
            for (k in 0 until n) {
                old = (old + bytes[dataStart + k]) and 0xFF
                out[k] = old.toByte() / 128f
            }
        }
        // Loop info: type bits 0-1 (0=none, 1=forward, 2=ping-pong); loopStart/loopLen
        // are byte offsets (halve for 16-bit). Ping-pong is treated as forward.
        val loopType = type and 0x03
        val loopStartB = u32(bytes, hp + 4).toInt()
        val loopLenB = u32(bytes, hp + 8).toInt()
        var loopS = -1
        var loopE = -1
        if (loopType != 0 && loopLenB > 0 && loopStartB >= 0) {
            val ls = loopStartB / div
            val le = (loopStartB + loopLenB) / div
            if (ls in 0 until n && le in (ls + 1)..n) { loopS = ls; loopE = le }
        }
        // XI/XM samples have no stored rate; C-4 plays at 8363 Hz by convention.
        // relativeNote offsets the pitch: root = 60 - relativeNote so playing that
        // note yields the sample's natural pitch and it transposes from there.
        val root = (60 - relativeNote).coerceIn(0, 127)
        return Loaded(SampleStore.Sample(out, 8363, loopS, loopE), root)
    }

    // ----------------------------------------------------------------- helpers

    private const val SHDR_REC = 46

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

    private fun ByteArray.tagAt(off: Int): String =
        if (off + 4 <= size) String(this, off, 4, Charsets.US_ASCII) else ""

    private fun u16(b: ByteArray, off: Int): Int =
        (b[off].toInt() and 0xFF) or ((b[off + 1].toInt() and 0xFF) shl 8)

    private fun u32(b: ByteArray, off: Int): Long =
        ((b[off].toInt() and 0xFF).toLong()) or
            ((b[off + 1].toInt() and 0xFF).toLong() shl 8) or
            ((b[off + 2].toInt() and 0xFF).toLong() shl 16) or
            ((b[off + 3].toInt() and 0xFF).toLong() shl 24)
}
