package com.ponzischeme89.memby.data

import com.ponzischeme89.memby.data.model.EmbyChapter

/**
 * Where an episode's opening titles sit, in milliseconds from the start of the file.
 *
 * Emby finds these itself and writes them into the item's chapter list as two markers,
 * `IntroStart` and `IntroEnd`, interleaved with the ordinary chapters in playback order.
 * So there is nothing to detect on this end and nothing to store: the answer is already in
 * the library, and reading it is one field on a request that was going to be made anyway.
 */
data class IntroSegment(val startMs: Long, val endMs: Long) {
    /**
     * Whether the playhead is inside the titles, with [lead] of the end held back.
     *
     * The lead is what stops the button appearing for the last half-second of a sequence,
     * where pressing it would be indistinguishable from doing nothing — and, in automatic
     * mode, what stops a seek to a point the film has already reached.
     */
    fun contains(positionMs: Long, lead: Long = 0L): Boolean =
        positionMs >= startMs && positionMs < endMs - lead
}

/**
 * The shortest span worth calling an intro. Emby occasionally writes a pair a couple of
 * seconds apart on a title whose opening it half-recognised, and a button that skips two
 * seconds is worse than no button: somebody presses it, the picture does not visibly move,
 * and the feature reads as broken.
 */
private const val INTRO_MINIMUM_MS = 5_000L

/**
 * The longest. A pair minutes apart is a mis-detection — a recap, a cold open, or two
 * unrelated markers read as a range — and honouring it would throw a viewer past the start
 * of the story.
 */
private const val INTRO_MAXIMUM_MS = 5 * 60 * 1_000L

private const val MARKER_INTRO_START = "IntroStart"
private const val MARKER_INTRO_END = "IntroEnd"

/**
 * Finds the title sequence in an item's chapter list.
 *
 * The rule exists twice — the gateway's copy is `introFromChapters` in
 * `server/internal/api/intro.go` — and the two are pinned by deliberately parallel tests
 * (`IntroTest`, `intro_test.go`). With no gateway there is nobody to ask, and a skip must
 * not land somewhere different depending on whether the container is up.
 *
 * Most of this is about refusing to answer. A pair that is out of order, too short, too
 * long, or missing half of itself produces null, and null is a perfectly good answer: the
 * player simply never offers the button. A wrong skip costs somebody the opening of a
 * scene, which is far worse than not being offered one.
 */
fun introSegmentFrom(chapters: List<EmbyChapter>): IntroSegment? {
    var startMs = -1L
    for (chapter in chapters) {
        when (chapter.markerType) {
            MARKER_INTRO_START ->
                // The first start wins, and a second is ignored rather than replacing it.
                // Two starts mean the markers are already untrustworthy; taking the later
                // one would pick the larger, more damaging skip of the two.
                if (startMs < 0L && chapter.startPositionTicks >= 0L) {
                    startMs = chapter.startPositionTicks / TICKS_PER_MILLISECOND
                }
            MARKER_INTRO_END -> {
                // An end before any start is a stray marker, not the close of a segment.
                if (startMs < 0L) continue
                val endMs = chapter.startPositionTicks / TICKS_PER_MILLISECOND
                val length = endMs - startMs
                if (length < INTRO_MINIMUM_MS || length > INTRO_MAXIMUM_MS) return null
                return IntroSegment(startMs = startMs, endMs = endMs)
            }
        }
    }
    return null
}

private const val TICKS_PER_MILLISECOND = 10_000L
