///|
/// Swimlane paths are structural connections. Containment never adds an arc.
pub fn Catalog::swimlane(
  view : @swimlane.Swimlane,
  limits~ : Limits = Limits::new(),
) -> CatalogResult {
  let build = builder(Swimlane(view), view.id, limits)
  let budget = build.budget
  if budget.failed {
    return build.finish()
  }
  if view.nodes.length() + view.groups.length() + view.lanes.length() >
    limits.nodes ||
    view.connections.length() > limits.edges {
    budget.error(
      "animation/plan-limit", "Swimlane catalog exceeds its element limits",
    )
    return build.finish()
  }
  let findings = @swimlane.validate(
    view,
    nodes_limit=limits.nodes,
    edges_limit=limits.edges,
    depth_limit=limits.depth,
    work_limit=limits.work,
  )
  if !findings.is_empty() {
    budget.diagnostics.append(findings)
    budget.failed = true
    return build.finish()
  }
  let groups : Map[String, @swimlane.Group] = Map([])
  let lanes : Map[String, @swimlane.Lane] = Map([])
  for group in view.groups {
    groups[group.id] = group
  }
  for lane in view.lanes {
    lanes[lane.id] = lane
  }
  fn visible(lane : String?, parent : String?) -> Bool {
    if lane.bind(id => lanes.get(id)) is Some(lane) && !lane.expanded {
      return false
    }
    let mut cursor = parent
    let mut depth = 0
    while cursor is Some(id) {
      if !budget.charge(1) {
        return false
      }
      if depth >= limits.depth {
        budget.error(
          "animation/plan-limit", "Swimlane containment depth limit exceeded",
        )
        return false
      }
      depth += 1
      guard groups.get(id) is Some(group) else {
        budget.error("animation/invalid-identity", "Missing swimlane group")
        return false
      }
      if !group.expanded {
        return false
      }
      cursor = group.parent
    }
    true
  }
  for lane in view.lanes {
    build.node({
      id: lane.id,
      label: lane.label,
      kind: SwimlaneLane,
      occurrence: None,
      source_node: None,
      swimlane_node: None,
    })
  }
  for group in view.groups {
    if visible(group.lane, group.parent) {
      build.node({
        id: group.id,
        label: group.label,
        kind: SwimlaneGroup,
        occurrence: None,
        source_node: None,
        swimlane_node: None,
      })
    } else {
      build.outside[group.id] = true
    }
  }
  for node in view.nodes {
    if visible(node.lane, node.groups.last()) {
      build.node({
        id: node.id,
        label: node.label,
        kind: SwimlaneNode,
        occurrence: None,
        source_node: None,
        swimlane_node: Some(node),
      })
    } else {
      build.outside[node.id] = true
    }
  }
  if budget.failed {
    return build.finish()
  }
  let parents : Map[String, String] = Map([])
  for group in view.groups {
    if (match group.parent {
        Some(_) => group.parent
        None => group.lane
      })
      is Some(parent) {
      parents[group.id] = parent
    }
  }
  for node in view.nodes {
    if (match node.groups.last() {
        Some(id) => Some(id)
        None => node.lane
      })
      is Some(parent) {
      parents[node.id] = parent
    }
  }
  fn display_anchor(id : String) -> String? {
    let mut current = id
    let mut depth = 0
    while !build.indices.contains(current) {
      if !budget.charge(1) {
        return None
      }
      if depth >= limits.depth {
        budget.error(
          "animation/plan-limit", "Swimlane display-anchor depth limit exceeded",
        )
        return None
      }
      depth += 1
      guard parents.get(current) is Some(parent) else { return None }
      current = parent
    }
    Some(current)
  }
  for i, connection in view.connections {
    if !budget.charge(
        connection.id.length() +
        connection.source.length() +
        connection.target.length() +
        1,
      ) {
      return build.finish()
    }
    if connection.style == Invisible {
      continue
    }
    if (
        !build.indices.contains(connection.source) &&
        !build.outside.contains(connection.source)
      ) ||
      (
        !build.indices.contains(connection.target) &&
        !build.outside.contains(connection.target)
      ) {
      budget.error(
        "animation/invalid-identity", "Swimlane connection endpoint is missing",
      )
      return build.finish()
    }
    let a = display_anchor(connection.source)
    let b = display_anchor(connection.target)
    if a == None || b == None {
      continue
    }
    if a == b &&
      (
        !build.indices.contains(connection.source) ||
        !build.indices.contains(connection.target)
      ) {
      continue
    }
    let key = "swimlane:\{view.id.length()}:\{view.id}:\{connection.id.length()}:\{connection.id}"
    if build.flows.contains(connection.id) {
      budget.error(
        "animation/invalid-identity", "Duplicate swimlane connection identity",
      )
      return build.finish()
    }
    build.flows[connection.id] = build.occurrences.length()
    build.occurrence({
      key,
      index: i,
      kind: Swimlane(connection.direction),
      path: [],
      source: None,
      flow: None,
      message: None,
      swimlane_connection: Some(connection),
    })
    // A visible collapsed ancestor is only a display anchor. Its hidden child
    // remains outside the route graph while the connection can still be played.
    if !build.indices.contains(connection.source) ||
      !build.indices.contains(connection.target) {
      continue
    }
    let kind = if connection.direction == Undirected {
      EdgeKind::Undirected
    } else {
      SwimlaneFlow
    }
    if connection.direction != Backward {
      build.edge({
        kind,
        source: connection.source,
        target: connection.target,
        occurrence: Some(key),
        reversed: false,
      })
    }
    if connection.direction == Backward ||
      connection.direction == Both ||
      connection.direction == Undirected {
      build.edge({
        kind,
        source: connection.target,
        target: connection.source,
        occurrence: Some(key),
        reversed: true,
      })
    }
  }
  build.finish()
}