package dev.inmo.tgbotapi.extensions.behaviour_builder import dev.inmo.micro_utils.coroutines.launchSafelyWithoutExceptions import dev.inmo.micro_utils.coroutines.subscribeSafelyWithoutExceptions import dev.inmo.micro_utils.fsm.common.* import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.micro_utils.coroutines.accumulatorFlow import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import kotlin.jvm.JvmName import kotlin.reflect.KClass /** * Interface which combine [BehaviourContext] and [StatesMachine]. Subcontext of triggers and states contexts must have * one common flow of updates and must not lose updates between updates * * @see DefaultBehaviourContextWithFSM * @see buildBehaviourWithFSM */ interface BehaviourContextWithFSM : BehaviourContext, StatesMachine { suspend fun start() = start(this) suspend fun launchStateHandling( state: T, contextUpdatesFlow: Flow, handlers: List> ): T? { return handlers.firstOrNull { it.checkHandleable(state) } ?.run { doInSubContext(updatesUpstreamFlow = contextUpdatesFlow) { handleState(state) } } } /** * Add NON STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Non strict means that * for input [State] will be used [KClass.isInstance] and any inheritor of [kClass] will pass this requirement * * @see BehaviourWithFSMStateHandlerHolder * @see onStateOrSubstate */ fun add(kClass: KClass, strict: Boolean = false, handler: BehaviourWithFSMStateHandler) /** * Add STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Strict means that * for input [State] will be used [State]::class == [kClass] and any [State] with exactly the same type will pass * requirements * * @see BehaviourWithFSMStateHandlerHolder * @see strictlyOn */ fun addStrict(kClass: KClass, handler: BehaviourWithFSMStateHandler) = add(kClass, strict = true, handler) override fun copy( bot: TelegramBot, scope: CoroutineScope, broadcastChannelsSize: Int, onBufferOverflow: BufferOverflow, upstreamUpdatesFlow: Flow?, updatesFilter: BehaviourContextAndTypeReceiver? ): BehaviourContextWithFSM companion object { operator fun invoke( behaviourContext: BehaviourContext, handlers: List>, statesManager: StatesManager ) = DefaultBehaviourContextWithFSM(behaviourContext, statesManager, handlers) } } /** * Add NON STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Non strict means that * for input [State] will be used [KClass.isInstance] and any inheritor of [kClass] will pass this requirement * * @see BehaviourWithFSMStateHandlerHolder * @see BehaviourContextWithFSM.add */ @Suppress("MemberVisibilityCanBePrivate") inline fun BehaviourContextWithFSM.onStateOrSubstate(handler: BehaviourWithFSMStateHandler) = add(I::class, strict = false, handler) /** * Add STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Strict means that * for input [State] will be used [State]::class == [kClass] and any [State] with exactly the same type will pass * requirements * * @see BehaviourWithFSMStateHandlerHolder * @see BehaviourContextWithFSM.addStrict */ @Suppress("MemberVisibilityCanBePrivate") inline fun BehaviourContextWithFSM.strictlyOn(handler: BehaviourWithFSMStateHandler) = addStrict(I::class, handler) /** * Default realization of [BehaviourContextWithFSM]. It uses [behaviourContext] as a base for this object as * [BehaviourContext], but managing substates contexts updates for avoiding of updates lost between states */ class DefaultBehaviourContextWithFSM( private val behaviourContext: BehaviourContext, private val statesManager: StatesManager, private val handlers: List> ) : BehaviourContext by behaviourContext, BehaviourContextWithFSM { private val updatesFlows = mutableMapOf>() private val additionalHandlers = mutableListOf>() private var actualHandlersList = additionalHandlers + handlers private fun getContextUpdatesFlow(context: Any) = updatesFlows.getOrPut(context) { allUpdatesFlow.accumulatorFlow(scope) } override suspend fun StatesMachine.handleState(state: T): T? = launchStateHandling( state, allUpdatesFlow, actualHandlersList ) override fun add(kClass: KClass, strict: Boolean, handler: BehaviourWithFSMStateHandler) { additionalHandlers.add(BehaviourWithFSMStateHandlerHolder(kClass, strict, handler)) actualHandlersList = additionalHandlers + handlers } override fun start(scope: CoroutineScope): Job = scope.launchSafelyWithoutExceptions { val statePerformer: suspend (T) -> Unit = { state: T -> val newState = launchStateHandling(state, getContextUpdatesFlow(state.context), actualHandlersList) if (newState != null) { statesManager.update(state, newState) } else { statesManager.endChain(state) } } statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { launch { statePerformer(it) } } statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { (old, new) -> if (old.context != new.context) { updatesFlows.remove(old.context) } launch { statePerformer(new) } } statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { updatesFlows.remove(it.context) } statesManager.getActiveStates().forEach { launch { statePerformer(it) } } } /** * Add NON STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Non strict means that * for input [State] will be used [KClass.isInstance] and any inheritor of [kClass] will pass this requirement * * @see BehaviourWithFSMStateHandlerHolder * @see BehaviourContextWithFSM.add */ @Suppress("MemberVisibilityCanBePrivate") inline fun onStateOrSubstate(handler: BehaviourWithFSMStateHandler) = add(I::class, strict = false, handler) /** * Add STRICT [handler] to list of available in future [BehaviourContextWithFSM]. Strict means that * for input [State] will be used [State]::class == [kClass] and any [State] with exactly the same type will pass * requirements * * @see BehaviourWithFSMStateHandlerHolder * @see BehaviourContextWithFSM.addStrict */ @Suppress("MemberVisibilityCanBePrivate") inline fun strictlyOn(handler: BehaviourWithFSMStateHandler) = addStrict(I::class, handler) override suspend fun startChain(state: T) { statesManager.startChain(state) } override fun copy( bot: TelegramBot, scope: CoroutineScope, broadcastChannelsSize: Int, onBufferOverflow: BufferOverflow, upstreamUpdatesFlow: Flow?, updatesFilter: BehaviourContextAndTypeReceiver? ): DefaultBehaviourContextWithFSM = BehaviourContextWithFSM( behaviourContext.copy(bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, updatesFilter), handlers, statesManager ) }