// 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.

///|
/// Engine API: layout contract shared between layout engines and exporters.
///
/// The layout pipeline is intentionally functional:
/// - engines take an immutable graph
/// - engines return a `@graph.LayoutPatch`
/// - callers apply the patch to obtain a new graph

///|
/// Layout configuration
pub struct LayoutConfig {
  /// Horizontal spacing between objects
  horizontal_spacing : Double
  /// Vertical spacing between objects
  vertical_spacing : Double
  /// Padding inside containers
  container_padding : Double
  /// Default object width
  default_width : Double
  /// Default object height
  default_height : Double
  /// Edge padding from objects
  edge_padding : Double
} derive(Show)

///|
/// Layout direction
pub(all) enum Direction {
  Down
  Right
  Up
  Left
} derive(Eq, Show)

///|
/// Layout error
pub(all) suberror LayoutError {
  InvalidGraph(String)
  CycleDetected(String)
  Unsupported(String)
} derive(Eq, Show)

///|
/// Construct an `engine_api.LayoutError` from outside this package.
pub fn invalid_graph(message : String) -> LayoutError {
  LayoutError::InvalidGraph(message)
}

///|
pub fn cycle_detected(message : String) -> LayoutError {
  LayoutError::CycleDetected(message)
}

///|
pub fn unsupported(message : String) -> LayoutError {
  LayoutError::Unsupported(message)
}

///|
pub fn LayoutConfig::new() -> LayoutConfig {
  {
    horizontal_spacing: 60.0,
    vertical_spacing: 100.0,
    container_padding: 50.0,
    default_width: 100.0,
    default_height: 60.0,
    edge_padding: 20.0,
  }
}

///|
pub fn LayoutConfig::with_spacing(
  horizontal : Double,
  vertical : Double,
) -> LayoutConfig {
  let config = LayoutConfig::new()
  { ..config, horizontal_spacing: horizontal, vertical_spacing: vertical }
}

///|
/// Layout engine interface.
///
/// Engines must not mutate their input graph. They return a patch that can be
/// applied by the caller.
pub(open) trait LayoutEngine {
  layout(Self, @graph.GraphInput, LayoutConfig, Direction) -> @graph.LayoutPatch raise LayoutError
}

///|
fn get_short_id(id : String) -> String {
  let mut last_dot = -1
  for i = 0; i < id.length(); i = i + 1 {
    if id[i] == '.' {
      last_dot = i
    }
  }
  if last_dot >= 0 && last_dot < id.length() - 1 {
    let buf = StringBuilder::new()
    for i = last_dot + 1; i < id.length(); i = i + 1 {
      match id[i].to_char() {
        Some(c) => buf.write_char(c)
        None => ()
      }
    }
    buf.to_string()
  } else {
    id
  }
}

///|
fn get_parent_id(id : String) -> String? {
  let mut last_dot = -1
  for i = 0; i < id.length(); i = i + 1 {
    if id[i] == '.' {
      last_dot = i
    }
  }
  if last_dot <= 0 {
    return None
  }
  let buf = StringBuilder::new()
  for i = 0; i < last_dot; i = i + 1 {
    match id[i].to_char() {
      Some(c) => buf.write_char(c)
      None => ()
    }
  }
  Some(buf.to_string())
}

///|
fn normalize_sequence_root(graph : @graph.GraphInput) -> @graph.GraphInput {
  if graph.root.shape_type != @graph.SequenceDiagram {
    return graph
  }
  let child_ids : Map[String, Bool] = {}
  for obj in graph.objects {
    for child_id in obj.child_ids {
      child_ids[child_id] = true
    }
  }
  let root_child_ids : Array[String] = []
  for obj in graph.objects {
    if not(child_ids.contains(obj.id)) {
      root_child_ids.push(obj.id)
    }
  }
  let r = graph.root
  let root = @graph.ObjectInput::from_parts(
    r.id,
    r.label,
    r.shape_type,
    r.style,
    r.box,
    r.label_box,
    root_child_ids,
    r.z_index,
    r.icon,
    r.tooltip,
    r.link,
    r.classes,
    r.grid_rows,
    r.grid_columns,
    r.grid_gap,
    r.horizontal_gap,
    r.vertical_gap,
    r.grid_column_span,
    r.grid_row_span,
    r.near,
    r.top,
    r.left,
  )
  @graph.GraphInput::from_parts(
    graph.name,
    root,
    graph.objects,
    graph.edges,
    graph.sequence_fragments,
    graph.activation_boxes,
    graph.sequence_notes,
    graph.sequence_fragments_layout,
    graph.layers,
    graph.scenarios,
    graph.steps,
    graph.legend,
  )
}

///|
fn find_sql_column_index(
  object_by_id : Map[String, @graph.ObjectInput],
  field_id : String,
) -> Int? {
  let parent_id = match get_parent_id(field_id) {
    Some(p) => p
    None => return None
  }
  let parent = match object_by_id.get(parent_id) {
    Some(p) => p
    None => return None
  }
  if parent.shape_type != @graph.SqlTable {
    return None
  }
  let field_name = get_short_id(field_id)
  let mut i = 0
  for child_id in parent.child_ids {
    if get_short_id(child_id) == field_name {
      return Some(i)
    }
    i = i + 1
  }
  None
}

///|
fn assign_sql_column_indices(graph : @graph.GraphInput) -> @graph.GraphInput {
  let object_by_id : Map[String, @graph.ObjectInput] = {}
  object_by_id[graph.root.id] = graph.root
  for obj in graph.objects {
    object_by_id[obj.id] = obj
  }
  let edges : Array[@graph.EdgeInput] = []
  for e in graph.edges {
    let src_column_index = match e.src_column_index {
      Some(i) => Some(i)
      None => find_sql_column_index(object_by_id, e.src_id)
    }
    let dst_column_index = match e.dst_column_index {
      Some(i) => Some(i)
      None => find_sql_column_index(object_by_id, e.dst_id)
    }
    edges.push(
      @graph.EdgeInput::from_parts(
        e.index,
        e.src_id,
        e.dst_id,
        e.src_arrow,
        e.dst_arrow,
        e.src_arrowhead,
        e.dst_arrowhead,
        e.src_arrowhead_label,
        e.dst_arrowhead_label,
        e.src_arrowhead_label_color,
        e.dst_arrowhead_label_color,
        e.src_anchor,
        e.dst_anchor,
        e.label,
        e.style,
        e.route,
        e.bend_points,
        e.is_curve,
        e.z_index,
        e.reference_count,
        e.label_box,
        src_column_index,
        dst_column_index,
      ),
    )
  }
  @graph.GraphInput::from_parts(
    graph.name,
    graph.root,
    graph.objects,
    edges,
    graph.sequence_fragments,
    graph.activation_boxes,
    graph.sequence_notes,
    graph.sequence_fragments_layout,
    graph.layers,
    graph.scenarios,
    graph.steps,
    graph.legend,
  )
}

///|
fn prepare_graph_for_layout(graph : @graph.GraphInput) -> @graph.GraphInput {
  let graph = normalize_sequence_root(graph)
  assign_sql_column_indices(graph)
}

///|
fn ensure_orthogonal_route(route : Array[@graph.Point]) -> Array[@graph.Point] {
  if route.length() != 2 {
    return route
  }
  let p0 = route[0]
  let p1 = route[1]
  let dx = (p1.x - p0.x).abs()
  let dy = (p1.y - p0.y).abs()
  if dx < 1.0 || dy < 1.0 {
    return route
  }
  [p0, @graph.Point::new(p0.x, p1.y), p1]
}

