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

package space.rcmd.android.sizzle.io

import space.rcmd.android.sizzle.model.Cell
import space.rcmd.android.sizzle.model.Mixer
import space.rcmd.android.sizzle.model.Pattern
import space.rcmd.android.sizzle.model.Project
import space.rcmd.android.sizzle.model.Scale
import space.rcmd.android.sizzle.model.TimeSignature
import space.rcmd.android.sizzle.model.ToolboxType

/**
 * Saves and loads a whole [Project] to/from the `.sng` text format.
 *
 * The format is line-oriented and human-readable (sections in [BRACKETS], one
 * record per line). It is our own layout, aligned in spirit with the reference
 * Sizzletracker project (github.com/reactorcoremeltdown/sizzletracker). A field
 * mapping to that project's exact byte layout is the remaining work — see
 * DEVELOPER_HANDOVER.md, "Project format". Because this writer/reader is fully
 * round-trip safe on its own, the app is usable today and the compatibility
 * shim can be added without touching any other code.
 *
 * Only non-empty tracker cells are written, keeping songs compact.
 */
object ProjectIo {
    private const val HEADER = "SIZZLETRACKER-SNG 1"

    // ------------------------------------------------------------------- write
    fun save(project: Project): String = buildString {
        appendLine(HEADER)
        appendLine("NAME=${project.name}")
        appendLine("TEMPO=${project.tempoBpm}")
        appendLine("SIG=${project.timeSignature.name}")
        appendLine("SCALE=${project.scale.name}")
        appendLine("ROOT=${project.rootNote}")
        appendLine("ACTIVE=${project.activePatternId}")

        project.patterns.forEach { p ->
            appendLine("[PATTERN id=${p.id} name=${p.name} len=${p.length}]")
            for (t in 0 until Pattern.TRACK_COUNT) {
                for (line in 0 until p.length) {
                    val cell = p.cell(t, line)
                    if (!cell.isEmpty) appendLine("$t,$line,${cell.note},${cell.velocity},${cell.channel}")
                }
            }
        }

        val arr = project.arrangement
        val mutes = arr.laneMuted.joinToString("") { if (it) "1" else "0" }
        appendLine("[ARRANGEMENT bars=${arr.lengthBars} loop=${arr.loopEnabled.b()} mutes=$mutes]")
        val usedBeats = arr.lastFilledBeat() + 1
        for (lane in arr.slots.indices) {
            for (beat in 0 until usedBeats) {
                val pid = arr.slots[lane][beat]
                if (pid >= 0) appendLine("$lane,$beat,$pid")
            }
        }
        appendLine("[REGION A en=${arr.regionA.enabled.b()} s=${arr.regionA.start} e=${arr.regionA.end} r=${arr.regionA.repeats}]")
        appendLine("[REGION B en=${arr.regionB.enabled.b()} s=${arr.regionB.start} e=${arr.regionB.end} r=${arr.regionB.repeats}]")

        appendLine("[MIXER]")
        project.mixer.channels.forEach { ch ->
            appendLine(
                "${ch.index},${ch.instrumentSlot},${ch.fxSlots.joinToString(",")}," +
                    "${ch.midiChannel},${ch.volume},${ch.mute.b()},${ch.solo.b()},${ch.limiter.b()}",
            )
        }

        appendLine("[TOOLBOX]")
        project.toolbox.forEach { slot ->
            if (!slot.isEmpty) {
                val params = slot.values.entries.joinToString(",") { "${it.key}=${it.value}" }
                appendLine("${slot.index}=${slot.type!!.name};${slot.name};$params")
            }
        }
    }

    // -------------------------------------------------------------------- read
    /**
     * Parse [text] into the *existing* [project], replacing its contents. Used to
     * load a song into the app's shared singleton without swapping the object the
     * ViewModel and audio engine already hold a reference to.
     */
    fun loadInto(project: Project, text: String): Project {
        // Reset everything to a clean slate first.
        project.patterns.clear()
        for (lane in project.arrangement.slots.indices)
            project.arrangement.slots[lane].fill(space.rcmd.android.sizzle.model.Arrangement.EMPTY)
        project.arrangement.laneMuted.fill(false)
        project.toolbox.forEach { it.clearSlot() }
        project.mixer.channels.forEach { ch ->
            ch.instrumentSlot = -1; ch.fxSlots.fill(-1); ch.midiChannel = 0
            ch.volume = 0.8f; ch.mute = false; ch.solo = false; ch.limiter = false
        }
        var section = ""
        var current: Pattern? = null

        text.lineSequence().map { it.trim() }.filter { it.isNotEmpty() }.forEach { line ->
            // Per-line safety net: a single malformed line is skipped rather than
            // aborting the whole load (which would leave a half-cleared project).
            runCatching {
            when {
                line == HEADER -> {}
                line.startsWith("[PATTERN") -> {
                    val attrs = parseAttrs(line)
                    current = Pattern(
                        id = attrs["id"]?.toIntOrNull() ?: project.patterns.size,
                        name = attrs["name"] ?: "PTN",
                        length = attrs["len"]?.toIntOrNull() ?: 16,
                    ).also { project.patterns.add(it) }
                    section = "PATTERN"
                }
                line.startsWith("[ARRANGEMENT") -> {
                    val attrs = parseAttrs(line)
                    // "bars" is the current canvas unit; older files stored "len" in
                    // beats — those simply get the default 256-bar canvas (their cells
                    // are restored from the explicit lane,beat,pid lines regardless).
                    project.arrangement.lengthBars =
                        attrs["bars"]?.toIntOrNull()
                            ?: space.rcmd.android.sizzle.model.Arrangement.MAX_BARS
                    project.arrangement.loopEnabled = attrs["loop"] == "1"
                    attrs["mutes"]?.let { m ->
                        val lanes = project.arrangement.laneMuted
                        for (i in 0 until minOf(m.length, lanes.size)) lanes[i] = m[i] == '1'
                    }
                    section = "ARRANGEMENT"
                }
                line.startsWith("[REGION A") -> applyRegion(project.arrangement.regionA, parseAttrs(line))
                line.startsWith("[REGION B") -> applyRegion(project.arrangement.regionB, parseAttrs(line))
                line.startsWith("[MIXER]") -> section = "MIXER"
                line.startsWith("[TOOLBOX]") -> section = "TOOLBOX"
                line.contains('=') && !line.startsWith("[") && section == "" ->
                    applyHeaderField(project, line)
                section == "PATTERN" -> applyPatternCell(current, line)
                section == "ARRANGEMENT" -> applyArrangementSlot(project, line)
                section == "MIXER" -> applyMixerRow(project, line)
                section == "TOOLBOX" -> applyToolboxRow(project, line)
            }
            }
        }
        // Guarantee one block per lane (lane i is tied to block i), padding any
        // that the file did not define so the arrangement/editor always resolve.
        for (id in 0 until space.rcmd.android.sizzle.model.Arrangement.LANE_COUNT) {
            if (project.patterns.none { it.id == id }) project.patterns.add(Pattern(id, name = "BLK${id + 1}"))
        }
        project.patterns.sortBy { it.id }
        project.activePatternId = project.activePatternId.coerceIn(0, project.patterns.size - 1)
        return project
    }

