package link.roadio.riff.ui import link.roadio.riff.indexer.core.media.MediaId /** Small comparison/navigation input; never contains children or a playback binding. */ data class BrowserRowKey( val id: String, val label: String, val kind: BrowserKind, val comparisonName: String = label, val genreId: Long? = null, val actionTier: Boolean = true, ) /** * A catalog-backed collection. Only get() creates a display row; keys and selections stay flat. * AI-INTENT: Equality must not recursively traverse a catalog when Compose compares UI state. * https://wiki.271.pics/Riff/1-1-0/catalog-consumption#player-browse-representation */ abstract class BrowserRows : AbstractList() { abstract fun key(index: Int): BrowserRowKey open val ready: Boolean get() = true open fun prepare() = Unit open fun findRow(id: String): BrowserItem? = indices.firstOrNull { rowId(it) == id }?.let(::get) open fun rowId(index: Int): String = key(index).id open fun mediaId(index: Int): MediaId? = get(index).mediaId open fun playable(index: Int): Boolean = get(index).playable protected open fun sameRows(other: BrowserRows): Boolean = this === other protected open fun rowHash(): Int = System.identityHashCode(this) final override fun equals(other: Any?): Boolean = this === other || (other is BrowserRows && sameRows(other)) final override fun hashCode(): Int = rowHash() } internal fun List.rowKey(index: Int): BrowserRowKey = if (this is BrowserRows) { key(index) } else { get(index).let { BrowserRowKey(it.id, it.label, it.kind, it.comparisonName, it.genreId, it.hierarchyUsesActionTier) } } internal fun List.rowById(id: String): BrowserItem? = if (this is BrowserRows) findRow(id) else firstOrNull { it.id == id } internal fun List.selectRows(indices: IntArray): List = SelectedBrowserRows(this, indices) private class SelectedBrowserRows( val source: List, val selected: IntArray, ) : BrowserRows() { override val size get() = selected.size override fun key(index: Int) = source.rowKey(selected[index]) override fun rowId(index: Int) = source.rowId(selected[index]) override fun mediaId(index: Int) = if (source is BrowserRows) source.mediaId(selected[index]) else source[selected[index]].mediaId override fun playable(index: Int) = if (source is BrowserRows) source.playable(selected[index]) else source[selected[index]].playable override fun get(index: Int) = source[selected[index]] override fun sameRows(other: BrowserRows) = other is SelectedBrowserRows && source === other.source && selected.contentEquals(other.selected) override fun rowHash() = 31 * System.identityHashCode(source) + selected.contentHashCode() } internal fun List.projectRows(transform: (BrowserItem) -> BrowserItem): List { val source = this return object : BrowserRows() { override val size get() = source.size override val ready get() = (source as? BrowserRows)?.ready != false override fun prepare() { (source as? BrowserRows)?.prepare() } override fun findRow(id: String) = source.rowById(id)?.let(transform) override fun key(index: Int) = source.rowKey(index) override fun rowId(index: Int) = source.rowId(index) override fun mediaId(index: Int) = if (source is BrowserRows) source.mediaId(index) else source[index].mediaId override fun playable(index: Int) = if (source is BrowserRows) source.playable(index) else source[index].playable override fun get(index: Int) = transform(source[index]) } } /** Catalog operations used by navigation, queues and AI; none require a recursive UI graph. */ interface BrowserCatalog { fun find(id: String): BrowserItem? fun tracks( contextKey: String, playableOnly: Boolean = true, ignoreLeadingArticles: Boolean = false, ): List fun track(mediaId: MediaId): BrowserItem? fun trackKey(mediaId: MediaId): BrowserRowKey? fun preferredBinding(rowId: String): Long? fun available(mediaId: MediaId): Boolean fun mediaIds(contextKey: String): Set fun entities(categoryId: String): List fun directoryPaths(): Map>> fun trackFacts(): List } data class BrowserMembership( val key: String, val mediaIds: Set, ) data class BrowserTrackFact( val mediaId: MediaId, val playable: Boolean, val reactionKey: String, val sourceBindingId: Long? = null, ) internal fun List.rowId(index: Int): String = if (this is BrowserRows) rowId(index) else get(index).id internal fun List.mediaIdsInOrder(): List = indices.mapNotNull { index -> if (this is BrowserRows) mediaId(index) else get(index).mediaId }