///|
fn normalize_routes(graph : @graph.GraphInput) -> @graph.GraphInput {
  let edges : Array[@graph.EdgeInput] = []
  for e in graph.edges {
    let route = if edge_prefers_curved(e) {
      e.route
    } else {
      ensure_orthogonal_route(e.route)
    }
    edges.push(
      @graph.EdgeInput::from_parts(
        e.index,
        e.src_id,
        e.dst_id,
        e.src_arrow,
        e.dst_arrow,
        e.src_arrowhead,
        e.dst_arrowhead,
        e.src_arrowhead_label,
        e.dst_arrowhead_label,
        e.src_arrowhead_label_color,
        e.dst_arrowhead_label_color,
        e.src_anchor,
        e.dst_anchor,
        e.label,
        e.style,
        route,
        e.bend_points,
        e.is_curve,
        e.z_index,
        e.reference_count,
        e.label_box,
        e.src_column_index,
        e.dst_column_index,
      ),
    )
  }
  @graph.GraphInput::from_parts(
    graph.name,
    graph.root,
    graph.objects,
    edges,
    graph.sequence_fragments,
    graph.activation_boxes,
    graph.sequence_notes,
    graph.sequence_fragments_layout,
    graph.layers,
    graph.scenarios,
    graph.steps,
    graph.legend,
  )
}

///|
fn edge_prefers_curved(edge : @graph.EdgeInput) -> Bool {
  match edge.style.curved {
    Some(v) => v.as_bool().unwrap_or(true)
    None => true
  }
}

///|
priv struct GridSemanticPlan {
  rows : Int
  cols : Int
  column_widths : Array[Double]
  row_heights : Array[Double]
  hgap : Double
  vgap : Double
  pad_x : Double
  pad_top : Double
  width : Double
  height : Double
}

///|
const D2_GRID_STARTING_THRESHOLD : Double = 1.2

///|
const D2_GRID_THRESHOLD_STEP_SIZE : Double = 0.25

///|
const D2_GRID_MIN_THRESHOLD_ATTEMPTS : Int = 1

///|
const D2_GRID_MAX_THRESHOLD_ATTEMPTS : Int = 3

///|
const D2_GRID_ATTEMPT_LIMIT : Int = 100000

///|
const D2_GRID_SKIP_LIMIT : Int = 10000000

///|
priv struct RootGridSemanticLayout {
  child_boxes : Map[String, @graph.Box]
}

///|
priv struct GridSemanticContext {
  groups : Map[String, Array[String]]
  child_ids : Map[String, Bool]
}

///|
fn prepare_layout_semantics(
  graph : @graph.GraphInput,
  config : LayoutConfig,
) -> (@graph.GraphInput, GridSemanticContext) {
  let groups = collect_grid_semantic_groups(graph)
  if groups.is_empty() {
    return (graph, { groups, child_ids: {} })
  }
  let pre_sized = apply_grid_semantic_parent_boxes(graph, groups, config)
  let child_ids : Map[String, Bool] = {}
  for _, ids in groups {
    for id in ids {
      child_ids[id] = true
    }
  }
  (detach_grid_children(pre_sized, groups), { groups, child_ids })
}

///|
fn restore_layout_semantics(
  original_graph : @graph.GraphInput,
  laid_out_graph : @graph.GraphInput,
  context : GridSemanticContext,
  config : LayoutConfig,
) -> @graph.GraphInput {
  if context.groups.is_empty() {
    laid_out_graph
  } else {
    restore_grid_semantics(original_graph, laid_out_graph, context, config)
  }
}

///|
fn collect_grid_semantic_groups(
  graph : @graph.GraphInput,
) -> Map[String, Array[String]] {
  let object_by_id : Map[String, @graph.ObjectInput] = {}
  object_by_id[graph.root.id] = graph.root
  for obj in graph.objects {
    object_by_id[obj.id] = obj
  }
  let incident_ids : Map[String, Bool] = {}
  for edge in graph.edges {
    incident_ids[edge.src_id] = true
    incident_ids[edge.dst_id] = true
  }
  let groups : Map[String, Array[String]] = {}
  let parents : Array[@graph.ObjectInput] = [graph.root]
  for obj in graph.objects {
    parents.push(obj)
  }
  for parent in parents {
    if parent.grid_rows is None && parent.grid_columns is None {
      continue
    }
    let child_ids : Array[String] = []
    let mut blocked = false
    for child_id in parent.child_ids {
      guard object_by_id.contains(child_id) else { continue }
      if incident_ids.contains(child_id) {
        blocked = true
        break
      }
      child_ids.push(child_id)
    }
    if blocked || child_ids.is_empty() {
      continue
    }
    groups[parent.id] = child_ids
  }
  groups
}

///|
fn apply_grid_semantic_parent_boxes(
  graph : @graph.GraphInput,
  groups : Map[String, Array[String]],
  config : LayoutConfig,
) -> @graph.GraphInput {
  let object_by_id : Map[String, @graph.ObjectInput] = {}
  object_by_id[graph.root.id] = graph.root
  for obj in graph.objects {
    object_by_id[obj.id] = obj
  }

  let parent_box_by_id : Map[String, @graph.Box] = {}
  for parent_id, child_ids in groups {
    if parent_id == graph.root.id {
      continue
    }
    let parent = match object_by_id.get(parent_id) {
      Some(v) => v
      None => continue
    }
    let child_size_by_id : Map[String, (Double, Double)] = {}
    for child_id in child_ids {
      match object_by_id.get(child_id) {
        Some(child) =>
          child_size_by_id[child_id] = semantic_object_size(child, config)
        None => ()
      }
    }
    match estimate_grid_semantic_plan(parent, child_ids, child_size_by_id) {
      Some(plan) => {
        let (x, y) = match parent.box {
          Some(box) => (box.x, box.y)
          None => (0.0, 0.0)
        }
        parent_box_by_id[parent_id] = @graph.Box::new(
          x,
          y,
          plan.width,
          plan.height,
        )
      }
      None => ()
    }
  }

  if parent_box_by_id.is_empty() {
    return graph
  }

  let objects : Array[@graph.ObjectInput] = []
  for obj in graph.objects {
    let box = match parent_box_by_id.get(obj.id) {
      Some(v) => Some(v)
      None => obj.box
    }
    objects.push(clone_object_with_parts(obj, box, obj.child_ids))
  }

  @graph.GraphInput::from_parts(
    graph.name,
    graph.root,
    objects,
    graph.edges,
    graph.sequence_fragments,
    graph.activation_boxes,
    graph.sequence_notes,
    graph.sequence_fragments_layout,
    graph.layers,
    graph.scenarios,
    graph.steps,
    graph.legend,
  )
}

