package solutions.tretter.mindmachine.session

import android.view.HapticFeedbackConstants
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.calculateCentroid
import androidx.compose.foundation.gestures.calculatePan
import androidx.compose.foundation.gestures.calculateZoom
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.input.pointer.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import solutions.tretter.mindmachine.session.TimelineEditorState.ActiveCurve
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

@Composable
fun TimelineEditorScreen(
    vm: MainViewModel,
    onBack: () -> Unit,
) {
    BackHandler { onBack() }

    val context = LocalContext.current
    val activity = context as? android.app.Activity

    LaunchedEffect(Unit) {
        if (activity != null) {
            val window = activity.window
            WindowCompat.setDecorFitsSystemWindows(window, false)
            val controller = WindowInsetsControllerCompat(window, window.decorView)
            controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
            controller.hide(WindowInsetsCompat.Type.systemBars())
        }
    }

    var revealSignal by remember { mutableIntStateOf(0) }
    TimelineEditorRoot(
        modifier = Modifier
            .fillMaxSize()
            .background(MaterialTheme.colorScheme.background),
        onRevealSystemBars = { revealSignal += 1 },
        revealSignal = revealSignal,
    ) {
        TimelineEditorContent(onBack = onBack)
    }
}

@Composable
private fun TimelineEditorRoot(
    modifier: Modifier,
    onRevealSystemBars: () -> Unit,
    revealSignal: Int,
    content: @Composable () -> Unit,
) {
    val context = LocalContext.current
    val activity = context as? android.app.Activity
    val window = activity?.window
    val controller = window?.let { remember(it) { WindowInsetsControllerCompat(it, it.decorView) } }

    LaunchedEffect(revealSignal) {
        if (window == null || controller == null) return@LaunchedEffect
        if (revealSignal == 0) return@LaunchedEffect
        controller.show(WindowInsetsCompat.Type.systemBars())
        kotlinx.coroutines.delay(2500)
        controller.hide(WindowInsetsCompat.Type.systemBars())
    }

    Box(
        modifier = modifier.pointerInput(Unit) {
            detectTapGestures(onTap = { onRevealSystemBars() })
        }
    ) {
        content()
    }
}

@Composable
private fun TimelineEditorContent(
    onBack: () -> Unit,
) {
    var state by remember { mutableStateOf(TimelineEditorState.default()) }

    Column(
        Modifier
            .fillMaxSize()
            .padding(12.dp),
        verticalArrangement = Arrangement.spacedBy(10.dp)
    ) {
        Text(
            "Minute-based timeline editor",
            color = MaterialTheme.colorScheme.onBackground,
            style = MaterialTheme.typography.titleLarge,
        )
        Text(
            "Every minute has a control dot. Tap left/right to choose a curve, drag dots up or down, pinch to zoom, and drag empty space to pan.",
            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f),
            style = MaterialTheme.typography.bodySmall,
        )
        TimelineGraph(
            title = "Visual",
            leftLabel = "Flash Interval",
            rightLabel = "Blank Interval",
            curveLeft = state.visualLeft,
            curveRight = state.visualRight,
            activeCurve = state.activeCurve,
            viewport = state.viewport,
            selection = state.selection,
            playheadSec = state.playheadSec,
            onViewportChanged = { state = state.copy(viewport = it) },
            onSelectionChanged = { sel -> state = state.copy(selection = sel).clampSelection() },
            onUpdateCurve = { curveId, newCurve -> state = state.withCurve(curveId, newCurve) },
            onTapSideSelect = { side ->
                state = state.copy(activeCurve = if (side == Side.LEFT) ActiveCurve.VISUAL_LEFT else ActiveCurve.VISUAL_RIGHT)
            }
        )
        TimelineGraph(
            title = "Audio",
            leftLabel = "Carrier Frequency",
            rightLabel = "Binaural Frequency",
            curveLeft = state.audioLeft,
            curveRight = state.audioRight,
            activeCurve = state.activeCurve,
            viewport = state.viewport,
            selection = state.selection,
            playheadSec = state.playheadSec,
            onViewportChanged = { state = state.copy(viewport = it) },
            onSelectionChanged = { sel -> state = state.copy(selection = sel).clampSelection() },
            onUpdateCurve = { curveId, newCurve -> state = state.withCurve(curveId, newCurve) },
            onTapSideSelect = { side ->
                state = state.copy(activeCurve = if (side == Side.LEFT) ActiveCurve.AUDIO_LEFT else ActiveCurve.AUDIO_RIGHT)
            }
        )
        Text(
            "Back to return.",
            color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f),
        )
    }
}

