///|
/// A tempo change in an editorial timeline. Beats are represented as a
/// continuous Double so a video frame can land between integer beat markers.
pub struct TempoPoint2 {
  beat : Double
  bpm : Double
} derive(Debug)

///|
pub fn tempo_point2(beat : Double, bpm : Double) -> TempoPoint2 {
  { beat, bpm: bpm.max(0.0001) }
}

///|
pub fn TempoPoint2::beat(self : TempoPoint2) -> Double {
  self.beat
}

///|
pub fn TempoPoint2::bpm(self : TempoPoint2) -> Double {
  self.bpm
}

///|
pub struct TempoTimeline {
  points : Array[TempoPoint2]
  origin_seconds : Double
} derive(Debug)

///|
pub fn TempoTimeline::new(
  points : Array[TempoPoint2],
  origin_seconds? : Double = 0.0,
) -> TempoTimeline {
  let sorted = points.copy()
  sorted.sort_by(fn(left, right) { left.beat.compare(right.beat) })
  { points: sorted, origin_seconds }
}

///|
pub fn TempoTimeline::points(self : TempoTimeline) -> Array[TempoPoint2] {
  self.points.copy()
}

///|
pub fn TempoTimeline::length(self : TempoTimeline) -> Int {
  self.points.length()
}

///|
fn TempoTimeline::at_beat(self : TempoTimeline, beat : Double) -> TempoPoint2 {
  if self.points.length() == 0 {
    return tempo_point2(0.0, 120.0)
  }
  let mut current = self.points[0]
  for point in self.points {
    if point.beat > beat {
      return current
    }
    current = point
  }
  current
}

