///|
/// Fill rule for a cpdf-style path.
pub(all) enum PdfPathWindingRule {
  PdfPathEvenOdd
  PdfPathNonZero
} derive(Debug, Eq, ToJson)

///|
/// One straight or Bezier path segment.
pub(all) enum PdfPathSegment {
  PdfPathStraight(Point2, Point2)
  PdfPathBezier(Point2, Point2, Point2, Point2)
} derive(Debug, Eq, ToJson)

///|
/// Whether a subpath is a hole in the containing path.
pub(all) enum PdfPathHole {
  PdfPathHole
  PdfPathNotHole
} derive(Debug, Eq, ToJson)

///|
/// Whether a subpath is open or closed.
pub(all) enum PdfPathClosure {
  PdfPathClosed
  PdfPathOpen
} derive(Debug, Eq, ToJson)

///|
/// A cpdf-style subpath.
pub(all) struct PdfPathSubpath {
  hole : PdfPathHole
  closure : PdfPathClosure
  segments : Array[PdfPathSegment]
} derive(Debug, Eq, ToJson)

///|
/// A cpdf-style path.
pub(all) struct PdfPath {
  winding_rule : PdfPathWindingRule
  subpaths : Array[PdfPathSubpath]
} derive(Debug, Eq, ToJson)

///|
/// A cpdfcontent drawn path, pairing path geometry with paint flags.
pub(all) struct PdfDrawnPath {
  stroked : Bool
  filled : Bool
  path : PdfPath
} derive(Debug, Eq, ToJson)

///|
fn pdf_path_point_content_json(point : Point2) -> Json {
  Json::object(
    Map([("x", Json::number(point.x)), ("y", Json::number(point.y))]),
  )
}

///|
/// Return cpdfcontent's JSON shape for one path segment.
pub fn PdfPathSegment::content_json(self : PdfPathSegment) -> Json {
  match self {
    PdfPathStraight(start, finish) =>
      Json::object(
        Map([
          (
            "line",
            Json::array([
              pdf_path_point_content_json(start),
              pdf_path_point_content_json(finish),
            ]),
          ),
        ]),
      )
    PdfPathBezier(start, control1, control2, finish) =>
      Json::object(
        Map([
          (
            "bezier",
            Json::array([
              pdf_path_point_content_json(start),
              pdf_path_point_content_json(control1),
              pdf_path_point_content_json(control2),
              pdf_path_point_content_json(finish),
            ]),
          ),
        ]),
      )
  }
}

///|
/// Compatibility wrapper for `PdfPathSegment::content_json`.
pub fn pdf_path_segment_content_json(segment : PdfPathSegment) -> Json {
  segment.content_json()
}

///|
/// Return cpdfcontent's JSON shape for one subpath.
///
/// The hole flag is intentionally not emitted, matching `cpdfcontent.ml`.
pub fn PdfPathSubpath::content_json(self : PdfPathSubpath) -> Json {
  Json::object(
    Map([
      ("closed", Json::boolean(self.closure == PdfPathClosed)),
      (
        "segments",
        Json::array([ for segment in self.segments => segment.content_json() ]),
      ),
    ]),
  )
}

///|
/// Compatibility wrapper for `PdfPathSubpath::content_json`.
pub fn pdf_path_subpath_content_json(subpath : PdfPathSubpath) -> Json {
  subpath.content_json()
}

///|
fn pdf_path_winding_rule_content_string(rule : PdfPathWindingRule) -> String {
  match rule {
    PdfPathEvenOdd => "even-odd"
    PdfPathNonZero => "non-zero"
  }
}

///|
/// Return cpdfcontent's JSON shape for a drawn path.
pub fn PdfDrawnPath::content_json(self : PdfDrawnPath) -> Json {
  Json::object(
    Map([
      (
        "winding",
        Json::string(
          pdf_path_winding_rule_content_string(self.path.winding_rule),
        ),
      ),
      (
        "subpaths",
        Json::array(
          [
            for subpath in self.path.subpaths => subpath.content_json()
          ],
        ),
      ),
      ("stroked", Json::boolean(self.stroked)),
      ("filled", Json::boolean(self.filled)),
    ]),
  )
}

///|
/// Compatibility wrapper for `PdfDrawnPath::content_json`.
pub fn pdf_drawn_path_content_json(path : PdfDrawnPath) -> Json {
  path.content_json()
}

