package com.ponzischeme89.memby.ui.player

/**
 * How fast the closing credits run, and what to do when the stream cannot keep up.
 *
 * Pure and unit-tested, apart from the player, for the usual reason the rest of this package
 * splits that way: the arithmetic is the part that can be wrong in a way nobody would notice
 * on a television, and the part a screenshot cannot show.
 *
 * The fallback is the half worth reading. Doubling the speed doubles the bitrate pulled from
 * Emby over HTTP, and a remuxed 4K file on a remote server may simply not sustain it — so
 * the target is a *ceiling that can fall*, never a speed that is set once and defended. It
 * steps down on a stall and never climbs back inside the same credit roll: a stream that
 * could not hold 2× thirty seconds ago is not one to keep testing over somebody's picture,
 * and a speed that oscillated would be worse than either end of it.
 */

/**
 * Where the picture goes while the panel has the right half: a little under half size, shifted
 * left by a little under a quarter of the width.
 *
 * The two are a pair and neither is a round number, which is the point. A clean 0.5 and 0.25
 * put the picture's left edge at exactly x=0 — mathematically the left half, and on a
 * television the first thing overscan cuts. Shrinking it slightly buys an inset at that edge
 * *and* clearance against the panel's own padding, so the gap between the credits and the
 * words about the next episode is deliberate rather than whatever was left over.
 *
 * They live here rather than privately in `PlayerActivity` so `EndCreditsScreenshotTest` can
 * place its stand-in picture at exactly the transform the activity applies; a capture that
 * guessed the split would prove nothing about whether the two halves balance.
 */
const val CREDITS_VIDEO_SCALE = 0.45f
const val CREDITS_VIDEO_SHIFT_X = 0.24f

/** Ordinary speed, and the floor the ceiling falls to. */
const val CREDITS_NORMAL_SPEED = 1.0f

/** What the credits aim for. */
const val CREDITS_TARGET_SPEED = 2.0f

/**
 * The ceilings, in the order they are given up.
 *
 * Three rather than a continuous climbdown: each step has to be held long enough to know
 * whether it worked, and a stream that stalls at 1.5× is telling us to stop rather than to
 * try 1.4×.
 */
val CREDITS_SPEED_STEPS: List<Float> = listOf(2.0f, 1.5f, CREDITS_NORMAL_SPEED)

/**
 * How long the picture takes to reach full speed.
 *
 * Long enough to hear as a ramp rather than a glitch — the music speeding up is most of what
 * tells a viewer the player did this deliberately — and short enough that it is over before
 * anybody reaches for the remote.
 */
const val CREDITS_RAMP_MS = 1_500L

/**
 * How many quantisation steps make up 1× — so the smallest change sent to the player is
 * 0.05×.
 *
 * Every call rebuilds the audio pipeline's resampler, so a ramp asking for 1.8001× after
 * 1.8000× is work for nothing. Quantising also makes the ramp reproducible in a test.
 */
private const val CREDITS_SPEED_STEPS_PER_UNIT = 20f

/**
 * The speed at [elapsedMs] into a ramp towards [ceiling].
 *
 * Eased out rather than linear, the same curve `DecelerateInterpolator` gives every other
 * transition in this package: most of the change happens immediately, so the effect reads as
 * the picture being let go rather than as a slow drift nobody attributes to anything.
 *
 * A ceiling at or below normal speed is answered with normal speed at every point, including
 * zero — a ramp to nowhere must not produce a curve.
 */
fun creditsSpeedAt(elapsedMs: Long, ceiling: Float): Float {
    if (ceiling <= CREDITS_NORMAL_SPEED) return CREDITS_NORMAL_SPEED
    if (elapsedMs <= 0L) return CREDITS_NORMAL_SPEED
    if (elapsedMs >= CREDITS_RAMP_MS) return quantiseSpeed(ceiling)
    val progress = elapsedMs.toFloat() / CREDITS_RAMP_MS
    val eased = 1f - (1f - progress) * (1f - progress)
    return quantiseSpeed(CREDITS_NORMAL_SPEED + (ceiling - CREDITS_NORMAL_SPEED) * eased)
}

/**
 * The next ceiling down after the stream failed to hold [ceiling].
 *
 * Returns [CREDITS_NORMAL_SPEED] once there is nowhere left to fall, which is also the
 * signal to stop trying: the caller treats reaching the floor as the end of the ramp rather
 * than as another step to schedule.
 */
fun creditsCeilingAfterStall(ceiling: Float): Float {
    val next = CREDITS_SPEED_STEPS.firstOrNull { it < ceiling - SPEED_EPSILON }
    return next ?: CREDITS_NORMAL_SPEED
}

/** Whether a ceiling still has any speeding up left in it. */
fun creditsSpeedIsActive(ceiling: Float): Boolean = ceiling > CREDITS_NORMAL_SPEED + SPEED_EPSILON

/**
 * How a speed reads on the chip in the corner of the panel: "2×", "1.5×".
 *
 * Built out of whole tenths rather than by formatting the float. A quantised 1.5 is not
 * exactly 1.5 in binary, so `toString` on it prints "1.5000001", and `String.format` would
 * print a comma for the decimal point on a set configured in half of Europe.
 */
fun creditsSpeedLabel(speed: Float): String {
    val tenths = Math.round(speed * 10f)
    val whole = tenths / 10
    val fraction = tenths % 10
    return if (fraction == 0) "$whole×" else "$whole.$fraction×"
}

/**
 * Rounds to the nearest step. Written as a multiply-round-divide so that the three speeds
 * that matter — 1.0, 1.5, 2.0 — come back exactly, which is what lets a test assert the ramp
 * reaches its ceiling.
 */
private fun quantiseSpeed(speed: Float): Float =
    Math.round(speed * CREDITS_SPEED_STEPS_PER_UNIT) / CREDITS_SPEED_STEPS_PER_UNIT

/** Floats compared by threshold, since every one of these has been through a division. */
private const val SPEED_EPSILON = 0.001f
