///|
/// Integer convenience path retained for the first motion-plan API.
pub fn bpmn_path(flow : @bpmn.Flow) -> String {
  let b = StringBuilder()
  for i, p in flow.points {
    b.write_string("\{if i == 0 { "M" } else { " L" }}\{p.x} \{p.y}")
  }
  b.to_string()
}

///|
fn BpmnPaint::shape(
  self : BpmnPaint,
  shape : @bpmn_layout.Shape,
  starts : ArrayView[@bpmn.Node],
) -> @writer.Node {
  let node = shape.node
  if !self.charge(node.markers.details.length()) ||
    !self.charge(node.markers.events.length()) {
    return self.element("g")
  }
  let attrs = self.metadata(node.id, node.source, node.properties)
  attrs.push(("id", "node-\{shape.index}"))
  attrs.push(("class", "bpmn-shape"))
  attrs.push(("data-kind", bpmn_kind(node.kind)))
  attrs.push(
    (
      "data-expanded",
      if node.expanded.unwrap_or(false) {
        "true"
      } else {
        "false"
      },
    ),
  )
  attrs.push(("data-black-box", if node.black_box { "true" } else { "false" }))
  if node.ownership.pool is Some(id) {
    attrs.push(("data-pool", id))
  }
  if node.ownership.subprocess is Some(id) {
    attrs.push(("data-subprocess", id))
  }
  if node.ownership.attached_to is Some(id) {
    attrs.push(("data-attached-to", id))
  }
  let events = StringBuilder()
  let mut units = 0
  for i, event in node.markers.events {
    let name = (if i == 0 { "" } else { "," }) + bpmn_event(event.kind)
    if name.length() > self.limit - self.used - units {
      self.fail("bpmn/render-limit", "Event metadata exceeds the output budget")
      break
    }
    units += name.length()
    events.write_string(name)
  }
  attrs.push(("data-events", events.to_string()))
  let r = shape.bounds
  let children = [
    self.element("rect", attrs=[
      ("class", "bpmn-bounds"),
      ("x", r.x.token),
      ("y", r.y.token),
      ("width", r.width.token),
      ("height", r.height.token),
      ("fill", "none"),
      ("stroke", "none"),
      ("pointer-events", "none"),
      ("aria-hidden", "true"),
    ]),
    self.outline(shape),
  ]
  children.append(self.markers(shape, starts))
  self.element("g", attrs~, children~)
}

///|
fn bpmn_background(kind : @bpmn.NodeKind, expanded : Bool) -> Bool {
  kind == Participant ||
  kind == Lane ||
  kind == Group ||
  (
    expanded &&
    (
      kind == SubProcess ||
      kind == AdHocSubProcess ||
      kind == Transaction ||
      kind == CallActivity
    )
  )
}

///|
fn bpmn_prefix(
  root : @writer.Node,
  raw : String?,
  paint : BpmnPaint,
) -> String? {
  let prefix = match raw {
    Some(raw) => {
      let b = StringBuilder()
      let mut length = 7
      b.write_string("bpmn-u-")
      for c in raw {
        let value = c.to_int().to_string(radix=16) + "-"
        if value.length() > paint.limit - length {
          paint.fail(
            "bpmn/render-limit", "SVG ID prefix exceeds the output budget",
          )
          return None
        }
        length += value.length()
        b.write_string(value)
      }
      b.to_string()
    }
    None => {
      let source = @writer.to_string(root)
      let mut hash : UInt64 = 14695981039346656037
      for i in 0.. ()
      Element(name, attrs, children) => {
        if name == "style" {
          occurrences += 4
        }
        for (key, _) in attrs {
          if key == "id" || key == "marker-start" || key == "marker-end" {
            occurrences += 1
          }
        }
        stack.append(children)
      }
    }
  }
  if prefix.length() > (paint.limit - paint.used) / occurrences ||
    !paint.reserve(prefix.length() * occurrences + 64) {
    paint.fail(
      "bpmn/render-limit", "Scoped SVG identifiers exceed the output budget",
    )
    return None
  }
  Some(prefix)
}

