package solutions.tretter.mindmachine.session

import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min

private const val MIN_DURATION_SEC = 60
private const val MAX_DURATION_SEC = 60 * 60
const val TIMELINE_MIN_GRANULARITY_SEC = 30
const val TIMELINE_MAX_GRANULARITY_SEC = 30 * 60
const val TIMELINE_DEFAULT_GRANULARITY_SEC = 60

data class TimelineEditorState(
    val durationSec: Int,
    val curveGranularitySec: Int,
    val flashSequence: List<FlashSequenceRow>,
    val selection: TimelineSelection,
    val viewport: TimelineViewport,
    val activeCurve: ActiveCurve,
    val visualLeft: TimelineCurve,
    val visualRight: TimelineCurve,
    val audioLeft: TimelineCurve,
    val audioRight: TimelineCurve,
    val isPlaying: Boolean,
    val playheadSec: Float,
) {
    enum class ActiveCurve {
        VISUAL_LEFT,
        VISUAL_RIGHT,
        AUDIO_LEFT,
        AUDIO_RIGHT,
    }

    companion object {
        fun default(durationSec: Int = 20 * 60): TimelineEditorState {
            val dur = snapDuration(durationSec)
            val granularitySec = normalizeGranularitySec(TIMELINE_DEFAULT_GRANULARITY_SEC, dur)
            val initial = TimelineCurve.constant(0.5f, dur, granularitySec)
            return TimelineEditorState(
                durationSec = dur,
                curveGranularitySec = granularitySec,
                flashSequence = defaultFlashSequence(),
                selection = TimelineSelection(0, dur),
                viewport = TimelineViewport(
                    startSec = 0f,
                    secondsPerScreen = min(10f * 60f, dur.toFloat())
                ),
                activeCurve = ActiveCurve.VISUAL_LEFT,
                visualLeft = initial,
                visualRight = initial,
                audioLeft = initial,
                audioRight = initial,
                isPlaying = false,
                playheadSec = 0f,
            )
        }

        fun snapDuration(durationSec: Int): Int =
            ((durationSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC) + 30) / 60) * 60

        fun allowedGranularitySteps(durationSec: Int): List<Int> {
            val duration = snapDuration(durationSec)
            val maxStep = min(TIMELINE_MAX_GRANULARITY_SEC, duration)
            return (TIMELINE_MIN_GRANULARITY_SEC..maxStep step TIMELINE_MIN_GRANULARITY_SEC)
                .filter { duration % it == 0 }
                .ifEmpty { listOf(duration) }
        }

        fun normalizeGranularitySec(granularitySec: Int, durationSec: Int): Int {
            val allowed = allowedGranularitySteps(durationSec)
            return allowed.minByOrNull { abs(it - granularitySec) } ?: allowed.first()
        }
    }

    fun curveFor(id: ActiveCurve): TimelineCurve = when (id) {
        ActiveCurve.VISUAL_LEFT -> visualLeft
        ActiveCurve.VISUAL_RIGHT -> visualRight
        ActiveCurve.AUDIO_LEFT -> audioLeft
        ActiveCurve.AUDIO_RIGHT -> audioRight
    }

    fun withCurve(id: ActiveCurve, curve: TimelineCurve): TimelineEditorState {
        val normalized = curve.ensureTimelinePoints(durationSec, curveGranularitySec)
        return when (id) {
            ActiveCurve.VISUAL_LEFT -> copy(visualLeft = normalized)
            ActiveCurve.VISUAL_RIGHT -> copy(visualRight = normalized)
            ActiveCurve.AUDIO_LEFT -> copy(audioLeft = normalized)
            ActiveCurve.AUDIO_RIGHT -> copy(audioRight = normalized)
        }
    }

    fun withFlashSequence(sequence: List<FlashSequenceRow>): TimelineEditorState =
        copy(flashSequence = sequence.ifEmpty { defaultFlashSequence() })

    fun extendDurationTo(atLeastSec: Int): TimelineEditorState {
        val newDur = snapDuration(atLeastSec)
        if (newDur <= durationSec) return this

        val normalizedGranularity = normalizeGranularitySec(curveGranularitySec, newDur)
        return copy(
            durationSec = newDur,
            curveGranularitySec = normalizedGranularity,
            selection = selection.copy(endSec = max(selection.endSec, newDur)),
            visualLeft = visualLeft.extendTo(newDur, normalizedGranularity),
            visualRight = visualRight.extendTo(newDur, normalizedGranularity),
            audioLeft = audioLeft.extendTo(newDur, normalizedGranularity),
            audioRight = audioRight.extendTo(newDur, normalizedGranularity),
        )
    }

    fun setDurationSec(newDurationSec: Int): TimelineEditorState {
        val clamped = snapDuration(newDurationSec)
        if (clamped == durationSec) return this
        val normalizedGranularity = normalizeGranularitySec(curveGranularitySec, clamped)

        return if (clamped > durationSec) {
            extendDurationTo(clamped).copy(
                curveGranularitySec = normalizedGranularity,
                selection = selection.copy(startSec = 0, endSec = clamped),
            )
        } else {
            val newViewport = viewport.copy(startSec = viewport.startSec.coerceAtMost(clamped.toFloat()))
            copy(
                durationSec = clamped,
                curveGranularitySec = normalizedGranularity,
                selection = TimelineSelection(0, clamped),
                visualLeft = visualLeft.trimTo(clamped, normalizedGranularity),
                visualRight = visualRight.trimTo(clamped, normalizedGranularity),
                audioLeft = audioLeft.trimTo(clamped, normalizedGranularity),
                audioRight = audioRight.trimTo(clamped, normalizedGranularity),
                viewport = newViewport,
                playheadSec = playheadSec.coerceIn(0f, clamped.toFloat()),
            )
        }
    }

    fun setCurveGranularitySec(newGranularitySec: Int): TimelineEditorState {
        val normalized = normalizeGranularitySec(newGranularitySec, durationSec)
        if (normalized == curveGranularitySec) return this

        return copy(
            curveGranularitySec = normalized,
            visualLeft = visualLeft.ensureTimelinePoints(durationSec, normalized),
            visualRight = visualRight.ensureTimelinePoints(durationSec, normalized),
            audioLeft = audioLeft.ensureTimelinePoints(durationSec, normalized),
            audioRight = audioRight.ensureTimelinePoints(durationSec, normalized),
        )
    }

    fun clampSelection(): TimelineEditorState {
        val start = selection.startSec.coerceIn(0, durationSec)
        var end = selection.endSec.coerceIn(0, durationSec)
        if (end - start < MIN_DURATION_SEC) end = (start + MIN_DURATION_SEC).coerceAtMost(durationSec)
        val clamped = TimelineSelection(start, end)
        return copy(durationSec = max(durationSec, clamped.endSec), selection = clamped).let {
            it.copy(durationSec = it.durationSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC))
        }
    }
}

