// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
/// Render-ready diagram model.
///
/// This module mirrors the responsibilities of a "target" format in other
/// implementations: it is the only input type renderers should consume.

///|
/// Typed values preserved from config data entries.
pub(all) enum ConfigDataValue {
  Text(String)
  StringArray(Array[String])
} derive(Eq, Debug)

///|
/// Configuration extracted from source (vars/config).
///
/// Caller-provided options may override these values.
pub struct Config {
  sketch : Bool?
  theme_id : Int?
  dark_theme_id : Int?
  theme_overrides : ThemeOverrides?
  dark_theme_overrides : ThemeOverrides?
  pad : Double?
  center : Bool?
  layout_engine : String?
  scale : Double?
  no_xml_tag : Bool?
  salt : String?
  omit_version : Bool?
  data : Map[String, ConfigDataValue]
} derive(Debug)

///|
pub fn Config::new() -> Config {
  {
    sketch: None,
    theme_id: None,
    dark_theme_id: None,
    theme_overrides: None,
    dark_theme_overrides: None,
    pad: None,
    center: None,
    layout_engine: None,
    scale: None,
    no_xml_tag: None,
    salt: None,
    omit_version: None,
    data: Map([]),
  }
}

///|
pub fn Config::from_parts(
  sketch : Bool?,
  theme_id : Int?,
  dark_theme_id : Int?,
  theme_overrides : ThemeOverrides?,
  dark_theme_overrides : ThemeOverrides?,
  pad : Double?,
  center : Bool?,
  layout_engine : String?,
  scale : Double?,
  no_xml_tag : Bool?,
  salt : String?,
  omit_version : Bool?,
  data : Map[String, ConfigDataValue],
) -> Config {
  {
    sketch,
    theme_id,
    dark_theme_id,
    theme_overrides,
    dark_theme_overrides,
    pad,
    center,
    layout_engine,
    scale,
    no_xml_tag,
    salt,
    omit_version,
    data,
  }
}

///|
/// Renderable shape.
///
/// This is derived from `@graph.ObjectInput` after layout has been applied.
pub struct Shape {
  id : String
  semantic_id : String
  label : String
  shape_type : @graph.ShapeType
  style : @graph.StyleInput
  box : @graph.Box?
  label_box : @graph.Box?
  label_width : Double?
  label_height : Double?
  child_ids : Array[String]
  z_index : Int
  icon : String?
  icon_position : String?
  tooltip : String?
  tooltip_position : String?
  link : String?
  label_position : String?
  classes : Array[String]
  language : String?
  sql_constraints : Array[String]
  grid_rows : Int?
  grid_columns : Int?
  grid_gap : Double?
  horizontal_gap : Double?
  vertical_gap : Double?
  grid_column_span : Int?
  grid_row_span : Int?
  near : String?
} derive(Debug)

///|
/// Renderable connection.
///
/// This is derived from `@graph.EdgeInput` after layout has been applied.
pub struct Connection {
  index : Int
  src_id : String
  dst_id : String
  src_syntax_id : String
  dst_syntax_id : String
  src_lookup_id : String
  dst_lookup_id : String
  identity_scope_depth : Int
  classes : Array[String]
  src_arrow : Bool
  dst_arrow : Bool
  src_arrowhead : @graph.ArrowheadType
  dst_arrowhead : @graph.ArrowheadType
  src_arrowhead_label : String?
  dst_arrowhead_label : String?
  src_arrowhead_label_color : String?
  dst_arrowhead_label_color : String?
  src_anchor : String?
  dst_anchor : String?
  label : String
  icon : String?
  icon_position : String?
  icon_border_radius : Double?
  link : String?
  style : @graph.StyleInput
  route : Array[@graph.Point]
  bend_points : Array[@graph.Point]?
  is_curve : Bool
  z_index : Int
  reference_count : Int
  label_box : @graph.Box?
  src_column_index : Int?
  dst_column_index : Int?
} derive(Debug)

///|
/// Build the renderable shape used by the SVG legend's scaled icon.
pub fn Shape::legend_icon(
  id : String,
  shape_type : @graph.ShapeType,
  style : @graph.StyleInput,
) -> Shape {
  {
    id,
    semantic_id: id,
    label: "",
    shape_type,
    style,
    box: Some(@graph.Box::new(0.0, 0.0, 120.0, 120.0)),
    label_box: None,
    label_width: None,
    label_height: None,
    child_ids: [],
    z_index: 0,
    icon: None,
    icon_position: None,
    tooltip: None,
    tooltip_position: None,
    link: None,
    label_position: None,
    classes: [],
    language: None,
    sql_constraints: [],
    grid_rows: None,
    grid_columns: None,
    grid_gap: None,
    horizontal_gap: None,
    vertical_gap: None,
    grid_column_span: None,
    grid_row_span: None,
    near: None,
  }
}

///|
/// Build the renderable connection used by the SVG legend's scaled icon.
pub fn Connection::legend_icon(
  src_id : String,
  dst_id : String,
  index : Int,
  src_arrow : Bool,
  dst_arrow : Bool,
  src_arrowhead : @graph.ArrowheadType,
  dst_arrowhead : @graph.ArrowheadType,
  style : @graph.StyleInput,
) -> Connection {
  {
    index,
    src_id,
    dst_id,
    src_syntax_id: src_id,
    dst_syntax_id: dst_id,
    src_lookup_id: src_id,
    dst_lookup_id: dst_id,
    identity_scope_depth: @graph.syntax_path_depth(src_id),
    classes: [],
    src_arrow,
    dst_arrow,
    src_arrowhead,
    dst_arrowhead,
    src_arrowhead_label: None,
    dst_arrowhead_label: None,
    src_arrowhead_label_color: None,
    dst_arrowhead_label_color: None,
    src_anchor: None,
    dst_anchor: None,
    label: "",
    icon: None,
    icon_position: None,
    icon_border_radius: None,
    link: None,
    style,
    route: [@graph.Point::new(0.0, 0.0), @graph.Point::new(48.0, 0.0)],
    bend_points: None,
    is_curve: false,
    z_index: 0,
    reference_count: 1,
    label_box: None,
    src_column_index: None,
    dst_column_index: None,
  }
}

///|
/// Render-ready diagram.
///
/// `shapes` does not include `root`.
pub struct Diagram {
  name : String
  config : Config?
  is_folder_only : Bool
  description : String
  font_family : String?
  mono_font_family : String?
  root : Shape
  shapes : Array[Shape]
  connections : Array[Connection]
  activation_boxes : Array[(String, @graph.Box)]
  sequence_notes : Array[(String, String, @graph.Box)]
  sequence_fragments : Array[@graph.SequenceFragmentLayout]
  layers : Array[Diagram]
  scenarios : Array[Diagram]
  steps : Array[Diagram]
  legend : @graph.LegendInput?
} derive(Debug)

///|
pub fn Diagram::has_layers(self : Diagram) -> Bool {
  self.layers.length() > 0
}

///|
pub fn Diagram::has_scenarios(self : Diagram) -> Bool {
  self.scenarios.length() > 0
}

///|
pub fn Diagram::has_steps(self : Diagram) -> Bool {
  self.steps.length() > 0
}

