package dev.inmo.tgbotapi.types.message import dev.inmo.tgbotapi.types.CustomEmojiId import dev.inmo.tgbotapi.types.chat.User import dev.inmo.tgbotapi.types.message.textsources.* import kotlinx.serialization.Serializable @Serializable internal data class RawMessageEntity( val type: String, val offset: Int, val length: Int, val url: String? = null, val user: User? = null, val language: String? = null, val custom_emoji_id: CustomEmojiId? = null ) { internal val range by lazy { offset until (offset + length) } } internal fun RawMessageEntity.asTextSource( source: String, subParts: List> ): TextSource { val sourceSubstring: String = source.substring(range) val subPartsWithRegulars by lazy { subParts.map { (it.first - offset) to it.second }.fillWithRegulars(sourceSubstring) } return when (type) { "mention" -> MentionTextSource(sourceSubstring, subPartsWithRegulars) "hashtag" -> HashTagTextSource(sourceSubstring, subPartsWithRegulars) "cashtag" -> CashTagTextSource(sourceSubstring, subPartsWithRegulars) "bot_command" -> BotCommandTextSource(sourceSubstring) "url" -> URLTextSource(sourceSubstring) "email" -> EMailTextSource(sourceSubstring, subPartsWithRegulars) "phone_number" -> PhoneNumberTextSource(sourceSubstring, subPartsWithRegulars) "bold" -> BoldTextSource(sourceSubstring, subPartsWithRegulars) "italic" -> ItalicTextSource(sourceSubstring, subPartsWithRegulars) "code" -> CodeTextSource(sourceSubstring) "pre" -> PreTextSource(sourceSubstring, language) "text_link" -> TextLinkTextSource( sourceSubstring, url ?: throw IllegalStateException("URL must not be null for text link") ) "text_mention" -> TextMentionTextSource( sourceSubstring, user ?: throw IllegalStateException("User must not be null for text mention"), subPartsWithRegulars ) "underline" -> UnderlineTextSource(sourceSubstring, subPartsWithRegulars) "strikethrough" -> StrikethroughTextSource(sourceSubstring, subPartsWithRegulars) "spoiler" -> SpoilerTextSource(sourceSubstring, subPartsWithRegulars) "custom_emoji" -> CustomEmojiTextSource(sourceSubstring, custom_emoji_id ?: error("For custom emoji custom_emoji_id should exists"), subPartsWithRegulars) else -> RegularTextSource(sourceSubstring) } } private inline operator fun > ClosedRange.contains(other: ClosedRange): Boolean { return start <= other.start && endInclusive >= other.endInclusive } internal fun List>.fillWithRegulars(source: String): TextSourcesList { var index = 0 val result = mutableListOf() for (i in indices) { val (offset, textSource) = get(i) if (offset - index > 0) { result.add(regular(source.substring(index, offset))) index = offset } result.add(textSource) index += textSource.source.length } if (index != source.length) { result.add(regular(source.substring(index, source.length))) } return result } private fun createTextSources( originalFullString: String, entities: RawMessageEntities ): List> { val mutableEntities = entities.toMutableList().apply { sortBy { it.offset } } val resultList = mutableListOf>() while (mutableEntities.isNotEmpty()) { var parent = mutableEntities.removeFirst() val subentities = mutableListOf() val toAddCutted = mutableListOf() while (mutableEntities.isNotEmpty()) { val potentialParent = mutableEntities.first() when { potentialParent.range.first > parent.range.last -> break potentialParent.range in parent.range -> { subentities.add(potentialParent) } potentialParent.offset == parent.offset && potentialParent.length > parent.length -> { subentities.add(parent) parent = potentialParent } else -> { // need to cut toAddCutted.add(potentialParent) } } mutableEntities.remove(potentialParent) } val subtextSources = if (subentities.isNotEmpty()) { mutableEntities.removeAll(subentities) if (toAddCutted.isNotEmpty()) { val borderIndex = parent.range.last + 1 mutableEntities.addAll( 0, toAddCutted.map { val firstLength = borderIndex - it.offset subentities.add(it.copy(length = firstLength)) it.copy( offset = borderIndex, length = it.length - firstLength ) } ) } createTextSources(originalFullString, subentities) } else { emptyList() } resultList.add( parent.offset to parent.asTextSource( originalFullString, subtextSources ) ) } return resultList } internal fun TextSource.toRawMessageEntities(offset: Int = 0): List { val source = source val length = source.length return listOfNotNull( when (this) { is MentionTextSource -> RawMessageEntity("mention", offset, length) is HashTagTextSource -> RawMessageEntity("hashtag", offset, length) is CashTagTextSource -> RawMessageEntity("cashtag", offset, length) is BotCommandTextSource -> RawMessageEntity("bot_command", offset, length) is URLTextSource -> RawMessageEntity("url", offset, length) is EMailTextSource -> RawMessageEntity("email", offset, length) is PhoneNumberTextSource -> RawMessageEntity("phone_number", offset, length) is BoldTextSource -> RawMessageEntity("bold", offset, length) is ItalicTextSource -> RawMessageEntity("italic", offset, length) is CodeTextSource -> RawMessageEntity("code", offset, length) is PreTextSource -> RawMessageEntity("pre", offset, length, language = language) is TextLinkTextSource -> RawMessageEntity("text_link", offset, length, url) is TextMentionTextSource -> RawMessageEntity("text_mention", offset, length, user = user) is UnderlineTextSource -> RawMessageEntity("underline", offset, length) is StrikethroughTextSource -> RawMessageEntity("strikethrough", offset, length) is SpoilerTextSource -> RawMessageEntity("spoiler", offset, length) is CustomEmojiTextSource -> RawMessageEntity("custom_emoji", offset, length, custom_emoji_id = customEmojiId) is RegularTextSource -> null } ) + if (this is MultilevelTextSource) { subsources.toRawMessageEntities(offset) } else { emptyList() } } internal fun TextSourcesList.toRawMessageEntities(preOffset: Int = 0): List { var i = preOffset return flatMap { textSource -> textSource.toRawMessageEntities(i).also { i += it.maxByOrNull { it.length }?.length ?: textSource.source.length } } } internal fun TextSourcesList.toRawMessageEntities(): List = toRawMessageEntities(0) internal fun RawMessageEntities.asTextSources(sourceString: String): TextSourcesList = createTextSources(sourceString, this).fillWithRegulars(sourceString) internal typealias RawMessageEntities = List