///|
/// Exact authored or deterministically computed path. The limit is checked
/// before appending each segment; no partially returned path escapes.
pub fn bpmn_geometry_path(
  edge : @bpmn_layout.Connection,
  max_units~ : Int32 = 16777216,
) -> String? {
  if max_units <= 0 {
    return None
  }
  let result = StringBuilder()
  let mut units = 0
  for i, p in edge.points {
    let prefix = if i == 0 { "M" } else { " L" }
    for part in [prefix, p.x.token, " ", p.y.token] {
      if part.length() > max_units - units {
        return None
      }
      units += part.length()
      result.write_string(part)
    }
  }
  Some(result.to_string())
}

///|
fn BpmnPaint::definitions(self : BpmnPaint) -> Array[@writer.Node] {
  let stroke = self.paint.color("arrow")
  let bg = self.paint.color("bg")
  fn marker(
    id : String,
    parts : ArrayView[@writer.Node],
    orient : String,
  ) -> @writer.Node {
    self.element(
      "marker",
      attrs=[
        ("id", id),
        ("viewBox", "-12 -7 28 14"),
        ("refX", "0"),
        ("refY", "0"),
        ("markerWidth", "28"),
        ("markerHeight", "14"),
        ("markerUnits", "userSpaceOnUse"),
        ("orient", orient),
      ],
      children=parts,
    )
  }
  [
    marker(
      "sequence-head",
      [self.path("M-10 -5 L0 0 L-10 5 Z", stroke, fill=stroke)],
      "auto-start-reverse",
    ),
    marker(
      "open-head",
      [self.path("M-10 -5 L0 0 L-10 5", stroke)],
      "auto-start-reverse",
    ),
    marker(
      "message-tail",
      [self.circle("0", "0", "3.5", stroke, fill=bg)],
      "auto-start-reverse",
    ),
    marker(
      "condition-tail",
      [self.path("M0 0 L6 -4 L12 0 L6 4 Z", stroke, fill=bg)],
      "auto",
    ),
    marker("default-tail", [self.path("M4 -5 L10 5", stroke)], "auto"),
  ]
}

///|
fn BpmnPaint::connection(
  self : BpmnPaint,
  edge : @bpmn_layout.Connection,
  source_kind : @bpmn.NodeKind?,
) -> @writer.Node {
  let flow = edge.flow
  let stroke = self.paint.color("arrow")
  guard bpmn_geometry_path(edge, max_units=self.limit - self.used) is Some(d) else {
    self.fail("bpmn/render-limit", "Connection path exceeds the output budget")
    return self.element("g")
  }
  let attrs = self.metadata(flow.id, flow.identity, flow.properties)
  attrs.push(("id", "flow-\{edge.index}"))
  attrs.push(("class", "bpmn-flow"))
  attrs.push(("data-kind", bpmn_connection(flow.kind)))
  attrs.push(("data-from", edge.source))
  attrs.push(("data-to", edge.target))
  if flow.condition is Some(condition) {
    attrs.push(("data-condition", condition))
  }
  let line_attrs = [
    ("class", "bpmn-connector"),
    ("d", d),
    ("fill", "none"),
    ("stroke", stroke),
    ("stroke-width", "1.5"),
    ("stroke-linejoin", "round"),
  ]
  match flow.kind {
    SequenceFlow => {
      line_attrs.push(("marker-end", "url(#sequence-head)"))
      if flow.is_default {
        line_attrs.push(("marker-start", "url(#default-tail)"))
      } else if flow.condition != None &&
        source_kind.map(@bpmn_layout.activity).unwrap_or(false) {
        line_attrs.push(("marker-start", "url(#condition-tail)"))
      }
    }
    MessageFlow => {
      line_attrs.push(("stroke-dasharray", "6 4"))
      line_attrs.push(("marker-start", "url(#message-tail)"))
      line_attrs.push(("marker-end", "url(#open-head)"))
    }
    Association => {
      line_attrs.push(("stroke-dasharray", "1 4"))
      line_attrs.push(("stroke-linecap", "round"))
      if flow.direction != None {
        line_attrs.push(("marker-end", "url(#open-head)"))
      }
      if flow.direction == Both {
        line_attrs.push(("marker-start", "url(#open-head)"))
      }
    }
    DataAssociation | DataInputAssociation | DataOutputAssociation => {
      line_attrs.push(("stroke-dasharray", "1 4"))
      line_attrs.push(("stroke-linecap", "round"))
      line_attrs.push(("marker-end", "url(#open-head)"))
    }
  }
  let children = [self.element("path", attrs=line_attrs)]
  if flow.kind == MessageFlow &&
    flow.geometry.bind(g => g.message_visible) is Some(visibility) {
    let point = @bpmn_layout.midpoint(edge.line).unwrap()
    children.push(
      self.element(
        "g",
        attrs=[
          ("transform", "translate(\{bn(point.x-12.0)} \{bn(point.y-12.0)})"),
        ],
        children=self.event_symbol(Message, visibility == NonInitiating, stroke),
      ),
    )
  }
  self.element("g", attrs~, children~)
}