internal enum class Side { LEFT, RIGHT }

internal object TimelineCurveColors {
    val Flash: Color = Color(0xFF00FF00)
    val Blank: Color = Color(0xFFFF0000)
    val Carrier: Color = Color(0xFF0000FF)
    val Binaural: Color = Color(0xFFFFFF00)
}

private enum class HandleDrag { NONE, START, END }

@Composable
internal fun TimelineGraph(
    title: String,
    leftLabel: String,
    rightLabel: String,
    curveLeft: TimelineCurve,
    curveRight: TimelineCurve,
    activeCurve: ActiveCurve,
    viewport: TimelineViewport,
    selection: TimelineSelection,
    playheadSec: Float,
    onViewportChanged: (TimelineViewport) -> Unit,
    onSelectionChanged: (TimelineSelection) -> Unit,
    onUpdateCurve: (ActiveCurve, TimelineCurve) -> Unit,
    onTapSideSelect: (Side) -> Unit,
) {
    val isVisual = title == "Visual"
    val leftId = if (isVisual) ActiveCurve.VISUAL_LEFT else ActiveCurve.AUDIO_LEFT
    val rightId = if (isVisual) ActiveCurve.VISUAL_RIGHT else ActiveCurve.AUDIO_RIGHT

    val selectedLeft = activeCurve == leftId
    val selectedRight = activeCurve == rightId

    val currentViewport by rememberUpdatedState(viewport)
    val currentSelection by rememberUpdatedState(selection)
    val currentCurveLeft by rememberUpdatedState(curveLeft)
    val currentCurveRight by rememberUpdatedState(curveRight)
    val currentOnViewportChanged by rememberUpdatedState(onViewportChanged)
    val currentOnSelectionChanged by rememberUpdatedState(onSelectionChanged)
    val currentOnUpdateCurve by rememberUpdatedState(onUpdateCurve)
    val currentOnTapSideSelect by rememberUpdatedState(onTapSideSelect)

    val context = LocalContext.current
    var dragMode by remember { mutableStateOf(HandleDrag.NONE) }
    var draggingPointIndex by remember { mutableStateOf<Int?>(null) }
    var draggingCurveId by remember { mutableStateOf<ActiveCurve?>(null) }

    val leftColor = when (leftId) {
        ActiveCurve.VISUAL_LEFT -> TimelineCurveColors.Flash
        ActiveCurve.AUDIO_LEFT -> TimelineCurveColors.Carrier
        else -> Color.White
    }
    val rightColor = when (rightId) {
        ActiveCurve.VISUAL_RIGHT -> TimelineCurveColors.Blank
        ActiveCurve.AUDIO_RIGHT -> TimelineCurveColors.Binaural
        else -> Color.White
    }

    Column(
        Modifier
            .fillMaxWidth()
            .height(260.dp)
            .background(MaterialTheme.colorScheme.surface, RoundedCornerShape(12.dp))
            .padding(10.dp)
    ) {
        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically) {
            Text(title, color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium)
            Text(
                "Zoom: ${"%.1f".format(viewport.secondsPerScreen / 60f)} min/screen",
                color = MaterialTheme.colorScheme.onSurface,
                style = MaterialTheme.typography.bodySmall,
            )
        }

        Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
            Text(
                leftLabel,
                color = if (selectedLeft) leftColor else leftColor.copy(alpha = 0.65f),
                modifier = Modifier.clickable { onTapSideSelect(Side.LEFT) }
            )
            Text(
                rightLabel,
                color = if (selectedRight) rightColor else rightColor.copy(alpha = 0.65f),
                modifier = Modifier.clickable { onTapSideSelect(Side.RIGHT) }
            )
        }

        val onCurveEditedHaptic = {
            (context as? android.app.Activity)?.window?.decorView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
        }

        val yLabels = if (isVisual) {
            listOf(
                formatMsLabel(ParameterMapping.logMap01(1f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
                formatMsLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
                formatMsLabel(ParameterMapping.logMap01(0f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
            )
        } else {
            listOf(
                formatHzLabel(ParameterMapping.logMap01(1f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
                formatHzLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
                formatHzLabel(ParameterMapping.logMap01(0f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
            )
        }
        val xStart = formatTimeLabel(viewport.startSec)
        val xMid = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen / 2f)
        val xEnd = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen)

        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Column(
                modifier = Modifier.height(190.dp).width(48.dp),
                verticalArrangement = Arrangement.SpaceBetween,
                horizontalAlignment = Alignment.End,
            ) {
                yLabels.forEach { label ->
                    Text(
                        text = label,
                        color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
                        style = MaterialTheme.typography.labelSmall,
                        textAlign = TextAlign.End,
                        modifier = Modifier.fillMaxWidth(),
                    )
                }
            }

            Box(
                modifier = Modifier
                    .weight(1f)
                    .height(190.dp)
                    .background(Color(0xFF0A0E18), RoundedCornerShape(10.dp))
                    .testTag("timeline_graph")
                    .pointerInput(Unit) {
                        awaitPointerEventScope {
                            while (true) {
                                val down = awaitFirstDown(requireUnconsumed = false)
                                var viewport = currentViewport
                                var selection = currentSelection
                                var curveLeft = currentCurveLeft
                                var curveRight = currentCurveRight
                                val w = size.width.toFloat()
                                val h = size.height.toFloat()
                                val hitPx = 32f

                                var activePointerId = down.id
                                var lastPos = down.position
                                var isTransform = false
                                var moved = false

                                val startX = viewport.timeToX(selection.startSec, w)
                                val endX = viewport.timeToX(selection.endSec, w)
                                val hits = buildList {
                                    curveLeft.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
                                        val point = curveLeft.points[idx]
                                        val x = viewport.timeToX(point.tSec, w)
                                        val y = valueToY(point.value01, h)
                                        val dx = down.position.x - x
                                        val dy = down.position.y - y
                                        add(Triple(leftId, idx, dx * dx + dy * dy))
                                    }
                                    curveRight.hitTestPoint(down.position, viewport, w, h)?.let { idx ->
                                        val point = curveRight.points[idx]
                                        val x = viewport.timeToX(point.tSec, w)
                                        val y = valueToY(point.value01, h)
                                        val dx = down.position.x - x
                                        val dy = down.position.y - y
                                        add(Triple(rightId, idx, dx * dx + dy * dy))
                                    }
                                }

                                val nearestHit = hits.minByOrNull { it.third }
                                if (nearestHit != null) {
                                    draggingCurveId = nearestHit.first
                                    draggingPointIndex = nearestHit.second
                                    dragMode = HandleDrag.NONE
                                    currentOnTapSideSelect(if (nearestHit.first == leftId) Side.LEFT else Side.RIGHT)
                                } else {
                                    draggingPointIndex = null
                                    draggingCurveId = null
                                    dragMode = when {
                                        abs(down.position.x - startX) <= hitPx -> HandleDrag.START
                                        abs(down.position.x - endX) <= hitPx -> HandleDrag.END
                                        else -> HandleDrag.NONE
                                    }
                                }

                                while (true) {
                                    val event = awaitPointerEvent(pass = PointerEventPass.Initial)
                                    val pressed = event.changes.filter { it.pressed }
                                    if (pressed.isEmpty()) break

                                    if (pressed.size >= 2) {
                                        isTransform = true
                                        draggingPointIndex = null
                                        draggingCurveId = null
                                        dragMode = HandleDrag.NONE
                                    } else if (pressed.size == 1 && isTransform) {
                                        isTransform = false
                                        lastPos = pressed.first().position
                                    }

                                    if (isTransform) {
                                        val zoom = event.calculateZoom()
                                        val pan = event.calculatePan()
                                        val centroid = event.calculateCentroid(useCurrent = true)
                                        if (zoom != 1f || pan.x != 0f || pan.y != 0f) {
                                            moved = true
                                            val newViewport = viewport.applyZoomPan(
                                                zoomChange = zoom,
                                                panPx = pan.x,
                                                widthPx = w,
                                                centroidPx = centroid.x,
                                            )
                                            viewport = newViewport
                                            currentOnViewportChanged(newViewport)
                                        }
                                        event.changes.forEach { it.consume() }
                                        continue
                                    }

                                    val primary = pressed.firstOrNull { it.id == activePointerId } ?: pressed.first()
                                    activePointerId = primary.id
                                    val delta = primary.position - lastPos
                                    lastPos = primary.position

                                    if (delta.getDistanceSquared() > 0.5f) moved = true

                                    val pointIndex = draggingPointIndex
                                    val curveId = draggingCurveId
                                    if (pointIndex != null && curveId != null) {
                                        val targetCurve = if (curveId == leftId) curveLeft else curveRight
                                        val constrainedY = constrainYForCurve(
                                            y = primary.position.y,
                                            height = h,
                                            curveId = curveId,
                                        )
                                        val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, constrainedY, h)
                                        if (curveId == leftId) curveLeft = updated else curveRight = updated
                                        currentOnUpdateCurve(curveId, updated)
                                        onCurveEditedHaptic()
                                        primary.consume()
                                        continue
                                    }

                                    val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
                                    when (dragMode) {
                                        HandleDrag.START -> {
                                            val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)
                                            selection = newSelection
                                            currentOnSelectionChanged(newSelection)
                                            primary.consume()
                                        }
                                        HandleDrag.END -> {
                                            val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))
                                            selection = newSelection
                                            currentOnSelectionChanged(newSelection)
                                            primary.consume()
                                        }
                                        HandleDrag.NONE -> {
                                            if (delta.x != 0f) {
                                                val newViewport = viewport.applyZoomPan(
                                                    zoomChange = 1f,
                                                    panPx = delta.x,
                                                    widthPx = w,
                                                    centroidPx = w / 2f,
                                                )
                                                viewport = newViewport
                                                currentOnViewportChanged(newViewport)
                                                primary.consume()
                                            }
                                        }
                                    }
                                }

                                if (!moved && !isTransform) {
                                    val side = if (down.position.x < size.width / 2f) Side.LEFT else Side.RIGHT
                                    currentOnTapSideSelect(side)
                                }
                                draggingPointIndex = null
                                draggingCurveId = null
                                dragMode = HandleDrag.NONE
                            }
                        }
                    }
            ) {
                Canvas(modifier = Modifier.fillMaxSize()) {
                    drawGrid()

                    val startX = viewport.timeToX(selection.startSec, size.width)
                    val endX = viewport.timeToX(selection.endSec, size.width)
                    drawRect(
                        color = Color.White.copy(alpha = 0.06f),
                        topLeft = Offset(min(startX, endX), 0f),
                        size = androidx.compose.ui.geometry.Size(abs(endX - startX), size.height)
                    )
                    drawLine(Color.White.copy(alpha = 0.35f), Offset(startX, 0f), Offset(startX, size.height), strokeWidth = 3f)
                    drawLine(Color.White.copy(alpha = 0.35f), Offset(endX, 0f), Offset(endX, size.height), strokeWidth = 3f)

                    val handleColor = Color.White.copy(alpha = 0.9f)
                    drawCircle(handleColor, radius = 10f, center = Offset(startX, size.height - 14f))
                    drawCircle(handleColor, radius = 10f, center = Offset(endX, size.height - 14f))

                    val playX = viewport.timeToX(playheadSec.toInt(), size.width)
                    drawLine(Color(0xFFFFFFFF).copy(alpha = 0.6f), Offset(playX, 0f), Offset(playX, size.height), strokeWidth = 2f)

                    drawCurve(curveLeft, selected = selectedLeft, color = leftColor, viewport = viewport)
                    drawCurve(curveRight, selected = selectedRight, color = rightColor, viewport = viewport)
                }
            }
        }

        Row(
            modifier = Modifier.fillMaxWidth().padding(top = 4.dp),
            horizontalArrangement = Arrangement.spacedBy(8.dp),
            verticalAlignment = Alignment.CenterVertically,
        ) {
            Spacer(modifier = Modifier.width(48.dp))
            Row(
                modifier = Modifier.weight(1f),
                horizontalArrangement = Arrangement.SpaceBetween,
            ) {
                Text(xStart, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall)
                Text(xMid, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall)
                Text(xEnd, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall)
            }
        }

        Text(
            "Dots follow selected granularity • Drag selected curve dots up/down • Pinch zoom • Drag background pan • Drag handles to set range",
            color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f),
            style = MaterialTheme.typography.bodySmall,
            textAlign = TextAlign.Center,
            modifier = Modifier
                .fillMaxWidth()
                .padding(top = 6.dp)
        )
    }
}