///|
fn detach_grid_children(
  graph : @graph.GraphInput,
  groups : Map[String, Array[String]],
) -> @graph.GraphInput {
  let group_child_ids : Map[String, Bool] = {}
  for _, child_ids in groups {
    for child_id in child_ids {
      group_child_ids[child_id] = true
    }
  }
  let objects : Array[@graph.ObjectInput] = []
  for obj in graph.objects {
    if group_child_ids.contains(obj.id) {
      continue
    }
    let child_ids = match groups.get(obj.id) {
      Some(filtered) => {
        let removed : Map[String, Bool] = {}
        for id in filtered {
          removed[id] = true
        }
        let kept : Array[String] = []
        for id in obj.child_ids {
          if !removed.contains(id) {
            kept.push(id)
          }
        }
        kept
      }
      None => obj.child_ids
    }
    objects.push(clone_object_with_parts(obj, obj.box, child_ids))
  }
  let root_child_ids = match groups.get(graph.root.id) {
    Some(filtered) => {
      let removed : Map[String, Bool] = {}
      for id in filtered {
        removed[id] = true
      }
      let kept : Array[String] = []
      for id in graph.root.child_ids {
        if !removed.contains(id) {
          kept.push(id)
        }
      }
      kept
    }
    None => graph.root.child_ids
  }
  let root = clone_object_with_parts(graph.root, graph.root.box, root_child_ids)
  @graph.GraphInput::from_parts(
    graph.name,
    root,
    objects,
    graph.edges,
    graph.sequence_fragments,
    graph.activation_boxes,
    graph.sequence_notes,
    graph.sequence_fragments_layout,
    graph.layers,
    graph.scenarios,
    graph.steps,
    graph.legend,
  )
}

///|
fn estimate_grid_semantic_plan(
  parent : @graph.ObjectInput,
  child_ids : Array[String],
  child_size_by_id : Map[String, (Double, Double)],
) -> GridSemanticPlan? {
  if child_ids.is_empty() {
    return None
  }
  let n = child_ids.length()
  let rows = match parent.grid_rows {
    Some(v) => if v > 0 { v } else { 0 }
    None => 0
  }
  let cols = match parent.grid_columns {
    Some(v) => if v > 0 { v } else { 0 }
    None => 0
  }
  let (rows, cols) = if rows > 0 && cols > 0 {
    (rows, cols)
  } else if rows > 0 {
    (rows, ceil_div(n, rows))
  } else if cols > 0 {
    (ceil_div(n, cols), cols)
  } else {
    return None
  }
  if rows <= 0 || cols <= 0 {
    return None
  }

  let column_widths : Array[Double] = []
  for _ in 0.. v
      None => continue
    }
    let row = i / cols
    let col = i % cols
    if child_w > column_widths[col] {
      column_widths[col] = child_w
    }
    if child_h > row_heights[row] {
      row_heights[row] = child_h
    }
  }

  let (hgap, vgap) = grid_semantic_gaps(parent)
  let (pad_x, pad_top, pad_bottom) = grid_semantic_paddings(parent, hgap, vgap)
  let width = sum_double(column_widths) +
    hgap * (cols - 1).to_double() +
    pad_x * 2.0
  let height = sum_double(row_heights) +
    vgap * (rows - 1).to_double() +
    pad_top +
    pad_bottom
  Some({
    rows,
    cols,
    column_widths,
    row_heights,
    hgap,
    vgap,
    pad_x,
    pad_top,
    width,
    height,
  })
}

///|
fn grid_semantic_gaps(parent : @graph.ObjectInput) -> (Double, Double) {
  let fallback = match parent.grid_gap {
    Some(v) => positive_or(v, 10.0)
    None => 40.0
  }
  let hgap = match parent.horizontal_gap {
    Some(v) => positive_or(v, fallback)
    None => fallback
  }
  let vgap = match parent.vertical_gap {
    Some(v) => positive_or(v, fallback)
    None => fallback
  }
  (hgap, vgap)
}

///|
fn grid_semantic_paddings(
  parent : @graph.ObjectInput,
  hgap : Double,
  vgap : Double,
) -> (Double, Double, Double) {
  match parent.grid_gap {
    Some(v) => {
      let pad = positive_or(v, 10.0)
      (pad, pad + 36.0, pad)
    }
    None => {
      let pad = max_double(60.0, max_double(hgap, vgap))
      (pad, pad, pad)
    }
  }
}

///|
fn root_grid_gaps(parent : @graph.ObjectInput) -> (Double, Double) {
  let default_gap = if parent.grid_rows is Some(_) ||
    parent.grid_columns is Some(_) {
    40.0
  } else {
    20.0
  }
  let mut vertical_gap = default_gap
  let mut horizontal_gap = default_gap
  match parent.grid_gap {
    Some(v) => {
      vertical_gap = v
      horizontal_gap = v
    }
    None => ()
  }
  match parent.vertical_gap {
    Some(v) => vertical_gap = v
    None => ()
  }
  match parent.horizontal_gap {
    Some(v) => horizontal_gap = v
    None => ()
  }
  (horizontal_gap, vertical_gap)
}

///|
fn root_grid_segment_size(
  segment : Array[Int],
  sizes : Array[Double],
  gap : Double,
) -> Double {
  let mut out = 0.0
  for i in segment {
    out = out + sizes[i]
  }
  if segment.length() > 1 {
    out = out + gap * (segment.length() - 1).to_double()
  }
  out
}

///|
fn root_grid_layout_distance(
  layout : Array[Array[Int]],
  target_size : Double,
  sizes : Array[Double],
  gap : Double,
) -> Double {
  let mut total = 0.0
  for segment in layout {
    let segment_size = root_grid_segment_size(segment, sizes, gap)
    total = total + (segment_size - target_size).abs()
  }
  total
}

///|
fn root_grid_gen_layout(
  count : Int,
  cut_indices : Array[Int],
) -> Array[Array[Int]] {
  let layout : Array[Array[Int]] = []
  let mut obj_index = 0
  let mut i = 0
  while i <= cut_indices.length() {
    let stop = if i < cut_indices.length() { cut_indices[i] } else { count - 1 }
    let row : Array[Int] = []
    while obj_index <= stop && obj_index < count {
      row.push(obj_index)
      obj_index = obj_index + 1
    }
    layout.push(row)
    i = i + 1
  }
  layout
}

///|
fn root_grid_cut_combinations(end : Int, cuts_left : Int) -> Array[Array[Int]] {
  let out : Array[Array[Int]] = []
  if end < 2 || cuts_left <= 0 {
    return out
  }
  let mut index = end - 1
  while index >= cuts_left {
    if cuts_left > 1 {
      let inner = root_grid_cut_combinations(index, cuts_left - 1)
      for cut_list in inner {
        let combined = cut_list.copy()
        combined.push(index - 1)
        out.push(combined)
      }
    } else {
      out.push([index - 1])
    }
    index = index - 1
  }
  out
}

///|
fn root_grid_fast_layout(
  sizes : Array[Double],
  target_size : Double,
  n_cuts : Int,
  gap : Double,
) -> Array[Array[Int]]? {
  if n_cuts <= 0 {
    return Some(root_grid_gen_layout(sizes.length(), []))
  }
  let mut debt = 0.0
  let division : Array[Int] = []
  let mut row_size = 0.0
  for i, size in sizes {
    if row_size == 0.0 {
      if size > target_size - debt {
        division.push(i)
        debt = debt + (size - target_size)
      } else {
        row_size = row_size + size
      }
      continue
    }
    if row_size + gap + size / 2.0 > target_size - debt {
      division.push(i - 1)
      debt = debt + (row_size - target_size)
      row_size = size
    } else {
      row_size = row_size + gap + size
    }
  }
  if division.length() == n_cuts {
    return Some(root_grid_gen_layout(sizes.length(), division))
  }
  None
}

///|
fn root_grid_sum(values : Array[Double]) -> Double {
  let mut s = 0.0
  for v in values {
    s = s + v
  }
  s
}

