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

package space.rcmd.android.sizzle.io

import space.rcmd.android.sizzle.model.Arrangement
import space.rcmd.android.sizzle.model.Cell
import space.rcmd.android.sizzle.model.Pattern
import space.rcmd.android.sizzle.model.Pitch
import space.rcmd.android.sizzle.model.Project
import space.rcmd.android.sizzle.model.TimeSignature

/**
 * Reads and writes the reference desktop **sizzletracker `.sng`** format
 * (github.com/reactorcoremeltdown/sizzletracker) so songs interchange with the
 * CLI version. It is line-oriented text:
 *
 * ```
 * version 1
 * bpm 120
 * sig 4 4
 *
 * block A 16 4
 * roll ####
 * track T1 0
 * 0 C-4 64 01
 * 4 E-4 .. ..
 * 8 OFF .. ..
 * endblock
 * ```
 *
 * The per-block `roll` (one char per beat, `#` = plays) maps directly onto our
 * lane-per-block arrangement. Velocity is hexadecimal; `..` means "default"; a
 * step channel of `..` inherits the track's declared channel. This format carries
 * the musical data only — mixer/toolbox settings use our own [ProjectIo].
 */
object SngFormat {

    // ------------------------------------------------------------------- write
    fun export(project: Project): String = buildString {
        appendLine("version 1")
        appendLine("bpm ${project.tempoBpm.toInt()}")
        appendLine("sig ${project.timeSignature.beatsPerBar} 4")
        appendLine()

        val arr = project.arrangement
        val usedBeats = arr.lastFilledBeat() + 1
        for (block in project.patterns) {
            val rollUsed = (0 until usedBeats).any { arr.patternAt(block.id, it) != Arrangement.EMPTY }
            val hasNotes = (0 until Pattern.TRACK_COUNT).any { t ->
                (0 until block.length).any { !block.cell(t, it).isEmpty }
            }
            if (!rollUsed && !hasNotes) continue // skip untouched blocks

            appendLine("block ${block.name} ${block.length} ${project.timeSignature.beatsPerBar}")

            val roll = buildString {
                for (beat in 0 until usedBeats) {
                    append(if (arr.patternAt(block.id, beat) != Arrangement.EMPTY) '#' else '.')
                }
            }.trimEnd('.')
            if (roll.isNotEmpty()) appendLine("roll $roll")

            for (t in 0 until Pattern.TRACK_COUNT) {
                val steps = (0 until block.length).filter { !block.cell(t, it).isEmpty }
                if (steps.isEmpty()) continue
                val defChannel = (project.mixer.channels[t].midiChannel - 1).coerceAtLeast(0)
                appendLine("track T${t + 1} $defChannel")
                for (line in steps) {
                    val cell = block.cell(t, line)
                    // A note-off has no velocity/channel (it's tied to its track), so
                    // those columns are written as ".." (the format's "none").
                    val note = if (cell.isNoteOff) "OFF" else Pitch.name(cell.note)
                    val vel = if (cell.isNoteOff) ".." else cell.velocity.toString(16).uppercase().padStart(2, '0')
                    val chan = if (cell.isNoteOff) ".." else cell.channel.toString().padStart(2, '0')
                    appendLine("$line $note $vel $chan")
                }
            }
            appendLine("endblock")
            appendLine()
        }
    }

    // -------------------------------------------------------------------- read
    /** Parse `.sng` [text] into the existing [project] (resetting its content). */
    fun importInto(project: Project, text: String): Project {
        // Clear blocks and arrangement (keep the 8 lane-blocks, wipe their cells).
        project.patterns.forEach { p -> for (t in 0 until Pattern.TRACK_COUNT) p.tracks[t].forEach { it.clear() } }
        for (lane in project.arrangement.slots.indices) project.arrangement.slots[lane].fill(Arrangement.EMPTY)
        project.arrangement.laneMuted.fill(false)

        var blockIndex = 0
        var current: Pattern? = null
        var track = 0
        var trackChannel = 1
        var maxRoll = 0

        text.lineSequence().map { it.trim() }.filter { it.isNotEmpty() }.forEach { line ->
            val tok = line.split(Regex("\\s+"))
            when (tok[0]) {
                "version" -> {}
                "bpm" -> project.tempoBpm = tok.getOrNull(1)?.toFloatOrNull() ?: 120f
                "sig" -> project.timeSignature = when (tok.getOrNull(1)?.toIntOrNull()) {
                    3 -> TimeSignature.THREE_FOUR
                    5 -> TimeSignature.FIVE_FOUR
                    else -> TimeSignature.FOUR_FOUR
                }
                "block" -> {
                    current = project.patterns.getOrNull(blockIndex)?.also { blk ->
                        tok.getOrNull(1)?.let { blk.name = it }
                        tok.getOrNull(2)?.toIntOrNull()?.let { blk.resize(it) }
                    }
                    blockIndex++
                    track = 0; trackChannel = 1
                }
                "roll" -> current?.let { blk ->
                    val roll = tok.getOrNull(1) ?: ""
                    roll.forEachIndexed { beat, ch -> if (ch == '#') project.arrangement.set(blk.id, beat, blk.id) }
                    maxRoll = maxOf(maxRoll, roll.length)
                }
                "track" -> {
                    track = tok.getOrNull(1)?.filter { it.isDigit() }?.toIntOrNull()?.minus(1)?.coerceIn(0, Pattern.TRACK_COUNT - 1) ?: 0
                    trackChannel = tok.getOrNull(2)?.toIntOrNull()?.plus(1)?.coerceIn(1, 16) ?: 1
                }
                "endblock" -> current = null
                else -> applyStep(project, current, track, trackChannel, tok)
            }
        }
        // Grow the canvas (in bars) if the imported roll is longer than the default.
        if (maxRoll > 0) {
            val beatsPerBar = project.timeSignature.beatsPerBar
            val neededBars = (maxRoll + beatsPerBar - 1) / beatsPerBar // ceil
            project.arrangement.lengthBars =
                maxOf(project.arrangement.lengthBars, neededBars).coerceAtMost(Arrangement.MAX_BARS)
        }
        project.activePatternId = 0
        return project
    }

    private fun applyStep(project: Project, block: Pattern?, track: Int, trackChannel: Int, tok: List<String>) {
        val blk = block ?: return
        val tick = tok.getOrNull(0)?.toIntOrNull() ?: return
        if (tick !in 0 until blk.length) return
        val cell = blk.cell(track, tick)
        cell.note = Pitch.parse(tok.getOrElse(1) { "..." })
        cell.velocity = (tok.getOrNull(2)?.takeIf { it != ".." }?.toIntOrNull(16) ?: Cell.MAX_VELOCITY)
            .coerceIn(0, Cell.MAX_VELOCITY)
        cell.channel = (tok.getOrNull(3)?.takeIf { it != ".." }?.toIntOrNull() ?: trackChannel)
            .coerceIn(1, Cell.MAX_CHANNEL)
    }
}