///|
pub fn Diagram::without_children(self : Diagram) -> Diagram {
  { ..self, layers: [], scenarios: [], steps: [] }
}

///|
pub fn Diagram::has_legend(self : Diagram) -> Bool {
  match self.legend {
    Some(l) => !l.is_empty() && l.position != Hidden
    None => false
  }
}

///|
pub fn Diagram::has_shape(self : Diagram, condition : (Shape) -> Bool) -> Bool {
  for board in self.layers {
    if board.has_shape(condition) {
      return true
    }
  }
  for board in self.scenarios {
    if board.has_shape(condition) {
      return true
    }
  }
  for board in self.steps {
    if board.has_shape(condition) {
      return true
    }
  }
  for shape in self.shapes {
    if condition(shape) {
      return true
    }
  }
  false
}

///|
/// Get a board by path.
///
/// Supports both explicit and shorthand paths:
/// - Explicit: ["layers", "x", "scenarios", "y"]
/// - Shorthand: ["x", "y"]
pub fn Diagram::get_board(
  self : Diagram,
  board_path : Array[String],
) -> Diagram? {
  get_board_at(self, board_path, 0)
}

///|
fn get_board_at(
  current : Diagram,
  board_path : Array[String],
  index : Int,
) -> Diagram? {
  if index >= board_path.length() {
    return Some(current)
  }

  let head = board_path[index]
  if index == board_path.length() - 1 && current.name == head {
    return Some(current)
  }

  if head == "layers" || head == "scenarios" || head == "steps" {
    if index + 1 >= board_path.length() {
      return None
    }
    let board_name = board_path[index + 1]
    match find_named_board(current, head, board_name) {
      Some(child) => return get_board_at(child, board_path, index + 2)
      None => return None
    }
  }

  match find_named_board_any(current, head) {
    Some(child) => get_board_at(child, board_path, index + 1)
    None => None
  }
}

///|
fn find_named_board(
  current : Diagram,
  board_kind : String,
  board_name : String,
) -> Diagram? {
  if board_kind == "layers" {
    for board in current.layers {
      if board.name == board_name {
        return Some(board)
      }
    }
    return None
  }
  if board_kind == "scenarios" {
    for board in current.scenarios {
      if board.name == board_name {
        return Some(board)
      }
    }
    return None
  }
  if board_kind == "steps" {
    for board in current.steps {
      if board.name == board_name {
        return Some(board)
      }
    }
    return None
  }
  None
}

///|
fn find_named_board_any(current : Diagram, board_name : String) -> Diagram? {
  for board in current.layers {
    if board.name == board_name {
      return Some(board)
    }
  }
  for board in current.scenarios {
    if board.name == board_name {
      return Some(board)
    }
  }
  for board in current.steps {
    if board.name == board_name {
      return Some(board)
    }
  }
  None
}

///|
pub fn Diagram::get_objects(self : Diagram) -> Array[Shape] {
  self.shapes
}

///|
pub fn Diagram::get_edges(self : Diagram) -> Array[Connection] {
  self.connections
}

///|
pub fn Diagram::find_object(self : Diagram, id : String) -> Shape? {
  if self.root.id == id {
    return Some(self.root)
  }
  for obj in self.shapes {
    if obj.id == id {
      return Some(obj)
    }
  }
  if self.root.semantic_id == id {
    return Some(self.root)
  }
  for obj in self.shapes {
    if obj.semantic_id == id {
      return Some(obj)
    }
  }
  None
}

///|
pub fn Diagram::bytes(self : Diagram) -> String {
  let out = StringBuilder::new()
  out.write_string(diagram_shape_array_json(self.shapes))
  out.write_string(diagram_connection_array_json(self.connections))
  out.write_string(diagram_shape_json(self.root))
  match self.config {
    Some(config) => out.write_string(diagram_config_json(config))
    None => ()
  }
  for board in self.layers {
    out.write_string(board.bytes())
  }
  for board in self.scenarios {
    out.write_string(board.bytes())
  }
  for board in self.steps {
    out.write_string(board.bytes())
  }
  out.to_string()
}

///|
pub fn Diagram::hash_id(self : Diagram, salt : String?) -> String {
  let bytes = self.bytes()
  let hash = match salt {
    Some(s) => d2_fnv32a(bytes + s)
    None => d2_fnv32a(bytes)
  }
  "d2-\{d2_u32_decimal(hash)}"
}

///|
pub fn Diagram::with_render_hash_config(
  self : Diagram,
  theme_id : Int,
  dark_theme_id : Int?,
  sketch : Bool,
) -> Diagram {
  let updated_config = match self.config {
    Some(config) =>
      Config::from_parts(
        Some(sketch),
        Some(theme_id),
        dark_theme_id,
        config.theme_overrides,
        config.dark_theme_overrides,
        config.pad,
        config.center,
        config.layout_engine,
        config.scale,
        config.no_xml_tag,
        config.salt,
        config.omit_version,
        config.data,
      )
    None =>
      Config::from_parts(
        Some(sketch),
        Some(theme_id),
        dark_theme_id,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        None,
        Map([]),
      )
  }
  let layers : Array[Diagram] = []
  for board in self.layers {
    layers.push(board.with_render_hash_config(theme_id, dark_theme_id, sketch))
  }
  let scenarios : Array[Diagram] = []
  for board in self.scenarios {
    scenarios.push(
      board.with_render_hash_config(theme_id, dark_theme_id, sketch),
    )
  }
  let steps : Array[Diagram] = []
  for board in self.steps {
    steps.push(board.with_render_hash_config(theme_id, dark_theme_id, sketch))
  }
  { ..self, config: Some(updated_config), layers, scenarios, steps }
}

///|
pub fn Diagram::get_nested_corpus(self : Diagram) -> String {
  let corpus = StringBuilder::new()
  corpus.write_string(self.get_corpus())
  for board in self.layers {
    corpus.write_string(board.get_nested_corpus())
  }
  for board in self.scenarios {
    corpus.write_string(board.get_nested_corpus())
  }
  for board in self.steps {
    corpus.write_string(board.get_nested_corpus())
  }
  corpus.to_string()
}

///|
pub fn Diagram::get_corpus(self : Diagram) -> String {
  let corpus = StringBuilder::new()
  let mut appendix_count = 0
  let suppressed_shape_ids : Map[String, Bool] = Map([])
  for shape in self.shapes {
    if shape.shape_type == Class || shape.shape_type == SqlTable {
      for child_id in shape.child_ids {
        suppressed_shape_ids[child_id] = true
      }
    }
  }
  for shape in self.shapes {
    if suppressed_shape_ids.contains(shape.id) {
      continue
    }
    corpus.write_string(shape.label)
    match shape.tooltip {
      Some(tooltip) =>
        if tooltip != "" {
          corpus.write_string(tooltip)
          appendix_count = appendix_count + 1
          corpus.write_string(appendix_count.to_string())
        }
      None => ()
    }
    match shape.link {
      Some(link) =>
        if link != "" {
          corpus.write_string(link)
          appendix_count = appendix_count + 1
          corpus.write_string(appendix_count.to_string())
        }
      None => ()
    }
    corpus.write_string(shape_structured_corpus(shape, self.shapes))
  }
  for connection in self.connections {
    corpus.write_string(connection.label)
    match connection.src_arrowhead_label {
      Some(label) => corpus.write_string(label)
      None => ()
    }
    match connection.dst_arrowhead_label {
      Some(label) => corpus.write_string(label)
      None => ()
    }
  }
  match self.legend {
    Some(legend) =>
      if !legend.is_empty() {
        corpus.write_string(legend_corpus(legend))
      }
    None => ()
  }
  corpus.to_string()
}

