package dev.ulfrx.recipe.ui.screens.shell.dock

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.FastOutSlowInEasing
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.input.pointer.PointerInputScope
import androidx.compose.ui.input.pointer.positionChanged
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import dev.ulfrx.recipe.ui.screens.shell.dock.DockPressEvent.Cancelled
import dev.ulfrx.recipe.ui.screens.shell.dock.DockPressEvent.Pressing
import dev.ulfrx.recipe.ui.screens.shell.dock.DockPressEvent.Released
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import kotlin.math.abs

private val PressOverlayBleed = 4.dp
private const val SLIDE_OUTWARD_STIFFNESS = Spring.StiffnessMediumLow
private const val SLIDE_SETTLE_STIFFNESS = Spring.StiffnessHigh
private const val OVERLAY_FADE_IN_DURATION_MS = 120
private const val OVERLAY_FADE_OUT_DURATION_MS = 40
private const val SETTLE_EPSILON_PX = 0.5f

internal data class DockOverlayAnimations(
    val overlayCenterX: Float,
    val overlayWidthPx: Float,
    val overlayAlpha: Float,
    val overlayPeakProgress: Float,
    val activeIndicatorAlpha: Float,
)

@Composable
internal fun rememberDockOverlayAnimations(
    pressState: DockPressState,
    activeIndex: Int,
    tabBounds: Map<Int, TabBounds>,
    dockWidthPx: Float,
    density: Density,
): DockOverlayAnimations {
    val activeBounds = tabBounds[activeIndex]
    val activeCenterX = activeBounds?.let { it.offsetXPx + it.widthPx / 2f } ?: 0f
    val bleedPx = with(density) { PressOverlayBleed.toPx() }
    val overlayWidthPx = (activeBounds?.widthPx ?: 0f) + 2 * bleedPx
    val centerXMin = overlayWidthPx / 2f
    val centerXMax = (dockWidthPx - overlayWidthPx / 2f).coerceAtLeast(centerXMin)
    val pressingXPx = (pressState as? DockPressState.Pressing)?.xPx
    val clampedPressX = pressingXPx?.coerceIn(centerXMin, centerXMax)

    val centerAnim = remember { Animatable(activeCenterX) }
    val overlayAlphaAnim = remember { Animatable(0f) }
    val activeAlphaAnim = remember { Animatable(1f) }

    var wasPressed by remember { mutableStateOf(false) }
    LaunchedEffect(clampedPressX, activeCenterX) {
        when {
            clampedPressX == null -> {
                wasPressed = false
                centerAnim.animateTo(
                    activeCenterX,
                    spring(
                        dampingRatio = Spring.DampingRatioNoBouncy,
                        stiffness = SLIDE_SETTLE_STIFFNESS,
                        visibilityThreshold = SETTLE_EPSILON_PX,
                    ),
                )
            }

            !wasPressed -> {
                wasPressed = true
                centerAnim.animateTo(
                    clampedPressX,
                    spring(
                        dampingRatio = Spring.DampingRatioNoBouncy,
                        stiffness = SLIDE_OUTWARD_STIFFNESS,
                        visibilityThreshold = SETTLE_EPSILON_PX,
                    ),
                )
            }

            else -> {
                centerAnim.snapTo(clampedPressX)
            }
        }
    }

    val pressing = pressState is DockPressState.Pressing
    val activeCenterXState = rememberUpdatedState(activeCenterX)
    var releaseSlideStartX by remember { mutableStateOf<Float?>(null) }
    LaunchedEffect(pressing) {
        if (pressing) {
            releaseSlideStartX = null
            activeAlphaAnim.snapTo(0f)
            overlayAlphaAnim.animateTo(
                targetValue = 1f,
                animationSpec = tween(OVERLAY_FADE_IN_DURATION_MS, easing = FastOutSlowInEasing),
            )
        } else {
            if (overlayAlphaAnim.value <= 0f) return@LaunchedEffect
            releaseSlideStartX = centerAnim.value
            if (overlayAlphaAnim.value < 1f) {
                val tailMs =
                    ((1f - overlayAlphaAnim.value) * OVERLAY_FADE_IN_DURATION_MS)
                        .toInt()
                        .coerceAtLeast(0)
                if (tailMs > 0) {
                    overlayAlphaAnim.animateTo(1f, tween(tailMs, easing = FastOutSlowInEasing))
                }
            }
            snapshotFlow {
                !centerAnim.isRunning &&
                    abs(centerAnim.value - activeCenterXState.value) < SETTLE_EPSILON_PX
            }.first { it }
            coroutineScope {
                launch {
                    overlayAlphaAnim.animateTo(
                        targetValue = 0f,
                        animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
                    )
                }
                launch {
                    activeAlphaAnim.animateTo(
                        targetValue = 1f,
                        animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
                    )
                }
            }
            releaseSlideStartX = null
        }
    }

    val releaseSlideProgress =
        run {
            val start = releaseSlideStartX
            if (start == null) {
                0f
            } else {
                val target = activeCenterXState.value
                val total = abs(target - start)
                if (total < 1f) {
                    0f
                } else {
                    (abs(centerAnim.value - start) / total).coerceIn(0f, 1f)
                }
            }
        }
    val overlayPeakProgress = overlayAlphaAnim.value * (1f - releaseSlideProgress)

    return DockOverlayAnimations(
        overlayCenterX = centerAnim.value,
        overlayWidthPx = overlayWidthPx,
        overlayAlpha = overlayAlphaAnim.value,
        overlayPeakProgress = overlayPeakProgress,
        activeIndicatorAlpha = activeAlphaAnim.value,
    )
}

internal suspend fun PointerInputScope.trackDockGesture(onPressEvent: (DockPressEvent) -> Unit) {
    awaitEachGesture {
        val pressDown = awaitFirstDown(requireUnconsumed = false)
        pressDown.consume()
        val pointerId = pressDown.id
        onPressEvent(Pressing(pressDown.position.x))

        while (true) {
            val pointerEvent = awaitPointerEvent()
            val pressChange = pointerEvent.changes.firstOrNull { it.id == pointerId }
            if (pressChange == null) {
                onPressEvent(Cancelled)
                break
            }
            if (!pressChange.pressed) {
                onPressEvent(Released(pressChange.position.x))
                pressChange.consume()
                break
            }
            if (pressChange.positionChanged()) {
                onPressEvent(Pressing(pressChange.position.x))
            }
            pressChange.consume()
        }
    }
}