private fun formatTimeLabel(totalSec: Float): String {
    val totalMinutes = (totalSec / 60f).toInt().coerceAtLeast(0)
    return "${totalMinutes} min"
}

private fun formatMsLabel(value: Float): String = "${value.toInt()} ms"

private fun formatHzLabel(value: Float): String = if (value >= 10f) {
    "${value.toInt()} Hz"
} else {
    String.format("%.1f Hz", value)
}

private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawGrid() {
    val stepX = size.width / 8f
    val stepY = size.height / 4f
    val grid = Color.White.copy(alpha = 0.10f)
    for (i in 1..7) {
        drawLine(grid, start = Offset(i * stepX, 0f), end = Offset(i * stepX, size.height))
    }
    for (j in 1..3) {
        drawLine(grid, start = Offset(0f, j * stepY), end = Offset(size.width, j * stepY))
    }
}

private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawCurve(
    curve: TimelineCurve,
    selected: Boolean,
    color: Color,
    viewport: TimelineViewport,
) {
    if (curve.points.isEmpty()) return
    val pts = curve.points.sortedBy { it.tSec }
    val path = Path()
    pts.forEachIndexed { idx, p ->
        val x = viewport.timeToX(p.tSec, size.width)
        val y = valueToY(p.value01, size.height)
        if (idx == 0) path.moveTo(x, y) else path.lineTo(x, y)
    }
    drawPath(
        path,
        color = if (selected) color else color.copy(alpha = 0.6f),
        style = androidx.compose.ui.graphics.drawscope.Stroke(width = if (selected) 5f else 3f)
    )

    pts.forEach { p ->
        val x = viewport.timeToX(p.tSec, size.width)
        if (x < -18f || x > size.width + 18f) return@forEach
        val y = valueToY(p.value01, size.height)
        drawCircle(color = color.copy(alpha = if (selected) 0.24f else 0.14f), radius = if (selected) 16f else 13f, center = Offset(x, y))
        drawCircle(color = color, radius = if (selected) 7.5f else 6f, center = Offset(x, y))
    }
}

private fun valueToY(value01: Float, height: Float): Float {
    val v = value01.coerceIn(0f, 1f)
    return (1f - v) * height
}

private fun constrainYForCurve(y: Float, height: Float, curveId: ActiveCurve): Float {
    val clampedY = y.coerceIn(0f, height)
    val value01 = (1f - (clampedY / height)).coerceIn(0f, 1f)

    return when (curveId) {
        ActiveCurve.AUDIO_LEFT -> {
            val hz = ParameterMapping.logMap01(value01, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)
            val bounded01 = ParameterMapping.invLog01(hz, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)
            valueToY(bounded01, height)
        }
        ActiveCurve.AUDIO_RIGHT -> {
            val hz = ParameterMapping.logMap01(value01, ParameterRanges.BINAURAL_HZ_MIN, ParameterRanges.BINAURAL_HZ_MAX)
            val bounded01 = ParameterMapping.invLog01(hz, ParameterRanges.BINAURAL_HZ_MIN, ParameterRanges.BINAURAL_HZ_MAX)
            valueToY(bounded01, height)
        }
        else -> clampedY
    }
}