data class TimelineSelection(
    val startSec: Int,
    val endSec: Int,
) {
    init {
        require(endSec >= startSec) { "endSec must be >= startSec" }
    }

    val durationSec: Int get() = endSec - startSec
}

data class TimelineViewport(
    val startSec: Float,
    val secondsPerScreen: Float,
) {
    fun applyZoomPan(
        zoomChange: Float,
        panPx: Float,
        widthPx: Float,
        centroidPx: Float,
    ): TimelineViewport {
        val minSecondsPerScreen = 30f
        val maxSecondsPerScreen = 24f * 60f * 60f

        val current = secondsPerScreen
        val newSecondsPerScreen = (current / zoomChange).coerceIn(minSecondsPerScreen, maxSecondsPerScreen)

        val secondsPerPxBefore = current / widthPx
        val secondsPerPxAfter = newSecondsPerScreen / widthPx

        val timeAtCentroidBefore = startSec + centroidPx * secondsPerPxBefore
        val newStart = timeAtCentroidBefore - centroidPx * secondsPerPxAfter

        val panSec = -panPx * secondsPerPxAfter
        return copy(
            startSec = (newStart + panSec).coerceAtLeast(0f),
            secondsPerScreen = newSecondsPerScreen,
        )
    }

    fun timeToX(tSec: Int, widthPx: Float): Float {
        val rel = tSec.toFloat() - startSec
        return (rel / secondsPerScreen) * widthPx
    }

    fun xToTimeSec(xPx: Float, widthPx: Float): Float {
        return startSec + (xPx / widthPx) * secondsPerScreen
    }
}

