///|
/// A measured point along a path or polyline.
pub(all) struct PathSample {
  distance : Double
  point : Vec2
  tangent : Vec2
} derive(Eq, Debug, ToJson)

///|
/// Sampling controls used by curve flattening and animation paths.
pub(all) struct PathSampleOptions {
  count : Int
  subdivisions : Int
  closed : Bool
} derive(Eq, Debug, ToJson)

///|
/// Creates safe defaults for evenly distributed samples.
pub fn PathSampleOptions::new(count : Int) -> PathSampleOptions {
  { count: if count < 0 { 0 } else { count }, subdivisions: 12, closed: false }
}

///|
/// Sets the number of curve subdivisions used during flattening.
pub fn PathSampleOptions::with_subdivisions(
  self : PathSampleOptions,
  subdivisions : Int,
) -> PathSampleOptions {
  { ..self, subdivisions: if subdivisions < 1 { 1 } else { subdivisions } }
}

///|
/// Requests a closed sampling loop.
pub fn PathSampleOptions::as_closed(
  self : PathSampleOptions,
) -> PathSampleOptions {
  { ..self, closed: true }
}

///|
/// A compact summary of a sampled path.
pub(all) struct PolylineMetrics {
  point_count : Int
  segment_count : Int
  length : Double
  bounds : Rect
  closed : Bool
} derive(Eq, Debug, ToJson)

///|
fn safe_steps(steps : Int) -> Int {
  if steps < 1 {
    1
  } else {
    steps
  }
}

///|
fn distance_or_zero(left : Vec2, right : Vec2) -> Double {
  left.distance(right)
}

