/**
 * Utilities related to Compose that don't belong in their own file.
 */

package theorycrafter.utils

import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.InteractionSource
import androidx.compose.material.DropdownMenuState
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.focusProperties
import androidx.compose.ui.platform.ClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextRange
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.LocalWindowExceptionHandlerFactory
import androidx.compose.ui.window.WindowExceptionHandler
import androidx.compose.ui.window.WindowExceptionHandlerFactory
import kotlinx.coroutines.delay
import kotlin.contracts.ExperimentalContracts
import kotlin.contracts.InvocationKind
import kotlin.contracts.contract
import kotlin.time.Duration


/**
 * If [condition] is true, returns the value returned by [modifier]; otherwise returns [this].
 */
@OptIn(ExperimentalContracts::class)
inline fun Modifier.thenIf(condition: Boolean, modifier: Modifier.() -> Modifier): Modifier {
    contract {
        callsInPlace(modifier, InvocationKind.AT_MOST_ONCE)
    }

    return if (condition)
        this.modifier()
    else
        this
}


/**
 * Returns a [TextFieldValue] with all the text selected.
 */
fun TextFieldValue.withAllTextSelected() = copy(selection = TextRange(0, text.length))


/**
 * Invokes [action] when the [InteractionSource] emits a [FocusInteraction.Focus] interaction.
 */
@Composable
inline fun InteractionSource.onGainedFocus(crossinline action: () -> Unit) {
    LaunchedEffect(this) {
        interactions.collect { interaction ->
            if (interaction is FocusInteraction.Focus)
                action()
        }
    }
}


/**
 * Invokes [action] when the [InteractionSource] emits a [FocusInteraction.Focus] and then [FocusInteraction.Unfocus].
 */
@Composable
inline fun InteractionSource.onLostFocus(crossinline action: () -> Unit) {
    LaunchedEffect(this) {
        var hasFocus = false
        interactions.collect { interaction ->
            when (interaction) {
                is FocusInteraction.Focus -> hasFocus = true
                is FocusInteraction.Unfocus -> {
                    if (hasFocus)
                        action()
                    hasFocus = false
                }
            }
        }
    }
}


/**
 * Makes the node non-focusable.
 */
fun Modifier.nonFocusable() = this.focusProperties { canFocus = false }


/**
 * Returns a [MutableSet] backed by the given map. This allows using a [SnapshotStateMap] as a set.
 */
fun <T> MutableMap<T, Unit>.asMutableSet(): MutableSet<T> = object: AbstractMutableSet<T>() {

    override fun add(element: T): Boolean {
        return this@asMutableSet.put(element, Unit) == null
    }

    override fun remove(element: T): Boolean {
        return this@asMutableSet.keys.remove(element)
    }

    override fun isEmpty(): Boolean {
        return this@asMutableSet.isEmpty()
    }

    override fun iterator(): MutableIterator<T> {
        return this@asMutableSet.keys.iterator()
    }

    override val size: Int
        get() = this@asMutableSet.size

}


/**
 * Returns a new snapshot [MutableSet].
 */
fun <T> mutableStateSetOf(): MutableSet<T> = mutableStateMapOf<T, Unit>().asMutableSet()


/**
 * Returns a [MutableState] that also calls the given function when the value is changed.
 */
fun <T> MutableState<T>.onSet(onSet: (T) -> Unit): MutableState<T> = object: MutableState<T> {

    override var value: T
        get() = this@onSet.value
        set(value) {
            this@onSet.value = value
            onSet(value)
        }

    override fun component1() = this@onSet.component1()

    override fun component2(): (T) -> Unit = {
        value = it
    }

}


/**
 * Returns a [DpOffset] with the given x offset and 0 y offset.
 */
fun DpOffsetX(x: Dp) = DpOffset(x = x, y = 0.dp)


/**
 * Returns a [DpOffset] with the given y offset and 0 x offset.
 */
fun DpOffsetY(y: Dp) = DpOffset(x = 0.dp, y = y)


/**
 * Copies the given text into the clipboard.
 */
fun ClipboardManager.setText(text: String) {
    setText(AnnotatedString(text))
}


/**
 * Returns a function that closes a [DropdownMenuState].
 */
val DropdownMenuState.closeFunction: () -> Unit
    get() = { status = DropdownMenuState.Status.Closed }


/**
 * Provides a [WindowExceptionHandlerFactory] that calls the given [WindowExceptionHandler] and then the handler
 * provided at the point of this call.
 */
@OptIn(ExperimentalComposeUiApi::class)
@Composable
fun ProvideAdditionalWindowExceptionHandler(
    handler: WindowExceptionHandler,
    content: @Composable () -> Unit
) {
    val nextHandlerFactory = LocalWindowExceptionHandlerFactory.current
    val currentHandler by rememberUpdatedState(handler)
    val newHandlerFactory: WindowExceptionHandlerFactory = remember(nextHandlerFactory) {
        WindowExceptionHandlerFactory { window ->
            val nextHandler = nextHandlerFactory.exceptionHandler(window)
            WindowExceptionHandler { throwable ->
                currentHandler.onException(throwable)
                nextHandler.onException(throwable)
            }
        }
    }

    CompositionLocalProvider(LocalWindowExceptionHandlerFactory provides newHandlerFactory) {
        content()
    }
}


/**
 * Returns the size of a double [ClosedFloatingPointRange].
 */
val ClosedFloatingPointRange<Double>.size
    get() = endInclusive - start


/**
 * A [CompositionLocal] specifying whether a test is being executed.
 */
val LocalIsTest: ProvidableCompositionLocal<Boolean> = staticCompositionLocalOf { false }


/**
 * Runs the given function after the given delay.
 */
@Composable
inline fun AfterDelay(delay: Duration, block: () -> Unit) {
    var delayPassed by remember { mutableStateOf(false) }
    LaunchedEffect(Unit) {
        delay(delay)
        delayPassed = true
    }
    if (delayPassed) {
        block()
    }
}