///|
fn root_grid_avg(values : Array[Double]) -> Double {
  if values.is_empty() {
    return 0.0
  }
  root_grid_sum(values) / values.length().to_double()
}

///|
fn root_grid_variance(values : Array[Double]) -> Double {
  if values.is_empty() {
    return 0.0
  }
  let mean = root_grid_avg(values)
  let mut total = 0.0
  for value in values {
    let dev = mean - value
    total = total + dev * dev
  }
  total / values.length().to_double()
}

///|
fn root_grid_stddev(values : Array[Double]) -> Double {
  root_grid_variance(values).sqrt()
}

///|
fn root_grid_best_layout(
  sizes : Array[Double],
  target_size : Double,
  n_cuts : Int,
  gap : Double,
) -> Array[Array[Int]] {
  if n_cuts <= 0 {
    return root_grid_gen_layout(sizes.length(), [])
  }
  let mut best_layout : Array[Array[Int]] = []
  let mut best_dist = 1000000000000.0
  let mut has_best = false
  let mut fast_is_best = false

  match root_grid_fast_layout(sizes, target_size, n_cuts, gap) {
    Some(fast_layout) => {
      let dist = root_grid_layout_distance(fast_layout, target_size, sizes, gap)
      if dist == 0.0 {
        return fast_layout
      }
      best_layout = fast_layout
      best_dist = dist
      has_best = true
      fast_is_best = true
    }
    None => ()
  }

  let sd = root_grid_stddev(sizes)
  let mut threshold_attempts = sd.ceil().to_int()
  if threshold_attempts < D2_GRID_MIN_THRESHOLD_ATTEMPTS {
    threshold_attempts = D2_GRID_MIN_THRESHOLD_ATTEMPTS
  } else if threshold_attempts > D2_GRID_MAX_THRESHOLD_ATTEMPTS {
    threshold_attempts = D2_GRID_MAX_THRESHOLD_ATTEMPTS
  }

  let combinations = root_grid_cut_combinations(sizes.length(), n_cuts)
  let mut ok_threshold = D2_GRID_STARTING_THRESHOLD
  let mut attempt = 0
  while attempt < threshold_attempts || !has_best {
    let starting_cache : Map[Int, Bool] = {}
    let mut count = 0
    let mut skip_count = 0
    for division in combinations {
      if count >= D2_GRID_ATTEMPT_LIMIT || skip_count >= D2_GRID_SKIP_LIMIT {
        break
      }
      let layout = root_grid_gen_layout(sizes.length(), division)
      let mut valid = true
      for i, segment in layout {
        let starting = i == 0
        if starting {
          let cached = starting_cache.get(segment.length())
          match cached {
            Some(ok) =>
              if !ok {
                valid = false
                break
              }
            None => {
              let segment_size = root_grid_segment_size(segment, sizes, gap)
              let mut ok = true
              if segment.length() > 1 &&
                segment_size > ok_threshold * target_size {
                skip_count = skip_count + 1
                ok = skip_count >= D2_GRID_SKIP_LIMIT
              }
              if ok && segment_size < target_size / ok_threshold {
                skip_count = skip_count + 1
                ok = skip_count >= D2_GRID_SKIP_LIMIT
              }
              starting_cache[segment.length()] = ok
              if !ok {
                valid = false
                break
              }
            }
          }
        } else {
          let segment_size = root_grid_segment_size(segment, sizes, gap)
          if segment.length() > 1 && segment_size > ok_threshold * target_size {
            skip_count = skip_count + 1
            if skip_count < D2_GRID_SKIP_LIMIT {
              valid = false
              break
            }
          }
          if segment_size < target_size / ok_threshold {
            skip_count = skip_count + 1
            if skip_count < D2_GRID_SKIP_LIMIT {
              valid = false
              break
            }
          }
        }
      }
      if !valid {
        continue
      }
      let dist = root_grid_layout_distance(layout, target_size, sizes, gap)
      if dist < best_dist {
        best_layout = layout
        best_dist = dist
        has_best = true
        fast_is_best = false
      } else if fast_is_best && dist == best_dist {
        best_layout = layout
        fast_is_best = false
      }
      count = count + 1
    }
    ok_threshold = ok_threshold + D2_GRID_THRESHOLD_STEP_SIZE
    if skip_count == 0 {
      break
    }
    if count == 0 && threshold_attempts < D2_GRID_MAX_THRESHOLD_ATTEMPTS {
      threshold_attempts = threshold_attempts + 1
    }
    attempt = attempt + 1
  }

  if has_best {
    best_layout
  } else {
    root_grid_gen_layout(sizes.length(), [])
  }
}