///|
pub fn Shape::get_id(self : Shape) -> String {
  self.id
}

///|
pub fn Shape::get_box(self : Shape) -> @graph.Box? {
  self.box
}

///|
pub fn Shape::center(self : Shape) -> @graph.Point? {
  match self.box {
    Some(b) => Some(b.center())
    None => None
  }
}

///|
fn json_string(value : String) -> String {
  Json::string(value).stringify()
}

///|
fn json_bool(value : Bool) -> String {
  Json::boolean(value).stringify()
}

///|
fn json_int(value : Int) -> String {
  value.to_string()
}

///|
fn json_double(value : Double) -> String {
  Json::number(value).stringify()
}

///|
fn json_array(items : Array[String]) -> String {
  let joined = items.join(",")
  "[\{joined}]"
}

///|
fn json_string_array(items : Array[String]) -> String {
  let out : Array[String] = []
  for item in items {
    out.push(json_string(item))
  }
  json_array(out)
}

///|
fn json_object(fields : Array[(String, String)]) -> String {
  let out : Array[String] = []
  for field in fields {
    out.push("\{json_string(field.0)}:\{field.1}")
  }
  let joined = out.join(",")
  "{\{joined}}"
}

///|
fn style_value_json(value : @graph.StyleValue) -> String {
  json_string(value.get_value())
}

///|
fn style_input_json(style : @graph.StyleInput) -> String {
  let fields : Array[(String, String)] = []
  let push = fn(key : String, value : @graph.StyleValue?) {
    match value {
      Some(v) => fields.push((key, style_value_json(v)))
      None => ()
    }
  }
  push("opacity", style.opacity)
  push("stroke", style.stroke)
  push("fill", style.fill)
  push("strokeWidth", style.stroke_width)
  push("strokeDash", style.stroke_dash)
  push("borderRadius", style.border_radius)
  push("shadow", style.shadow)
  push("threeD", style.three_d)
  push("multiple", style.multiple)
  push("font", style.font)
  push("fontSize", style.font_size)
  push("fontColor", style.font_color)
  push("bold", style.bold)
  push("italic", style.italic)
  push("underline", style.underline)
  push("animated", style.animated)
  push("filled", style.filled)
  push("doubleBorder", style.double_border)
  push("textTransform", style.text_transform)
  push("fontFamily", style.font_family)
  push("fillPattern", style.fill_pattern)
  push("sketch", style.sketch)
  push("curved", style.curved)
  json_object(fields)
}

///|
fn point_json(point : @graph.Point) -> String {
  json_object([("x", json_double(point.x)), ("y", json_double(point.y))])
}

///|
fn point_array_json(points : Array[@graph.Point]) -> String {
  let items : Array[String] = []
  for point in points {
    items.push(point_json(point))
  }
  json_array(items)
}

///|
fn box_json(box : @graph.Box) -> String {
  json_object([
    ("x", json_double(box.x)),
    ("y", json_double(box.y)),
    ("width", json_double(box.width)),
    ("height", json_double(box.height)),
  ])
}

///|
fn shape_type_json(shape_type : @graph.ShapeType) -> String {
  json_string(shape_type.to_shape_string())
}

///|
fn arrowhead_json(arrowhead : @graph.ArrowheadType) -> String {
  json_string(arrowhead.to_string())
}

///|
fn diagram_shape_json(shape : Shape) -> String {
  let fields : Array[(String, String)] = [
    ("id", json_string(shape.id)),
    ("semanticId", json_string(shape.semantic_id)),
    ("label", json_string(shape.label)),
    ("shapeType", shape_type_json(shape.shape_type)),
    ("style", style_input_json(shape.style)),
    ("childIds", json_string_array(shape.child_ids)),
    ("zIndex", json_int(shape.z_index)),
    ("classes", json_string_array(shape.classes)),
    ("sqlConstraints", json_string_array(shape.sql_constraints)),
  ]
  match shape.box {
    Some(box) => fields.push(("box", box_json(box)))
    None => ()
  }
  match shape.label_box {
    Some(box) => fields.push(("labelBox", box_json(box)))
    None => ()
  }
  match shape.label_width {
    Some(value) => fields.push(("labelWidth", json_double(value)))
    None => ()
  }
  match shape.label_height {
    Some(value) => fields.push(("labelHeight", json_double(value)))
    None => ()
  }
  match shape.icon {
    Some(value) => fields.push(("icon", json_string(value)))
    None => ()
  }
  match shape.icon_position {
    Some(value) => fields.push(("iconPosition", json_string(value)))
    None => ()
  }
  match shape.tooltip {
    Some(value) => fields.push(("tooltip", json_string(value)))
    None => ()
  }
  match shape.tooltip_position {
    Some(value) => fields.push(("tooltipPosition", json_string(value)))
    None => ()
  }
  match shape.link {
    Some(value) => fields.push(("link", json_string(value)))
    None => ()
  }
  match shape.label_position {
    Some(value) => fields.push(("labelPosition", json_string(value)))
    None => ()
  }
  match shape.language {
    Some(value) => fields.push(("language", json_string(value)))
    None => ()
  }
  match shape.grid_rows {
    Some(value) => fields.push(("gridRows", json_int(value)))
    None => ()
  }
  match shape.grid_columns {
    Some(value) => fields.push(("gridColumns", json_int(value)))
    None => ()
  }
  match shape.grid_gap {
    Some(value) => fields.push(("gridGap", json_double(value)))
    None => ()
  }
  match shape.horizontal_gap {
    Some(value) => fields.push(("horizontalGap", json_double(value)))
    None => ()
  }
  match shape.vertical_gap {
    Some(value) => fields.push(("verticalGap", json_double(value)))
    None => ()
  }
  match shape.grid_column_span {
    Some(value) => fields.push(("gridColumnSpan", json_int(value)))
    None => ()
  }
  match shape.grid_row_span {
    Some(value) => fields.push(("gridRowSpan", json_int(value)))
    None => ()
  }
  match shape.near {
    Some(value) => fields.push(("near", json_string(value)))
    None => ()
  }
  json_object(fields)
}

///|
fn diagram_shape_array_json(shapes : Array[Shape]) -> String {
  let items : Array[String] = []
  for shape in shapes {
    items.push(diagram_shape_json(shape))
  }
  json_array(items)
}

