// Copyright (c) 2026 BigSaltyMan. BSL-1.0; see LICENSE.
// Closed traversal and budget adaptation reuse the existing periodic corner
// generator and Boolean cleanup; no new upstream geometry formulas are copied.

///|
fn stroke_closed_prepare(
  path : Path64,
  budget : CallBudget,
) -> Path64 raise ScanAbort {
  // Check the original single route before allocating wrappers or copies.
  let loc = location(StrokeGenerate, Some(0), None)
  if check_limits(budget.limits.input, StrokeGenerate, 1) is Some(error) {
    raise ScanAbort(error)
  }
  if path.length() > budget.limits.input.max_input_vertices {
    raise ScanAbort(
      ResourceLimitExceeded(
        location(
          StrokeGenerate,
          Some(0),
          Some(budget.limits.input.max_input_vertices),
        ),
        InputVertices,
        budget.limits.input.max_input_vertices,
      ),
    )
  }
  // Separate charges avoid overflowing a sum of path and vertex counts.
  budget_spend(budget, 0, 2, loc)
  for i = 0; i < path.length(); i = i + 1 {
    let loc = location(StrokeGenerate, Some(0), Some(i))
    budget_spend(budget, 0, 1, loc)
    if check_point(path[i], loc) is Some(error) {
      raise ScanAbort(error)
    }
  }
  // This existing helper only removes consecutive repeats; it charges its
  // independent array, every visit and every retained slot before allocation.
  let prepared = stroke_batch_prepare(path, budget)
  while prepared.length() > 1 {
    offset_spend(budget, 0, 1)
    if prepared[prepared.length() - 1] != prepared[0] {
      break
    }
    ignore(prepared.pop())
  }
  prepared
}

///|
fn[T] stroke_closed_compute(
  path : Path64,
  width : Double,
  options : OffsetOptions,
  limits : ScanLimits,
  extract : (ScanState) -> T raise ScanAbort,
  empty : (CallBudget) -> T raise ScanAbort,
  budget? : CallBudget,
) -> Result[T, ClipError] {
  if width.is_nan() || width.is_inf() || width < 0.0 {
    return Err(
      InvalidInput(
        location(StrokeParameters, None, None),
        "Stroke width must be finite and non-negative",
      ),
    )
  }
  if offset_options_error(options, StrokeParameters) is Some(error) {
    return Err(error)
  }
  let budget = match budget {
    Some(value) => value
    None =>
      new_call_budget(
        limits,
        generate_stage=StrokeGenerate,
        cleanup_stage=StrokeCleanup,
      )
  }
  if budget.limits.input.max_input_paths < 0 ||
    budget.limits.input.max_input_vertices < 0 ||
    budget.limits.max_nodes < 0 ||
    budget.limits.max_work < 0 {
    return Err(
      InvalidInput(
        location(StrokeParameters, None, None),
        "Stroke limits must be non-negative",
      ),
    )
  }
  try {
    let prepared = stroke_closed_prepare(path, budget)
    if width < 1.0 || prepared.length() < 3 {
      return Ok(empty(budget))
    }
    offset_spend(budget, 1, 0)
    let reversed : Path64 = []
    for i = prepared.length() - 1; i >= 0; i = i - 1 {
      offset_spend(budget, 1, 1)
      reversed.push(prepared[i])
    }
    // Array plus two slots. Preserve the directed rings, including the negative
    // inner traversal; forcing both positive would fill the central hole.
    offset_spend(budget, 3, 2)
    let traversals = [prepared, reversed]
    let generated = offset_generate(traversals, width / 2.0, options, budget)
    offset_spend(budget, 1, 0)
    let empty : Paths64 = []
    boolean_compute(
      generated,
      empty,
      Union,
      budget.limits,
      Positive,
      extract,
      budget~,
      internal_input=true,
    )
  } catch {
    ScanAbort(error) => Err(error)
  }
}

///|
fn stroke_closed_execute(
  path : Path64,
  width : Double,
  options : OffsetOptions,
  limits : ScanLimits,
  budget? : CallBudget,
) -> Result[Paths64, ClipError] {
  stroke_closed_compute(
    path,
    width,
    options,
    limits,
    fn(state) raise ScanAbort {
      output_extract(state, preserve_collinear=false)
    },
    fn(budget) raise ScanAbort {
      // Preserve the original flat shortcut: one array, no extra work unit.
      offset_spend(budget, 1, 0)
      []
    },
    budget?,
  )
}

///|
fn stroke_closed_tree_execute(
  path : Path64,
  width : Double,
  options : OffsetOptions,
  limits : ScanLimits,
  budget? : CallBudget,
) -> Result[PolyTree64, ClipError] {
  stroke_closed_compute(
    path,
    width,
    options,
    limits,
    tree_build,
    tree_empty,
    budget?,
  )
}

///|
/// Since 0.4.0: hierarchy of closed-centerline coverage, built during final cleanup.
/// Width is full width; there are joins but no caps. Roots and holes describe coverage.
pub fn stroke_closed_tree(
  path : Path64,
  width : Double,
  options? : OffsetOptions = Default::default(),
  limits? : Limits = Default::default(),
) -> Result[PolyTree64, ClipError] {
  stroke_closed_tree_execute(path, width, options, {
    ..Default::default(),
    input: limits,
  })
}

///|
/// Since 0.4.0: widen one implicitly closed integer centerline, with no end caps.
/// Width is full width; all vertices, including the seam, use the chosen join.
/// Returns cleaned coverage, possibly with holes; does not fill the centerline interior.
pub fn stroke_closed_polyline(
  path : Path64,
  width : Double,
  options? : OffsetOptions = Default::default(),
  limits? : Limits = Default::default(),
) -> Result[Paths64, ClipError] {
  stroke_closed_execute(path, width, options, {
    input: limits,
    max_nodes: 500000,
    max_work: 5000000,
  })
}