///|
fn estimate_root_grid_semantic_layout(
  parent : @graph.ObjectInput,
  child_ids : Array[String],
  child_size_by_id : Map[String, (Double, Double)],
) -> RootGridSemanticLayout? {
  if child_ids.is_empty() {
    return None
  }
  let n = child_ids.length()
  let mut rows = match parent.grid_rows {
    Some(v) => if v > 0 { v } else { 0 }
    None => 0
  }
  let mut columns = match parent.grid_columns {
    Some(v) => if v > 0 { v } else { 0 }
    None => 0
  }
  if rows == 0 && columns == 0 {
    return None
  }

  let mut row_directed = false
  if rows != 0 && columns != 0 {
    row_directed = true
    let mut capacity = rows * columns
    while capacity < n {
      if row_directed {
        rows = rows + 1
        capacity = capacity + columns
      } else {
        columns = columns + 1
        capacity = capacity + rows
      }
    }
  } else if columns == 0 {
    row_directed = true
    if n < rows {
      rows = n
    }
  } else if n < columns {
    columns = n
  }

  let (horizontal_gap, vertical_gap) = root_grid_gaps(parent)
  let widths : Array[Double] = []
  let heights : Array[Double] = []
  for id in child_ids {
    match child_size_by_id.get(id) {
      Some((w, h)) => {
        widths.push(w)
        heights.push(h)
      }
      None => {
        widths.push(0.0)
        heights.push(0.0)
      }
    }
  }

  let final_widths = widths.copy()
  let final_heights = heights.copy()
  let final_x : Array[Double] = []
  let final_y : Array[Double] = []
  for _ in 0.. {
      if row_directed {
        let index = row_index * columns + column_index
        if index < n {
          Some(index)
        } else {
          None
        }
      } else {
        let index = column_index * rows + row_index
        if index < n {
          Some(index)
        } else {
          None
        }
      }
    }

    for i in 0..
            if final_heights[index] > row_height {
              row_height = final_heights[index]
            }
          None => break
        }
      }
      row_heights[i] = row_height
    }
    for j in 0..
            if final_widths[index] > column_width {
              column_width = final_widths[index]
            }
          None => break
        }
      }
      col_widths[j] = column_width
    }

    if row_directed {
      let mut cursor_y = 0.0
      for i in 0.. {
              final_widths[index] = col_widths[j]
              final_heights[index] = row_heights[i]
              final_x[index] = cursor_x
              final_y[index] = cursor_y
              cursor_x = cursor_x + col_widths[j] + horizontal_gap
            }
            None => break
          }
        }
        cursor_y = cursor_y + row_heights[i] + vertical_gap
      }
    } else {
      let mut cursor_x = 0.0
      for j in 0.. {
              final_widths[index] = col_widths[j]
              final_heights[index] = row_heights[i]
              final_x[index] = cursor_x
              final_y[index] = cursor_y
              cursor_y = cursor_y + row_heights[i] + vertical_gap
            }
            None => break
          }
        }
        cursor_x = cursor_x + col_widths[j] + horizontal_gap
      }
    }

    for w in col_widths {
      total_width = total_width + w + horizontal_gap
    }
    for h in row_heights {
      total_height = total_height + h + vertical_gap
    }
    total_width = total_width - horizontal_gap
    total_height = total_height - vertical_gap
  } else if row_directed {
    let mut sum_width = 0.0
    for w in final_widths {
      sum_width = sum_width + w
    }
    let target_width = (sum_width + horizontal_gap * (n - rows).to_double()) /
      rows.to_double()
    let layout = root_grid_best_layout(
      final_widths,
      target_width,
      rows - 1,
      horizontal_gap,
    )

    let row_widths : Array[Double] = []
    let mut max_x = 0.0
    for row in layout {
      let row_width = root_grid_segment_size(row, final_widths, horizontal_gap)
      row_widths.push(row_width)
      if row_width > max_x {
        max_x = row_width
      }
    }

    for i, row in layout {
      let row_width = row_widths[i]
      if row_width == max_x {
        continue
      }
      let delta = max_x - row_width
      let mut widest = 0.0
      for index in row {
        if final_widths[index] > widest {
          widest = final_widths[index]
        }
      }
      let diffs : Array[Double] = []
      let mut total_diff = 0.0
      for index in row {
        let diff = widest - final_widths[index]
        diffs.push(diff)
        total_diff = total_diff + diff
      }
      if total_diff > 0.0 {
        let growth = if delta < total_diff { delta } else { total_diff }
        for j, index in row {
          final_widths[index] = final_widths[index] +
            diffs[j] / total_diff * growth
        }
      }
      if delta > total_diff {
        let growth = (delta - total_diff) / row.length().to_double()
        for index in row {
          final_widths[index] = final_widths[index] + growth
        }
      }
    }

    let mut cursor_y = 0.0
    for row in layout {
      let mut row_height = 0.0
      let mut cursor_x = 0.0
      for index in row {
        final_x[index] = cursor_x
        final_y[index] = cursor_y
        cursor_x = cursor_x + final_widths[index] + horizontal_gap
        if final_heights[index] > row_height {
          row_height = final_heights[index]
        }
      }
      for index in row {
        final_heights[index] = row_height
      }
      cursor_y = cursor_y + row_height + vertical_gap
    }
    total_width = max_x
    total_height = cursor_y - vertical_gap
  } else {
    let mut sum_height = 0.0
    for h in final_heights {
      sum_height = sum_height + h
    }
    let target_height = (sum_height + vertical_gap * (n - columns).to_double()) /
      columns.to_double()
    let layout = root_grid_best_layout(
      final_heights,
      target_height,
      columns - 1,
      vertical_gap,
    )

    let column_heights : Array[Double] = []
    let mut max_y = 0.0
    for column in layout {
      let column_height = root_grid_segment_size(
        column, final_heights, vertical_gap,
      )
      column_heights.push(column_height)
      if column_height > max_y {
        max_y = column_height
      }
    }

    for i, column in layout {
      let column_height = column_heights[i]
      if column_height == max_y {
        continue
      }
      let delta = max_y - column_height
      let mut tallest = 0.0
      for index in column {
        if final_heights[index] > tallest {
          tallest = final_heights[index]
        }
      }
      let diffs : Array[Double] = []
      let mut total_diff = 0.0
      for index in column {
        let diff = tallest - final_heights[index]
        diffs.push(diff)
        total_diff = total_diff + diff
      }
      if total_diff > 0.0 {
        let growth = if delta < total_diff { delta } else { total_diff }
        for j, index in column {
          final_heights[index] = final_heights[index] +
            diffs[j] / total_diff * growth
        }
      }
      if delta > total_diff {
        let growth = (delta - total_diff) / column.length().to_double()
        for index in column {
          final_heights[index] = final_heights[index] + growth
        }
      }
    }

    let mut cursor_x = 0.0
    for column in layout {
      let mut column_width = 0.0
      let mut cursor_y = 0.0
      for index in column {
        final_x[index] = cursor_x
        final_y[index] = cursor_y
        cursor_y = cursor_y + final_heights[index] + vertical_gap
        if final_widths[index] > column_width {
          column_width = final_widths[index]
        }
      }
      for index in column {
        final_widths[index] = column_width
      }
      cursor_x = cursor_x + column_width + horizontal_gap
    }
    total_width = cursor_x - horizontal_gap
    total_height = max_y
  }

  let child_boxes : Map[String, @graph.Box] = {}
  for i, id in child_ids {
    child_boxes[id] = @graph.Box::new(
      final_x[i],
      final_y[i],
      final_widths[i],
      final_heights[i],
    )
  }
  ignore(total_width)
  ignore(total_height)
  Some({ child_boxes, })
}

///|
fn restore_grid_semantics(
  original_graph : @graph.GraphInput,
  laid_out_graph : @graph.GraphInput,
  context : GridSemanticContext,
  config : LayoutConfig,
) -> @graph.GraphInput {
  let original_by_id : Map[String, @graph.ObjectInput] = {}
  original_by_id[original_graph.root.id] = original_graph.root
  for obj in original_graph.objects {
    original_by_id[obj.id] = obj
  }
  let laid_out_by_id : Map[String, @graph.ObjectInput] = {}
  laid_out_by_id[laid_out_graph.root.id] = laid_out_graph.root
  for obj in laid_out_graph.objects {
    laid_out_by_id[obj.id] = obj
  }

  let override_boxes : Map[String, @graph.Box] = {}
  for parent_id, child_ids in context.groups {
    if parent_id == original_graph.root.id {
      let parent = original_graph.root
      let child_size_by_id : Map[String, (Double, Double)] = {}
      for child_id in child_ids {
        let child = match original_by_id.get(child_id) {
          Some(v) => v
          None => continue
        }
        child_size_by_id[child_id] = semantic_object_size(child, config)
      }
      match
        estimate_root_grid_semantic_layout(parent, child_ids, child_size_by_id) {
        Some(root_layout) =>
          for child_id, box in root_layout.child_boxes {
            override_boxes[child_id] = box
          }
        None => ()
      }
      continue
    }
    let parent = match laid_out_by_id.get(parent_id) {
      Some(v) => v
      None => continue
    }
    let parent_box = match parent.box {
      Some(v) => v
      None => continue
    }
    let child_size_by_id : Map[String, (Double, Double)] = {}
    for child_id in child_ids {
      let child = match original_by_id.get(child_id) {
        Some(v) => v
        None => continue
      }
      child_size_by_id[child_id] = semantic_object_size(child, config)
    }
    let plan = match
      estimate_grid_semantic_plan(parent, child_ids, child_size_by_id) {
      Some(v) => v
      None => continue
    }

    let parent_new = parent_box
    override_boxes[parent_id] = parent_new

    let mut y = parent_new.y + plan.pad_top
    for row in 0..= child_ids.length() {
          break
        }
        let child_id = child_ids[idx]
        let (child_w, child_h) = match child_size_by_id.get(child_id) {
          Some(v) => v
          None => continue
        }
        let cell_w = plan.column_widths[col]
        let cell_h = plan.row_heights[row]
        let _ = child_w
        let _ = child_h
        override_boxes[child_id] = @graph.Box::new(x, y, cell_w, cell_h)
        x = x + cell_w + plan.hgap
      }
      y = y + plan.row_heights[row] + plan.vgap
    }
  }
  normalize_override_boxes_origin(override_boxes)

  let objects : Array[@graph.ObjectInput] = []
  for obj in original_graph.objects {
    if context.child_ids.contains(obj.id) {
      let override_box = override_boxes.get(obj.id)
      let box = match override_box {
        Some(v) => Some(v)
        None => obj.box
      }
      let label_box = obj.label_box
      objects.push(
        clone_object_with_parts_and_label(obj, box, label_box, obj.child_ids),
      )
      continue
    }
    let laid_out_obj = match laid_out_by_id.get(obj.id) {
      Some(v) => v
      None => obj
    }
    let override_box = override_boxes.get(obj.id)
    let box = match override_box {
      Some(v) => Some(v)
      None => laid_out_obj.box
    }
    let label_box = match override_box {
      Some(_) =>
        if context.groups.contains(obj.id) {
          None
        } else {
          laid_out_obj.label_box
        }
      None => laid_out_obj.label_box
    }
    let child_ids = if context.groups.contains(obj.id) {
      obj.child_ids
    } else {
      laid_out_obj.child_ids
    }
    objects.push(
      clone_object_with_parts_and_label(laid_out_obj, box, label_box, child_ids),
    )
  }

  let root_child_ids = if context.groups.contains(original_graph.root.id) {
    original_graph.root.child_ids
  } else {
    laid_out_graph.root.child_ids
  }
  let root = clone_object_with_parts(
    laid_out_graph.root,
    laid_out_graph.root.box,
    root_child_ids,
  )

  @graph.GraphInput::from_parts(
    laid_out_graph.name,
    root,
    objects,
    laid_out_graph.edges,
    laid_out_graph.sequence_fragments,
    laid_out_graph.activation_boxes,
    laid_out_graph.sequence_notes,
    laid_out_graph.sequence_fragments_layout,
    laid_out_graph.layers,
    laid_out_graph.scenarios,
    laid_out_graph.steps,
    laid_out_graph.legend,
  )
}

