///|
/// Returns all explicit points in a path, including curve control points.
pub fn Path::control_points(self : Path) -> Array[Vec2] {
  let points = []
  for segment in self.segments {
    match segment {
      MoveTo(x, y) => points.push(Vec2::new(x, y))
      LineTo(x, y) => points.push(Vec2::new(x, y))
      QuadTo(x1~, y1~, x~, y~) => {
        points.push(Vec2::new(x1, y1))
        points.push(Vec2::new(x, y))
      }
      CubicTo(x1~, y1~, x2~, y2~, x~, y~) => {
        points.push(Vec2::new(x1, y1))
        points.push(Vec2::new(x2, y2))
        points.push(Vec2::new(x, y))
      }
      ClosePath => ()
    }
  }
  points
}

///|
pub fn Path::bounds(self : Path) -> Rect {
  bounds_of_points(self.control_points())
}

///|
/// Approximates path length with a configurable number of curve subdivisions.
pub fn Path::length(self : Path, subdivisions? : Int = 12) -> Double {
  let mut total = 0.0
  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
      }
      LineTo(x, y) => {
        let next = Vec2::new(x, y)
        total += current.distance(next)
        current = next
      }
      QuadTo(x1~, y1~, x~, y~) => {
        let p0 = current
        let p1 = Vec2::new(x1, y1)
        let p2 = Vec2::new(x, y)
        let mut previous = p0
        for i in 1..<=subdivisions {
          let t = i.to_double() / subdivisions.to_double()
          let u = 1.0 - t
          let next = p0
            .scale(u * u)
            .add(p1.scale(2.0 * u * t))
            .add(p2.scale(t * t))
          total += previous.distance(next)
          previous = next
        }
        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)
        let mut previous = p0
        for i in 1..<=subdivisions {
          let t = i.to_double() / subdivisions.to_double()
          let u = 1.0 - t
          let next = 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))
          total += previous.distance(next)
          previous = next
        }
        current = p3
      }
      ClosePath => {
        total += current.distance(start)
        current = start
      }
    }
  }
  total
}

///|
/// Subdivides a cubic curve into a polyline, useful for hit testing and sampling.
pub fn cubic_bezier(
  p0 : Vec2,
  p1 : Vec2,
  p2 : Vec2,
  p3 : Vec2,
  steps : Int,
) -> Array[Vec2] {
  let result = []
  if steps > 0 {
    for i in 0..<=steps {
      let t = i.to_double() / steps.to_double()
      let u = 1.0 - t
      result.push(
        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
}

///|
/// Creates a polyline path from points, optionally closing it.
pub fn polyline(points : Array[Vec2], closed? : Bool = false) -> Path {
  let path = Path::new()
  if points.length() > 0 {
    let _ = path.move_to(points[0].x, points[0].y)
    for point in points[1:] {
      let _ = path.line_to(point.x, point.y)
    }
    if closed {
      let _ = path.close()
    }
  }
  path
}