///|
/// Pure swimlane geometry has no dependency on BPMN document or geometry types.
pub struct Limits {
  nodes : Int32
  edges : Int32
  depth : Int32
  work : Int32
  output_units : Int32
} derive(Eq, Debug)

///|
pub fn Limits::new(
  nodes~ : Int32 = 10000,
  edges~ : Int32 = 50000,
  depth~ : Int32 = 256,
  work~ : Int32 = 1000000,
  output_units~ : Int32 = 16777216,
) -> Limits {
  { nodes, edges, depth, work, output_units, }
}

///|
pub(all) struct Point {
  x : Double
  y : Double
} derive(Eq, Debug)

///|
pub(all) struct Rect {
  x : Double
  y : Double
  width : Double
  height : Double
} derive(Eq, Debug)

///|
pub fn Rect::contains(
  self : Rect,
  other : Rect,
  tolerance~ : Double = 0.0,
) -> Bool {
  other.x >= self.x - tolerance &&
  other.y >= self.y - tolerance &&
  other.x + other.width <= self.x + self.width + tolerance &&
  other.y + other.height <= self.y + self.height + tolerance
}

///|
pub fn Rect::overlaps(self : Rect, other : Rect) -> Bool {
  self.x < other.x + other.width &&
  other.x < self.x + self.width &&
  self.y < other.y + other.height &&
  other.y < self.y + self.height
}

///|
pub(all) enum Kind {
  Lane
  Group
  Node
} derive(Eq, Debug)

///|
pub struct Label {
  text : String
  box : Rect
  size : Double
  weight : Int32
  owner : String
  family : @theme.Family?
} derive(Eq, Debug)

///|
pub struct Shape {
  id : String
  index : Int32
  kind : Kind
  shape : String
  box : Rect
  outline_box : Rect
  label : Label?
  parent : String?
  lane : String?
  expanded : Bool
} derive(Eq, Debug)

///|
pub struct Connection {
  connection : @model.Connection
  index : Int32
  source : String
  target : String
  line : ReadOnlyArray[Point]
  label : Label?
} derive(Eq, Debug)

///|
pub struct Geometry {
  view : @model.Swimlane
  shapes : ReadOnlyArray[Shape]
  connections : ReadOnlyArray[Connection]
  width : Int32
  height : Int32
} derive(Eq, Debug)

///|
pub struct Result {
  geometry : Geometry?
  diagnostics : ReadOnlyArray[@diagrams.Diagnostic]
} derive(Eq, Debug)

///|
priv struct Budget {
  limits : Limits
  diagnostics : Array[@diagrams.Diagnostic]
  mut work : Int32
  mut failed : Bool
}

///|
fn Budget::error(self : Budget, code : String, message : String) -> Unit {
  if !self.failed {
    self.diagnostics.push(@diagrams.Diagnostic::error(code, message))
  }
  self.failed = true
}

///|
fn Budget::charge(self : Budget, amount : Int32) -> Bool {
  if amount < 0 || amount > self.limits.work - self.work {
    self.error("swimlane/layout-limit", "Swimlane geometry work limit exceeded")
    return false
  }
  self.work += amount
  true
}

///|
/// Canonical coordinates have three fractional digits and never use exponent notation.
pub fn svg_number(value : Double) -> String {
  let scaled = (value * 1000.0).round().to_int64()
  if scaled == 0L {
    return "0"
  }
  let negative = scaled < 0L
  let magnitude = if negative { -scaled } else { scaled }
  let whole = magnitude / 1000L
  let fraction = (magnitude % 1000L).to_int()
  let prefix = if negative { "-" } else { "" }
  if fraction == 0 {
    return "\{prefix}\{whole}"
  }
  let digits = "\{if fraction < 100 { "0" } else { "" }}\{if fraction < 10 { "0" } else { "" }}\{fraction}"
  "\{prefix}\{whole}." + digits.trim_end(chars="0").to_owned()
}

///|
/// An inert plain-text projection. Source HTML is never inserted into SVG.
pub fn label_text(text : String) -> String {
  let normalized = plain_text(text)
  let out = StringBuilder()
  let mut cursor = 0
  let mut chunk = 0
  fn letter(n : Int32) -> Bool {
    (n >= 65 && n <= 90) || (n >= 97 && n <= 122)
  }
  while cursor < normalized.length() {
    if normalized.code_unit_at(cursor).to_int() != 60 {
      cursor += 1
      continue
    }
    if normalized.view(start_offset=cursor).has_prefix("") is Some(relative) {
        out.write_string(
          normalized.view(start_offset=chunk, end_offset=cursor).to_owned(),
        )
        cursor += 4 + relative + 3
        chunk = cursor
        continue
      } else {
        cursor = normalized.length()
        continue
      }
    }
    let mut name = cursor + 1
    if name < normalized.length() &&
      normalized.code_unit_at(name).to_int() == 47 {
      name += 1
    }
    if name >= normalized.length() ||
      (
        !letter(normalized.code_unit_at(name).to_int()) &&
        !normalized.view(start_offset=cursor).has_prefix("