package link.roadio.riff.userstate import link.roadio.riff.storage.hash.nativeXxh3 import link.roadio.riff.userstate.model.MediaKeyModel import link.roadio.riff.userstate.model.QueueItemModel import java.io.ByteArrayOutputStream import java.nio.ByteBuffer import java.nio.ByteOrder /** Canonical identity framing; generation/position must be added only where identity owns them. * Authority: https://wiki.271.pics/Riff/1-1-0/identity#canonical-hash-input */ class ContentIds( private val hash: (ByteArray) -> Long = ::nativeXxh3, ) { // AI-INTENT: Construction and validation must share the exact semantic framing. // https://wiki.271.pics/Riff/1-1-0/identity#identity-material fun queueRevision(items: List): Long = id("queue-revision") { int(items.size) items.forEach { item -> nullable(item.occurrenceId) media(item.mediaId) } } fun queueOccurrence( creation: Long, ordinal: Int, value: MediaKeyModel, ): Long = id("queue-occurrence") { long(creation) int(ordinal) media(value) } fun savedBookQueue( profileSlot: Int, titleId: Long, ): Long = id("saved-book-queue") { int(profileSlot) long(titleId) } fun savedMusicQueue( profileSlot: Int, creationId: Long, ): Long = id("saved-music-queue") { int(profileSlot) long(creationId) } fun bookmark( profileSlot: Int, queueId: Long, occurrenceId: Long, positionUs: Long, ): Long = id("bookmark") { int(profileSlot) long(queueId) long(occurrenceId) long(positionUs) } fun id( domain: String, content: Input.() -> Unit, ): Long { val input = Input() input.raw("riff-id-v1".toByteArray(Charsets.UTF_8)) input.text(domain) input.content() return hash(input.bytes()) } class Input { private val output = ByteArrayOutputStream() fun raw(value: ByteArray) { output.write(value) } fun int(value: Int) { raw( ByteBuffer .allocate(4) .order(ByteOrder.LITTLE_ENDIAN) .putInt(value) .array(), ) } fun long(value: Long) { raw( ByteBuffer .allocate(8) .order(ByteOrder.LITTLE_ENDIAN) .putLong(value) .array(), ) } fun bool(value: Boolean) { output.write(if (value) 1 else 0) } fun text(value: String) { val bytes = value.toByteArray(Charsets.UTF_8) int(bytes.size) raw(bytes) } fun nullable(value: Long?) { bool(value != null) value?.let(::long) } fun media(value: MediaKeyModel) { long(value.hi) bool(value.loPresent) if (value.loPresent) long(value.lo) } fun bytes(): ByteArray = output.toByteArray() } } fun Long.contentHex(): String = java.lang.Long .toUnsignedString(this, 16) .padStart(16, '0')