///|
fn diagram_connection_json(connection : Connection) -> String {
  let fields : Array[(String, String)] = [
    ("index", json_int(connection.index)),
    ("srcId", json_string(connection.src_id)),
    ("dstId", json_string(connection.dst_id)),
    ("srcSyntaxId", json_string(connection.src_syntax_id)),
    ("dstSyntaxId", json_string(connection.dst_syntax_id)),
    ("srcLookupId", json_string(connection.src_lookup_id)),
    ("dstLookupId", json_string(connection.dst_lookup_id)),
    ("srcArrow", json_bool(connection.src_arrow)),
    ("dstArrow", json_bool(connection.dst_arrow)),
    ("srcArrowhead", arrowhead_json(connection.src_arrowhead)),
    ("dstArrowhead", arrowhead_json(connection.dst_arrowhead)),
    ("label", json_string(connection.label)),
    ("style", style_input_json(connection.style)),
    ("route", point_array_json(connection.route)),
    ("isCurve", json_bool(connection.is_curve)),
    ("zIndex", json_int(connection.z_index)),
    ("referenceCount", json_int(connection.reference_count)),
  ]
  match connection.src_arrowhead_label {
    Some(value) => fields.push(("srcArrowheadLabel", json_string(value)))
    None => ()
  }
  match connection.dst_arrowhead_label {
    Some(value) => fields.push(("dstArrowheadLabel", json_string(value)))
    None => ()
  }
  match connection.src_arrowhead_label_color {
    Some(value) => fields.push(("srcArrowheadLabelColor", json_string(value)))
    None => ()
  }
  match connection.dst_arrowhead_label_color {
    Some(value) => fields.push(("dstArrowheadLabelColor", json_string(value)))
    None => ()
  }
  match connection.src_anchor {
    Some(value) => fields.push(("srcAnchor", json_string(value)))
    None => ()
  }
  match connection.dst_anchor {
    Some(value) => fields.push(("dstAnchor", json_string(value)))
    None => ()
  }
  match connection.icon {
    Some(value) => fields.push(("icon", json_string(value)))
    None => ()
  }
  match connection.icon_position {
    Some(value) => fields.push(("iconPosition", json_string(value)))
    None => ()
  }
  match connection.icon_border_radius {
    Some(value) => fields.push(("iconBorderRadius", json_double(value)))
    None => ()
  }
  match connection.link {
    Some(value) => fields.push(("link", json_string(value)))
    None => ()
  }
  match connection.bend_points {
    Some(points) => fields.push(("bendPoints", point_array_json(points)))
    None => ()
  }
  match connection.label_box {
    Some(box) => fields.push(("labelBox", box_json(box)))
    None => ()
  }
  match connection.src_column_index {
    Some(value) => fields.push(("srcColumnIndex", json_int(value)))
    None => ()
  }
  match connection.dst_column_index {
    Some(value) => fields.push(("dstColumnIndex", json_int(value)))
    None => ()
  }
  json_object(fields)
}

///|
fn diagram_connection_array_json(connections : Array[Connection]) -> String {
  let items : Array[String] = []
  for connection in connections {
    items.push(diagram_connection_json(connection))
  }
  json_array(items)
}

///|
fn config_data_value_json(value : ConfigDataValue) -> String {
  match value {
    Text(text) => json_string(text)
    StringArray(values) => json_string_array(values)
  }
}

///|
fn theme_overrides_json(overrides : ThemeOverrides) -> String {
  let fields : Array[(String, String)] = []
  for entry in overrides.entries() {
    fields.push((entry.0.to_lower(), json_string(entry.1)))
  }
  json_object(fields)
}

///|
fn sorted_config_data_json(values : Map[String, ConfigDataValue]) -> String {
  let keys : Array[String] = []
  for key, _ in values {
    keys.push(key)
  }
  keys.sort_by(fn(a, b) { a.compare(b) })
  let fields : Array[(String, String)] = []
  for key in keys {
    fields.push((key, config_data_value_json(values[key])))
  }
  json_object(fields)
}

///|
fn diagram_config_json(config : Config) -> String {
  let fields : Array[(String, String)] = []
  match config.sketch {
    Some(value) => fields.push(("sketch", json_bool(value)))
    None => ()
  }
  match config.theme_id {
    Some(value) => fields.push(("themeID", json_int(value)))
    None => ()
  }
  match config.dark_theme_id {
    Some(value) => fields.push(("darkThemeID", json_int(value)))
    None => ()
  }
  match config.pad {
    Some(value) => fields.push(("pad", json_double(value)))
    None => ()
  }
  match config.center {
    Some(value) => fields.push(("center", json_bool(value)))
    None => ()
  }
  match config.layout_engine {
    Some(value) => fields.push(("layoutEngine", json_string(value)))
    None => ()
  }
  match config.theme_overrides {
    Some(overrides) =>
      if !overrides.is_empty() {
        fields.push(("themeOverrides", theme_overrides_json(overrides)))
      }
    None => ()
  }
  match config.dark_theme_overrides {
    Some(overrides) =>
      if !overrides.is_empty() {
        fields.push(("darkThemeOverrides", theme_overrides_json(overrides)))
      }
    None => ()
  }
  if !config.data.is_empty() {
    fields.push(("data", sorted_config_data_json(config.data)))
  }
  json_object(fields)
}

///|
fn legend_corpus(legend : @graph.LegendInput) -> String {
  let corpus = StringBuilder::new()
  match legend.title {
    Some(title) =>
      if title == "" {
        corpus.write_string("Legend")
      } else {
        corpus.write_string(title)
      }
    None => corpus.write_string("Legend")
  }
  for entry in legend.entries {
    match entry {
      ShapeEntry(label~, ..) => corpus.write_string(label)
      ConnectionEntry(label~, ..) => corpus.write_string(label)
      CustomEntry(..) => ()
      Separator => ()
    }
  }
  corpus.to_string()
}

///|
fn object_children(parent : Shape, all_shapes : Array[Shape]) -> Array[Shape] {
  let by_id : Map[String, Shape] = Map([])
  for shape in all_shapes {
    by_id[shape.id] = shape
  }
  let children : Array[Shape] = []
  for child_id in parent.child_ids {
    match by_id.get(child_id) {
      Some(shape) => children.push(shape)
      None => ()
    }
  }
  children
}

///|
fn short_shape_name(shape : Shape) -> String {
  @graph.syntax_path_last_display(shape.id)
}

///|
fn trim_surrounding_double_quotes(s : String) -> String {
  if s.length() >= 2 && s[0] == '"' && s[s.length() - 1] == '"' {
    let out = StringBuilder::new()
    for i = 1; i < s.length() - 1; i = i + 1 {
      match s[i].to_char() {
        Some(ch) => out.write_char(ch)
        None => ()
      }
    }
    out.to_string()
  } else {
    s
  }
}

///|
fn split_class_member_visibility(raw_name : String) -> (String, Bool, String) {
  if raw_name == "" {
    return ("", false, "")
  }
  match raw_name[0].to_char() {
    Some(ch) =>
      if ch == '+' || ch == '-' || ch == '#' || ch == '~' {
        let rest = StringBuilder::new()
        for i = 1; i < raw_name.length(); i = i + 1 {
          match raw_name[i].to_char() {
            Some(c) => rest.write_char(c)
            None => ()
          }
        }
        (rest.to_string(), true, ch.to_string())
      } else {
        (raw_name, false, "")
      }
    None => (raw_name, false, "")
  }
}