///|
fn pdf_path_min_double(left : Double, right : Double) -> Double {
  if left < right {
    left
  } else {
    right
  }
}

///|
fn pdf_path_max_double(left : Double, right : Double) -> Double {
  if left > right {
    left
  } else {
    right
  }
}

///|
fn pdf_path_bounds_of_points(
  p0 : Point2,
  p1 : Point2,
  p2 : Point2,
  p3 : Point2,
) -> PdfRectangle {
  {
    min_x: pdf_path_min_double(
      pdf_path_min_double(p0.x, p1.x),
      pdf_path_min_double(p2.x, p3.x),
    ),
    min_y: pdf_path_min_double(
      pdf_path_min_double(p0.y, p1.y),
      pdf_path_min_double(p2.y, p3.y),
    ),
    max_x: pdf_path_max_double(
      pdf_path_max_double(p0.x, p1.x),
      pdf_path_max_double(p2.x, p3.x),
    ),
    max_y: pdf_path_max_double(
      pdf_path_max_double(p0.y, p1.y),
      pdf_path_max_double(p2.y, p3.y),
    ),
  }
}

///|
fn pdf_path_union_bounds(
  left : PdfRectangle,
  right : PdfRectangle,
) -> PdfRectangle {
  {
    min_x: pdf_path_min_double(left.min_x, right.min_x),
    min_y: pdf_path_min_double(left.min_y, right.min_y),
    max_x: pdf_path_max_double(left.max_x, right.max_x),
    max_y: pdf_path_max_double(left.max_y, right.max_y),
  }
}

///|
/// Return cpdfcontent's control-point bounding box for a path segment.
///
/// Bezier bounds intentionally include the two control points rather than
/// solving for curve extrema, matching `cpdfcontent.ml`'s `bbox_of_segment`.
pub fn PdfPathSegment::bounds(self : PdfPathSegment) -> PdfRectangle {
  match self {
    PdfPathStraight(start, finish) =>
      pdf_path_bounds_of_points(start, finish, start, finish)
    PdfPathBezier(start, control1, control2, finish) =>
      pdf_path_bounds_of_points(start, control1, control2, finish)
  }
}

///|
/// Compatibility wrapper for `PdfPathSegment::bounds`.
pub fn pdf_path_segment_bounds(segment : PdfPathSegment) -> PdfRectangle {
  segment.bounds()
}

///|
fn pdf_path_empty_bounds() -> PdfRectangle {
  { min_x: 0.0, min_y: 0.0, max_x: 0.0, max_y: 0.0 }
}

///|
/// Return cpdfcontent's axis-aligned bounding box for a path.
///
/// Empty paths return a zero rectangle, matching `cpdfcontent.ml`.
pub fn PdfPath::bounds(self : PdfPath) -> PdfRectangle {
  let mut found = false
  let mut bounds = pdf_path_empty_bounds()
  for subpath in self.subpaths {
    for segment in subpath.segments {
      let segment_bounds = segment.bounds()
      if found {
        bounds = pdf_path_union_bounds(bounds, segment_bounds)
      } else {
        bounds = segment_bounds
        found = true
      }
    }
  }
  bounds
}

///|
/// Compatibility wrapper for `PdfPath::bounds`.
pub fn pdf_path_bounds(path : PdfPath) -> PdfRectangle {
  path.bounds()
}

///|
fn PdfPath::pdf_path_bounds_optional(self : PdfPath) -> PdfRectangle? {
  let mut found = false
  let mut bounds = pdf_path_empty_bounds()
  for subpath in self.subpaths {
    for segment in subpath.segments {
      let segment_bounds = segment.bounds()
      if found {
        bounds = pdf_path_union_bounds(bounds, segment_bounds)
      } else {
        bounds = segment_bounds
        found = true
      }
    }
  }
  if found {
    Some(bounds)
  } else {
    None
  }
}

///|
/// Transform every point in a path segment.
pub fn PdfPathSegment::transform(
  self : PdfPathSegment,
  matrix : TransformMatrix,
) -> PdfPathSegment {
  match self {
    PdfPathStraight(start, finish) =>
      PdfPathStraight(matrix.apply(start), matrix.apply(finish))
    PdfPathBezier(start, control1, control2, finish) =>
      PdfPathBezier(
        matrix.apply(start),
        matrix.apply(control1),
        matrix.apply(control2),
        matrix.apply(finish),
      )
  }
}

