package com.ponzischeme89.memby.ui.player

import kotlin.math.abs

/**
 * The arithmetic and the wording behind Left/Right skipping, kept apart from
 * [PlayerActivity] so it can be unit-tested without a player, a decoder or a server.
 *
 * The shape that matters is [SeekPreview]: a press does not seek. It moves a *target* that
 * the OSD renders, and the seek is committed once the presses stop. Seeking over HTTP
 * costs several requests before a frame is decoded (see "Time to first frame" in
 * CLAUDE.md), so four quick presses of Right must be one seek of two minutes rather than
 * four seeks the viewer waits through in turn — which is also what makes the number on
 * screen honest while they are still pressing.
 */

/**
 * The last second of a title is not a place to land: seeking there ends playback, which on
 * an episode rolls into the next one. Fast-forward stops short of the end instead.
 */
private const val SEEK_END_GUARD_MS: Long = 1_000L

/**
 * Where the next commit will land, and how far that is from where the burst of presses
 * started. [offsetMs] is deliberately cumulative rather than one step: the viewer is
 * reading a running total ("Forward 1 min 30 secs"), not the last button they pressed.
 */
data class SeekPreview(val targetMs: Long, val offsetMs: Long)

/**
 * Adds one step to an in-flight preview, or starts one from the playhead.
 *
 * [previous] is the base when there is one, never the live position: the film keeps
 * playing while somebody holds down Right, and measuring each step from the playhead
 * would quietly swallow part of every press after the first.
 */
fun accumulateSeek(
    previous: SeekPreview?,
    positionMs: Long,
    durationMs: Long,
    stepMs: Long,
    forward: Boolean,
): SeekPreview {
    val origin = previous?.let { it.targetMs - it.offsetMs } ?: positionMs.coerceAtLeast(0L)
    val base = previous?.targetMs ?: positionMs.coerceAtLeast(0L)
    val step = if (forward) stepMs else -stepMs
    val target = clampSeekTarget(base + step, durationMs)
    return SeekPreview(targetMs = target, offsetMs = target - origin)
}

/** Bounds a target to the title: never before the start, never onto its final second. */
fun clampSeekTarget(targetMs: Long, durationMs: Long): Long {
    if (durationMs <= 0L) return targetMs.coerceAtLeast(0L)
    val ceiling = (durationMs - SEEK_END_GUARD_MS).coerceAtLeast(0L)
    return targetMs.coerceIn(0L, ceiling)
}

/**
 * "30 seconds", "1 min 30 secs" — the amount alone, with no sign. Direction is said by the
 * glyph and by the wording around it ("Back …" / "Forward …"), because a minus sign read
 * across a room at the size this is drawn is not a difference anybody can see.
 */
fun seekAmountLabel(offsetMs: Long): String {
    val totalSeconds = (abs(offsetMs) + 500L) / 1_000L
    val minutes = totalSeconds / 60L
    val seconds = totalSeconds % 60L
    return when {
        minutes == 0L -> if (seconds == 1L) "1 second" else "$seconds seconds"
        seconds == 0L -> if (minutes == 1L) "1 min" else "$minutes mins"
        else -> "${if (minutes == 1L) "1 min" else "$minutes mins"} " +
            if (seconds == 1L) "1 sec" else "$seconds secs"
    }
}

/** Clock for the OSD: hours only when the title has them, so a 42-minute episode reads short. */
fun formatSeekClock(positionMs: Long): String {
    val totalSeconds = positionMs.coerceAtLeast(0L) / 1_000L
    val hours = totalSeconds / 3_600L
    val minutes = (totalSeconds % 3_600L) / 60L
    val seconds = totalSeconds % 60L
    return if (hours > 0L) {
        "%d:%02d:%02d".format(hours, minutes, seconds)
    } else {
        "%d:%02d".format(minutes, seconds)
    }
}

/**
 * "12:34 / 1:45:00". Both halves are formatted against the *duration*, so a position under
 * an hour in a two-hour film still reads with the hours field its total has.
 */
fun seekPositionLabel(targetMs: Long, durationMs: Long): String {
    if (durationMs <= 0L) return formatSeekClock(targetMs)
    val position = if (durationMs >= 3_600_000L && targetMs < 3_600_000L) {
        "0:" + formatSeekClock(targetMs).padStart(5, '0')
    } else {
        formatSeekClock(targetMs)
    }
    return "$position / ${formatSeekClock(durationMs)}"
}