///|
fn normalize_override_boxes_origin(
  override_boxes : Map[String, @graph.Box],
) -> Unit {
  if override_boxes.is_empty() {
    return
  }
  let mut min_x = 1000000000.0
  let mut min_y = 1000000000.0
  for _, box in override_boxes {
    if box.x < min_x {
      min_x = box.x
    }
    if box.y < min_y {
      min_y = box.y
    }
  }
  let shift_x = if min_x < 0.0 { -min_x } else { 0.0 }
  let shift_y = if min_y < 0.0 { -min_y } else { 0.0 }
  if shift_x == 0.0 && shift_y == 0.0 {
    return
  }
  let ids : Array[String] = []
  for id, _ in override_boxes {
    ids.push(id)
  }
  for id in ids {
    if override_boxes.get(id) is Some(box) {
      override_boxes[id] = @graph.Box::new(
        box.x + shift_x,
        box.y + shift_y,
        box.width,
        box.height,
      )
    }
  }
}

///|
fn clone_object_with_parts(
  obj : @graph.ObjectInput,
  box : @graph.Box?,
  child_ids : Array[String],
) -> @graph.ObjectInput {
  clone_object_with_parts_and_label(obj, box, obj.label_box, child_ids)
}

///|
fn clone_object_with_parts_and_label(
  obj : @graph.ObjectInput,
  box : @graph.Box?,
  label_box : @graph.Box?,
  child_ids : Array[String],
) -> @graph.ObjectInput {
  @graph.ObjectInput::from_parts(
    obj.id,
    obj.label,
    obj.shape_type,
    obj.style,
    box,
    label_box,
    child_ids,
    obj.z_index,
    obj.icon,
    obj.tooltip,
    obj.link,
    obj.classes,
    obj.grid_rows,
    obj.grid_columns,
    obj.grid_gap,
    obj.horizontal_gap,
    obj.vertical_gap,
    obj.grid_column_span,
    obj.grid_row_span,
    obj.near,
    obj.top,
    obj.left,
  )
}

///|
fn semantic_object_size(
  obj : @graph.ObjectInput,
  config : LayoutConfig,
) -> (Double, Double) {
  match obj.box {
    Some(box) => {
      let (auto_width, auto_height) = estimate_semantic_object_size(obj, config)
      let width = if box.width > 0.0 { box.width } else { auto_width }
      let height = if box.height > 0.0 { box.height } else { auto_height }
      (
        positive_or(width, config.default_width),
        positive_or(height, config.default_height),
      )
    }
    None => estimate_semantic_object_size(obj, config)
  }
}

///|
fn estimate_semantic_object_size(
  obj : @graph.ObjectInput,
  config : LayoutConfig,
) -> (Double, Double) {
  let has_explicit_width = match obj.box {
    Some(box) => box.width > 0.0
    None => false
  }
  let has_explicit_height = match obj.box {
    Some(box) => box.height > 0.0
    None => false
  }
  let with_inner_label_padding = !(has_explicit_width || has_explicit_height)
  let font_size = match obj.style.font_size {
    Some(v) => v.as_int().unwrap_or(16)
    None => 16
  }
  let is_bold = style_bool(obj.style.bold, false)
  let is_italic = style_bool(obj.style.italic, false)
  let is_mono = obj.shape_type == @graph.Code ||
    style_is_mono(obj.style.font_family)
  let label_dims = if obj.shape_type == @graph.Code {
    @text_metrics.measure_label_code(obj.label, font_size, is_bold, is_italic)
  } else {
    @text_metrics.measure_label(
      obj.label,
      font_size,
      is_mono,
      is_bold,
      is_italic,
    )
  }
  let mut content_width = label_dims.width
  let mut content_height = label_dims.height

  if obj.shape_type == @graph.Code {
    content_width = content_width + font_size.to_double()
    content_height = content_height + font_size.to_double()
    // D2 parity: code shape size is measured text + 0.5em padding each side only.
    return (content_width, content_height)
  } else if with_inner_label_padding &&
    obj.label != "" &&
    obj.shape_type != @graph.Text {
    content_width = content_width + D2_INNER_LABEL_PADDING
    content_height = content_height + D2_INNER_LABEL_PADDING
  }

  if obj.shape_type == @graph.Text {
    if content_width < D2_MIN_SHAPE_SIZE {
      content_width = D2_MIN_SHAPE_SIZE
    }
    if content_height < D2_MIN_SHAPE_SIZE {
      content_height = D2_MIN_SHAPE_SIZE
    }
    return (content_width, content_height)
  }

  if obj.shape_type == @graph.Image {
    return (128.0, 128.0)
  }

  let base_padding = semantic_shape_default_padding(obj.shape_type)
  let mut padding_x = base_padding.0
  let mut padding_y = base_padding.1
  if has_explicit_width {
    padding_x = 0.0
  }
  if has_explicit_height {
    padding_y = 0.0
  }
  if obj.icon is Some(_) &&
    obj.shape_type != @graph.SqlTable &&
    obj.shape_type != @graph.Class &&
    obj.shape_type != @graph.Code &&
    obj.shape_type != @graph.Text {
    let label_height = label_dims.height + D2_INNER_LABEL_PADDING
    if !has_explicit_width {
      padding_x = padding_x + label_height
    }
    if !has_explicit_height {
      padding_y = padding_y + label_height
    }
  }

  let fitted = semantic_shape_dimensions_to_fit(
    obj.shape_type,
    content_width,
    content_height,
    padding_x,
    padding_y,
  )
  let mut width = fitted.0
  let mut height = fitted.1

  if obj.shape_type == @graph.Circle || obj.shape_type == @graph.Square {
    let side = if width > height { width } else { height }
    width = side
    height = side
  } else if obj.shape_type == @graph.Person {
    let fit = limit_ar(width, height, D2_PERSON_AR_LIMIT)
    width = fit.0
    height = fit.1
  } else if obj.shape_type == @graph.Oval {
    let fit = limit_ar(width, height, D2_OVAL_AR_LIMIT)
    width = fit.0
    height = fit.1
  } else if obj.shape_type == @graph.C4Person {
    let fit = limit_ar(width, height, D2_C4_PERSON_AR_LIMIT)
    width = fit.0
    height = fit.1
  }

  (
    if width > 0.0 {
      width
    } else {
      positive_or(config.default_width, 1.0)
    },
    if height > 0.0 {
      height
    } else {
      positive_or(config.default_height, 1.0)
    },
  )
}

