// Measurable path operations for inspectors, snapping, and export tooling.

///|
pub(all) struct PathMeasurement {
  length : Double
  area : Double
  centroid : @math.Vec2
  bounds : @math.Rect2D
  subpath_count : Int
  point_count : Int
} derive(Eq, Debug)

///|
fn polyline_length(points : Array[@math.Vec2]) -> Double {
  let mut result = 0.0
  for i = 1; i < points.length(); i = i + 1 {
    result = result + points[i - 1].distance(points[i])
  }
  result
}

///|
fn polyline_signed_area(points : Array[@math.Vec2]) -> Double {
  if points.length() < 3 {
    0.0
  } else {
    @math.signed_area(points)
  }
}

///|
fn polyline_centroid(points : Array[@math.Vec2], area : Double) -> @math.Vec2 {
  if points.length() < 3 || area.abs() < 0.000000000001 {
    if points.is_empty() {
      @math.Vec2::zero()
    } else {
      let mut sum = @math.Vec2::zero()
      for i = 0; i < points.length(); i = i + 1 {
        sum = sum.add(points[i])
      }
      sum.scale(1.0 / points.length().to_double())
    }
  } else {
    let mut cx = 0.0
    let mut cy = 0.0
    for i = 0; i < points.length(); i = i + 1 {
      let current = points[i]
      let next = points[(i + 1) % points.length()]
      let cross = current.x * next.y - next.x * current.y
      cx = cx + (current.x + next.x) * cross
      cy = cy + (current.y + next.y) * cross
    }
    @math.Vec2::new(cx / (6.0 * area), cy / (6.0 * area))
  }
}

///|
pub fn measure_path(
  path : Path,
  tolerance : Double,
  max_segments : Int,
) -> PathMeasurement {
  let subpaths = flatten_path(path, tolerance, max_segments)
  let mut length = 0.0
  let mut area = 0.0
  let mut weighted_centroid = @math.Vec2::zero()
  let mut bounds = @math.Rect2D::empty()
  let mut point_count = 0
  for i = 0; i < subpaths.length(); i = i + 1 {
    let points = subpaths[i]
    let sub_area = polyline_signed_area(points)
    length = length + polyline_length(points)
    area = area + sub_area
    weighted_centroid = weighted_centroid.add(
      polyline_centroid(points, sub_area).scale(sub_area),
    )
    point_count = point_count + points.length()
    for j = 0; j < points.length(); j = j + 1 {
      bounds = bounds.union(@math.Rect2D::from_point(points[j]))
    }
  }
  let centroid = if area.abs() < 0.000000000001 {
    if point_count == 0 {
      @math.Vec2::zero()
    } else {
      let mut sum = @math.Vec2::zero()
      for i = 0; i < subpaths.length(); i = i + 1 {
        for j = 0; j < subpaths[i].length(); j = j + 1 {
          sum = sum.add(subpaths[i][j])
        }
      }
      sum.scale(1.0 / point_count.to_double())
    }
  } else {
    weighted_centroid.scale(1.0 / area)
  }
  {
    length,
    area,
    centroid,
    bounds,
    subpath_count: subpaths.length(),
    point_count,
  }
}

///|
pub fn ShapeGeometry::measure(
  self : ShapeGeometry,
  tolerance : Double,
  max_segments : Int,
) -> PathMeasurement {
  measure_path(self.to_path(), tolerance, max_segments)
}

///|
fn locate_on_polyline(
  points : Array[@math.Vec2],
  target : Double,
) -> (@math.Vec2, @math.Vec2, Double) {
  if points.is_empty() {
    (@math.Vec2::zero(), @math.Vec2::unit_x(), 0.0)
  } else if points.length() == 1 || target <= 0.0 {
    let direction = if points.length() > 1 {
      points[1].sub(points[0]).normalize()
    } else {
      @math.Vec2::unit_x()
    }
    (points[0], direction, 0.0)
  } else {
    let mut distance_before = 0.0
    for i = 1; i < points.length(); i = i + 1 {
      let start = points[i - 1]
      let end = points[i]
      let segment = start.distance(end)
      if target <= distance_before + segment {
        let t = if segment <= 0.000000000001 {
          0.0
        } else {
          (target - distance_before) / segment
        }
        return (
          start.lerp(end, @math.clamp_double(t, 0.0, 1.0)),
          end.sub(start).normalize(),
          distance_before,
        )
      }
      distance_before = distance_before + segment
    }
    let last = points[points.length() - 1]
    let previous = if points.length() > 1 {
      points[points.length() - 2]
    } else {
      last
    }
    (last, last.sub(previous).normalize(), distance_before)
  }
}

