import '../modelos/pista_local.dart'; /// Immutable snapshot of a local-music play queue (Design ADR-1): an ordered /// list of a folder's direct audio children plus the currently-playing /// index. Pure Dart, no side effects — every "move" method returns a NEW /// instance rather than mutating in place, mirroring how /// `ControladorReconexion` was extracted from `PluriWaveAudioHandler` /// (`controlador_reconexion.dart`) so this stays fully unit-testable without /// the handler. (That last clause used to read "which cannot be instantiated /// in unit tests"; it can — see `construirControlesTransporte`'s doc in /// `servicio_audio.dart`. Keeping the queue logic out of the handler is /// still worth it, but for design reasons, not for that one.) class ColaLocal { const ColaLocal({required this.pistas, this.indice = 0}); /// The folder's direct audio children, in play order — already /// name-sorted or Fisher-Yates shuffled by the caller before /// construction (Design ADR-6). This class itself never re-orders them. final List pistas; /// The currently-playing position within [pistas]. final int indice; /// Whether [indice] points at a real entry in [pistas] (Design "empty /// list" edge case: an empty queue has no current track). bool get hayActual => indice >= 0 && indice < pistas.length; /// The track at [indice]. Only meaningful when [hayActual] is `true` — /// callers must check it first; this getter does not guard the read /// itself. NodoLocal get actual => pistas[indice]; /// The queue advanced one position (Design ADR-3's advance flow): /// `null` when [indice] is already the last position (or [pistas] is /// empty) — end-of-queue, per Design ADR-4 (no wraparound to track 1). ColaLocal? conSiguiente() { final siguiente = indice + 1; if (siguiente >= pistas.length) return null; return ColaLocal(pistas: pistas, indice: siguiente); } /// The queue moved back one position, CLAMPED at 0 (Design ADR-4: /// `skipToPrevious` at the first track restarts it instead of throwing or /// wrapping to the last track). ColaLocal conAnterior() { final anterior = indice > 0 ? indice - 1 : 0; return ColaLocal(pistas: pistas, indice: anterior); } } /// Outcome of [decidirAvanceCola] (Design ADR-3): a pure decision the caller /// (`PluriWaveAudioHandler._manejarFinPista`) acts on with /// static-review-only field mutations — this function itself has no side /// effects. enum DecisionAvanceCola { /// No advance: not a completion event, no active local queue (radio /// isolation), or the re-entry latch is already armed (an advance is /// already resolving its URI). ninguna, /// Advance to the next track — the caller marks the re-entry latch and /// resolves/plays `colaLocal.conSiguiente()`. avanzar, /// The queue reached its end — the caller deactivates the queue and stops /// playback (Design ADR-4, no wraparound). desactivar, } /// Pure queue-advance decision (Design ADR-3), extracted so it is /// unit-testable without instantiating `PluriWaveAudioHandler` — the /// established `ControladorReconexion` precedent (see this file's class /// doc comment). Deliberately takes NO `just_audio` type: the caller maps /// `ProcessingState.completed` to [trackCompletado] before calling this. /// /// Double-gated: [trackCompletado] must be `true` AND [colaLocal] must be /// non-null (radio never sets `_colaLocal`, so a `null` queue is a /// structural proxy for "not local-queue playback" — Design "single /// load-bearing invariant"). [avanzandoCola] is the re-entry latch: a /// second `completed` emission while an earlier advance is still resolving /// its URI must be a no-op, not a double-advance. DecisionAvanceCola decidirAvanceCola({ required ColaLocal? colaLocal, required bool avanzandoCola, required bool trackCompletado, }) { if (!trackCompletado) return DecisionAvanceCola.ninguna; if (colaLocal == null) return DecisionAvanceCola.ninguna; if (avanzandoCola) return DecisionAvanceCola.ninguna; return colaLocal.conSiguiente() == null ? DecisionAvanceCola.desactivar : DecisionAvanceCola.avanzar; } /// Mid-await race guard (Design ADR-3's advance flow): `true` only when /// [siguienteEsperado] is the EXACT SAME instance still installed as /// [colaLocalActual] — a deliberate `identical()` check, NOT `==`, so a /// user action that replaced `_colaLocal` with a structurally-equal-but /// distinct `ColaLocal` during the async URI-resolve gap is correctly /// detected as stale and aborts the advance, instead of silently racing an /// external play/stop. bool avanceEsValido(ColaLocal? colaLocalActual, ColaLocal? siguienteEsperado) => identical(colaLocalActual, siguienteEsperado);