data class TimelineCurve(
    val points: List<TimelinePoint>
) {
    companion object {
        fun constant(value01: Float, durationSec: Int, granularitySec: Int = TIMELINE_DEFAULT_GRANULARITY_SEC): TimelineCurve {
            val v = value01.coerceIn(0f, 1f)
            val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec, durationSec)
            return TimelineCurve(
                points = (0..durationSec step stepSec).map { TimelinePoint(it, v) }
            )
        }
    }

    fun valueAt(tSec: Float): Float {
        val sorted = points.sortedBy { it.tSec }
        if (sorted.isEmpty()) return 0.5f
        if (sorted.size == 1) return sorted.first().value01
        val t = tSec.coerceAtLeast(0f)
        if (t <= sorted.first().tSec) return sorted.first().value01
        if (t >= sorted.last().tSec) return sorted.last().value01
        val rightIdx = sorted.indexOfFirst { it.tSec.toFloat() >= t }.coerceAtLeast(1)
        val left = sorted[rightIdx - 1]
        val right = sorted[rightIdx]
        val dt = (right.tSec - left.tSec).toFloat().coerceAtLeast(1e-3f)
        val u = ((t - left.tSec) / dt).coerceIn(0f, 1f)
        return (left.value01 + (right.value01 - left.value01) * u).coerceIn(0f, 1f)
    }

    fun ensureTimelinePoints(durationSec: Int, granularitySec: Int): TimelineCurve {
        val snappedDuration = TimelineEditorState.snapDuration(durationSec)
        val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec, snappedDuration)
        val normalized = (0..snappedDuration step stepSec).map { tSec ->
            TimelinePoint(tSec, valueAt(tSec.toFloat()))
        }
        return copy(points = normalized)
    }

    fun trimTo(newDurationSec: Int, granularitySec: Int): TimelineCurve = ensureTimelinePoints(newDurationSec, granularitySec)

    fun movePointVertical(index: Int, newY: Float, heightPx: Float): TimelineCurve {
        if (index !in points.indices) return this
        val v = (1f - (newY / heightPx)).coerceIn(0f, 1f)
        val updated = points.toMutableList()
        updated[index] = updated[index].copy(value01 = v)
        return copy(points = updated)
    }

    fun movePointVerticalWithPropagation(index: Int, newY: Float, heightPx: Float): TimelineCurve {
        if (index !in points.indices) return this
        val v = (1f - (newY / heightPx)).coerceIn(0f, 1f)
        val updated = points.toMutableList()
        for (i in index until updated.size) {
            updated[i] = updated[i].copy(value01 = v)
        }
        return copy(points = updated)
    }

    fun hitTestPoint(offset: androidx.compose.ui.geometry.Offset, viewport: TimelineViewport, widthPx: Float, heightPx: Float): Int? {
        val radiusPx = 36f
        return points.mapIndexedNotNull { idx, p ->
            val x = viewport.timeToX(p.tSec, widthPx)
            if (x < -radiusPx || x > widthPx + radiusPx) return@mapIndexedNotNull null
            val y = (1f - p.value01) * heightPx
            val dx = offset.x - x
            val dy = offset.y - y
            val dist2 = dx * dx + dy * dy
            if (dist2 <= radiusPx * radiusPx) idx to dist2 else null
        }.minByOrNull { it.second }?.first
    }

    fun extendTo(newDurationSec: Int, granularitySec: Int): TimelineCurve = ensureTimelinePoints(newDurationSec, granularitySec)
}

data class TimelinePoint(
    val tSec: Int,
    val value01: Float,
)
