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

package space.rcmd.android.sizzle.model

/**
 * The lower-half piano-roll arrangement. It has [LANE_COUNT] lanes stacked
 * vertically and up to [MAX_BEATS] beats horizontally. Each slot holds the id of
 * the [Pattern] to trigger on that beat (or [EMPTY]). Per the spec, one marked
 * slot triggers exactly ONE beat of that pattern/block, even if the pattern is
 * longer — so the arrangement grid resolution is one-beat-per-column.
 *
 * The editable canvas is denominated in *bars* ([lengthBars]); its width in beats
 * depends on the active time signature ([canvasBeats]). Playback is unaffected by
 * the canvas width — the sequencer stops at [lastFilledBeat].
 */
class Arrangement {
    /** slots[lane][beat] = patternId, or [EMPTY]. */
    val slots: Array<IntArray> = Array(LANE_COUNT) { IntArray(MAX_BEATS) { EMPTY } }

    /** Per-lane mute. A muted lane's blocks are skipped by the sequencer (its notes
     *  never sound) while its cells stay in place. Read live on the audio thread. */
    val laneMuted: BooleanArray = BooleanArray(LANE_COUNT)

    fun isLaneMuted(lane: Int): Boolean = lane in 0 until LANE_COUNT && laneMuted[lane]

    /** Editable canvas width in *bars*. */
    var lengthBars: Int = MAX_BARS

    /** Canvas width in beats for [sig], clamped to the backing array. One bar is
     *  [TimeSignature.beatsPerBar] beats, so the canvas spans [lengthBars] bars
     *  exactly regardless of signature. */
    fun canvasBeats(sig: TimeSignature): Int =
        (lengthBars * sig.beatsPerBar).coerceIn(1, MAX_BEATS)

    /** Loop transport state (the toolbar under the roll). */
    var loopEnabled: Boolean = false
    val regionA: LoopRegion = LoopRegion()
    val regionB: LoopRegion = LoopRegion()

    fun patternAt(lane: Int, beat: Int): Int =
        if (beat in 0 until MAX_BEATS) slots[lane][beat] else EMPTY

    /**
     * The highest beat index holding any non-empty cell across all lanes, or -1 if
     * the arrangement is empty. Playback uses this to stop after the last filled
     * block instead of running out across the empty canvas through trailing silence.
     */
    fun lastFilledBeat(): Int {
        for (beat in MAX_BEATS - 1 downTo 0) {
            for (lane in 0 until LANE_COUNT) {
                if (slots[lane][beat] != EMPTY) return beat
            }
        }
        return -1
    }

    fun set(lane: Int, beat: Int, patternId: Int) {
        if (lane in 0 until LANE_COUNT && beat in 0 until MAX_BEATS) {
            slots[lane][beat] = patternId
        }
    }

    companion object {
        const val LANE_COUNT = 8
        /** Editable canvas length, in bars. */
        const val MAX_BARS = 256
        /** Backing array width in beats: enough to hold [MAX_BARS] at the widest
         *  supported signature (5/4 → 5 beats per bar). */
        const val MAX_BEATS = MAX_BARS * 5
        const val EMPTY = -1
    }
}

/**
 * An A or B loop region on the arrangement roll. [start] and [end] are beat
 * indices (inclusive start, exclusive end); [repeats] is how many times the
 * region plays before control moves on. A region is only active when it has a
 * non-empty span AND [enabled] is set (toggled by the A/B buttons).
 */
class LoopRegion(
    var enabled: Boolean = false,
    var start: Int = 0,
    var end: Int = 0,
    var repeats: Int = 2,
) {
    val isValid: Boolean get() = enabled && end > start
}