///|
/// Compatibility wrapper for `PdfPathSegment::transform`.
pub fn pdf_path_segment_transform(
  segment : PdfPathSegment,
  matrix : TransformMatrix,
) -> PdfPathSegment {
  segment.transform(matrix)
}

///|
fn pdf_path_subpath_transform(
  subpath : PdfPathSubpath,
  matrix : TransformMatrix,
) -> PdfPathSubpath {
  {
    hole: subpath.hole,
    closure: subpath.closure,
    segments: [
      for segment in subpath.segments => segment.transform(matrix)
    ],
  }
}

///|
/// Transform every point in a path.
pub fn PdfPath::transform(self : PdfPath, matrix : TransformMatrix) -> PdfPath {
  {
    winding_rule: self.winding_rule,
    subpaths: [
      for subpath in self.subpaths => {
        pdf_path_subpath_transform(subpath, matrix)
      }
    ],
  }
}

///|
/// Compatibility wrapper for `PdfPath::transform`.
pub fn pdf_path_transform(path : PdfPath, matrix : TransformMatrix) -> PdfPath {
  path.transform(matrix)
}

///|
fn pdf_path_content_quad(bounds : PdfRectangle) -> PdfContentBoundingBox {
  {
    x0: bounds.min_x,
    y0: bounds.min_y,
    x1: bounds.min_x,
    y1: bounds.max_y,
    x2: bounds.max_x,
    y2: bounds.max_y,
    x3: bounds.max_x,
    y3: bounds.min_y,
  }
}

///|
fn pdf_path_stroke_expand_bounds(
  bounds : PdfRectangle,
  ctm : TransformMatrix,
  line_width : Double,
) -> PdfRectangle {
  let origin = ctm.apply(point2(0.0, 0.0))
  let stroke = ctm.apply(point2(line_width, line_width))
  let dx = (stroke.x - origin.x).abs()
  let dy = (stroke.y - origin.y).abs()
  {
    min_x: bounds.min_x - dx / 2.0,
    min_y: bounds.min_y - dy / 2.0,
    max_x: bounds.max_x + dx / 2.0,
    max_y: bounds.max_y + dy / 2.0,
  }
}

///|
/// Return cpdfcontent's emitted path bounding-box quad.
///
/// The path is first transformed by the current transformation matrix. When
/// `stroking` is true, the transformed line-width vector expands the box by
/// half its absolute x/y displacement, matching `emit_path_bounding_box`.
pub fn PdfPath::content_bounding_box(
  self : PdfPath,
  ctm : TransformMatrix,
  line_width : Double,
  stroking : Bool,
) -> PdfContentBoundingBox? {
  match self.transform(ctm).pdf_path_bounds_optional() {
    Some(bounds) => {
      let bounds = if stroking {
        pdf_path_stroke_expand_bounds(bounds, ctm, line_width)
      } else {
        bounds
      }
      Some(pdf_path_content_quad(bounds))
    }
    None => None
  }
}

///|
/// Compatibility wrapper for `PdfPath::content_bounding_box`.
pub fn pdf_path_content_bounding_box(
  path : PdfPath,
  ctm : TransformMatrix,
  line_width : Double,
  stroking : Bool,
) -> PdfContentBoundingBox? {
  path.content_bounding_box(ctm, line_width, stroking)
}

///|
/// Bezier control-line factor used by cpdf for circular arcs.
pub let pdf_shape_kappa : Double = (2.0.sqrt() - 1.0) / 3.0 * 4.0

///|
/// Source-spelled compatibility alias for cpdfshape's `kappa`.
pub let pdf_kappa : Double = pdf_shape_kappa

///|
fn pdf_shape_mod_float(value : Double, modulus : Double) -> Double {
  value - (value / modulus).floor() * modulus
}

///|
/// Restrict an angle to the closest multiple of `step`, modulo `2 * pi`.
pub fn pdf_restrict_angle(step : Double, angle : Double) -> Double {
  let full = 2.0 * @math.PI
  let lower = pdf_shape_mod_float((angle / step).floor() * step, full)
  let upper = pdf_shape_mod_float(lower + step, full)
  if (lower - angle).abs() < (upper - angle).abs() {
    lower
  } else {
    upper
  }
}