///|
fn TempoTimeline::seconds_before(self : TempoTimeline, beat : Double) -> Double {
  if self.points.length() == 0 {
    return beat * 0.5
  }
  let mut seconds = 0.0
  let mut previous_beat = self.points[0].beat
  let mut previous_bpm = self.points[0].bpm
  if beat <= previous_beat {
    return (beat - previous_beat) * 60.0 / previous_bpm
  }
  for index in 1.. Double {
  self.origin_seconds + self.seconds_before(beat)
}

///|
pub fn TempoTimeline::seconds_to_beat(
  self : TempoTimeline,
  seconds : Double,
) -> Double {
  if self.points.length() == 0 {
    return seconds * 2.0
  }
  let target = seconds - self.origin_seconds
  let mut accumulated = 0.0
  let mut previous_beat = self.points[0].beat
  let mut previous_bpm = self.points[0].bpm
  if target <= 0.0 {
    return previous_beat + target * previous_bpm / 60.0
  }
  for index in 1.. Double {
  self.at_beat(self.seconds_to_beat(seconds)).bpm
}

///|
pub fn TempoTimeline::quantize_beat(
  _self : TempoTimeline,
  beat : Double,
  subdivision : Int,
) -> Double {
  let grid = subdivision.max(1).to_double()
  let scaled = beat * grid
  let floor_value = @math.floor(scaled)
  let ceil_value = floor_value + 1.0
  let chosen = if scaled - floor_value < ceil_value - scaled {
    floor_value
  } else {
    ceil_value
  }
  chosen / grid
}

///|
pub enum GroovePattern {
  StraightGroove
  SwingGroove(Double)
  TripletGroove
  CustomGroove(Array[Double])
} derive(Debug)

///|
pub fn groove_patterns() -> Array[GroovePattern] {
  [
    StraightGroove,
    SwingGroove(0.5),
    TripletGroove,
    CustomGroove([0.0, 0.33, 0.66]),
  ]
}

///|
pub fn apply_groove(beat : Double, pattern : GroovePattern) -> Double {
  let cell = @math.floor(beat)
  let fraction = beat - cell
  match pattern {
    StraightGroove => beat
    SwingGroove(amount) => {
      let swing = clamp(amount, 0.5, 0.75)
      if fraction < 0.5 {
        cell + fraction * 2.0 * swing
      } else {
        cell + swing + (fraction - 0.5) * 2.0 * (1.0 - swing)
      }
    }
    TripletGroove => cell + fraction * 1.5
    CustomGroove(offsets) =>
      if offsets.length() == 0 {
        beat
      } else {
        let position = fraction * offsets.length().to_double()
        let index = @math.floor(position).to_int().min(offsets.length() - 1)
        cell +
        offsets[index] +
        (position - index.to_double()) / offsets.length().to_double()
      }
  }
}

///|
pub struct TimeSignature2 {
  numerator : Int
  denominator : Int
} derive(Debug)

///|
pub fn time_signature2(numerator : Int, denominator : Int) -> TimeSignature2 {
  { numerator: numerator.max(1), denominator: denominator.max(1) }
}

///|
pub fn TimeSignature2::numerator(self : TimeSignature2) -> Int {
  self.numerator
}

///|
pub fn TimeSignature2::denominator(self : TimeSignature2) -> Int {
  self.denominator
}

///|
pub fn TimeSignature2::bar_beats(self : TimeSignature2) -> Double {
  self.numerator.to_double() * 4.0 / self.denominator.to_double()
}

///|
pub fn TimeSignature2::beat_in_bar(
  self : TimeSignature2,
  beat : Double,
) -> Double {
  let bar = @math.floor(beat / self.bar_beats())
  beat - bar * self.bar_beats()
}

///|
pub struct BeatCue {
  beat : Double
  label : String
  duration : Double
  strength : Double
} derive(Debug)

///|
pub fn beat_cue(
  beat : Double,
  label : String,
  duration? : Double = 0.0,
  strength? : Double = 1.0,
) -> BeatCue {
  { beat, label, duration: duration.max(0.0), strength: clamp01(strength) }
}

///|
pub fn BeatCue::beat(self : BeatCue) -> Double {
  self.beat
}

///|
pub fn BeatCue::label(self : BeatCue) -> String {
  self.label
}

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

///|
pub fn BeatCue::strength(self : BeatCue) -> Double {
  self.strength
}

///|
pub fn BeatCue::seconds(self : BeatCue, tempo : TempoTimeline) -> Double {
  tempo.beat_to_seconds(self.beat)
}

///|
pub struct BeatGrid {
  tempo : TempoTimeline
  signature : TimeSignature2
  groove : GroovePattern
} derive(Debug)

///|
pub fn beat_grid(tempo : TempoTimeline, signature : TimeSignature2) -> BeatGrid {
  { tempo, signature, groove: StraightGroove }
}

///|
pub fn BeatGrid::tempo(self : BeatGrid) -> TempoTimeline {
  self.tempo
}

///|
pub fn BeatGrid::signature(self : BeatGrid) -> TimeSignature2 {
  self.signature
}

///|
pub fn BeatGrid::groove(self : BeatGrid) -> GroovePattern {
  self.groove
}

///|
pub fn BeatGrid::with_groove(
  self : BeatGrid,
  groove : GroovePattern,
) -> BeatGrid {
  { tempo: self.tempo, signature: self.signature, groove }
}

///|
pub fn BeatGrid::beat_to_seconds(self : BeatGrid, beat : Double) -> Double {
  self.tempo.beat_to_seconds(apply_groove(beat, self.groove))
}

///|
pub fn BeatGrid::seconds_to_beat(self : BeatGrid, seconds : Double) -> Double {
  self.tempo.seconds_to_beat(seconds)
}

///|
pub fn BeatGrid::bar_start(self : BeatGrid, bar : Int) -> Double {
  bar.to_double() * self.signature.bar_beats()
}

///|
pub fn BeatGrid::bar_index(self : BeatGrid, beat : Double) -> Int {
  @math.floor(beat / self.signature.bar_beats()).to_int()
}

///|
pub fn BeatGrid::quantize(
  self : BeatGrid,
  beat : Double,
  subdivision : Int,
) -> Double {
  self.tempo.quantize_beat(beat, subdivision)
}

///|
pub struct CueTimeline {
  cues : Array[BeatCue]
  grid : BeatGrid
} derive(Debug)

///|
pub fn CueTimeline::new(grid : BeatGrid) -> CueTimeline {
  { cues: [], grid }
}

///|
pub fn CueTimeline::add(self : CueTimeline, cue : BeatCue) -> Unit {
  self.cues.push(cue)
  self.cues.sort_by(fn(left, right) { left.beat.compare(right.beat) })
}

///|
pub fn CueTimeline::length(self : CueTimeline) -> Int {
  self.cues.length()
}

///|
pub fn CueTimeline::cues(self : CueTimeline) -> Array[BeatCue] {
  self.cues.copy()
}

///|
pub fn CueTimeline::grid(self : CueTimeline) -> BeatGrid {
  self.grid
}

///|
pub fn CueTimeline::at_seconds(
  self : CueTimeline,
  seconds : Double,
) -> Array[BeatCue] {
  let result : Array[BeatCue] = []
  let beat = self.grid.seconds_to_beat(seconds)
  for cue in self.cues {
    if cue.beat <= beat && beat <= cue.beat + cue.duration {
      result.push(cue)
    }
  }
  result
}

///|
pub fn CueTimeline::next_after(
  self : CueTimeline,
  seconds : Double,
) -> BeatCue? {
  for cue in self.cues {
    if cue.seconds(self.grid.tempo) > seconds {
      return Some(cue)
    }
  }
  None
}

///|
pub fn CueTimeline::window(
  self : CueTimeline,
  start : Double,
  end : Double,
) -> Array[BeatCue] {
  let result : Array[BeatCue] = []
  let first = self.grid.seconds_to_beat(start)
  let last = self.grid.seconds_to_beat(end)
  for cue in self.cues {
    if cue.beat + cue.duration >= first && cue.beat <= last {
      result.push(cue)
    }
  }
  result
}