/**
 * Improves the Compose bring-into-view functionality by allowing to extend the rectangle to bring into view.
 */

@file:OptIn(ExperimentalFoundationApi::class)

package compose.utils

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.relocation.BringIntoViewRequester
import androidx.compose.foundation.relocation.bringIntoViewRequester
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.layout.LayoutCoordinates
import androidx.compose.ui.layout.onPlaced
import androidx.compose.ui.modifier.modifierLocalConsumer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.toSize
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch


/**
 * When the element is focused, bring its bounds into view, after expanding it by the given margins.
 */
fun Modifier.bringIntoViewWhenFocusedWithMargins(margins: PaddingValues = PaddingValues()) =
    bringIntoViewWhenFocused { density, layoutDirection, layoutCoordinates ->
        elementBoundsWithMargins(density, layoutDirection, layoutCoordinates, margins)
    }


/**
 * When the element is focused, bring the bounds determined by the given function into view.
 * The returned bounds are in the element's own coordinate system.
 */
fun Modifier.bringIntoViewWhenFocused(
    bounds: (Density, LayoutDirection, LayoutCoordinates) -> Rect
) = composed {
    val coroutineScope = rememberCoroutineScope()
    val bringIntoViewRequester = remember { BringIntoViewRequester() }
    var targetRect by remember { mutableStateOf<Rect?>(null) }
    val density by rememberUpdatedState(LocalDensity.current)
    val layoutDirection by rememberUpdatedState(LocalLayoutDirection.current)

    this
        .onPlaced {
            targetRect = bounds(density, layoutDirection, it)
        }
        .bringIntoViewRequester(bringIntoViewRequester)
        .onFocusChanged {
            if (it.isFocused) {
                val rect = targetRect ?: return@onFocusChanged
                coroutineScope.launch {
                    // Without the delay, the "built-in" mechanism for bringing into view the focused element
                    // causes a call to scrollableState.animateScrollBy after ours, effectively overriding it
                    // By delaying, we're making sure we're the ones overriding the built-in mechanism.
                    delay(32)
                    bringIntoViewRequester.bringIntoView(rect)
                }
            }
        }
}


/**
 * When the element is selected (see [ModifierLocalSelected]), bring its bounds into view, after expanding it by the
 * given margins.
 */
fun Modifier.bringIntoViewWhenSelectedWithMargins(margins: PaddingValues = PaddingValues()) =
    bringIntoViewWhenSelected { density, layoutDirection, layoutCoordinates ->
        elementBoundsWithMargins(density, layoutDirection, layoutCoordinates, margins)
    }


/**
 * When the element is selected (see [ModifierLocalSelected]), bring the bounds determined by the given function into
 * view.
 * The returned bounds are in the element's own coordinate system.
 */
@OptIn(ExperimentalComposeUiApi::class)
fun Modifier.bringIntoViewWhenSelected(
    bounds: (Density, LayoutDirection, LayoutCoordinates) -> Rect
) = composed {
    val coroutineScope = rememberCoroutineScope()
    val bringIntoViewRequester = remember { BringIntoViewRequester() }
    var targetRect by remember { mutableStateOf<Rect?>(null) }
    val density by rememberUpdatedState(LocalDensity.current)
    val layoutDirection by rememberUpdatedState(LocalLayoutDirection.current)

    this
        .onPlaced {
            targetRect = bounds(density, layoutDirection, it)
        }
        .bringIntoViewRequester(bringIntoViewRequester)
        .modifierLocalConsumer {
            if (ModifierLocalSelected.current == true) {
                targetRect?.let { rect ->
                    coroutineScope.launch {
                        // Without the delay, the "built-in" mechanism for bringing into view the focused element
                        // causes a call to scrollableState.animateScrollBy after ours, effectively overriding it
                        // By delaying, we're making sure we're the ones overriding the build-in mechanism.
                        delay(32)
                        bringIntoViewRequester.bringIntoView(rect)
                    }
                }
            }
        }
}


/**
 * Returns the bounds of the element, expanded by [margins].
 */
private fun elementBoundsWithMargins(
    density: Density,
    layoutDirection: LayoutDirection,
    layoutCoordinates: LayoutCoordinates,
    margins: PaddingValues
): Rect = with(density) {
    val size = layoutCoordinates.size.toSize()
    Rect(
        left = -margins.calculateLeftPadding(layoutDirection).toPx(),
        top = -margins.calculateTopPadding().toPx(),
        right = size.width + margins.calculateRightPadding(layoutDirection).toPx(),
        bottom = size.height + margins.calculateBottomPadding().toPx()
    )
}