///|
fn layout_failure(diagnostics : ArrayView[@diagrams.Diagnostic]) -> Result {
  { geometry: None, diagnostics: ReadOnlyArray::from_array(diagnostics), }
}

///|
fn horizontal(direction : @model.Direction) -> Bool {
  direction == LR || direction == RL
}

///|
fn reversed(direction : @model.Direction) -> Bool {
  direction == BT || direction == RL
}

///|
/// Measured responsibility bands, built bottom-up without recursive ownership walks.
pub fn layout(
  view : @model.Swimlane,
  limits~ : Limits = Limits::new(),
) -> Result {
  let diagnostics = []
  let budget = Budget::{ limits, diagnostics, work: 0, failed: false, }
  let real_count = view.lanes.length() +
    view.groups.length() +
    view.nodes.length()
  let count = real_count + 1
  let unowned = real_count
  if limits.nodes < 0 ||
    limits.edges < 0 ||
    limits.depth < 0 ||
    limits.work <= 0 ||
    limits.output_units <= 0 ||
    real_count > limits.nodes ||
    view.connections.length() > limits.edges {
    budget.error(
      "swimlane/layout-limit", "Swimlane geometry input exceeds its explicit limits",
    )
    return layout_failure(diagnostics)
  }
  fn text(value : String) -> Bool {
    if value.length() > 50000000 {
      budget.error(
        "swimlane/layout-limit", "A text extent exceeds its safe numeric bound",
      )
      return false
    }
    budget.charge(value.length() + 1)
  }
  for
    value in view.title
    .iter()
    .concat(view.accessibility_title.iter())
    .concat(view.accessibility_description.iter()) {
    if !text(value) {
      return layout_failure(diagnostics)
    }
  }
  if !text(view.id) {
    return layout_failure(diagnostics)
  }
  for lane in view.lanes {
    if !text(lane.id) || !text(lane.label) {
      return layout_failure(diagnostics)
    }
  }
  for group in view.groups {
    if !text(group.id) || !text(group.label) {
      return layout_failure(diagnostics)
    }
    for id in group.parent.iter().concat(group.lane.iter()) {
      if !text(id) {
        return layout_failure(diagnostics)
      }
    }
  }
  for node in view.nodes {
    if !text(node.id) || !text(node.label) || !text(node.shape) {
      return layout_failure(diagnostics)
    }
    for id in node.lane.iter().concat(node.groups.iter()) {
      if !text(id) {
        return layout_failure(diagnostics)
      }
    }
  }
  for edge in view.connections {
    for value in [edge.id, edge.source, edge.target, edge.label] {
      if !text(value) {
        return layout_failure(diagnostics)
      }
    }
  }
  let parents = Array::make(count, -1)
  let visible = Array::make(count, true)
  let ids : Map[String, Int32] = Map([])
  let kinds : Array[Kind] = []
  let texts = []
  let shapes = []
  let expanded = []
  let directions : Array[@model.Direction] = []
  let lanes : Array[String?] = []
  let explicit_direction : Array[Bool] = []
  fn add(
    id : String,
    kind : Kind,
    text : String,
    shape : String,
    open : Bool,
    lane : String?,
    direction : @model.Direction?,
  ) {
    ids[id] = kinds.length()
    kinds.push(kind)
    texts.push(text)
    shapes.push(shape)
    expanded.push(open)
    lanes.push(lane)
    directions.push(direction.unwrap_or(view.direction))
    explicit_direction.push(direction != None)
  }
  for lane in view.lanes {
    add(
      lane.id,
      Lane,
      lane.label,
      "rect",
      lane.expanded,
      Some(lane.id),
      lane.direction,
    )
  }
  for group in view.groups {
    add(
      group.id,
      Group,
      group.label,
      "rect",
      group.expanded,
      group.lane,
      group.direction,
    )
  }
  for node in view.nodes {
    if !@model.built_in_shapes.contains(node.shape) {
      budget.error(
        "swimlane/unsupported-shape",
        "Unsupported vector or asset shape \{node.shape} on \{node.id}",
      )
      return layout_failure(diagnostics)
    }
    for (key, value) in node.metadata {
      if !budget.charge(key.length() + value.length() + 1) {
        return layout_failure(diagnostics)
      }
    }
    if node.properties is Some(properties) {
      if !budget.charge(properties.link.map(s => s.length()).unwrap_or(0)) {
        return layout_failure(diagnostics)
      }
      for (key, value) in properties.metadata {
        if !budget.charge(key.length() + value.length() + 1) {
          return layout_failure(diagnostics)
        }
      }
    }
    add(
      node.id,
      Node,
      project_label(node.label, node.label_kind),
      node.shape,
      true,
      node.lane,
      None,
    )
    if node.label_kind != Text {
      diagnostics.push(
        @diagrams.Diagnostic::warning(
          "swimlane/plain-text-label",
          "Label on \{node.id} uses a safe plain-text projection",
        ),
      )
    }
  }
  add("", Group, "", "rect", true, None, None)
  let group_start = view.lanes.length()
  let node_start = group_start + view.groups.length()
  for i, group in view.groups {
    parents[group_start + i] = group.parent
      .map(id => Some(id))
      .unwrap_or(group.lane)
      .bind(id => ids.get(id))
      .unwrap_or(-1)
  }
  for i, node in view.nodes {
    parents[node_start + i] = node.groups
      .last()
      .map(id => Some(id))
      .unwrap_or(node.lane)
      .bind(id => ids.get(id))
      .unwrap_or(-1)
  }
  let mut has_unowned = false
  for i in 0..= 0 {
      if !budget.charge(1) {
        return layout_failure(diagnostics)
      }
      if cursor != unowned {
        depth += 1
      }
      if cursor == i || seen.contains(cursor) || depth > limits.depth {
        budget.error(
          if depth > limits.depth {
            "swimlane/layout-limit"
          } else {
            "swimlane/ownership-cycle"
          },
          "Invalid or excessive ownership at \{i}",
        )
        return layout_failure(diagnostics)
      }
      seen[cursor] = true
      if !expanded[cursor] {
        visible[i] = false
      }
      if !direction_found && explicit_direction[cursor] {
        directions[i] = directions[cursor]
        direction_found = true
      }
      cursor = parents[cursor]
    }
  }
  let validation = @model.validate(view)
  if validation.iter().any(d => d.severity == @diagrams.Severity::Error) {
    diagnostics.append(validation)
    return layout_failure(diagnostics)
  }
  let children : Map[Int32, Array[Int32]] = Map([])
  for i in 0.. values.push(i)
        None => children[parents[i]] = [i]
      }
    }
  }
  let selected : Array[(Int32, Int32, Int32)] = []
  let graph_links = []
  fn anchor(id : String) -> Int32 {
    let mut i = ids[id]
    while !visible[i] && parents[i] >= 0 {
      if !budget.charge(1) {
        return i
      }
      i = parents[i]
    }
    i
  }
  for i, edge in view.connections {
    if edge.label_kind != Text {
      diagnostics.push(
        @diagrams.Diagnostic::warning(
          "swimlane/plain-text-label",
          "Connection label on \{edge.id} uses a safe plain-text projection",
        ),
      )
    }
    if !budget.charge(1) {
      return layout_failure(diagnostics)
    }
    let a = anchor(edge.source)
    let b = anchor(edge.target)
    // A hidden internal edge has no visible occurrence. Boundary handoffs retain
    // their logical endpoints in the connection, with a separate display anchor.
    if !visible[a] ||
      !visible[b] ||
      (a == b && (!visible[ids[edge.source]] || !visible[ids[edge.target]])) {
      continue
    }
    selected.push((i, a, b))
    graph_links.push((a, b, edge.min_length))
  }
  let members = Array::makei(count, i => i).filter(i => visible[i])
  let global_ranks = weighted_ranks(members, graph_links, budget)
  let subtree_rank = Array::make(count, 0)
  for i in members {
    subtree_rank[i] = global_ranks.get(i).unwrap_or(0)
  }
  let sizes : Array[(Double, Double)] = Array::make(count, (0.0, 0.0))
  let offsets : Array[Point] = Array::make(count, { x: 0.0, y: 0.0, })
  let mut gap = 80.0
  for edge in view.connections {
    let (w, h) = measure(project_label(edge.label, edge.label_kind))
    gap = Double::max(gap, Double::max(w, h) + 56.0)
  }
  for i in members {
    let (w, h) = measure(
      plain_text(texts[i]),
      weight=if kinds[i] == Node { 400 } else { 700 },
    )
    if kinds[i] == Node {
      let inset = ![
        "rect", "rounded", "stadium", "subroutine", "lin-rect", "div-rect", "win-pane",
        "browser", "console", "state", "composite", "text", "tag-rect", "person",
      ].contains(shapes[i])
      let factor = if shapes[i] == "tri" || shapes[i] == "flip-tri" {
        3.0
      } else if inset {
        2.0
      } else {
        1.0
      }
      let mut width = Double::max(112.0, w * factor + 48.0)
      let mut height = Double::max(64.0, h * factor + 40.0)
      if ["circle", "doublecircle", "fr-circ"].contains(shapes[i]) {
        width = Double::max(64.0, (w * w + h * h).sqrt() + 48.0)
        height = width
      }
      if shapes[i] == "bang" {
        height = Double::max(height, 88.0)
      }
      if shapes[i] == "person" {
        height = Double::max(112.0, (h + 40.0) / 0.66)
      }
      if [
          "hourglass", "bolt", "sm-circ", "f-circ", "fr-circ", "cross-circ", "anchor",
          "fork",
        ].contains(shapes[i]) {
        let (gw, gh) = glyph_size(shapes[i], directions[i])
        if shapes[i] == "fork" && horizontal(directions[i]) {
          width = gw + w + 32.0
          height = Double::max(gh, h + 16.0)
        } else {
          width = Double::max(gw, w + 16.0)
          height = gh + h + 32.0
        }
      }
      sizes[i] = (width, height)
    } else {
      sizes[i] = (Double::max(160.0, w + 40.0), Double::max(96.0, h + 56.0))
    }
  }
  let stack = [(-1, false)]
  while stack.pop() is Some((scope, leave)) {
    if !budget.charge(1) {
      return layout_failure(diagnostics)
    }
    let items = children.get(scope).unwrap_or([])
    if !leave {
      stack.push((scope, true))
      for item in items {
        if kinds[item] != Node {
          stack.push((item, false))
        }
      }
      continue
    }
    if scope == -1 {
      // Top-level responsibility bands keep declaration order across the flow.
      let mut cross = 32.0
      let mut length = 0.0
      for item in items {
        length = Double::max(
          length,
          if horizontal(view.direction) {
            sizes[item].0
          } else {
            sizes[item].1
          },
        )
      }
      for item in items {
        if kinds[item] == Lane || item == unowned {
          sizes[item] = if horizontal(view.direction) {
            (length, sizes[item].1)
          } else {
            (sizes[item].0, length)
          }
        }
        offsets[item] = if horizontal(view.direction) {
          { x: 32.0, y: cross, }
        } else {
          { x: cross, y: 32.0, }
        }
        cross += (if horizontal(view.direction) {
            sizes[item].1
          } else {
            sizes[item].0
          }) +
          32.0
      }
      continue
    }
    if items.is_empty() || !expanded[scope] {
      continue
    }
    let dir = directions[scope]
    let axis = horizontal(dir)
    let projected = []
    fn child(index : Int32) -> Int32? {
      let mut cursor = index
      while parents[cursor] != scope {
        if !budget.charge(1) || parents[cursor] < 0 {
          return None
        }
        cursor = parents[cursor]
      }
      Some(cursor)
    }
    for (edge, a, b) in selected {
      if child(a) is Some(a) && child(b) is Some(b) && a != b {
        projected.push((a, b, view.connections[edge].min_length))
      }
    }
    let ranks = weighted_ranks(items, projected, budget)
    let rows : Map[Int32, Array[Int32]] = Map([])
    let row_order = []
    for item in items {
      let rank = ranks.get(item).unwrap_or(0)
      // A scope with its own direction has a explicit_direction rank system. Other lane
      // scopes align cross-lane handoffs to the global structural ranks.
      let rank = if kinds[scope] == Lane && !explicit_direction[scope] {
        Int32::max(rank, subtree_rank[item])
      } else {
        rank
      }
      if !rows.contains(rank) {
        rows[rank] = []
        row_order.push(rank)
      }
      rows[rank].push(item)
    }
    row_order.sort()
    let (header_w, header_h) = if scope == unowned {
      (0.0, 0.0)
    } else {
      measure(plain_text(texts[scope]), weight=700)
    }
    let mut flow = 0.0
    let mut extent = 0.0
    let mut previous_rank = 0
    for rank in row_order {
      if rank > previous_rank + 1 {
        flow += (rank - previous_rank - 1).to_double() * gap
      }
      previous_rank = rank
      let row = rows[rank]
      let mut cross = 0.0
      let mut length = 0.0
      for item in row {
        length = Double::max(
          length,
          if axis {
            sizes[item].0
          } else {
            sizes[item].1
          },
        )
      }
      for item in row {
        offsets[item] = if axis {
          { x: flow, y: cross, }
        } else {
          { x: cross, y: flow, }
        }
        cross += (if axis { sizes[item].1 } else { sizes[item].0 }) + gap
      }
      extent = Double::max(extent, Double::max(0.0, cross - gap))
      flow += length + gap
    }
    flow = Double::max(0.0, flow - gap)
    let pad_x = 32.0
    let pad_y = if scope == unowned { 32.0 } else { header_h + 40.0 }
    let body_w = if axis { flow } else { extent }
    let body_h = if axis { extent } else { flow }
    sizes[scope] = (
      Double::max(header_w + 40.0, body_w + 64.0),
      body_h + pad_y + 32.0,
    )
    for item in items {
      let p = offsets[item]
      let x = if axis && reversed(dir) {
        body_w - p.x - sizes[item].0
      } else {
        p.x
      }
      let y = if !axis && reversed(dir) {
        body_h - p.y - sizes[item].1
      } else {
        p.y
      }
      offsets[item] = { x: x + pad_x, y: y + pad_y, }
    }
    let mut first = true
    for item in items {
      if first || subtree_rank[item] < subtree_rank[scope] {
        subtree_rank[scope] = subtree_rank[item]
        first = false
      }
    }
  }
  if budget.failed {
    return layout_failure(diagnostics)
  }
  let positions = Array::make(count, { x: 0.0, y: 0.0, })
  let stack = children.get(-1).unwrap_or([]).copy()
  while stack.pop() is Some(i) {
    if !budget.charge(1) {
      return layout_failure(diagnostics)
    }
    let parent = if parents[i] >= 0 {
      positions[parents[i]]
    } else {
      Point::{ x: 0.0, y: 0.0, }
    }
    positions[i] = { x: parent.x + offsets[i].x, y: parent.y + offsets[i].y, }
    stack.append(children.get(i).unwrap_or([]))
  }
  let result_shapes : Array[Shape] = []
  let index_ids = Array::make(count, "")
  for (id, i) in ids {
    index_ids[i] = id
  }
  let mut width = 64.0
  let mut height = 64.0
  for i in members {
    if i == unowned {
      continue
    }
    let box : Rect = {
      x: positions[i].x,
      y: positions[i].y,
      width: sizes[i].0,
      height: sizes[i].1,
    }
    let id = index_ids[i]
    let kind = kinds[i]
    let parent = if parents[i] >= 0 && parents[i] != unowned {
      Some(index_ids[parents[i]])
    } else {
      None
    }
    let external = [
      "hourglass", "bolt", "sm-circ", "f-circ", "fr-circ", "cross-circ", "anchor",
      "fork",
    ].contains(shapes[i])
    let (tw, th) = measure(plain_text(texts[i]))
    let (gw, gh) = glyph_size(shapes[i], directions[i])
    let outline_box = if external {
      if shapes[i] == "fork" && horizontal(directions[i]) {
        Rect::{
          x: box.x,
          y: box.y + (box.height - gh) / 2.0,
          width: gw,
          height: gh,
        }
      } else {
        Rect::{
          x: box.x + (box.width - gw) / 2.0,
          y: box.y,
          width: gw,
          height: gh,
        }
      }
    } else {
      box
    }
    let label_box = if external {
      if shapes[i] == "fork" && horizontal(directions[i]) {
        Rect::{
          x: box.x + gw + 24.0,
          y: box.y + (box.height - th) / 2.0,
          width: tw,
          height: th,
        }
      } else {
        Rect::{
          x: box.x + (box.width - tw) / 2.0,
          y: box.y + gh + 24.0,
          width: tw,
          height: th,
        }
      }
    } else if shapes[i] == "cylinder" || shapes[i] == "lin-cyl" {
      let top = if shapes[i] == "lin-cyl" { 0.36 } else { 0.3 }
      Rect::{
        x: box.x,
        y: box.y + box.height * top,
        width: box.width,
        height: box.height * (1.0 - top),
      }
    } else if shapes[i] == "person" {
      Rect::{
        x: box.x,
        y: box.y + box.height * 0.34,
        width: box.width,
        height: box.height * 0.66,
      }
    } else {
      box
    }
    let label = make_label(
      texts[i],
      id,
      label_box,
      kind != Node,
      if kind == Node {
        Some(family(shapes[i]))
      } else {
        None
      },
    )
    result_shapes.push({
      id,
      index: i,
      kind,
      shape: shapes[i],
      box,
      outline_box,
      label,
      parent,
      lane: lanes[i],
      expanded: expanded[i],
    })
    width = Double::max(width, box.x + box.width + 32.0)
    height = Double::max(height, box.y + box.height + 32.0)
  }
  let connections = route_connections(
    view, selected, result_shapes, parents, budget,
  )
  if budget.failed {
    return layout_failure(diagnostics)
  }
  for edge in connections {
    for p in edge.line {
      width = Double::max(width, p.x + 32.0)
      height = Double::max(height, p.y + 32.0)
    }
    if edge.label is Some(label) {
      width = Double::max(width, label.box.x + label.box.width + 32.0)
      height = Double::max(height, label.box.y + label.box.height + 32.0)
    }
  }
  if width > 1000000000.0 || height > 1000000000.0 {
    budget.error(
      "swimlane/layout-limit", "Measured diagram exceeds the coordinate bound",
    )
    return layout_failure(diagnostics)
  }
  {
    geometry: Some({
      view,
      shapes: ReadOnlyArray::from_array(result_shapes),
      connections: ReadOnlyArray::from_array(connections),
      width: width.ceil().to_int(),
      height: height.ceil().to_int(),
    }),
    diagnostics: ReadOnlyArray::from_array(diagnostics),
  }
}

///|
fn glyph_size(shape : String, direction : @model.Direction) -> (Double, Double) {
  match shape {
    "hourglass" => (96.0, 64.0)
    "bolt" => (64.0, 80.0)
    "sm-circ" => (18.0, 18.0)
    "f-circ" => (24.0, 24.0)
    "fr-circ" => (32.0, 32.0)
    "cross-circ" => (64.0, 64.0)
    "anchor" => (12.0, 12.0)
    "fork" => if horizontal(direction) { (12.0, 84.0) } else { (84.0, 12.0) }
    _ => (0.0, 0.0)
  }
}