package dev.inmo.micro_utils.fsm.common.managers import dev.inmo.micro_utils.fsm.common.State import dev.inmo.micro_utils.fsm.common.StatesManager import kotlinx.coroutines.flow.* import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock /** * Implement this repo if you want to use some custom repo for [DefaultStatesManager] */ interface DefaultStatesManagerRepo { /** * Must save [state] as current state of chain with [State.context] of [state] */ suspend fun set(state: T) /** * Remove exactly [state]. In case if internally [State.context] is busy with different [State], that [State] should * NOT be removed */ suspend fun removeState(state: T) /** * @return Current list of available and saved states */ suspend fun getStates(): List /** * @return Current state by [context] */ suspend fun getContextState(context: Any): T? /** * @return Current state by [context] */ suspend fun contains(context: Any): Boolean = getContextState(context) != null } /** * @param repo This repo will be used as repository for storing states. All operations with this repo will happen BEFORE * any event will be sent to [onChainStateUpdated], [onStartChain] or [onEndChain]. By default, will be used * [InMemoryDefaultStatesManagerRepo] or you may create custom [DefaultStatesManagerRepo] and pass as [repo] parameter * @param onStartContextsConflictResolver Receive current [State] and the state passed with [startChain]. In case when * this callback will return true, currently placed on the [State.context] [State] will be replaced by new state * with [endChain] with current state * @param onUpdateContextsConflictResolver Receive old [State], new one and the state currently placed on new [State.context] * key. In case when this callback will returns true, the state placed on [State.context] of new will be replaced by * new state by using [endChain] with that state */ open class DefaultStatesManager( protected val repo: DefaultStatesManagerRepo = InMemoryDefaultStatesManagerRepo(), protected val onStartContextsConflictResolver: suspend (current: T, new: T) -> Boolean = { _, _ -> false }, protected val onUpdateContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean = { _, _, _ -> false } ) : StatesManager { protected val _onChainStateUpdated = MutableSharedFlow>(0) override val onChainStateUpdated: Flow> = _onChainStateUpdated.asSharedFlow() protected val _onStartChain = MutableSharedFlow(0) override val onStartChain: Flow = _onStartChain.asSharedFlow() protected val _onEndChain = MutableSharedFlow(0) override val onEndChain: Flow = _onEndChain.asSharedFlow() protected val mapMutex = Mutex() constructor( repo: DefaultStatesManagerRepo, onContextsConflictResolver: suspend (old: T, new: T, currentNew: T) -> Boolean ) : this ( repo, onUpdateContextsConflictResolver = onContextsConflictResolver ) override suspend fun update(old: T, new: T) = mapMutex.withLock { val stateByOldContext: T? = repo.getContextState(old.context) when { stateByOldContext != old -> return@withLock stateByOldContext == null || old.context == new.context -> { repo.removeState(old) repo.set(new) _onChainStateUpdated.emit(old to new) } else -> { val stateOnNewOneContext = repo.getContextState(new.context) if (stateOnNewOneContext == null || onUpdateContextsConflictResolver(old, new, stateOnNewOneContext)) { stateOnNewOneContext ?.let { endChainWithoutLock(it) } repo.removeState(old) repo.set(new) _onChainStateUpdated.emit(old to new) } } } } override suspend fun startChain(state: T) = mapMutex.withLock { val stateOnContext = repo.getContextState(state.context) if (stateOnContext == null || onStartContextsConflictResolver(stateOnContext, state)) { stateOnContext ?.let { endChainWithoutLock(it) } repo.set(state) _onStartChain.emit(state) } } protected open suspend fun endChainWithoutLock(state: T) { if (repo.getContextState(state.context) == state) { repo.removeState(state) _onEndChain.emit(state) } } override suspend fun endChain(state: T) { mapMutex.withLock { endChainWithoutLock(state) } } override suspend fun getActiveStates(): List = repo.getStates() }