///|
/// Returns the length of a polyline, optionally including its closing edge.
pub fn polyline_length(points : Array[Vec2], closed? : Bool = false) -> Double {
  if points.length() < 2 {
    0.0
  } else {
    let mut total = 0.0
    for i in 1.. Array[Double] {
  let distances = []
  if points.length() > 0 {
    distances.push(0.0)
    let mut total = 0.0
    for i in 1.. Vec2 {
  right.sub(left).normalized()
}

///|
/// Finds a point at a distance along an open or closed polyline.
pub fn point_on_polyline(
  points : Array[Vec2],
  distance : Double,
  closed? : Bool = false,
) -> Vec2 {
  if points.length() == 0 {
    Vec2::zero()
  } else if points.length() == 1 {
    points[0]
  } else {
    let total = polyline_length(points, closed~)
    if total <= 0.0 {
      points[0]
    } else {
      let target = if closed {
        let wrapped = distance % total
        if wrapped < 0.0 {
          wrapped + total
        } else {
          wrapped
        }
      } else {
        distance.max(0.0).min(total)
      }
      let limit = if closed { points.length() } else { points.length() - 1 }
      let mut travelled = 0.0
      let mut result = points[0]
      for i in 0..= points.length() { 0 } else { i + 1 }
        let left = points[i]
        let right = points[next_index]
        let segment = distance_or_zero(left, right)
        if segment > 0.0 && target <= travelled + segment {
          let amount = (target - travelled) / segment
          result = left.lerp(right, amount)
          break
        }
        travelled += segment
        result = right
      }
      result
    }
  }
}

///|
/// Finds the unit tangent at a distance along a polyline.
pub fn tangent_on_polyline(
  points : Array[Vec2],
  distance : Double,
  closed? : Bool = false,
) -> Vec2 {
  if points.length() < 2 {
    Vec2::zero()
  } else {
    let total = polyline_length(points, closed~)
    if total <= 0.0 {
      Vec2::zero()
    } else {
      let target = if closed {
        let wrapped = distance % total
        if wrapped < 0.0 {
          wrapped + total
        } else {
          wrapped
        }
      } else {
        distance.max(0.0).min(total)
      }
      let limit = if closed { points.length() } else { points.length() - 1 }
      let mut travelled = 0.0
      let mut result = Vec2::zero()
      for i in 0..= points.length() { 0 } else { i + 1 }
        let left = points[i]
        let right = points[next_index]
        let segment = distance_or_zero(left, right)
        if segment > 0.0 && target <= travelled + segment {
          result = segment_tangent(left, right)
          break
        }
        travelled += segment
        result = segment_tangent(left, right)
      }
      result
    }
  }
}

///|
/// Evenly distributes samples over a polyline by arc length.
pub fn sample_polyline(
  points : Array[Vec2],
  count : Int,
  closed? : Bool = false,
) -> Array[PathSample] {
  let result = []
  if points.length() == 0 || count <= 0 {
    result
  } else if count == 1 {
    result.push({
      distance: 0.0,
      point: points[0],
      tangent: tangent_on_polyline(points, 0.0, closed~),
    })
    result
  } else {
    let total = polyline_length(points, closed~)
    for i in 0.. PolylineMetrics {
  {
    point_count: points.length(),
    segment_count: if points.length() < 2 {
      0
    } else if closed {
      points.length()
    } else {
      points.length() - 1
    },
    length: polyline_length(points, closed~),
    bounds: bounds_of_points(points),
    closed,
  }
}

///|
/// Removes consecutive points closer than the given tolerance.
pub fn deduplicate_polyline(
  points : Array[Vec2],
  tolerance : Double,
) -> Array[Vec2] {
  let result = []
  if points.length() > 0 {
    result.push(points[0])
    let threshold = tolerance.max(0.0)
    for point in points[1:] {
      if point.distance(result[result.length() - 1]) > threshold {
        result.push(point)
      }
    }
  }
  result
}

///|
/// Resamples a polyline while preserving its total arc length.
pub fn resample_polyline(
  points : Array[Vec2],
  count : Int,
  closed? : Bool = false,
) -> Array[Vec2] {
  let result = []
  for sample in sample_polyline(points, count, closed~) {
    result.push(sample.point)
  }
  result
}

///|
/// Simplifies a polyline using a radial-distance pass.
pub fn simplify_sampled_polyline(
  points : Array[Vec2],
  tolerance : Double,
) -> Array[Vec2] {
  let cleaned = deduplicate_polyline(points, tolerance)
  if cleaned.length() < 3 {
    cleaned
  } else {
    let result = [cleaned[0]]
    let threshold = tolerance.max(0.0)
    for i in 1..<(cleaned.length() - 1) {
      let previous = result[result.length() - 1]
      let next = cleaned[i + 1]
      let direct = previous.distance(next)
      let through = previous.distance(cleaned[i]) + cleaned[i].distance(next)
      if through - direct > threshold {
        result.push(cleaned[i])
      }
    }
    result.push(cleaned[cleaned.length() - 1])
    result
  }
}

///|
/// Applies a moving-average filter to a polyline.
pub fn smooth_polyline(
  points : Array[Vec2],
  radius : Int,
  closed? : Bool = false,
) -> Array[Vec2] {
  if points.length() == 0 || radius <= 0 {
    points.copy()
  } else {
    let result = []
    let span = radius * 2 + 1
    for i in 0.. Array[Vec2] {
  let result = []
  let steps = safe_steps(subdivisions)
  let mut current = Vec2::zero()
  let mut start = Vec2::zero()
  for segment in self.segments {
    match segment {
      MoveTo(x, y) => {
        current = Vec2::new(x, y)
        start = current
        result.push(current)
      }
      LineTo(x, y) => {
        current = Vec2::new(x, y)
        result.push(current)
      }
      QuadTo(x1~, y1~, x~, y~) => {
        let p0 = current
        let p1 = Vec2::new(x1, y1)
        let p2 = Vec2::new(x, y)
        for i in 1..<=steps {
          let t = i.to_double() / steps.to_double()
          let u = 1.0 - t
          let point = p0
            .scale(u * u)
            .add(p1.scale(2.0 * u * t))
            .add(p2.scale(t * t))
          result.push(point)
        }
        current = p2
      }
      CubicTo(x1~, y1~, x2~, y2~, x~, y~) => {
        let p0 = current
        let p1 = Vec2::new(x1, y1)
        let p2 = Vec2::new(x2, y2)
        let p3 = Vec2::new(x, y)
        for i in 1..<=steps {
          let t = i.to_double() / steps.to_double()
          let u = 1.0 - t
          let point = p0
            .scale(u * u * u)
            .add(p1.scale(3.0 * u * u * t))
            .add(p2.scale(3.0 * u * t * t))
            .add(p3.scale(t * t * t))
          result.push(point)
        }
        current = p3
      }
      ClosePath => {
        if current.distance(start) > 0.0 {
          result.push(start)
        }
        current = start
      }
    }
  }
  result
}

///|
/// Samples a path by flattening its curves and measuring arc length.
pub fn Path::sample(
  self : Path,
  options : PathSampleOptions,
) -> Array[PathSample] {
  sample_polyline(
    self.flatten(subdivisions=options.subdivisions),
    options.count,
    closed=options.closed,
  )
}

///|
/// Returns a path point and tangent at normalized progress in [0, 1].
pub fn Path::point_at(self : Path, progress : Double) -> PathSample {
  let points = self.flatten(subdivisions=16)
  let total = polyline_length(points)
  let distance = total * progress.max(0.0).min(1.0)
  {
    distance,
    point: point_on_polyline(points, distance),
    tangent: tangent_on_polyline(points, distance),
  }
}

///|
/// Creates a path from measured samples, preserving the sampled tangent as a hint.
pub fn samples_to_path(
  samples : Array[PathSample],
  closed? : Bool = false,
) -> Path {
  let path = Path::new()
  if samples.length() > 0 {
    let _ = path.move_to(samples[0].point.x, samples[0].point.y)
    for sample in samples[1:] {
      let _ = path.line_to(sample.point.x, sample.point.y)
    }
    if closed {
      let _ = path.close()
    }
  }
  path
}

///|
/// Generates a dashed polyline by alternating visible and hidden distances.
pub fn dash_polyline(
  points : Array[Vec2],
  dash : Array[Double],
  closed? : Bool = false,
) -> Array[Array[Vec2]] {
  let result = []
  if points.length() < 2 || dash.length() == 0 {
    result
  } else {
    let total = polyline_length(points, closed~)
    let mut position = 0.0
    let mut pattern_index = 0
    let mut visible = true
    while position < total {
      let segment_length = dash[pattern_index % dash.length()].max(0.000001)
      let end = (position + segment_length).min(total)
      if visible {
        result.push([
          point_on_polyline(points, position, closed~),
          point_on_polyline(points, end, closed~),
        ])
      }
      position = end
      pattern_index += 1
      visible = !visible
    }
    result
  }
}

///|
/// Offsets a sampled polyline along its local normals.
pub fn offset_samples(
  samples : Array[PathSample],
  distance : Double,
) -> Array[Vec2] {
  let result = []
  for sample in samples {
    let normal = Vec2::new(-sample.tangent.y, sample.tangent.x)
    result.push(sample.point.add(normal.scale(distance)))
  }
  result
}