///|
const D2_DEFAULT_PADDING : Double = 40.0

///|
const D2_DEFAULT_ARC_DEPTH : Double = 24.0

///|
const D2_PARALLELOGRAM_WEDGE_WIDTH : Double = 26.0

///|
const D2_STORED_DATA_WEDGE_WIDTH : Double = 15.0

///|
const D2_STEP_WEDGE_WIDTH : Double = 35.0

///|
const D2_PACKAGE_TOP_MAX_HEIGHT : Double = 55.0

///|
const D2_PACKAGE_VERTICAL_SCALAR : Double = 0.2

///|
const D2_CALLOUT_DEFAULT_TIP_HEIGHT : Double = 45.0

///|
const D2_PAGE_CORNER_WIDTH : Double = 20.8164

///|
const D2_PAGE_CORNER_HEIGHT : Double = 20.348

///|
const D2_DOC_PATH_HEIGHT : Double = 18.925

///|
const D2_DOC_PATH_INNER_BOTTOM : Double = 14.0

///|
const D2_INNER_LABEL_PADDING : Double = 5.0

///|
const D2_MIN_SHAPE_SIZE : Double = 5.0

///|
const D2_PERSON_AR_LIMIT : Double = 1.5

///|
const D2_OVAL_AR_LIMIT : Double = 3.0

///|
const D2_C4_PERSON_AR_LIMIT : Double = 1.5

///|
const D2_C4_HEAD_RADIUS_FACTOR : Double = 0.22

///|
const D2_C4_BODY_TOP_FACTOR : Double = 0.8

///|
const CLOUD_WIDE_INNER_WIDTH : Double = 0.819

///|
const CLOUD_WIDE_INNER_HEIGHT : Double = 0.548

///|
const CLOUD_WIDE_ASPECT_BOUNDARY : Double = (1.0 + D2_DEFAULT_PADDING) /
  D2_DEFAULT_PADDING

///|
const CLOUD_TALL_INNER_WIDTH : Double = 0.549

///|
const CLOUD_TALL_INNER_HEIGHT : Double = 0.820

///|
const CLOUD_TALL_ASPECT_BOUNDARY : Double = D2_DEFAULT_PADDING /
  (1.0 + D2_DEFAULT_PADDING)

///|
const CLOUD_SQUARE_INNER_WIDTH : Double = 0.663

///|
const CLOUD_SQUARE_INNER_HEIGHT : Double = 0.663

///|
fn semantic_shape_default_padding(
  shape_type : @graph.ShapeType,
) -> (Double, Double) {
  match shape_type {
    @graph.Circle => {
      let p = D2_DEFAULT_PADDING / 2.0.sqrt()
      (p, p)
    }
    @graph.Diamond => (D2_DEFAULT_PADDING / 4.0, D2_DEFAULT_PADDING / 2.0)
    @graph.Hexagon => (D2_DEFAULT_PADDING / 2.0, D2_DEFAULT_PADDING / 2.0)
    @graph.Cylinder => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
    @graph.Queue => (D2_DEFAULT_PADDING / 2.0, D2_DEFAULT_PADDING)
    @graph.StoredData => (D2_DEFAULT_PADDING - 10.0, D2_DEFAULT_PADDING)
    @graph.Person => (10.0, D2_DEFAULT_PADDING)
    @graph.C4Person => (10.0, D2_DEFAULT_PADDING)
    @graph.Page =>
      (D2_DEFAULT_PADDING, D2_PAGE_CORNER_HEIGHT + D2_DEFAULT_PADDING)
    @graph.Document =>
      (
        D2_DEFAULT_PADDING,
        D2_DEFAULT_PADDING * D2_DOC_PATH_INNER_BOTTOM / D2_DOC_PATH_HEIGHT,
      )
    @graph.Package => (D2_DEFAULT_PADDING, 0.8 * D2_DEFAULT_PADDING)
    @graph.Step =>
      (D2_DEFAULT_PADDING / 4.0, D2_DEFAULT_PADDING + D2_STEP_WEDGE_WIDTH)
    @graph.Callout => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
    @graph.Cloud => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING / 2.0)
    _ => (D2_DEFAULT_PADDING, D2_DEFAULT_PADDING)
  }
}