///|
fn paint_bpmn(
  view : @bpmn.Bpmn,
  theme : @theme.Theme,
  mode : Mode,
  id_prefix : String?,
  policy : @bpmn.GeometryPolicy,
  limits : @bpmn_layout.Limits,
  precomputed? : @bpmn_layout.Result,
  connectors~ : ConnectorOptions = ConnectorOptions::new(),
) -> @render.Rendered {
  if !connector_options_valid(connectors) {
    return @render.Rendered::failed("image/svg+xml", [
      @diagrams.Diagnostic::error(
        "connector/invalid-option", "Invalid connector options",
      ),
    ])
  }
  let limits = limits.with_minimum_approach(connectors.minimum_approach)
  let theme = @theme.Theme::{
    dark: @bpmn_layout.bpmn_palette(theme.dark),
    light: @bpmn_layout.bpmn_palette(theme.light),
  }
  let result = precomputed.unwrap_or_else(() => {
    @bpmn_layout.layout(view, policy~, limits~)
  })
  let diagnostics = result.diagnostics.iter().to_array()
  guard result.geometry is Some(geometry) else {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  let c = BpmnPaint::{
    paint: Paint::{ theme, mode, },
    limit: limits.output_units,
    diagnostics,
    used: 0,
    failed: false,
    work: 0,
    work_limit: limits.work,
  }
  let starts : Map[String, Array[@bpmn.Node]] = Map([])
  let kinds = Map([])
  if !c.charge(view.nodes.length()) {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  for node in view.nodes {
    kinds[node.id] = node.kind
    if node.kind == StartEvent && node.ownership.subprocess is Some(owner) {
      match starts.get(owner) {
        Some(events) => events.push(node)
        None => starts[owner] = [node]
      }
    }
  }
  let background = []
  let foreground = []
  let labels = []
  let containers : Array[@bpmn_layout.Shape] = []
  for shape in geometry.shapes {
    if c.failed {
      break
    }
    if bpmn_background(shape.node.kind, shape.node.expanded.unwrap_or(false)) {
      containers.push(shape)
    } else {
      foreground.push(c.shape(shape, starts.get(shape.node.id).unwrap_or([])))
    }
    if shape.label is Some(label) {
      labels.push(c.label(label))
    }
  }
  // Outer containers paint first even when XML/model order places a child first.
  containers.sort_by((a, b) => {
    let area = (b.box.width * b.box.height).compare(a.box.width * a.box.height)
    if area == 0 {
      a.index.compare(b.index)
    } else {
      area
    }
  })
  for shape in containers {
    background.push(c.shape(shape, starts.get(shape.node.id).unwrap_or([])))
  }
  let flows = []
  for edge in geometry.connections {
    if c.failed {
      break
    }
    flows.push(c.connection(edge, kinds.get(edge.source)))
    if edge.label is Some(label) {
      labels.push(c.label(label))
    }
  }
  let definitions = c.definitions()
  if mode == Adaptive {
    definitions.push(c.element("style", children=[c.text(style_sheet(theme))]))
  }
  let root = c.element(
    "svg",
    attrs=[
      ("xmlns", "http://www.w3.org/2000/svg"),
      ("viewBox", "0 0 \{geometry.width} \{geometry.height}"),
      ("width", geometry.width.to_string()),
      ("height", geometry.height.to_string()),
      ("role", "img"),
      ("data-diagram", "bpmn"),
    ],
    children=[
      c.element("title", children=[c.text(view.label)]),
      c.element("desc", children=[
        c.text(
          "BPMN view with \{geometry.shapes.length()} visible shapes and \{geometry.connections.length()} connections. Structural flow is illustrative. Conditions, timers and execution outcomes are not evaluated.",
        ),
      ]),
      c.element("defs", children=definitions),
      c.element("rect", attrs=[
        ("class", "bg"),
        ("x", "0"),
        ("y", "0"),
        ("width", geometry.width.to_string()),
        ("height", geometry.height.to_string()),
        ("fill", c.paint.color("bg")),
      ]),
      c.element(
        "g",
        attrs=[
          (
            "transform",
            "translate(\{geometry.translate_x} \{geometry.translate_y})",
          ),
          ("class", "bpmn-viewport"),
        ],
        children=[
          c.element(
            "g",
            attrs=[("class", "bpmn-backgrounds")],
            children=background,
          ),
          c.element("g", attrs=[("class", "bpmn-connections")], children=flows),
          c.element("g", attrs=[("class", "bpmn-nodes")], children=foreground),
          c.element("g", attrs=[("class", "bpmn-labels")], children=labels),
        ],
      ),
    ],
  )
  if c.failed {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  let obstacles : Array[ConnectorObstacle] = []
  let labels : Array[(String, ConnectorObstacle)] = []
  for shape in geometry.shapes {
    if !bpmn_background(shape.node.kind, shape.node.expanded.unwrap_or(false)) {
      let r = shape.box
      obstacles.push({
        owner: shape.node.id,
        x: r.x,
        y: r.y,
        width: r.width,
        height: r.height,
      })
    }
    if shape.label is Some(label) {
      let r = label.box
      labels.push(
        (
          "",
          {
            owner: shape.node.id,
            x: r.x,
            y: r.y,
            width: r.width,
            height: r.height,
          },
        ),
      )
    }
  }
  for edge in geometry.connections {
    if edge.label is Some(label) {
      let r = label.box
      labels.push(
        (
          "",
          {
            owner: edge.flow.id,
            x: r.x,
            y: r.y,
            width: r.width,
            height: r.height,
          },
        ),
      )
    }
  }
  let display_options = if geometry.source == Supplied &&
    !connectors.style_supplied_bpmn {
    ConnectorOptions::new(crossing=Plain, limits=connectors.limits)
  } else {
    connectors
  }
  guard finalize_connectors(
      root,
      display_options,
      diagnostics,
      obstacles=obstacles[:],
      labels=labels[:],
      viewport=(
        -geometry.translate_x.to_double(),
        -geometry.translate_y.to_double(),
        geometry.width.to_double(),
        geometry.height.to_double(),
      ),
      supplied=geometry.source == Supplied,
    )
    is Some(root) else {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  guard bpmn_prefix(root, id_prefix, c) is Some(prefix) else {
    return @render.Rendered::failed("image/svg+xml", diagnostics)
  }
  let palettes = match mode {
    Adaptive => [("dark", theme.dark), ("light", theme.light)]
    Fixed(Dark) => [("dark", theme.dark)]
    Fixed(Light) => [("light", theme.light)]
  }
  for (name, palette) in palettes {
    for finding in @bpmn_layout.check(geometry, palette, work_limit=limits.work) {
      diagnostics.push(
        @diagrams.Diagnostic::warning(
          "bpmn/legibility",
          "\{name}: \{finding.message}",
        ),
      )
    }
  }
  @render.Rendered::ok(
    @writer.to_string(scope_svg(root, prefix, theme)),
    "image/svg+xml",
    diagnostics~,
  )
}