///|
/// Render one trace frame as a deterministic, standalone SVG document.
#deprecated("Legacy static renderer; use render_trace_html or Schema v1 JSON. This API will be removed in v1.0.", skip_current_package=true)
pub fn render_trace_svg(
  trace : AlgorithmTrace,
  step? : Int = -1,
  width? : Int = 900,
) -> String raise {
  let scene = trace_scene_at(trace, step)
  let height = scene_height(scene)
  let out = StringBuilder()
  out.write_string(
    "\n",
  )
  out.write_string("\n")
  svg_text(out, 28, 38, trace.title, 24, "#f8fafc")
  svg_text(out, 28, 64, trace.algorithm, 14, "#94a3b8")
  let mut y = 92
  for object in scene.objects {
    match object {
      Sequence(value) => y = render_sequence_svg(out, value, scene, y)
      Sets(value) => y = render_sets_svg(out, value, scene, y)
      Graph(value) => y = render_graph_svg(out, value, scene, y)
      Grid(value) => y = render_grid_svg(out, value, scene, y)
    }
  }
  let annotation = trace_annotation_at(trace, step)
  match annotation {
    Some(note) => {
      svg_text(out, 28, height - 48, note.title, 16, "#f8fafc")
      svg_text(out, 28, height - 24, note.body, 13, "#cbd5e1")
    }
    None => ()
  }
  out.write_string("\n")
  out.to_string()
}

///|
/// Render every recorded step. An empty trace yields its initial scene.
#deprecated("Legacy static renderer; use render_trace_html or Schema v1 JSON. This API will be removed in v1.0.", skip_current_package=true)
pub fn render_trace_svg_frames(
  trace : AlgorithmTrace,
  width? : Int = 900,
) -> Array[String] raise {
  if trace.steps.is_empty() {
    [render_trace_svg(trace, step=-1, width~)]
  } else {
    trace.steps.map(fn(step) raise {
      render_trace_svg(trace, step=step.index, width~)
    })
  }
}

///|
fn trace_scene_at(trace : AlgorithmTrace, step : Int) -> Scene raise {
  if trace.steps.is_empty() || step < 0 {
    trace.initial_scene
  } else if step >= trace.steps.length() {
    fail("Trace step is outside the recorded range")
  } else {
    trace.steps[step].scene
  }
}

///|
fn trace_annotation_at(trace : AlgorithmTrace, step : Int) -> Annotation? {
  if step < 0 || step >= trace.steps.length() {
    None
  } else {
    trace.steps[step].annotation
  }
}

///|
fn scene_height(scene : Scene) -> Int {
  let mut height = 150
  for object in scene.objects {
    height += match object {
      Sequence(_) => 120
      Sets(value) => 86 + value.groups.length() * 46
      Graph(value) => 130 + (value.nodes.length() + 3) / 4 * 90
      Grid(value) => 56 + value.height * 44
    }
  }
  height
}

///|
fn render_sequence_svg(
  out : StringBuilder,
  value : SequenceState,
  scene : Scene,
  y : Int,
) -> Int {
  svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
  for i, item in value.items {
    let x = 28 + i * 72
    svg_box(out, x, y + 34, 58, 48, highlight_color(scene, value.id, item.id))
    svg_text(out, x + 29, y + 64, item.value, 15, "#0f172a", anchor="middle")
    svg_text(out, x + 29, y + 100, item.id, 10, "#94a3b8", anchor="middle")
  }
  y + 120
}

///|
fn render_sets_svg(
  out : StringBuilder,
  value : SetState,
  scene : Scene,
  y : Int,
) -> Int {
  svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
  let mut row = y + 36
  for group in value.groups {
    let members = group.members.join(", ")
    svg_box(out, 28, row, 820, 34, highlight_color(scene, value.id, group.id))
    svg_text(out, 42, row + 22, "\{group.label}: \{members}", 13, "#0f172a")
    row += 46
  }
  row + 24
}