    // ----- small helpers -----
    private fun Boolean.b() = if (this) "1" else "0"

    private fun parseAttrs(line: String): Map<String, String> =
        line.trim('[', ']').substringAfter(' ', "").split(' ')
            .mapNotNull { tok -> tok.split('=', limit = 2).takeIf { it.size == 2 }?.let { it[0] to it[1] } }
            .toMap()

    private fun applyHeaderField(p: Project, line: String) {
        val (k, v) = line.split('=', limit = 2)
        when (k) {
            "NAME" -> p.name = v
            "TEMPO" -> p.tempoBpm = v.toFloatOrNull() ?: 120f
            "SIG" -> p.timeSignature = runCatching { TimeSignature.valueOf(v) }.getOrDefault(TimeSignature.FOUR_FOUR)
            "SCALE" -> p.scale = runCatching { Scale.valueOf(v) }.getOrDefault(Scale.CHROMATIC)
            "ROOT" -> p.rootNote = v.toIntOrNull() ?: 0
            "ACTIVE" -> p.activePatternId = v.toIntOrNull() ?: 0
        }
    }

    private fun applyPatternCell(pattern: Pattern?, line: String) {
        val p = pattern ?: return
        val f = line.split(',')
        if (f.size < 5) return
        val t = f[0].toIntOrNull() ?: return
        val ln = f[1].toIntOrNull() ?: return
        if (t !in 0 until Pattern.TRACK_COUNT || ln !in 0 until p.length) return
        p.cell(t, ln).apply {
            note = f[2].toIntOrNull() ?: Cell.EMPTY
            velocity = (f[3].toIntOrNull() ?: Cell.MAX_VELOCITY).coerceIn(0, Cell.MAX_VELOCITY)
            channel = (f[4].toIntOrNull() ?: 1).coerceIn(1, Cell.MAX_CHANNEL)
        }
    }

    private fun applyArrangementSlot(p: Project, line: String) {
        val f = line.split(',')
        if (f.size < 3) return
        val lane = f[0].toIntOrNull() ?: return
        val beat = f[1].toIntOrNull() ?: return
        val pid = f[2].toIntOrNull() ?: return
        p.arrangement.set(lane, beat, pid)
    }

    private fun applyRegion(region: space.rcmd.android.sizzle.model.LoopRegion, a: Map<String, String>) {
        region.enabled = a["en"] == "1"
        region.start = a["s"]?.toIntOrNull() ?: 0
        region.end = a["e"]?.toIntOrNull() ?: 0
        region.repeats = a["r"]?.toIntOrNull() ?: 2
    }

    private fun applyMixerRow(p: Project, line: String) {
        val f = line.split(',')
        if (f.size < 10) return
        val idx = f[0].toIntOrNull() ?: return
        val ch = p.mixer.channels.getOrNull(idx) ?: return
        ch.instrumentSlot = f[1].toIntOrNull() ?: -1
        for (i in 0 until Mixer.FX_SLOTS) ch.fxSlots[i] = f[2 + i].toIntOrNull() ?: -1
        ch.midiChannel = f[6].toIntOrNull() ?: 0
        ch.volume = f[7].toFloatOrNull() ?: 0.8f
        ch.mute = f[8] == "1"
        ch.solo = f[9] == "1"
        ch.limiter = f.getOrNull(10) == "1" // optional: absent in older saves
    }

    private fun applyToolboxRow(p: Project, line: String) {
        val idx = line.substringBefore('=').toIntOrNull() ?: return
        val rest = line.substringAfter('=')
        val parts = rest.split(';')
        if (parts.size < 2) return
        val type = ToolboxType.entries.firstOrNull { it.name == parts[0] } ?: return
        val slot = p.toolbox.getOrNull(idx) ?: return
        slot.fill(type)
        slot.name = parts[1]
        if (parts.size >= 3 && parts[2].isNotBlank()) {
            parts[2].split(',').forEach { kv ->
                val e = kv.split('=', limit = 2)
                if (e.size == 2) slot.set(e[0], e[1])
            }
        }
    }
}