///|
fn class_member_corpus(child : Shape) -> String {
  let raw_name = short_shape_name(child)
  let (name_without_visibility, has_explicit_visibility, visibility) = split_class_member_visibility(
    raw_name,
  )
  let is_method = name_without_visibility.contains("(")
  let effective_visibility = if has_explicit_visibility {
    visibility
  } else {
    "+"
  }
  let display_name = effective_visibility + name_without_visibility
  let raw_type = trim_surrounding_double_quotes(child.label)
  let type_name = if raw_type == "" ||
    raw_type == raw_name ||
    raw_type == name_without_visibility ||
    raw_type == display_name {
    if is_method {
      "void"
    } else {
      ""
    }
  } else {
    raw_type
  }
  name_without_visibility + type_name + effective_visibility
}

///|
fn sql_table_constraint_abbr(child : Shape) -> String {
  let abbrs : Array[String] = []
  for constraint in child.sql_constraints {
    abbrs.push(
      match constraint {
        "primary_key" => "PK"
        "foreign_key" => "FK"
        "unique" => "UNQ"
        _ => constraint
      },
    )
  }
  abbrs.join(", ")
}

///|
fn shape_structured_corpus(shape : Shape, all_shapes : Array[Shape]) -> String {
  let corpus = StringBuilder::new()
  match shape.shape_type {
    Class =>
      for child in object_children(shape, all_shapes) {
        corpus.write_string(class_member_corpus(child))
      }
    SqlTable =>
      for child in object_children(shape, all_shapes) {
        corpus.write_string(short_shape_name(child))
        corpus.write_string(child.label)
        corpus.write_string(sql_table_constraint_abbr(child))
      }
    _ => ()
  }
  corpus.to_string()
}

///|
fn d2_fnv32a(text : String) -> Int {
  let mut h : Int = -2128831035
  let bytes = @utf8.encode(text[:])
  for b in bytes {
    h = (h ^ b.to_int()) & 0xffffffff
    h = (h * 16777619) & 0xffffffff
  }
  h
}

///|
fn d2_u32_decimal(v : Int) -> String {
  let unsigned = if v < 0 {
    Int64::from_int(v) + 4294967296L
  } else {
    Int64::from_int(v)
  }
  unsigned.to_string()
}

///|
fn project_double_for_target(value : Double) -> Double {
  value.to_int().to_double()
}

///|
fn project_box_for_target(box : @graph.Box?) -> @graph.Box? {
  match box {
    Some(b) =>
      Some(
        @graph.Box::new(
          project_double_for_target(b.x),
          project_double_for_target(b.y),
          project_double_for_target(b.width),
          project_double_for_target(b.height),
        ),
      )
    None => None
  }
}

///|
const D2_ROUTE_TRUNCATE_SCALE : Double = 1000.0

///|
fn truncate_decimals_like_d2(v : Double) -> Double {
  (v * D2_ROUTE_TRUNCATE_SCALE).to_int().to_double() / D2_ROUTE_TRUNCATE_SCALE
}

///|
fn truncate_float32_like_d2(v : Double) -> Double {
  Float::from_double(v).to_double()
}

///|
fn normalize_route_like_d2(route : Array[@graph.Point]) -> Array[@graph.Point] {
  let out : Array[@graph.Point] = []
  for p in route {
    let x = truncate_float32_like_d2(truncate_decimals_like_d2(p.x))
    let y = truncate_float32_like_d2(truncate_decimals_like_d2(p.y))
    out.push(@graph.Point::new(x, y))
  }
  out
}

///|
fn normalize_bend_points_like_d2(
  bend_points : Array[@graph.Point]?,
) -> Array[@graph.Point]? {
  match bend_points {
    Some(points) => Some(normalize_route_like_d2(points))
    None => None
  }
}

///|

///|
fn shape_from_object(obj : @graph.ObjectInput) -> Shape {
  let projected_metrics_box = project_box_for_target(obj.label_box)
  let projected_label_box = if obj.child_ids.is_empty() {
    None
  } else {
    // Keep container label anchor coordinates in full precision.
    // The reference computes text placement from float label anchors; truncating here
    // introduces 0.5px drift in centered container titles (e.g. legend labels).
    obj.label_box
  }
  let label_width = match projected_metrics_box {
    Some(label_box) => Some(label_box.width)
    None => None
  }
  let label_height = match projected_metrics_box {
    Some(label_box) => Some(label_box.height)
    None => None
  }
  {
    id: obj.abs_id_syntax,
    semantic_id: obj.id,
    label: obj.label,
    shape_type: obj.shape_type,
    style: obj.style,
    box: project_box_for_target(obj.box),
    label_box: projected_label_box,
    label_width,
    label_height,
    child_ids: obj.child_ids,
    z_index: obj.z_index,
    icon: obj.icon,
    icon_position: obj.icon_position,
    tooltip: obj.tooltip,
    tooltip_position: obj.tooltip_position,
    link: obj.link,
    label_position: obj.label_position,
    classes: obj.classes,
    language: obj.language,
    sql_constraints: obj.sql_constraints,
    grid_rows: obj.grid_rows,
    grid_columns: obj.grid_columns,
    grid_gap: obj.grid_gap,
    horizontal_gap: obj.horizontal_gap,
    vertical_gap: obj.vertical_gap,
    grid_column_span: obj.grid_column_span,
    grid_row_span: obj.grid_row_span,
    near: obj.near,
  }
}

///|
fn connection_from_edge(
  edge : @graph.EdgeInput,
  src_id? : String = edge.src_id,
  dst_id? : String = edge.dst_id,
  src_syntax_id? : String = edge.src_id_syntax,
  dst_syntax_id? : String = edge.dst_id_syntax,
  src_lookup_id? : String = edge.src_id_syntax,
  dst_lookup_id? : String = edge.dst_id_syntax,
  identity_scope_depth? : Int = @graph.syntax_path_depth(src_syntax_id),
) -> Connection {
  {
    index: edge.index,
    src_id,
    dst_id,
    src_syntax_id,
    dst_syntax_id,
    src_lookup_id,
    dst_lookup_id,
    identity_scope_depth,
    classes: edge.classes,
    src_arrow: edge.src_arrow,
    dst_arrow: edge.dst_arrow,
    src_arrowhead: edge.src_arrowhead,
    dst_arrowhead: edge.dst_arrowhead,
    src_arrowhead_label: edge.src_arrowhead_label,
    dst_arrowhead_label: edge.dst_arrowhead_label,
    src_arrowhead_label_color: edge.src_arrowhead_label_color,
    dst_arrowhead_label_color: edge.dst_arrowhead_label_color,
    src_anchor: edge.src_anchor,
    dst_anchor: edge.dst_anchor,
    label: edge.label,
    icon: edge.icon,
    icon_position: edge.icon_position,
    icon_border_radius: edge.icon_border_radius,
    link: edge.link,
    style: edge.style,
    route: normalize_route_like_d2(edge.route),
    bend_points: normalize_bend_points_like_d2(edge.bend_points),
    is_curve: edge.is_curve,
    z_index: edge.z_index,
    reference_count: edge.reference_count,
    label_box: edge.label_box,
    src_column_index: edge.src_column_index,
    dst_column_index: edge.dst_column_index,
  }
}

