// Copyright (c) 2026 BigSaltyMan. BSL-1.0; see LICENSE.

///|
fn[T] rect_clip_polygons_compute(
  rect : Bounds64,
  paths : Paths64,
  fill_rule : FillRule,
  limits : ScanLimits,
  extract : (ScanState) -> T raise ScanAbort,
  empty : (CallBudget) -> T raise ScanAbort,
  budget? : CallBudget,
) -> Result[T, ClipError] {
  let budget = match budget {
    Some(value) => value
    None =>
      new_call_budget(
        limits,
        generate_stage=RectClipEvaluate,
        cleanup_stage=RectClipEvaluate,
      )
  }
  let loc = location(RectClipParameters, None, None)
  if rect.min_x < -COORDINATE_LIMIT || rect.min_x > COORDINATE_LIMIT {
    return Err(CoordinateOutOfRange(loc, X, rect.min_x))
  }
  if rect.max_x < -COORDINATE_LIMIT || rect.max_x > COORDINATE_LIMIT {
    return Err(CoordinateOutOfRange(loc, X, rect.max_x))
  }
  if rect.min_y < -COORDINATE_LIMIT || rect.min_y > COORDINATE_LIMIT {
    return Err(CoordinateOutOfRange(loc, Y, rect.min_y))
  }
  if rect.max_y < -COORDINATE_LIMIT || rect.max_y > COORDINATE_LIMIT {
    return Err(CoordinateOutOfRange(loc, Y, rect.max_y))
  }
  if rect.min_x > rect.max_x || rect.min_y > rect.max_y {
    return Err(InvalidInput(loc, "Rectangle minimum exceeds maximum"))
  }
  let work = match
    scan_check_inputs(
      paths,
      [],
      budget.limits,
      spent_work=budget.stats.work,
      raw_stage=RectClipParameters,
    ) {
    Ok(work) => work
    Err(error) => return Err(error)
  }
  try {
    budget_spend(budget, 0, work, loc)
    if paths.is_empty() || rect.min_x == rect.max_x || rect.min_y == rect.max_y {
      // Output-specific empty storage; no window or scan allocation.
      return Ok(empty(budget))
    }
    // Four immutable points + four slots + ring container + operand container/slot.
    // Four corner constructions and two container constructions: 11 nodes / 6 work.
    budget_spend(budget, 11, 6, location(RectClipEvaluate, None, None))
    let window = [
      [
        Point64::new(rect.min_x, rect.min_y),
        Point64::new(rect.max_x, rect.min_y),
        Point64::new(rect.max_x, rect.max_y),
        Point64::new(rect.min_x, rect.max_y),
      ],
    ]
    boolean_compute(
      paths,
      window,
      Intersection,
      budget.limits,
      match fill_rule {
        EvenOdd => Parity
        NonZero => SignedNonZero
      },
      extract,
      budget~,
      internal_input=true,
    )
  } catch {
    ScanAbort(error) => Err(error)
  }
}

///|
fn rect_clip_polygons_execute(
  rect : Bounds64,
  paths : Paths64,
  fill_rule : FillRule,
  limits : ScanLimits,
  budget? : CallBudget,
) -> Result[Paths64, ClipError] {
  rect_clip_polygons_compute(
    rect,
    paths,
    fill_rule,
    limits,
    fn(state) raise ScanAbort {
      output_extract(state, preserve_collinear=false)
    },
    fn(budget) raise ScanAbort {
      // Preserve the flat shortcut's one result container and work unit.
      budget_spend(budget, 1, 1, location(RectClipOutput, None, None))
      []
    },
    budget?,
  )
}

///|
fn rect_clip_polygons_tree_execute(
  rect : Bounds64,
  paths : Paths64,
  fill_rule : FillRule,
  limits : ScanLimits,
  budget? : CallBudget,
) -> Result[PolyTree64, ClipError] {
  rect_clip_polygons_compute(
    rect,
    paths,
    fill_rule,
    limits,
    tree_build,
    tree_empty,
    budget?,
  )
}

///|
/// Since 0.3.0: intersect a closed integer region with an axis-aligned window.
/// Uses Boolean filling and grid rules; only caller paths count toward input limits.
pub fn rect_clip_polygons(
  rect : Bounds64,
  paths : Paths64,
  fill_rule? : FillRule = EvenOdd,
  limits? : Limits = Default::default(),
) -> Result[Paths64, ClipError] {
  rect_clip_polygons_execute(rect, paths, fill_rule, {
    ..Default::default(),
    input: limits,
  })
}

///|
/// Since 0.3.0: hierarchy of the region after window intersection, built in one scan.
/// A hole cut open by the window becomes part of the outer contour.
pub fn rect_clip_polygons_tree(
  rect : Bounds64,
  paths : Paths64,
  fill_rule? : FillRule = EvenOdd,
  limits? : Limits = Default::default(),
) -> Result[PolyTree64, ClipError] {
  rect_clip_polygons_tree_execute(rect, paths, fill_rule, {
    ..Default::default(),
    input: limits,
  })
}