///|
/// Renders a snapshot and highlights every audit finding in a self-contained SVG.
pub fn render_audit_svg(
  snapshot : LayoutSnapshot,
  report : AuditReport,
) -> String {
  let mut output = "\n"
  output = output + "MoonLayout Audit report\n"
  output = output + "" + escape_xml(report_summary(report)) + "\n"
  output = output +
    "\n"
  output = output + "\n"
  for edge in snapshot.edges {
    match edge_points_for_svg(snapshot, edge) {
      Some((source, target)) => {
        let problem = edge_has_violation(report, edge_key_for_svg(edge))
        let color = if problem { "#ff6b6b" } else { "#58c4dd" }
        let marker = if problem { "arrow-error" } else { "arrow-ok" }
        output = output +
          "\n"
      }
      None => ()
    }
  }
  for node in snapshot.nodes {
    let x = node_left_for_svg(node)
    let y = node_top_for_svg(node)
    let color = if node_has_violation(report, node.id) {
      "#ff6b6b"
    } else {
      "#f3a847"
    }
    output = output +
      "" +
      "" +
      escape_xml(node.id) +
      "\n"
  }
  output = output +
    "" +
    escape_xml(report_summary(report)) +
    "\n"
  output + "\n"
}

///|
fn report_summary(report : AuditReport) -> String {
  "status=" +
  (if report.passed { "PASS" } else { "FAIL" }) +
  " overlaps=" +
  report.overlap_pairs.to_string() +
  " crossings=" +
  report.edge_crossings.to_string() +
  " out_of_bounds=" +
  report.out_of_bounds.to_string()
}

///|
fn edge_points_for_svg(
  snapshot : LayoutSnapshot,
  edge : Edge,
) -> (PositionedNode, PositionedNode)? {
  match
    (
      find_node_for_svg(snapshot.nodes, edge.source),
      find_node_for_svg(snapshot.nodes, edge.target),
    ) {
    (Some(source), Some(target)) => Some((source, target))
    _ => None
  }
}

///|
fn find_node_for_svg(
  nodes : Array[PositionedNode],
  id : String,
) -> PositionedNode? {
  for node in nodes {
    if node.id == id {
      return Some(node)
    }
  }
  None
}

///|
fn edge_key_for_svg(edge : Edge) -> String {
  edge.source + "->" + edge.target
}

///|
fn edge_has_violation(report : AuditReport, key : String) -> Bool {
  for violation in report.violations {
    if violation.kind == "edge_crossing" &&
      (violation.first == key || violation.second == key) {
      return true
    }
  }
  false
}

///|
fn node_has_violation(report : AuditReport, id : String) -> Bool {
  for violation in report.violations {
    if violation.first == id || violation.second == id {
      return true
    }
  }
  false
}

///|
fn node_left_for_svg(node : PositionedNode) -> Int {
  node.x - node.width / 2
}

///|
fn node_top_for_svg(node : PositionedNode) -> Int {
  node.y - node.height / 2
}

///|
fn escape_xml(input : String) -> String {
  input
  .replace_all(old="&", new="&")
  .replace_all(old="<", new="<")
  .replace_all(old=">", new=">")
  .replace_all(old="\"", new=""")
}