///|
/// A layout score used to compare candidate compositions.
pub(all) struct LayoutMetrics {
  occupied_area : Double
  path_length : Double
  element_count : Int
  density : Double
  bounds : Rect
} derive(Eq, Debug, ToJson)

///|
pub fn measure_paths(paths : Array[Path], canvas : Rect) -> LayoutMetrics {
  let mut occupied = 0.0
  let mut length = 0.0
  let mut bounds = Rect::new(0.0, 0.0, 0.0, 0.0)
  let mut first = true
  for path in paths {
    let box_ = path.bounds()
    if first {
      bounds = box_
      first = false
    } else {
      bounds = bounds.union(box_)
    }
    occupied += box_.width * box_.height
    length += path.length()
  }
  let canvas_area = canvas.width * canvas.height
  {
    occupied_area: occupied,
    path_length: length,
    element_count: paths.length(),
    density: if canvas_area == 0.0 {
      0.0
    } else {
      occupied / canvas_area
    },
    bounds,
  }
}

///|
/// Returns an evenly spaced set of anchor positions inside a canvas.
pub fn anchor_grid(
  canvas : Rect,
  columns : Int,
  rows : Int,
  margin : Double,
) -> Array[Vec2] {
  let anchors = []
  if columns > 0 && rows > 0 {
    let inner = canvas.expand(-margin)
    let sx = inner.width / columns.to_double()
    let sy = inner.height / rows.to_double()
    for row in 0.. Array[Rect] {
  let result = []
  let mut cursor_x = canvas.x
  let mut cursor_y = canvas.y
  let mut row_height = 0.0
  for item in sizes {
    let width = item.0
    let height = item.1
    if cursor_x + width > canvas.right() && cursor_x > canvas.x {
      cursor_x = canvas.x
      cursor_y += row_height + gap
      row_height = 0.0
    }
    if cursor_y + height <= canvas.bottom() {
      result.push(Rect::new(cursor_x, cursor_y, width, height))
      cursor_x += width + gap
      if height > row_height {
        row_height = height
      }
    }
  }
  result
}

///|
/// Distributes points on a spiral with a golden-angle increment.
pub fn golden_spiral(
  center : Vec2,
  count : Int,
  radial_step : Double,
  start_angle? : Double = 0.0,
) -> Array[Vec2] {
  let result = []
  let golden_angle = @math.PI * (3.0 - 5.0.sqrt())
  for i in 0.. Double {
  let coverage = if metrics.density > 1.0 { 0.0 } else { metrics.density }
  let center = metrics.bounds.center()
  let canvas_center = canvas.center()
  let max_distance = canvas.center().distance(Vec2::new(canvas.x, canvas.y))
  let balance = if max_distance == 0.0 {
    1.0
  } else {
    1.0 - center.distance(canvas_center) / max_distance
  }
  let complexity = if metrics.element_count == 0 {
    0.0
  } else {
    (metrics.path_length / (canvas.width + canvas.height)).min(1.0)
  }
  (coverage * 0.45 + balance * 0.35 + complexity * 0.2).max(0.0).min(1.0)
}

///|
pub fn normalize_points(points : Array[Vec2], canvas : Rect) -> Array[Vec2] {
  let result = []
  let box_ = bounds_of_points(points)
  if box_.width == 0.0 || box_.height == 0.0 {
    result
  } else {
    let sx = canvas.width / box_.width
    let sy = canvas.height / box_.height
    let scale = if sx < sy { sx } else { sy }
    for point in points {
      result.push(
        Vec2::new(
          canvas.x + (point.x - box_.x) * scale,
          canvas.y + (point.y - box_.y) * scale,
        ),
      )
    }
    result
  }
}