///|
fn semantic_shape_dimensions_to_fit(
  shape_type : @graph.ShapeType,
  width : Double,
  height : Double,
  padding_x : Double,
  padding_y : Double,
) -> (Double, Double) {
  match shape_type {
    @graph.Circle => {
      let length = if width + padding_x > height + padding_y {
        width + padding_x
      } else {
        height + padding_y
      }
      let diameter = (2.0.sqrt() * length).ceil()
      (diameter, diameter)
    }
    @graph.Oval => {
      let theta = @math.atan2(height, width)
      let padded_width = width + padding_x * @math.cos(theta)
      let padded_height = height + padding_y * @math.sin(theta)
      let mut total_width = (2.0.sqrt() * padded_width).ceil()
      let mut total_height = (2.0.sqrt() * padded_height).ceil()
      let fit = limit_ar(total_width, total_height, D2_OVAL_AR_LIMIT)
      total_width = fit.0
      total_height = fit.1
      (total_width, total_height)
    }
    @graph.Diamond =>
      ((2.0 * (width + padding_x)).ceil(), (2.0 * (height + padding_y)).ceil())
    @graph.Hexagon =>
      ((1.5 * (width + padding_x)).ceil(), (1.5 * (height + padding_y)).ceil())
    @graph.Parallelogram =>
      (
        (width + padding_x + D2_PARALLELOGRAM_WEDGE_WIDTH * 2.0).ceil(),
        (height + padding_y).ceil(),
      )
    @graph.Cylinder =>
      (
        (width + padding_x).ceil(),
        (height + padding_y + 3.0 * D2_DEFAULT_ARC_DEPTH).ceil(),
      )
    @graph.Queue =>
      (
        (width + padding_x + 3.0 * D2_DEFAULT_ARC_DEPTH).ceil(),
        (height + padding_y).ceil(),
      )
    @graph.StoredData =>
      (
        (width + padding_x + 2.0 * D2_STORED_DATA_WEDGE_WIDTH).ceil(),
        (height + padding_y).ceil(),
      )
    @graph.Person => {
      let mut total_width = width + padding_x
      let mut total_height = height + padding_y
      let fit = limit_ar(total_width, total_height, D2_PERSON_AR_LIMIT)
      total_width = fit.0
      total_height = fit.1
      (total_width.ceil(), total_height.ceil())
    }
    @graph.C4Person => {
      let content_width = width + padding_x
      let content_height = height + padding_y
      let mut total_width = content_width / 0.9
      let head_radius = total_width * D2_C4_HEAD_RADIUS_FACTOR
      let body_top = head_radius + head_radius * D2_C4_BODY_TOP_FACTOR
      let vertical_padding = total_width * 0.06
      let mut total_height = content_height + body_top + vertical_padding
      let min_height = total_width * 0.95
      if total_height < min_height {
        total_height = min_height
      }
      let fit = limit_ar(total_width, total_height, D2_C4_PERSON_AR_LIMIT)
      total_width = fit.0
      total_height = fit.1
      (total_width.ceil(), total_height.ceil())
    }
    @graph.Cloud => {
      let total_width = width + padding_x
      let total_height = height + padding_y
      let aspect_ratio = total_width / total_height
      if aspect_ratio > CLOUD_WIDE_ASPECT_BOUNDARY {
        (
          (total_width / CLOUD_WIDE_INNER_WIDTH).ceil(),
          (total_height / CLOUD_WIDE_INNER_HEIGHT).ceil(),
        )
      } else if aspect_ratio < CLOUD_TALL_ASPECT_BOUNDARY {
        (
          (total_width / CLOUD_TALL_INNER_WIDTH).ceil(),
          (total_height / CLOUD_TALL_INNER_HEIGHT).ceil(),
        )
      } else {
        (
          (total_width / CLOUD_SQUARE_INNER_WIDTH).ceil(),
          (total_height / CLOUD_SQUARE_INNER_HEIGHT).ceil(),
        )
      }
    }
    @graph.Page => {
      let mut total_width = width + padding_x
      let mut total_height = height + padding_y
      if total_height < 3.0 * D2_PAGE_CORNER_HEIGHT {
        total_width = total_width + D2_PAGE_CORNER_WIDTH
      }
      if total_width < 2.0 * D2_PAGE_CORNER_WIDTH {
        total_width = 2.0 * D2_PAGE_CORNER_WIDTH
      }
      if total_height < D2_PAGE_CORNER_HEIGHT {
        total_height = D2_PAGE_CORNER_HEIGHT
      }
      (total_width.ceil(), total_height.ceil())
    }
    @graph.Document =>
      (
        (width + padding_x).ceil(),
        ((height + padding_y) * D2_DOC_PATH_HEIGHT / D2_DOC_PATH_INNER_BOTTOM).ceil(),
      )
    @graph.Package => {
      let inner_height = height + padding_y
      let top_height = inner_height *
        D2_PACKAGE_VERTICAL_SCALAR /
        (1.0 - D2_PACKAGE_VERTICAL_SCALAR)
      let total_height = inner_height +
        (if top_height < D2_PACKAGE_TOP_MAX_HEIGHT {
          top_height
        } else {
          D2_PACKAGE_TOP_MAX_HEIGHT
        })
      ((width + padding_x).ceil(), total_height.ceil())
    }
    @graph.Step =>
      (
        (width + padding_x + 2.0 * D2_STEP_WEDGE_WIDTH).ceil(),
        (height + padding_y).ceil(),
      )
    @graph.Callout => {
      let mut total_height = height + padding_y
      if total_height < D2_CALLOUT_DEFAULT_TIP_HEIGHT {
        total_height = total_height * 2.0
      } else {
        total_height = total_height + D2_CALLOUT_DEFAULT_TIP_HEIGHT
      }
      ((width + padding_x).ceil(), total_height.ceil())
    }
    _ => ((width + padding_x).ceil(), (height + padding_y).ceil())
  }
}

///|
fn limit_ar(
  width : Double,
  height : Double,
  aspect_ratio : Double,
) -> (Double, Double) {
  let mut w = width
  let mut h = height
  if w <= 0.0 && h <= 0.0 {
    return (1.0, 1.0)
  }
  if w <= 0.0 {
    w = h / aspect_ratio
  }
  if h <= 0.0 {
    h = w / aspect_ratio
  }
  if h > 0.0 && w / h > aspect_ratio {
    h = w / aspect_ratio
  } else if w > 0.0 && h / w > aspect_ratio {
    w = h / aspect_ratio
  }
  (w, h)
}

///|
fn style_bool(value : @graph.StyleValue?, default : Bool) -> Bool {
  match value {
    Some(v) => v.as_bool().unwrap_or(default)
    None => default
  }
}

///|
fn style_is_mono(value : @graph.StyleValue?) -> Bool {
  match value {
    Some(v) =>
      match v.get_value().to_lower() {
        "mono" | "monospace" | "code" => true
        _ => false
      }
    None => false
  }
}

///|
fn positive_or(value : Double, fallback : Double) -> Double {
  if value > 0.0 {
    value
  } else {
    fallback
  }
}

///|
fn ceil_div(n : Int, d : Int) -> Int {
  if n <= 0 || d <= 0 {
    0
  } else {
    (n + d - 1) / d
  }
}

///|
fn sum_double(values : Array[Double]) -> Double {
  let mut sum = 0.0
  for v in values {
    sum = sum + v
  }
  sum
}

///|
fn max_double(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
fn min_int(a : Int, b : Int) -> Int {
  if a < b {
    a
  } else {
    b
  }
}

///|
/// Perform layout on a graph (including variants) with a specific engine.
///
/// Returns a new graph with layout applied.
pub fn[Engine : LayoutEngine] layout_with_engine(
  engine : Engine,
  graph : @graph.GraphInput,
  config : LayoutConfig,
  direction : Direction,
) -> @graph.GraphInput raise LayoutError {
  let prepared = prepare_graph_for_layout(graph)
  let (engine_graph, semantic_ctx) = prepare_layout_semantics(prepared, config)
  let laid_out_core = if engine_graph.objects.is_empty() {
    engine_graph
  } else {
    let patch = engine.layout(engine_graph, config, direction)
    @graph.apply_layout(engine_graph, patch)
  }
  let laid_out = restore_layout_semantics(
    prepared, laid_out_core, semantic_ctx, config,
  )
  let layers : Array[@graph.LayerInput] = []
  for layer in laid_out.layers {
    layers.push(
      @graph.LayerInput::new(
        layer.name,
        layout_with_engine(engine, layer.graph, config, direction),
      ),
    )
  }
  let scenarios : Array[@graph.ScenarioInput] = []
  for scenario in laid_out.scenarios {
    scenarios.push(
      @graph.ScenarioInput::new(
        scenario.name,
        layout_with_engine(engine, scenario.graph, config, direction),
      ),
    )
  }
  let steps : Array[@graph.StepInput] = []
  for step in laid_out.steps {
    steps.push(
      @graph.StepInput::new(
        step.name,
        layout_with_engine(engine, step.graph, config, direction),
      ),
    )
  }
  normalize_routes(
    @graph.GraphInput::from_parts(
      laid_out.name,
      laid_out.root,
      laid_out.objects,
      laid_out.edges,
      laid_out.sequence_fragments,
      laid_out.activation_boxes,
      laid_out.sequence_notes,
      laid_out.sequence_fragments_layout,
      layers,
      scenarios,
      steps,
      laid_out.legend,
    ),
  )
}