package ch.threema.common

import kotlin.reflect.KProperty
import kotlin.time.Duration
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withTimeoutOrNull

suspend fun <T : Any> Flow<T>.await(item: T) {
    first { it == item }
}

suspend fun <T : Any> Flow<T?>.awaitNonNull(): T =
    filterNotNull().first()

suspend fun <T : Any> Flow<T?>.awaitNull() {
    first { it == null }
}

suspend inline fun waitAtMost(timeout: Duration, noinline block: suspend () -> Unit) {
    withTimeoutOrNull(timeout) { block() }
}

/**
 * Creates a [StateFlow] which will always hold the same [value].
 */
fun <T> stateFlowOf(value: T): StateFlow<T> = MutableStateFlow(value)

/**
 * Similar to [combine], but produces a [StateFlow] instead of a regular [Flow].
 *
 * Note: This should be used sparingly and only with very simple and cheap transformations, as the transformation
 * is run for every collector every time a new value is emitted from any of the source flows, as well as every time
 * [StateFlow.value] is called on the resulting [StateFlow]. It is mainly viable in cases where at most one collector
 * will be active at a time and `value` is never called. In most cases, it is better to use [combine] in combination with [Flow.stateIn].
 */
fun <T1, T2, R> combineStates(
    stateFlow1: StateFlow<T1>,
    stateFlow2: StateFlow<T2>,
    transformation: (T1, T2) -> R,
): StateFlow<R> =
    DerivedStateFlow(
        getValue = {
            transformation(stateFlow1.value, stateFlow2.value)
        },
        flow = combine(stateFlow1, stateFlow2, transformation),
    )

/**
 * Similar to [combine], but produces a [StateFlow] instead of a regular [Flow].
 *
 * Note: This should be used sparingly and only with very simple and cheap transformations, as the transformation
 * is run for every collector every time a new value is emitted from any of the source flows, as well as every time
 * [StateFlow.value] is called on the resulting [StateFlow]. It is mainly viable in cases where at most one collector
 * will be active at a time and `value` is never called. In most cases, it is better to use [combine] in combination with [Flow.stateIn].
 */
fun <T1, T2, T3, R> combineStates(
    stateFlow1: StateFlow<T1>,
    stateFlow2: StateFlow<T2>,
    stateFlow3: StateFlow<T3>,
    transformation: (T1, T2, T3) -> R,
): StateFlow<R> =
    DerivedStateFlow(
        getValue = {
            transformation(stateFlow1.value, stateFlow2.value, stateFlow3.value)
        },
        flow = combine(stateFlow1, stateFlow2, stateFlow3, transformation),
    )

/**
 * Similar to [map], but produces a [StateFlow] instead of a regular [Flow].
 *
 * Note: This should be used sparingly and only with very simple and cheap transformations, as the transformation
 * is run for every collector every time a new value is emitted from the source flow, as well as every time
 * [StateFlow.value] is called on the resulting [StateFlow]. It is mainly viable in cases where at most one collector
 * will be active at a time and `value` is never called. In most cases, it is better to use [map] in combination with [Flow.stateIn].
 */
fun <T, R> StateFlow<T>.mapState(transformation: (T) -> R): StateFlow<R> =
    DerivedStateFlow(
        getValue = {
            transformation(value)
        },
        flow = map(transformation),
    )

operator fun <T> StateFlow<T>.getValue(receiver: Any, property: KProperty<*>): T = value

operator fun <T> MutableStateFlow<T>.setValue(receiver: Any, property: KProperty<*>, t: T) {
    this.value = t
}

suspend inline fun <T> MutableSharedFlow<T>.awaitAtLeastOneSubscriber() {
    subscriptionCount.first { it > 0 }
}

/**
 * Returns a [Flow] whose values are generated by [transform] function that process the most recently emitted values by each flow.
 *
 * The receiver of the [transform] is [FlowCollector] and thus `transform` is a
 * generic function that may transform emitted element, skip it or emit it multiple times.
 */
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, R> combine(
    flow: Flow<T1>,
    flow2: Flow<T2>,
    flow3: Flow<T3>,
    flow4: Flow<T4>,
    flow5: Flow<T5>,
    flow6: Flow<T6>,
    transform: suspend (T1, T2, T3, T4, T5, T6) -> R,
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> ->
    transform(
        args[0] as T1,
        args[1] as T2,
        args[2] as T3,
        args[3] as T4,
        args[4] as T5,
        args[5] as T6,
    )
}

/**
 * Returns a [Flow] whose values are generated by [transform] function that process the most recently emitted values by each flow.
 *
 * The receiver of the [transform] is [FlowCollector] and thus `transform` is a
 * generic function that may transform emitted element, skip it or emit it multiple times.
 */
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, T7, R> combine(
    flow: Flow<T1>,
    flow2: Flow<T2>,
    flow3: Flow<T3>,
    flow4: Flow<T4>,
    flow5: Flow<T5>,
    flow6: Flow<T6>,
    flow7: Flow<T7>,
    transform: suspend (T1, T2, T3, T4, T5, T6, T7) -> R,
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7) { args: Array<*> ->
    transform(
        args[0] as T1,
        args[1] as T2,
        args[2] as T3,
        args[3] as T4,
        args[4] as T5,
        args[5] as T6,
        args[6] as T7,
    )
}

/**
 * Returns a [Flow] whose values are generated by [transform] function that process the most recently emitted values by each flow.
 *
 * The receiver of the [transform] is [FlowCollector] and thus `transform` is a
 * generic function that may transform emitted element, skip it or emit it multiple times.
 */
@Suppress("UNCHECKED_CAST")
fun <T1, T2, T3, T4, T5, T6, T7, T8, R> combine(
    flow: Flow<T1>,
    flow2: Flow<T2>,
    flow3: Flow<T3>,
    flow4: Flow<T4>,
    flow5: Flow<T5>,
    flow6: Flow<T6>,
    flow7: Flow<T7>,
    flow8: Flow<T8>,
    transform: suspend (T1, T2, T3, T4, T5, T6, T7, T8) -> R,
): Flow<R> = combine(flow, flow2, flow3, flow4, flow5, flow6, flow7, flow8) { args: Array<*> ->
    transform(
        args[0] as T1,
        args[1] as T2,
        args[2] as T3,
        args[3] as T4,
        args[4] as T5,
        args[5] as T6,
        args[6] as T7,
        args[7] as T8,
    )
}
