///|
fn quoted(value : String) -> String {
  "\"" +
  value
  .replace_all(old="\\", new="\\\\")
  .replace_all(old="\"", new="\\\"")
  .replace_all(old="\n", new="\\n") +
  "\""
}

///|
fn Endpoint::text(self : Endpoint) -> String {
  match self {
    Start(top) => if top { "(*top)" } else { "(*)" }
    Final => "(*)"
    Action(text, code, decoration) =>
      quoted(text) +
      code.map(code => " as " + code).unwrap_or("") +
      optional_text(decoration, " ", "")
    Alias(code) => code
    Synchronization(code) => "===" + code + "==="
  }
}

///|
fn optional_text(value : String, before : String, after : String) -> String {
  if value.is_empty() {
    ""
  } else {
    before + value + after
  }
}

///|
fn Kind::text(self : Kind) -> String {
  match self {
    Action(text, end, decoration) =>
      if decoration.has_prefix("after:") {
        ":" + text + end + " " + after(decoration, "after:")
      } else {
        decoration + ":" + text + end
      }
    Start => "start"
    Stop => "stop"
    End => "end"
    If(condition, label) =>
      "if (" + condition + ") then" + optional_text(label, " (", ")")
    ElseIfIncoming(incoming, condition, label) =>
      "(" +
      incoming +
      ") elseif (" +
      condition +
      ") then" +
      optional_text(label, " (", ")")
    ElseIf(condition, label) =>
      "elseif (" + condition + ") then" + optional_text(label, " (", ")")
    Else(label) => "else" + optional_text(label, " (", ")")
    EndIf => "endif"
    Switch(condition) => "switch (" + condition + ")"
    Case(label) => "case (" + label + ")"
    EndSwitch => "endswitch"
    While(condition, label) =>
      "while (" + condition + ")" + optional_text(label, " is (", ")")
    EndWhile(label) => "endwhile" + optional_text(label, " (", ")")
    Repeat(action) => "repeat" + optional_text(action, " ", "")
    RepeatWhile(condition, yes, no) =>
      "repeat while (" +
      condition +
      ")" +
      optional_text(yes, " is (", ")") +
      optional_text(no, " not (", ")")
    Backward(text) => "backward:" + text + ";"
    Break => "break"
    Fork => "fork"
    ForkAgain => "fork again"
    EndFork(merge, label) =>
      if merge {
        "end merge"
      } else {
        "end fork" + optional_text(label, " {", "}")
      }
    Split => "split"
    SplitAgain => "split again"
    EndSplit => "end split"
    Kill => "kill"
    Detach => "detach"
    Connector(label) => "(" + label + ")"
    Label(name) => "label " + name
    Goto(name) => "goto " + name
    Group(kind, label, before, decoration) =>
      kind +
      " " +
      optional_text(before, "", " ") +
      quoted(label) +
      optional_text(decoration, " ", "") +
      " {"
    EndGroup => "}"
    Lane(code, label, color) =>
      "|" +
      optional_text(color, "", "|") +
      code +
      "|" +
      (if label == code { "" } else { " " + label })
    Note(placement, text, code, decoration) =>
      (if placement == "floating" {
        "floating note"
      } else {
        "note " + placement
      }) +
      code.map(code => " as " + code).unwrap_or("") +
      optional_text(decoration, " ", "") +
      "\n" +
      text +
      "\nend note"
    NoteOnLink(text, decoration) =>
      "note on link" +
      optional_text(decoration, " ", "") +
      "\n" +
      text +
      "\nend note"
    Arrow(_, text) => text + (if text.has_suffix(";") { "" } else { ";" })
    Edge(from, arrow, target, label) =>
      from.map(e => e.text() + " ").unwrap_or("") +
      arrow +
      (if label.is_empty() { " " } else { "[" + label + "] " }) +
      target.text()
    ActionList(text, depth) => "*".repeat(depth) + " " + text
    LegacyDecision(from, arrow, condition, label) =>
      from.map(e => e.text() + " ").unwrap_or("") +
      arrow +
      optional_text(label, "[", "]") +
      " if " +
      quoted(condition) +
      " then"
    LegacyIf(condition) => "if " + quoted(condition) + " then"
    Presentation(text) => text
  }
}

///|
/// Write the body from typed values. Invalid delimiters cannot be represented
/// safely and return a finding rather than stale source.
pub fn print_body(
  document : Document,
  limits~ : @envelope.Limits = @envelope.Limits::new(),
) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
  fn fail(
    message : String,
  ) -> Result[String, ReadOnlyArray[@diagrams.Diagnostic]] {
    Err([@diagrams.Diagnostic::error("activity/invalid-ast", message)])
  }
  if !limits.valid() || document.items.length() > limits.nodes {
    return fail("Invalid limits or too many activity statements")
  }
  let out = StringBuilder()
  let mut used = 0
  for statement in document.items {
    let text = statement.kind.text()
    used += text.length() + 1
    if used > limits.output || used > limits.work {
      return Err([
        @diagrams.Diagnostic::error(
          "activity/limit", "Activity print limit exceeded",
        ),
      ])
    }
    if text.contains("@start") || text.contains("@end") {
      return fail("Block markers are not activity values")
    }
    match statement.kind {
      Action(_, end, _) =>
        if ![";", "]", "}", "/", "|", ">", "<"].contains(end) {
          return fail("Invalid action delimiter")
        }
      _ => ()
    }
    out.write_string(text)
    out.write_char('\n')
  }
  let output = out.to_string()
  let parsed = read(
    @envelope.scan_body(Activity, output, limits~),
    limits~,
    dialect=document.dialect,
  )
  if parsed.diagnostics.iter().any(d => d.severity == Error) {
    return fail("Typed activity values cannot be read in their dialect")
  }
  match parsed.document {
    Some(value) if value.items.iter().map(s => s.kind).to_array() ==
      document.items.iter().map(s => s.kind).to_array() => Ok(output)
    _ => fail("Typed activity values are not canonically representable")
  }
}