///|
fn render_graph_svg(
  out : StringBuilder,
  value : GraphState,
  scene : Scene,
  y : Int,
) -> Int {
  svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
  let points = layout_graph(value)
  out.write_string(
    "\n",
  )
  for edge in value.edges {
    let from = graph_point(points, edge.from)
    let to = graph_point(points, edge.to)
    let marker = if edge.directed {
      " marker-end=\"url(#frontierlab-arrow)\""
    } else {
      ""
    }
    if edge.from == edge.to {
      out.write_string(
        "\n",
      )
    } else {
      out.write_string(
        "\n",
      )
    }
    if edge.label != "" {
      svg_text(
        out,
        (from.x + to.x) / 2,
        y + (from.y + to.y) / 2 - 10,
        edge.label,
        11,
        "#cbd5e1",
        anchor="middle",
      )
    }
  }
  for node in value.nodes {
    let point = graph_point(points, node.id)
    out.write_string(
      "\n",
    )
    svg_text(
      out,
      point.x,
      y + point.y + 6,
      node.label,
      13,
      "#0f172a",
      anchor="middle",
    )
  }
  y + 110 + (value.nodes.length() + 3) / 4 * 90
}

///|
fn render_grid_svg(
  out : StringBuilder,
  value : GridState,
  scene : Scene,
  y : Int,
) -> Int {
  svg_text(out, 28, y + 20, value.label, 16, "#e2e8f0")
  for cell in value.cells {
    let x = 28 + cell.x * 44
    let cy = y + 34 + cell.y * 44
    let fill = if cell.blocked {
      "#1e293b"
    } else {
      highlight_color(scene, value.id, cell.id)
    }
    svg_box(out, x, cy, 40, 40, fill)
    if cell.label != "" {
      svg_text(
        out,
        x + 20,
        cy + 25,
        cell.label,
        11,
        if cell.blocked {
          "#f8fafc"
        } else {
          "#0f172a"
        },
        anchor="middle",
      )
    }
  }
  y + 56 + value.height * 44
}

///|
priv struct GraphLayoutPoint {
  id : String
  x : Int
  y : Int
}

///|
fn graph_point(
  points : Array[GraphLayoutPoint],
  id : String,
) -> GraphLayoutPoint {
  points[points.search_by(fn(point) { point.id == id }).unwrap()]
}

///|
fn layout_graph(graph : GraphState) -> Array[GraphLayoutPoint] {
  let count = graph.nodes.length()
  let indegree = Array::make(count, 0)
  let level = Array::make(count, 0)
  for edge in graph.edges {
    if edge.from != edge.to {
      let to = graph.nodes.search_by(fn(node) { node.id == edge.to }).unwrap()
      indegree[to] += 1
    }
  }
  let queue : Array[Int] = []
  for i in 0.. String {
  let target = TargetRef::entity(object_id, entity_id)
  match scene.highlights.search_by(fn(item) { item.target == target }) {
    None => "#f8fafc"
    Some(index) =>
      match scene.highlights[index].role {
        Current => "#f59e0b"
        Candidate => "#38bdf8"
        Compared => "#c084fc"
        Changed => "#fb7185"
        Visited => "#93c5fd"
        Frontier => "#22d3ee"
        Result => "#4ade80"
        Error => "#ef4444"
      }
  }
}

///|
fn svg_box(
  out : StringBuilder,
  x : Int,
  y : Int,
  width : Int,
  height : Int,
  fill : String,
) -> Unit {
  out.write_string(
    "\n",
  )
}

///|
fn svg_text(
  out : StringBuilder,
  x : Int,
  y : Int,
  text : String,
  size : Int,
  color : String,
  anchor? : String = "start",
) -> Unit {
  out.write_string(
    "\{escape_markup(text)}\n",
  )
}

///|
fn escape_markup(text : String) -> String {
  let out = StringBuilder()
  for char in text {
    match char {
      '&' => out.write_string("&")
      '<' => out.write_string("<")
      '>' => out.write_string(">")
      '"' => out.write_string(""")
      '\'' => out.write_string("'")
      _ => out.write_char(char)
    }
  }
  out.to_string()
}