package link.roadio.riff.player.library import link.roadio.cuetree.CatalogView import link.roadio.cuetree.Node import link.roadio.cuetree.ROOT_IDENTITY_ATTRIBUTE import link.roadio.riff.indexer.core.media.MediaId import link.roadio.riff.ui.PlaybackSourceRef import link.roadio.riff.ui.PlaybackSourceSegment import link.roadio.riff.ui.TocChapter /** Source identity and chapter coordinates are shared by browse rows and playback preparation. */ internal class CuetreePlaybackBindings( private val snapshot: CatalogView, private val hash: (ByteArray) -> Long, ) { fun binding( index: Int, base: Int, ): CatalogPlaybackBinding? { val nodes = snapshot.nodes val node = nodes[index] if (node.mediaId == null || !nodes[base].baseIdentity) return null val path = mutableListOf() var cursor = base while (true) { val current = nodes[cursor] if (current.isRoot) break path += current.name cursor = snapshot.parentOf(cursor) ?: return null } val rootIdentity = nodes[cursor].attributes[ROOT_IDENTITY_ATTRIBUTE]?.singleOrNull() ?: return null val components = path.asReversed() val key = MediaId(node.id.hi, node.id.lo ?: 0L, node.id.lo != null).hex() // AI-INTENT: Preserve the binding material across startup and browser projections. // https://wiki.271.pics/Riff/LATEST/cuetree-integration val identity = "$rootIdentity\u0000${components.joinToString( "\u0000", )}\u0000$key\u0000${node.span}\u0000${node.attributes.toSortedMap()}" return CatalogPlaybackBinding( PlaybackSourceRef( bindingId = hash(identity.toByteArray(Charsets.UTF_8)), rootBindingId = hash(rootIdentity.toByteArray(Charsets.UTF_8)), segments = components.map { PlaybackSourceSegment(it, "") }, beginUs = node.span?.beginUs, endUs = node.span?.endUs, ), indexedChapters(nodes[base], node), ) } } internal data class CatalogPlaybackBinding( val source: PlaybackSourceRef, val chapters: List, ) /** Consume indexed observations; never parse files, reorder invalid starts or invent file zero. */ internal fun indexedChapters( source: Node, occurrence: Node = source, ): List { val table = source.tables.firstOrNull { table -> table.sections.size > 1 && table.sections.all { it.reference == null && it.span.beginUs != null } && table.sections.zipWithNext().all { (left, right) -> left.span.beginUs!! < right.span.beginUs!! } } ?: return emptyList() val begin = occurrence.span?.beginUs ?: 0L val end = occurrence.span?.endUs val chapters = table.sections.mapNotNull { section -> val start = requireNotNull(section.span.beginUs) if (start < begin || (end != null && start >= end)) return@mapNotNull null TocChapter((start - begin) / 1_000L, section.attributes["title"]?.firstOrNull()?.takeIf(String::isNotBlank)) } return chapters.takeIf { it.size > 1 && it.zipWithNext().all { (a, b) -> a.startMs < b.startMs } }.orEmpty() }