///|
fn sequence_connection_syntax_id(
  id : String,
  object_by_abs_id : Map[String, @graph.ObjectInput],
) -> String {
  let parts = @graph.split_syntax_path(id)
  let kept : Array[String] = []
  let prefix : Array[String] = []
  for part in parts {
    prefix.push(part)
    let abs_id = prefix.join(".")
    let is_group = match object_by_abs_id.get(abs_id) {
      Some(obj) => obj.classes.contains("__diago_sequence_group")
      None => false
    }
    if !is_group {
      kept.push(part)
    }
  }
  kept.join(".")
}

///|
fn sequence_participant_ids(graph : @graph.GraphInput) -> Array[String] {
  if !graph.root.child_ids.is_empty() {
    let ids : Array[String] = []
    let exists : Map[String, Bool] = Map([])
    for obj in graph.objects {
      exists[obj.abs_id_syntax] = true
    }
    for id in graph.root.child_ids {
      if exists.contains(id) {
        guard graph.find_object(id) is Some(obj) else { continue }
        if !obj.classes.contains("__diago_sequence_group") {
          ids.push(id)
        }
      }
    }
    if !ids.is_empty() {
      return ids
    }
  }
  let child_ids : Map[String, Bool] = Map([])
  for obj in graph.objects {
    for child_id in obj.child_ids {
      child_ids[child_id] = true
    }
  }
  let ids : Array[String] = []
  for obj in graph.objects {
    if !child_ids.contains(obj.abs_id_syntax) {
      ids.push(obj.abs_id_syntax)
    }
  }
  ids
}

///|
fn sequence_parent_id_by_child_id(
  graph : @graph.GraphInput,
) -> Map[String, String] {
  let out : Map[String, String] = Map([])
  for child_id in graph.root.child_ids {
    out[child_id] = graph.root.abs_id_syntax
  }
  for obj in graph.objects {
    for child_id in obj.child_ids {
      out[child_id] = obj.abs_id_syntax
    }
  }
  out
}

///|
fn sequence_participant_alias_by_abs_id(
  graph : @graph.GraphInput,
  participant_ids : Array[String],
  object_by_abs_id : Map[String, @graph.ObjectInput],
) -> Map[String, String] {
  let participant_by_local_id : Map[String, String] = Map([])
  for participant_id in participant_ids {
    match object_by_abs_id.get(participant_id) {
      Some(obj) => {
        participant_by_local_id[obj.id_val] = obj.abs_id_syntax
        participant_by_local_id[obj.id_syntax] = obj.abs_id_syntax
      }
      None => participant_by_local_id[participant_id] = participant_id
    }
  }
  let out : Map[String, String] = Map([])
  for participant_id in participant_ids {
    out[participant_id] = participant_id
  }
  for obj in graph.objects {
    if out.contains(obj.abs_id_syntax) {
      continue
    }
    match participant_by_local_id.get(obj.id_val) {
      Some(participant_id) => out[obj.abs_id_syntax] = participant_id
      None =>
        match participant_by_local_id.get(obj.id_syntax) {
          Some(participant_id) => out[obj.abs_id_syntax] = participant_id
          None => ()
        }
    }
  }
  out
}

///|
fn sequence_top_participant_id(
  id : String,
  participant_exists : Map[String, Bool],
  parent_id_by_child_id : Map[String, String],
  participant_alias_by_abs_id : Map[String, String],
) -> String {
  sequence_resolve_top_participant_id(
    id, id, participant_exists, parent_id_by_child_id, participant_alias_by_abs_id,
  )
}

///|
fn sequence_resolve_top_participant_id(
  current : String,
  fallback : String,
  participant_exists : Map[String, Bool],
  parent_id_by_child_id : Map[String, String],
  participant_alias_by_abs_id : Map[String, String],
) -> String {
  match participant_alias_by_abs_id.get(current) {
    Some(mapped_id) => mapped_id
    None =>
      if participant_exists.contains(current) {
        current
      } else {
        match parent_id_by_child_id.get(current) {
          Some(parent_id) =>
            sequence_resolve_top_participant_id(
              parent_id, fallback, participant_exists, parent_id_by_child_id, participant_alias_by_abs_id,
            )
          None => fallback
        }
      }
  }
}

///|
const D2_SEQUENCE_LIFELINE_STROKE_WIDTH : Int = 2

///|
const D2_SEQUENCE_LIFELINE_STROKE_DASH : Int = 6

///|
const D2_SEQUENCE_MESSAGE_Z_INDEX : Int = 4

///|
const D2_SEQUENCE_MIN_MESSAGE_DISTANCE : Double = 30.0

///|
const D2_SEQUENCE_VERTICAL_PAD : Double = 40.0

///|
const D2_SEQUENCE_LIFELINE_LABEL_PAD : Double = 5.0

///|
fn sequence_lifeline_start_y(
  graph : @graph.GraphInput,
  shape : Shape,
  box : @graph.Box,
) -> Double {
  let mut actor_bottom = box.y + box.height
  match graph.find_object(shape.id) {
    Some(obj) =>
      match obj.box {
        Some(raw_box) => actor_bottom = raw_box.y + raw_box.height
        None => ()
      }
    None => ()
  }
  if shape.label == "" {
    return actor_bottom
  }
  let outside_bottom = match shape.label_position {
    Some(position) => position.has_prefix("OUTSIDE_BOTTOM_")
    None => shape.shape_type == Person || shape.shape_type == Image
  }
  if outside_bottom {
    match shape.label_height {
      Some(height) =>
        actor_bottom = actor_bottom + height + D2_SEQUENCE_LIFELINE_LABEL_PAD
      None => ()
    }
  }
  actor_bottom
}

///|
fn sequence_lifeline_style(
  actor_style : @graph.StyleInput,
) -> @graph.StyleInput {
  let stroke_dash = match actor_style.stroke_dash {
    Some(v) => Some(v)
    None => Some(@graph.StyleValue::from_int(D2_SEQUENCE_LIFELINE_STROKE_DASH))
  }
  @graph.StyleInput::from_parts(
    None,
    match actor_style.stroke {
      Some(value) =>
        if value.get_value() == "B1" {
          Some(@graph.StyleValue::from_string("B2"))
        } else {
          Some(value)
        }
      None => Some(@graph.StyleValue::from_string("B2"))
    },
    None,
    Some(@graph.StyleValue::from_int(D2_SEQUENCE_LIFELINE_STROKE_WIDTH)),
    stroke_dash,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
    None,
  )
}

///|
fn append_sequence_lifeline_connections(
  graph : @graph.GraphInput,
  shapes : Array[Shape],
  connections : Array[Connection],
) -> Unit {
  if graph.root.shape_type != SequenceDiagram {
    return
  }

  let shape_by_id : Map[String, Shape] = Map([])
  for shape in shapes {
    shape_by_id[shape.id] = shape
  }

  let participant_ids = sequence_participant_ids(graph)
  if participant_ids.is_empty() {
    return
  }
  for i = 0; i < connections.length(); i = i + 1 {
    let conn = connections[i]
    if conn.z_index < D2_SEQUENCE_MESSAGE_Z_INDEX {
      connections[i] = { ..conn, z_index: D2_SEQUENCE_MESSAGE_Z_INDEX }
    }
  }

  let mut end_y = 0.0
  for conn in connections {
    for point in conn.route {
      if point.y > end_y {
        end_y = point.y
      }
    }
  }
  for note in graph.sequence_notes {
    let note_box = note.2
    let note_bottom = note_box.y + note_box.height
    if note_bottom > end_y {
      end_y = note_bottom
    }
  }

  for id in participant_ids {
    match shape_by_id.get(id) {
      Some(shape) =>
        match shape.box {
          Some(box) => {
            let actor_bottom = box.y + box.height
            if actor_bottom > end_y {
              end_y = actor_bottom
            }
          }
          None => ()
        }
      None => ()
    }
  }

  if end_y <= 0.0 {
    return
  }
  let lifeline_end_y = end_y +
    D2_SEQUENCE_MIN_MESSAGE_DISTANCE +
    D2_SEQUENCE_VERTICAL_PAD

  append_sequence_lifelines(
    graph, participant_ids, shape_by_id, lifeline_end_y, connections,
  )
}