///|
fn locate_on_path(
  path : Path,
  target : Double,
  tolerance : Double,
  max_segments : Int,
) -> (@math.Vec2, @math.Vec2) {
  let subpaths = flatten_path(path, tolerance, max_segments)
  let mut remaining = if target < 0.0 { 0.0 } else { target }
  for i = 0; i < subpaths.length(); i = i + 1 {
    let points = subpaths[i]
    let length = polyline_length(points)
    if remaining <= length || i == subpaths.length() - 1 {
      let located = locate_on_polyline(points, remaining)
      return (located.0, located.1)
    }
    remaining = remaining - length
  }
  (@math.Vec2::zero(), @math.Vec2::unit_x())
}

///|
pub fn point_at_length(
  path : Path,
  distance : Double,
  tolerance : Double,
  max_segments : Int,
) -> @math.Vec2 {
  locate_on_path(path, distance, tolerance, max_segments).0
}

///|
pub fn tangent_at_length(
  path : Path,
  distance : Double,
  tolerance : Double,
  max_segments : Int,
) -> @math.Vec2 {
  locate_on_path(path, distance, tolerance, max_segments).1
}

///|
pub fn path_is_closed(path : Path, tolerance : Double) -> Bool {
  let subpaths = flatten_path(path, tolerance, 64)
  if subpaths.is_empty() {
    false
  } else {
    let first = subpaths[0]
    first.length() > 2 &&
    first[0].distance(first[first.length() - 1]) <= tolerance.abs()
  }
}

///|
pub fn resample_path(
  path : Path,
  spacing : Double,
  tolerance : Double,
  max_segments : Int,
) -> Array[@math.Vec2] {
  let measurement = measure_path(path, tolerance, max_segments)
  let step = if spacing <= 0.000000000001 { 1.0 } else { spacing }
  let result : Array[@math.Vec2] = []
  let mut distance = 0.0
  while distance <= measurement.length {
    result.push(point_at_length(path, distance, tolerance, max_segments))
    distance = distance + step
  }
  if result.is_empty() ||
    result[result.length() - 1].distance(
      point_at_length(path, measurement.length, tolerance, max_segments),
    ) >
    tolerance.abs() {
    result.push(
      point_at_length(path, measurement.length, tolerance, max_segments),
    )
  }
  result
}

///|
fn simplify_polyline_recursive(
  points : Array[@math.Vec2],
  start : Int,
  end : Int,
  tolerance_sq : Double,
  kept : Map[Int, Bool],
) -> Unit {
  if end <= start + 1 {
    return
  }
  let mut best_distance = tolerance_sq
  let mut best_index = -1
  for i = start + 1; i < end; i = i + 1 {
    let distance = @math.distance_squared_to_segment(
      points[i],
      points[start],
      points[end],
    )
    if distance > best_distance {
      best_distance = distance
      best_index = i
    }
  }
  if best_index >= 0 {
    kept[best_index] = true
    simplify_polyline_recursive(points, start, best_index, tolerance_sq, kept)
    simplify_polyline_recursive(points, best_index, end, tolerance_sq, kept)
  }
}

///|
pub fn simplify_polyline(
  points : Array[@math.Vec2],
  tolerance : Double,
) -> Array[@math.Vec2] {
  if points.length() < 3 {
    points.copy()
  } else {
    let kept : Map[Int, Bool] = Map([])
    kept[0] = true
    kept[points.length() - 1] = true
    simplify_polyline_recursive(
      points,
      0,
      points.length() - 1,
      tolerance.abs() * tolerance.abs(),
      kept,
    )
    let result : Array[@math.Vec2] = []
    for i = 0; i < points.length(); i = i + 1 {
      match kept.get(i) {
        Some(_) => result.push(points[i])
        None => ()
      }
    }
    result
  }
}