///|
fn pdf_shape_transform_point(
  center_x : Double,
  center_y : Double,
  radius : Double,
  angle : Double,
  point : Point2,
) -> Point2 {
  let x = point.x * radius
  let y = point.y * radius
  point2(
    center_x + x * @math.cos(angle) - y * @math.sin(angle),
    center_y + x * @math.sin(angle) + y * @math.cos(angle),
  )
}

///|
fn pdf_shape_quarter(
  angle : Double,
  center_x : Double,
  center_y : Double,
  radius : Double,
) -> PdfPathSegment {
  PdfPathBezier(
    pdf_shape_transform_point(
      center_x,
      center_y,
      radius,
      angle,
      point2(1.0, 0.0),
    ),
    pdf_shape_transform_point(
      center_x,
      center_y,
      radius,
      angle,
      point2(1.0, pdf_shape_kappa),
    ),
    pdf_shape_transform_point(
      center_x,
      center_y,
      radius,
      angle,
      point2(pdf_shape_kappa, 1.0),
    ),
    pdf_shape_transform_point(
      center_x,
      center_y,
      radius,
      angle,
      point2(0.0, 1.0),
    ),
  )
}

///|
fn pdf_path_segment_end(segment : PdfPathSegment) -> Point2 {
  match segment {
    PdfPathStraight(_, end) => end
    PdfPathBezier(_, _, _, end) => end
  }
}

///|
fn pdf_path_segment_with_start(
  segment : PdfPathSegment,
  start : Point2,
) -> PdfPathSegment {
  match segment {
    PdfPathStraight(_, end) => PdfPathStraight(start, end)
    PdfPathBezier(_, control1, control2, end) =>
      PdfPathBezier(start, control1, control2, end)
  }
}

///|
fn pdf_shape_join_segments(
  segments : ArrayView[PdfPathSegment],
) -> Array[PdfPathSegment] {
  if segments.length() <= 1 {
    segments.to_owned()
  } else {
    let output : Array[PdfPathSegment] = [segments[0]]
    let mut previous_end = pdf_path_segment_end(segments[0])
    for index in 1.. PdfPathSubpath {
  { hole, closure, segments: segments.to_owned() }
}

///|
fn pdf_path(
  winding_rule : PdfPathWindingRule,
  subpaths : ArrayView[PdfPathSubpath],
) -> PdfPath {
  { winding_rule, subpaths: subpaths.to_owned() }
}

///|
/// Build a cpdf-style circular path from four Bezier quarter arcs.
pub fn pdf_shape_circle(
  center_x : Double,
  center_y : Double,
  radius : Double,
) -> PdfPath {
  let segments = pdf_shape_join_segments([
    pdf_shape_quarter(0.0, center_x, center_y, radius),
    pdf_shape_quarter(@math.PI / 2.0, center_x, center_y, radius),
    pdf_shape_quarter(@math.PI, center_x, center_y, radius),
    pdf_shape_quarter(3.0 * @math.PI / 2.0, center_x, center_y, radius),
  ])
  pdf_path(PdfPathNonZero, [
    pdf_path_subpath(PdfPathNotHole, PdfPathClosed, segments),
  ])
}

///|
/// Source-spelled compatibility alias for cpdfshape's `circle`.
pub fn pdf_circle(
  center_x : Double,
  center_y : Double,
  radius : Double,
) -> PdfPath {
  pdf_shape_circle(center_x, center_y, radius)
}

///|
/// Build a cpdf-style rectangle path from a top-left point, width, and height.
pub fn pdf_shape_rectangle(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> PdfPath {
  pdf_path(PdfPathEvenOdd, [
    pdf_path_subpath(PdfPathNotHole, PdfPathClosed, [
      PdfPathStraight(point2(x, y), point2(x + width, y)),
      PdfPathStraight(point2(x + width, y), point2(x + width, y + height)),
      PdfPathStraight(point2(x + width, y + height), point2(x, y + height)),
      PdfPathStraight(point2(x, y + height), point2(x, y)),
    ]),
  ])
}

///|
/// Source-spelled compatibility alias for cpdfshape's `rectangle`.
pub fn pdf_rectangle(
  x : Double,
  y : Double,
  width : Double,
  height : Double,
) -> PdfPath {
  pdf_shape_rectangle(x, y, width, height)
}