///|
/// An exact, unreduced rational timestamp. `denominator` is always positive;
/// callers can compare two values by checked cross multiplication or retain
/// the fraction when a movie tick does not land on an integer media tick.
pub struct Av1RationalTime {
  numerator : Int64
  denominator : Int64
} derive(Eq, Debug)

///|
/// Mapping a movie presentation tick through an explicit MP4 edit list.
/// Empty edits intentionally carry no media time. End is returned after the
/// final edit; an edit list is interpreted as half-open movie intervals.
pub(all) enum Av1PresentationPoint {
  Empty
  Media(Av1RationalTime)
  End
} derive(Eq, Debug)

///|
/// An explicit presentation timeline for one demuxed track. An empty edit
/// list is retained as raw media timing and does not synthesize a default
/// MP4 edit. For an explicit edit list, movie ticks are mapped to media ticks
/// with exact rational arithmetic and unit playback rate.
pub struct Av1PresentationTimeline {
  priv media_timescale : Int64
  priv movie_timescale : Int64?
  priv edits : Array[Av1TrackEdit]
}

///|
let av1_timeline_i64_max : Int64 = 9223372036854775807L

///|
let av1_timeline_i64_min : Int64 = -9223372036854775807L - 1L

///|
fn av1_timeline_error(kind : DecodeErrorKind, message : String) -> DecodeError {
  decode_error(kind, "container.timeline", message)
}

///|
fn av1_timeline_add(a : Int64, b : Int64) -> Result[Int64, DecodeError] {
  if (b > 0L && a > av1_timeline_i64_max - b) ||
    (b < 0L && a < av1_timeline_i64_min - b) {
    return Err(
      av1_timeline_error(
        LimitExceeded,
        "Presentation timeline arithmetic overflows Int64",
      ),
    )
  }
  Ok(a + b)
}

///|
fn av1_timeline_sub(a : Int64, b : Int64) -> Result[Int64, DecodeError] {
  if b == av1_timeline_i64_min {
    return Err(
      av1_timeline_error(
        LimitExceeded,
        "Presentation timeline subtraction overflows Int64",
      ),
    )
  }
  av1_timeline_add(a, -b)
}

///|
fn av1_timeline_mul(a : Int64, b : Int64) -> Result[Int64, DecodeError] {
  if a == 0L || b == 0L {
    return Ok(0L)
  }
  if (a == av1_timeline_i64_min && b == -1L) ||
    (b == av1_timeline_i64_min && a == -1L) {
    return Err(
      av1_timeline_error(
        LimitExceeded,
        "Presentation timeline multiplication overflows Int64",
      ),
    )
  }
  let overflow = if a > 0L {
    if b > 0L {
      a > av1_timeline_i64_max / b
    } else {
      a > av1_timeline_i64_min / b
    }
  } else if b > 0L {
    a < av1_timeline_i64_min / b
  } else {
    a < av1_timeline_i64_max / b
  }
  if overflow {
    Err(
      av1_timeline_error(
        LimitExceeded,
        "Presentation timeline multiplication overflows Int64",
      ),
    )
  } else {
    Ok(a * b)
  }
}

///|
/// Construct the explicit presentation mapping for a demuxed track. The
/// demuxer already validates MP4 edit syntax and currently emits unit-rate
/// edits; this layer validates the timescale relationship and retains each
/// edit, including repeated and empty entries, without rewriting it.
pub fn Av1PresentationTimeline::new(
  track : Av1DemuxedTrack,
) -> Result[Av1PresentationTimeline, DecodeError] {
  if track.timescale <= 0L {
    return Err(
      av1_timeline_error(InvalidData, "Track timescale must be positive"),
    )
  }
  if track.edits.length() > 0 && track.movie_timescale is None {
    return Err(
      av1_timeline_error(
        InvalidData,
        "An explicit edit list requires a positive movie timescale",
      ),
    )
  }
  match track.movie_timescale {
    Some(value) =>
      if value <= 0L {
        return Err(
          av1_timeline_error(InvalidData, "Movie timescale must be positive"),
        )
      }
    None => ()
  }
  for edit in track.edits {
    if edit.duration < 0L || edit.media_time < -1L {
      return Err(
        av1_timeline_error(
          InvalidData,
          "Edit duration or media time is invalid",
        ),
      )
    }
    if edit.media_rate != 65536 {
      return Err(
        av1_timeline_error(
          Unsupported,
          "Only unit-rate edit lists are supported by the timeline",
        ),
      )
    }
  }
  Ok({
    media_timescale: track.timescale,
    movie_timescale: track.movie_timescale,
    edits: track.edits.copy(),
  })
}

///|
pub fn Av1PresentationTimeline::media_timescale(
  self : Av1PresentationTimeline,
) -> Int64 {
  self.media_timescale
}

///|
pub fn Av1PresentationTimeline::movie_timescale(
  self : Av1PresentationTimeline,
) -> Int64? {
  self.movie_timescale
}

///|
/// True when the source supplied an edit list. False means raw media timing
/// is intentionally in force, even if the movie has a movie timescale.
pub fn Av1PresentationTimeline::has_edits(
  self : Av1PresentationTimeline,
) -> Bool {
  !self.edits.is_empty()
}

///|
pub fn Av1PresentationTimeline::edit_count(
  self : Av1PresentationTimeline,
) -> Int {
  self.edits.length()
}

///|
/// Map one movie tick to the exact media timestamp selected by the edit list.
/// For an empty edit the result is `Empty`; after the final edit it is `End`.
/// With no edits, the input is already a raw media tick and is returned as an
/// integer rational, preserving the decoder's default media-timeline policy.
pub fn Av1PresentationTimeline::map_movie_time(
  self : Av1PresentationTimeline,
  movie_time : Int64,
) -> Result[Av1PresentationPoint, DecodeError] {
  if movie_time < 0L {
    return Err(
      av1_timeline_error(InvalidData, "Movie time must not be negative"),
    )
  }
  if self.edits.is_empty() {
    return Ok(Media({ numerator: movie_time, denominator: 1L, }))
  }
  let movie_scale = match self.movie_timescale {
    Some(value) => value
    None =>
      return Err(
        av1_timeline_error(InvalidData, "Edit list has no movie timescale"),
      )
  }
  let mut cursor = 0L
  for edit in self.edits {
    let end = match av1_timeline_add(cursor, edit.duration) {
      Ok(value) => value
      Err(error) => return Err(error)
    }
    if movie_time < end {
      if edit.media_time == -1L {
        return Ok(Empty)
      }
      let offset = match av1_timeline_sub(movie_time, cursor) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let base = match av1_timeline_mul(edit.media_time, movie_scale) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let advance = match av1_timeline_mul(offset, self.media_timescale) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      let numerator = match av1_timeline_add(base, advance) {
        Ok(value) => value
        Err(error) => return Err(error)
      }
      return Ok(Media({ numerator, denominator: movie_scale, }))
    }
    cursor = end
  }
  Ok(End)
}