///|
fn append_sequence_lifelines(
  graph : @graph.GraphInput,
  participant_ids : Array[String],
  shape_by_id : Map[String, Shape],
  lifeline_end_y : Double,
  connections : Array[Connection],
) -> Unit {
  let mut next_index = 0
  for conn in connections {
    if conn.index >= next_index {
      next_index = conn.index + 1
    }
  }

  for id in participant_ids {
    match shape_by_id.get(id) {
      Some(shape) =>
        match shape.box {
          Some(box) => {
            let start_y = sequence_lifeline_start_y(graph, shape, box)
            let x = box.x + box.width / 2.0
            let style = sequence_lifeline_style(shape.style)
            connections.push({
              index: next_index,
              src_id: id,
              dst_id: "",
              src_syntax_id: id,
              dst_syntax_id: "",
              src_lookup_id: id,
              dst_lookup_id: "",
              identity_scope_depth: @graph.syntax_path_depth(id),
              classes: [],
              src_arrow: false,
              dst_arrow: false,
              src_arrowhead: None,
              dst_arrowhead: None,
              src_arrowhead_label: None,
              dst_arrowhead_label: None,
              src_arrowhead_label_color: None,
              dst_arrowhead_label_color: None,
              src_anchor: None,
              dst_anchor: None,
              label: "",
              icon: None,
              icon_position: None,
              icon_border_radius: None,
              link: None,
              style,
              route: [
                @graph.Point::new(
                  truncate_float32_like_d2(truncate_decimals_like_d2(x)),
                  truncate_float32_like_d2(truncate_decimals_like_d2(start_y)),
                ),
                @graph.Point::new(
                  truncate_float32_like_d2(truncate_decimals_like_d2(x)),
                  lifeline_end_y,
                ),
              ],
              bend_points: None,
              is_curve: false,
              z_index: 1,
              reference_count: 1,
              label_box: None,
              src_column_index: None,
              dst_column_index: None,
            })
            next_index += 1
          }
          None => ()
        }
      None => ()
    }
  }
}

///|
fn nested_sequence_participant_for_endpoint(
  endpoint_id : String,
  participant_ids : Array[String],
) -> String? {
  let endpoint_parts = @graph.split_syntax_path(endpoint_id)
  for participant_id in participant_ids {
    let participant_key = @graph.syntax_path_last(participant_id).to_lower()
    for part in endpoint_parts {
      if part.to_lower() == participant_key {
        return Some(participant_id)
      }
    }
  }
  None
}

///|
fn append_nested_sequence_lifeline_connections(
  graph : @graph.GraphInput,
  shapes : Array[Shape],
  connections : Array[Connection],
  activation_ids : Map[String, Bool],
) -> Unit {
  if graph.root.shape_type == SequenceDiagram {
    return
  }
  let shape_by_id : Map[String, Shape] = Map([])
  for shape in shapes {
    shape_by_id[shape.id] = shape
  }
  for container in shapes {
    if container.shape_type != SequenceDiagram {
      continue
    }
    let participant_ids : Array[String] = []
    for id in container.child_ids {
      match shape_by_id.get(id) {
        Some(shape) =>
          if !shape.classes.contains("__diago_sequence_group") {
            participant_ids.push(id)
          }
        None => ()
      }
    }
    if participant_ids.is_empty() {
      continue
    }
    let container_box = match container.box {
      Some(box) => box
      None => continue
    }
    let descendant_ids : Map[String, Bool] = Map([])
    let queue : Array[String] = participant_ids.copy()
    let mut cursor = 0
    while cursor < queue.length() {
      let id = queue[cursor]
      cursor += 1
      if descendant_ids.contains(id) {
        continue
      }
      descendant_ids[id] = true
      match shape_by_id.get(id) {
        Some(shape) =>
          for child_id in shape.child_ids {
            queue.push(child_id)
          }
        None => ()
      }
    }

    let mut end_y = 0.0
    for i = 0; i < connections.length(); i = i + 1 {
      let connection = connections[i]
      let endpoints_inside = if connection.route.is_empty() {
        false
      } else {
        let first = connection.route[0]
        let last = connection.route[connection.route.length() - 1]
        first.x >= container_box.x &&
        first.y >= container_box.y &&
        first.x <= container_box.x + container_box.width &&
        first.y <= container_box.y + container_box.height &&
        last.x >= container_box.x &&
        last.y >= container_box.y &&
        last.x <= container_box.x + container_box.width &&
        last.y <= container_box.y + container_box.height
      }
      let semantic_descendants = descendant_ids.contains(
          connection.src_lookup_id,
        ) &&
        descendant_ids.contains(connection.dst_lookup_id)
      if !semantic_descendants && !endpoints_inside {
        continue
      }
      let src_participant = nested_sequence_participant_for_endpoint(
        connection.src_lookup_id,
        participant_ids,
      )
      let dst_participant = nested_sequence_participant_for_endpoint(
        connection.dst_lookup_id,
        participant_ids,
      )
      let normalized_connection = match (src_participant, dst_participant) {
        (Some(src), Some(dst)) =>
          {
            ..connection,
            src_id: if activation_ids.contains(connection.src_lookup_id) {
              connection.src_id
            } else {
              src
            },
            dst_id: if activation_ids.contains(connection.dst_lookup_id) {
              connection.dst_id
            } else {
              dst
            },
            src_lookup_id: if activation_ids.contains(connection.src_lookup_id) {
              connection.src_lookup_id
            } else {
              src
            },
            dst_lookup_id: if activation_ids.contains(connection.dst_lookup_id) {
              connection.dst_lookup_id
            } else {
              dst
            },
            z_index: if connection.z_index < D2_SEQUENCE_MESSAGE_Z_INDEX {
              D2_SEQUENCE_MESSAGE_Z_INDEX
            } else {
              connection.z_index
            },
          }
        _ =>
          if connection.z_index < D2_SEQUENCE_MESSAGE_Z_INDEX {
            { ..connection, z_index: D2_SEQUENCE_MESSAGE_Z_INDEX }
          } else {
            connection
          }
      }
      connections[i] = normalized_connection
      for point in connection.route {
        if point.y > end_y {
          end_y = point.y
        }
      }
    }
    for participant_id in participant_ids {
      match shape_by_id.get(participant_id) {
        Some(shape) =>
          match shape.box {
            Some(box) =>
              if box.y + box.height > end_y {
                end_y = box.y + box.height
              }
            None => ()
          }
        None => ()
      }
    }
    for note in graph.sequence_notes {
      let note_box = note.2
      if note_box.x >= container_box.x &&
        note_box.y >= container_box.y &&
        note_box.x + note_box.width <= container_box.x + container_box.width &&
        note_box.y + note_box.height <= container_box.y + container_box.height &&
        note_box.y + note_box.height > end_y {
        end_y = note_box.y + note_box.height
      }
    }
    if end_y <= 0.0 {
      continue
    }
    append_sequence_lifelines(
      graph,
      participant_ids,
      shape_by_id,
      end_y + D2_SEQUENCE_MIN_MESSAGE_DISTANCE + D2_SEQUENCE_VERTICAL_PAD,
      connections,
    )
  }
}

