///|
/// Export-time calculations for frame budgets, timecode, and deterministic
/// chunking. These helpers keep host-specific file writing outside the core.
pub struct FrameBudget2 {
  frame_count : Int
  frame_bytes : Int
  total_bytes : Int
  duration : Double
  throughput : Double
} derive(Debug)

///|
pub fn frame_budget2(
  schedule : FrameSchedule2,
  bytes_per_frame : Int,
  throughput : Double,
) -> FrameBudget2 {
  let frames = schedule.frame_count()
  {
    frame_count: frames,
    frame_bytes: bytes_per_frame.max(0),
    total_bytes: frames * bytes_per_frame.max(0),
    duration: schedule.duration(),
    throughput: throughput.max(0.0),
  }
}

///|
pub fn FrameBudget2::frame_count(self : FrameBudget2) -> Int {
  self.frame_count
}

///|
pub fn FrameBudget2::frame_bytes(self : FrameBudget2) -> Int {
  self.frame_bytes
}

///|
pub fn FrameBudget2::total_bytes(self : FrameBudget2) -> Int {
  self.total_bytes
}

///|
pub fn FrameBudget2::duration(self : FrameBudget2) -> Double {
  self.duration
}

///|
pub fn FrameBudget2::throughput(self : FrameBudget2) -> Double {
  self.throughput
}

///|
pub fn FrameBudget2::estimated_seconds(self : FrameBudget2) -> Double {
  if self.throughput == 0.0 {
    0.0
  } else {
    self.total_bytes.to_double() / self.throughput
  }
}

///|
pub fn FrameBudget2::estimated_megabytes(self : FrameBudget2) -> Double {
  self.total_bytes.to_double() / 1000000.0
}

///|
pub struct Timecode2 {
  hours : Int
  minutes : Int
  seconds : Int
  frames : Int
  frame_rate : Double
} derive(Debug)

///|
pub fn timecode2(seconds : Double, frame_rate : Double) -> Timecode2 {
  let rate = frame_rate.max(0.001)
  let total_frames = @math.floor(seconds.max(0.0) * rate + 0.5).to_int()
  let frames_per_hour = (rate * 3600.0).to_int().max(1)
  let frames_per_minute = (rate * 60.0).to_int().max(1)
  let hours = total_frames / frames_per_hour
  let minutes = total_frames % frames_per_hour / frames_per_minute
  let whole_seconds = total_frames % frames_per_minute / rate.to_int().max(1)
  let frames = total_frames % rate.to_int().max(1)
  { hours, minutes, seconds: whole_seconds, frames, frame_rate: rate }
}

///|
pub fn Timecode2::hours(self : Timecode2) -> Int {
  self.hours
}

///|
pub fn Timecode2::minutes(self : Timecode2) -> Int {
  self.minutes
}

///|
pub fn Timecode2::seconds(self : Timecode2) -> Int {
  self.seconds
}

///|
pub fn Timecode2::frames(self : Timecode2) -> Int {
  self.frames
}

///|
pub fn Timecode2::frame_rate(self : Timecode2) -> Double {
  self.frame_rate
}

///|
pub fn Timecode2::to_seconds(self : Timecode2) -> Double {
  self.hours.to_double() * 3600.0 +
  self.minutes.to_double() * 60.0 +
  self.seconds.to_double() +
  self.frames.to_double() / self.frame_rate
}

///|
pub fn Timecode2::frame_index(self : Timecode2) -> Int {
  @math.floor(self.to_seconds() * self.frame_rate + 0.5).to_int()
}

///|
pub struct ExportChunk2 {
  start_frame : Int
  end_frame : Int
  start_time : Double
  end_time : Double
  bytes : Int
} derive(Debug)

///|
pub fn ExportChunk2::start_frame(self : ExportChunk2) -> Int {
  self.start_frame
}

///|
pub fn ExportChunk2::end_frame(self : ExportChunk2) -> Int {
  self.end_frame
}

///|
pub fn ExportChunk2::start_time(self : ExportChunk2) -> Double {
  self.start_time
}

///|
pub fn ExportChunk2::end_time(self : ExportChunk2) -> Double {
  self.end_time
}

///|
pub fn ExportChunk2::bytes(self : ExportChunk2) -> Int {
  self.bytes
}

///|
pub fn split_export_chunks(
  schedule : FrameSchedule2,
  chunk_size : Int,
  bytes_per_frame : Int,
) -> Array[ExportChunk2] {
  let result : Array[ExportChunk2] = []
  let size = chunk_size.max(1)
  let total = schedule.frame_count()
  let mut start = 0
  while start < total {
    let end = (start + size).min(total)
    result.push({
      start_frame: start,
      end_frame: end - 1,
      start_time: schedule.time_at(start),
      end_time: schedule.time_at(end - 1),
      bytes: (end - start) * bytes_per_frame.max(0),
    })
    start = end
  }
  result
}

///|
pub fn export_chunk_count(schedule : FrameSchedule2, chunk_size : Int) -> Int {
  let size = chunk_size.max(1)
  (schedule.frame_count() + size - 1) / size
}

///|
pub fn export_frame_index(schedule : FrameSchedule2, time : Double) -> Int {
  let minimum = schedule.index_at(schedule.start())
  let maximum = schedule.index_at(schedule.end())
  schedule.index_at(time).max(minimum).min(maximum)
}

///|
pub fn export_time_for_frame(schedule : FrameSchedule2, frame : Int) -> Double {
  schedule.time_at((frame - schedule.index_at(schedule.start())).max(0))
}

///|
pub fn export_duration_for_frames(
  frame_count : Int,
  frame_rate : Double,
) -> Double {
  if frame_count <= 1 {
    0.0
  } else {
    (frame_count - 1).to_double() / frame_rate.max(0.001)
  }
}

///|
pub fn estimate_compressed_bytes(
  frame_count : Int,
  width : Int,
  height : Int,
  bits_per_pixel : Double,
  compression : Double,
) -> Int {
  let raw_bits = frame_count.max(0).to_double() *
    width.max(1).to_double() *
    height.max(1).to_double() *
    bits_per_pixel.max(0.0)
  (raw_bits / 8.0 * compression.max(0.000001)).to_int()
}

///|
pub fn validate_export_dimensions(
  width : Int,
  height : Int,
  maximum : Int,
) -> Bool {
  width > 0 && height > 0 && width <= maximum && height <= maximum
}

///|
pub fn aspect_ratio2(width : Int, height : Int) -> Double {
  if height <= 0 {
    0.0
  } else {
    width.to_double() / height.to_double()
  }
}

///|
pub fn fit_dimensions2(width : Int, height : Int, maximum : Int) -> (Int, Int) {
  let scale = maximum.to_double() / width.max(height).to_double()
  (
    (width.to_double() * scale).to_int().max(1),
    (height.to_double() * scale).to_int().max(1),
  )
}