package dev.inmo.tgbotapi.extensions.behaviour_builder import dev.inmo.micro_utils.coroutines.* import dev.inmo.micro_utils.fsm.common.* import dev.inmo.micro_utils.fsm.common.utils.StateHandlingErrorHandler import dev.inmo.micro_utils.fsm.common.utils.defaultStateHandlingErrorHandler import dev.inmo.tgbotapi.bot.TelegramBot import dev.inmo.tgbotapi.types.update.abstracts.Update import dev.inmo.tgbotapi.extensions.behaviour_builder.utils.handlers_registrar.TriggersHolder import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock 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) /** * 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?, triggersHolder: TriggersHolder ): BehaviourContextWithFSM fun copy( bot: TelegramBot = this.bot, scope: CoroutineScope = this.scope, broadcastChannelsSize: Int = 100, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, upstreamUpdatesFlow: Flow? = null, triggersHolder: TriggersHolder = this.triggersHolder, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler() ): BehaviourContextWithFSM = copy( bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder ) fun copy( bot: TelegramBot = this.bot, scope: CoroutineScope = this.scope, broadcastChannelsSize: Int = 100, onBufferOverflow: BufferOverflow = BufferOverflow.SUSPEND, upstreamUpdatesFlow: Flow? = null, triggersHolder: TriggersHolder = this.triggersHolder, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler(), updatesFilter: BehaviourContextAndTypeReceiver? = null ): BehaviourContextWithFSM = copy( bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder, onStateHandlingErrorHandler ) companion object { operator fun invoke( behaviourContext: BehaviourContext, handlers: List>, statesManager: StatesManager, onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler() ) = DefaultBehaviourContextWithFSM(behaviourContext, statesManager, handlers, onStateHandlingErrorHandler) } } /** * 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 * @param onStateHandlingErrorHandler Will be used in case if state handling has not been successfully completed in [launchStateHandling] */ class DefaultBehaviourContextWithFSM( private val behaviourContext: BehaviourContext, private val statesManager: StatesManager, private val handlers: List>, private val onStateHandlingErrorHandler: StateHandlingErrorHandler = defaultStateHandlingErrorHandler() ) : BehaviourContext by behaviourContext, BehaviourContextWithFSM { private val updatesFlows = mutableMapOf>() private val additionalHandlers = mutableListOf>() private var actualHandlersList = additionalHandlers + handlers protected val statesJobs = mutableMapOf() protected val statesJobsMutex = Mutex() override suspend fun launchStateHandling(state: T, handlers: List>): T? { return launchStateHandling(state, handlers, onStateHandlingErrorHandler) } private fun getSubContext(context: Any) = updatesFlows.getOrPut(context) { createSubContext() } override suspend fun StatesMachine.handleState(state: T): T? { return getSubContext( state.context ).launchStateHandling( state, 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 = getSubContext(state.context).launchStateHandling(state, actualHandlersList) if (newState != null) { statesManager.update(state, newState) } else { statesManager.endChain(state) } } fun Job.enableRemoveOnCompletion(state: T) { invokeOnCompletion { launchSafelyWithoutExceptions { statesJobsMutex.withLock { if (this@enableRemoveOnCompletion === statesJobs[state]) { statesJobs.remove(state) } } } } } statesManager.onStartChain.subscribeSafelyWithoutExceptions(this) { statesJobsMutex.withLock { runCatchingSafely { statesJobs.remove(it) ?.cancel() } statesJobs[it] = launch { statePerformer(it) }.apply { enableRemoveOnCompletion(it) } } } statesManager.onEndChain.subscribeSafelyWithoutExceptions(this) { statesJobsMutex.withLock { runCatchingSafely { statesJobs.remove(it) ?.cancel() } } updatesFlows.remove(it.context) } statesManager.onChainStateUpdated.subscribeSafelyWithoutExceptions(this) { (old, new) -> statesJobsMutex.withLock { runCatchingSafely { statesJobs.remove(old) ?.cancel() } runCatchingSafely { statesJobs.remove(new) ?.cancel() } statesJobs[new] = launch { statePerformer(new) }.apply { enableRemoveOnCompletion(new) } } if (old.context != new.context) { updatesFlows.remove(old.context) } } statesManager.getActiveStates().forEach { statesJobsMutex.withLock { runCatchingSafely { statesJobs.remove(it) ?.cancel() } statesJobs[it] = launch { statePerformer(it) }.apply { enableRemoveOnCompletion(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?, triggersHolder: TriggersHolder ): DefaultBehaviourContextWithFSM = BehaviourContextWithFSM( behaviourContext.copy(bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder), handlers, statesManager, onStateHandlingErrorHandler ) override fun copy( bot: TelegramBot, scope: CoroutineScope, broadcastChannelsSize: Int, onBufferOverflow: BufferOverflow, upstreamUpdatesFlow: Flow?, triggersHolder: TriggersHolder, onStateHandlingErrorHandler: StateHandlingErrorHandler ): DefaultBehaviourContextWithFSM = BehaviourContextWithFSM( behaviourContext.copy(bot, scope, broadcastChannelsSize, onBufferOverflow, upstreamUpdatesFlow, triggersHolder), handlers, statesManager, onStateHandlingErrorHandler ) }