///|
/// Convert a laid-out graph to a render-ready diagram.
///
/// This is the only conversion renderers should need.
pub fn from_graph(graph : @graph.GraphInput, config : Config?) -> Diagram {
  let shapes : Array[Shape] = []
  let hidden_sequence_aliases : Map[String, Bool] = Map([])
  let sequence_aliases : Map[String, String] = Map([])
  let sequence_parents : Map[String, String] = Map([])
  if graph.root.shape_type == SequenceDiagram {
    let object_by_abs_id : Map[String, @graph.ObjectInput] = Map([])
    for obj in graph.objects {
      object_by_abs_id[obj.abs_id_syntax] = obj
    }
    let participant_ids = sequence_participant_ids(graph)
    let participant_set : Map[String, Bool] = Map([])
    for id in participant_ids {
      participant_set[id] = true
    }
    let aliases = sequence_participant_alias_by_abs_id(
      graph, participant_ids, object_by_abs_id,
    )
    for id, participant_id in aliases {
      sequence_aliases[id] = participant_id
    }
    let parents = sequence_parent_id_by_child_id(graph)
    for id, parent in parents {
      sequence_parents[id] = parent
    }
    for obj in graph.objects {
      if !participant_set.contains(obj.abs_id_syntax) &&
        aliases.contains(obj.abs_id_syntax) &&
        !obj.child_ids.is_empty() {
        hidden_sequence_aliases[obj.abs_id_syntax] = true
      }
    }
  }
  let activation_ids : Map[String, Bool] = Map([])
  for activation in graph.activation_boxes {
    activation_ids[activation.0] = true
  }
  for obj in graph.objects {
    if hidden_sequence_aliases.contains(obj.abs_id_syntax) {
      continue
    }
    let base_shape = shape_from_object(obj)
    let shape = if obj.classes.contains("__diago_sequence_note") {
      let mut current = obj.abs_id_syntax
      let mut render_id = obj.abs_id_syntax
      while true {
        match sequence_parents.get(current) {
          Some(parent) =>
            match sequence_aliases.get(parent) {
              Some(participant_id) => {
                render_id = participant_id + "." + obj.id_syntax
                break
              }
              None => current = parent
            }
          None => break
        }
      }
      { ..base_shape, id: render_id }
    } else {
      base_shape
    }
    shapes.push(
      if activation_ids.contains(obj.abs_id_syntax) {
        {
          ..shape,
          label: "",
          label_width: None,
          label_height: None,
          z_index: 2,
        }
      } else if shape.classes.contains("__diago_sequence_group") {
        { ..shape, z_index: 3 }
      } else if shape.classes.contains("__diago_sequence_note") {
        { ..shape, z_index: 5 }
      } else {
        shape
      },
    )
  }
  let connections : Array[Connection] = []
  if graph.root.shape_type == SequenceDiagram {
    let object_by_abs_id : Map[String, @graph.ObjectInput] = Map([])
    for obj in graph.objects {
      object_by_abs_id[obj.abs_id_syntax] = obj
    }
    let participant_ids = sequence_participant_ids(graph)
    let participant_exists : Map[String, Bool] = Map([])
    for participant_id in participant_ids {
      participant_exists[participant_id] = true
    }
    let parent_id_by_child_id = sequence_parent_id_by_child_id(graph)
    let participant_alias_by_abs_id = sequence_participant_alias_by_abs_id(
      graph, participant_ids, object_by_abs_id,
    )
    for edge in graph.edges {
      let src_lookup_id = sequence_top_participant_id(
        edge.src_id_syntax,
        participant_exists,
        parent_id_by_child_id,
        participant_alias_by_abs_id,
      )
      let dst_lookup_id = sequence_top_participant_id(
        edge.dst_id_syntax,
        participant_exists,
        parent_id_by_child_id,
        participant_alias_by_abs_id,
      )
      let src_display_id = if activation_ids.contains(edge.src_id_syntax) {
        edge.src_id
      } else {
        src_lookup_id
      }
      let dst_display_id = if activation_ids.contains(edge.dst_id_syntax) {
        edge.dst_id
      } else {
        dst_lookup_id
      }
      let src_syntax_id = sequence_connection_syntax_id(
        edge.src_id_syntax,
        object_by_abs_id,
      )
      let dst_syntax_id = sequence_connection_syntax_id(
        edge.dst_id_syntax,
        object_by_abs_id,
      )
      connections.push(
        connection_from_edge(
          edge,
          src_id=src_display_id,
          dst_id=dst_display_id,
          src_syntax_id~,
          dst_syntax_id~,
          src_lookup_id~,
          dst_lookup_id~,
          identity_scope_depth=@graph.syntax_path_depth(edge.src_id_syntax),
        ),
      )
    }
  } else {
    let object_by_abs_id : Map[String, @graph.ObjectInput] = Map([])
    for obj in graph.objects {
      object_by_abs_id[obj.abs_id_syntax] = obj
    }
    for edge in graph.edges {
      let src_syntax_id = sequence_connection_syntax_id(
        edge.src_id_syntax,
        object_by_abs_id,
      )
      let dst_syntax_id = sequence_connection_syntax_id(
        edge.dst_id_syntax,
        object_by_abs_id,
      )
      connections.push(
        connection_from_edge(
          edge,
          src_syntax_id~,
          dst_syntax_id~,
          identity_scope_depth=@graph.syntax_path_depth(edge.src_id_syntax),
        ),
      )
    }
  }
  append_sequence_lifeline_connections(graph, shapes, connections)
  append_nested_sequence_lifeline_connections(
    graph, shapes, connections, activation_ids,
  )
  let layers : Array[Diagram] = []
  for layer in graph.layers {
    layers.push(from_graph(layer.graph, config))
  }
  let scenarios : Array[Diagram] = []
  for scenario in graph.scenarios {
    scenarios.push(from_graph(scenario.graph, config))
  }
  let steps : Array[Diagram] = []
  for step in graph.steps {
    steps.push(from_graph(step.graph, config))
  }
  {
    name: graph.name,
    config,
    is_folder_only: graph.is_folder_only,
    description: "",
    font_family: None,
    mono_font_family: None,
    root: shape_from_object(graph.root),
    shapes,
    connections,
    activation_boxes: graph.activation_boxes,
    sequence_notes: graph.sequence_notes,
    sequence_fragments: graph.sequence_fragments_layout,
    layers,
    scenarios,
    steps,
    legend: graph.legend,
  }
}