///|
/// GIF motion samples the final measured connector, including bridge curves.
/// Segment subdivision is bounded and independent of a browser clock or DOM.
suberror GifError {
  GifInvalid(String)
} derive(Debug)

///|
pub extend GifError with @debug.Debug::{to_repr}

///|
priv struct GifSegment {
  a : @svg.ConnectorPoint
  b : @svg.ConnectorPoint
  length : Double
}

///|
priv struct GifPath {
  segments : ReadOnlyArray[GifSegment]
  length : Double
  first : @svg.ConnectorPoint
}

///|
fn gif_distance(a : @svg.ConnectorPoint, b : @svg.ConnectorPoint) -> Double {
  let dx = a.x - b.x
  let dy = a.y - b.y
  (dx * dx + dy * dy).sqrt()
}

///|
fn gif_lerp(
  a : @svg.ConnectorPoint,
  b : @svg.ConnectorPoint,
  t : Double,
) -> @svg.ConnectorPoint {
  { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t, }
}

///|
fn gif_curve(
  start : @svg.ConnectorPoint,
  command : @svg.ConnectorCommand,
  t : Double,
) -> @svg.ConnectorPoint {
  match command {
    Move(p) => p
    Line(p) => gif_lerp(start, p, t)
    Quadratic(control, end) =>
      gif_lerp(gif_lerp(start, control, t), gif_lerp(control, end, t), t)
    Cubic(a, b, end) => {
      let ab = gif_lerp(a, b, t)
      gif_lerp(
        gif_lerp(gif_lerp(start, a, t), ab, t),
        gif_lerp(ab, gif_lerp(b, end, t), t),
        t,
      )
    }
  }
}

///|
fn gif_path(
  path : @svg.ConnectorPath,
  max_segments : Int32,
) -> GifPath raise GifError {
  guard max_segments > 0 else {
    raise GifInvalid("Invalid path segment budget")
  }
  let segments : Array[GifSegment] = []
  let mut start : @svg.ConnectorPoint = { x: 0.0, y: 0.0, }
  let mut first = start
  let mut total = 0.0
  let mut seen = false
  for command in path.commands {
    if command is Move(p) {
      start = p
      if !seen {
        first = p
        seen = true
      }
      continue
    }
    fn split(lo : Double, hi : Double, depth : Int32) -> Unit raise GifError {
      let a = gif_curve(start, command, lo)
      let b = gif_curve(start, command, hi)
      let mut error = 0.0
      for fraction in [0.25, 0.5, 0.75] {
        error = error.max(
          gif_distance(
            gif_curve(start, command, lo + (hi - lo) * fraction),
            gif_lerp(a, b, fraction),
          ),
        )
      }
      if error > 0.025 {
        guard depth < 16 else {
          raise GifInvalid("Connector subdivision depth exhausted")
        }
        split(lo, (lo + hi) * 0.5, depth + 1)
        split((lo + hi) * 0.5, hi, depth + 1)
      } else {
        guard segments.length() < max_segments else {
          raise GifInvalid("Connector segment budget exhausted")
        }
        let length = gif_distance(a, b)
        total += length
        segments.push({ a, b, length, })
      }
    }
    split(0.0, 1.0, 0)
    start = gif_curve(start, command, 1.0)
  }
  guard seen else { raise GifInvalid("Connector has no starting point") }
  { segments: ReadOnlyArray::from_array(segments), length: total, first, }
}

///|
fn GifPath::point(self : GifPath, progress : Double) -> @svg.ConnectorPoint {
  if progress <= 0.0 {
    return self.first
  }
  if progress >= 1.0 {
    return self.segments.last().map(s => s.b).unwrap_or(self.first)
  }
  let mut remaining = self.length * progress.max(0.0).min(1.0)
  for segment in self.segments {
    if segment.length > 0.0 && remaining <= segment.length {
      return gif_lerp(segment.a, segment.b, remaining / segment.length)
    }
    remaining -= segment.length
  }
  self.segments.last().map(s => s.b).unwrap_or(self.first)
}