///|
/// The font stack of archify. The SVG has no font data.
let font_stack = "'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, Consolas, 'DejaVu Sans Mono', 'Liberation Mono', monospace"

///|
/// The CSS custom properties of `p`, one on each line with `indent`.
fn variables(p : @theme.Palette, indent : String) -> String {
  let buf = StringBuilder()
  let put = (name : String, c : @theme.Color) => buf.write_string(
    "\{indent}--\{name}: \{c.to_css()};\n",
  )
  put("bg", p.bg)
  put("text", p.text)
  put("muted", p.muted)
  put("mask", p.mask)
  put("arrow", p.arrow)
  for f in @theme.all_families {
    let c = p.family(f)
    put("\{f.name()}-fill", c.fill)
    put("\{f.name()}-stroke", c.stroke)
    put("\{f.name()}-ink", c.ink)
  }
  buf.to_string()
}

///|
/// The style sheet of the adaptive mode. Dark is the default. The viewer's
/// light preference selects light, and a `data-theme` attribute on the root
/// overrides both.
fn style_sheet(t : @theme.Theme, selector? : String = "svg") -> String {
  let buf = StringBuilder()
  buf.write_string("\n\{selector} {\n\{variables(t.dark, "  ")}}\n")
  buf.write_string(
    "@media (prefers-color-scheme: light) {\n  \{selector} {\n\{variables(t.light, "    ")}  }\n}\n",
  )
  buf.write_string("\{selector}[data-theme=\"dark\"] {\n\{variables(t.dark, "  ")}}\n")
  buf.write_string("\{selector}[data-theme=\"light\"] {\n\{variables(t.light, "  ")}}\n")
  buf.to_string()
}

///|
/// The palette selected by fixed output.
pub(all) enum Variant { Dark; Light } derive(Eq, Debug)

///|
/// Adaptive uses CSS variables; fixed writes literal presentation attributes.
pub(all) enum Mode { Adaptive; Fixed(Variant) } derive(Eq, Debug)

///|
priv struct Paint {
  theme : @theme.Theme
  mode : Mode
}

///|
fn Paint::color(self : Paint, token : String) -> String {
  match self.mode {
    Adaptive => "var(--\{token})"
    Fixed(v) => {
      let p = match v { Dark => self.theme.dark; Light => self.theme.light }
      let c = match token {
        "bg" => p.bg; "mask" => p.mask; "text" => p.text; "muted" => p.muted; "arrow" => p.arrow
        _ => {
          let mut found = p.text
          for f in @theme.all_families {
            let c = p.family(f)
            if token == "\{f.name()}-fill" { found = c.fill }
            if token == "\{f.name()}-stroke" { found = c.stroke }
            if token == "\{f.name()}-ink" { found = c.ink }
          }
          found
        }
      }
      c.to_css()
    }
  }
}

///|
fn Paint::ink(self : Paint, ink : @layout.Ink) -> String {
  match ink {
    Primary => self.color("text")
    Muted => self.color("muted")
    Accent(f) => self.color("\{f.name()}-ink")
    Color(c) => c.to_css()
  }
}

///|
fn rect(class : String, r : @layout.Rect, radius : Int, fill : String, stroke? : String) -> @writer.Node {
  let attrs = [("class", class), ("x", r.x.to_string()), ("y", r.y.to_string()),
    ("width", r.width.to_string()), ("height", r.height.to_string()), ("rx", radius.to_string()), ("fill", fill)]
  if stroke is Some(s) { attrs.push(("stroke", s)); attrs.push(("stroke-width", "1.5")) }
  @writer.element("rect", attrs~)
}

///|
fn path(class : String, d : String, stroke : String, dash? : String, width? : String = "1.4") -> @writer.Node {
  let attrs = [("class", class), ("d", d), ("fill", "none"), ("stroke", stroke), ("stroke-width", width)]
  if dash is Some(d) { attrs.push(("stroke-dasharray", d)) }
  @writer.element("path", attrs~)
}

///|
fn segment(s : @layout.Segment) -> String { "M\{s.x1} \{s.y1} L\{s.x2} \{s.y2}" }

///|
fn Paint::label(self : Paint, class : String, l : @layout.Label) -> @writer.Node {
  let children = l.content.split("\n").to_array().mapi((i, line) => {
    @writer.element("tspan", attrs=[("x", l.x.to_string()), ("y", (l.baseline + i * (l.size + 4)).to_string())], children=[@writer.text(line.to_owned())])
  })
  @writer.element("text", attrs=[("class", class), ("x", l.x.to_string()), ("y", l.baseline.to_string()),
    ("text-anchor", "middle"), ("font-family", font_stack), ("font-size", l.size.to_string()),
    ("font-weight", l.weight.to_string()), ("fill", self.ink(l.ink))], children~)
}

///|
fn metadata(attrs : Array[(String, String)], link : @sequence.Link?, description : String?) -> Unit {
  if link is Some(l) {
    let scheme = match l.scheme { Smithy => "smithy"; OpenRpc => "openrpc" }
    attrs.push(("data-link", "\{scheme}:\{l.target}"))
  }
  if description is Some(d) { attrs.push(("data-description", d)) }
}

///|
fn head_name(h : @sequence.Head) -> String {
  match h {
    NoHead => "none"; Arrow => "arrow"; OpenArrow => "open"; Cross => "cross"; Async => "async"
    HalfArrow(upper~, filled~) => "half-\{if upper { "upper" } else { "lower" }}-\{if filled { "filled" } else { "open" }}"
    Central(h) => "central-\{head_name(h)}"
  }
}

///|
/// Markers point along the path; auto-start-reverse turns a sender head back.
fn marker(id : String, head : @sequence.Head, stroke : String, reversed : Bool) -> @writer.Node {
  let (d, filled) = match head {
    Arrow => ("M-9,-4 L0,0 L-9,4 z", true)
    OpenArrow => ("M-9,-4 L0,0 L-9,4", false)
    Cross => ("M-4,-4 L4,4 M-4,4 L4,-4", false)
    Async => ("M-9,-4 L0,0", false)
    HalfArrow(upper~, filled~) => {
      let upper = if reversed { !upper } else { upper }
      ((if upper { "M-9,-4 L0,0" } else { "M-9,4 L0,0" }) + (if filled { " L-9,0 z" } else { "" }), filled)
    }
    Central(h) => return marker(id, h, stroke, reversed)
    NoHead => ("", false)
  }
  @writer.element("marker", attrs=[("id", id), ("viewBox", "-10 -5 15 10"), ("refX", "0"), ("refY", "0"),
    ("markerWidth", "15"), ("markerHeight", "10"), ("markerUnits", "userSpaceOnUse"), ("orient", "auto-start-reverse")], children=[
    @writer.element("path", attrs=[("d", d), ("fill", if filled { stroke } else { "none" }), ("stroke", stroke), ("stroke-width", "1.4")]),
  ])
}

///|
fn description(s : @sequence.Sequence, m : Int) -> String {
  let ids = s.participants.map(p => p.id).iter().to_array()
  let n = ids.length()
  let parts = if n == 1 { "1 participant" } else { "\{n} participants" }
  let list = if n > 0 { " (\{ids.join(", ")})" } else { "" }
  let msgs = if m == 1 { "1 message" } else { "\{m} messages" }
  "Sequence diagram with \{parts}\{list} and \{msgs}."
}

///|
fn paint(sequence : @sequence.Sequence, theme : @theme.Theme, mode : Mode, id_prefix : String?) -> (String, Array[@diagrams.Diagnostic]) {
  let g = @layout.layout(sequence)
  let p = Paint::{ theme, mode }
  let diagnostics : Array[@diagrams.Diagnostic] = []
  let defs : Array[@writer.Node] = []
  if mode == Adaptive { defs.push(@writer.element("style", children=[@writer.text(style_sheet(theme))])) }
  let bg = p.color("bg")
  let mask = p.color("mask")
  let arrow = p.color("arrow")
  let backgrounds : Array[@writer.Node] = []
  let foregrounds : Array[@writer.Node] = []
  // A theme-background mask makes every author tint independent of nesting.
  fn band(class : String, box : @layout.Rect, colour : String?) -> Unit {
    backgrounds.push(rect("band-mask", box, 6, bg))
    let fill = match colour {
      Some(raw) => match @theme.parse_color(raw) {
        Some(c) => c.tint().to_css()
        None => {
          diagnostics.push(@diagrams.Diagnostic::warning("svg/invalid-colour", "Cannot read colour '\{raw}'; using the theme colour."))
          p.color("external-fill")
        }
      }
      None => p.color("external-fill")
    }
    backgrounds.push(rect(class, box, 6, fill, stroke=arrow))
  }
  for group in g.groups {
    band("box-group", group.box, group.group.colour)
    foregrounds.push(rect("mask", group.chip, 3, mask))
    foregrounds.push(p.label("group-label", group.label))
  }
  for frame in g.frames {
    band("frame", frame.box, frame.colour)
    if frame.chip is Some(c) { foregrounds.push(rect("mask", c, 3, mask)) }
    if frame.header is Some(l) { foregrounds.push(p.label("frame-label", l)) }
    for section in frame.sections {
      if section.divider is Some(s) { foregrounds.push(path("section-divider", segment(s), arrow, dash="4 4")) }
      if section.chip is Some(c) { foregrounds.push(rect("mask", c, 3, mask)) }
      if section.label is Some(l) { foregrounds.push(p.label("section-label", l)) }
    }
  }
  let lifelines : Array[@writer.Node] = []
  for participant in g.participants {
    lifelines.push(path("lifeline", "M\{participant.lifeline_x} \{participant.lifeline_top} V\{participant.lifeline_bottom}", arrow, dash="3 7", width="0.8"))
    if participant.destroyed {
      let x = participant.lifeline_x
      let y = participant.lifeline_bottom
      foregrounds.push(path("destruction", "M\{x - 6} \{y - 6} L\{x + 6} \{y + 6} M\{x - 6} \{y + 6} L\{x + 6} \{y - 6}", p.color("security-stroke")))
    }
  }
  let bars : Array[@writer.Node] = []
  for a in g.activations {
    let mut family : @theme.Family = Backend
    for part in g.participants { if part.id == a.participant { family = part.family } }
    bars.push(rect("activation-mask", a.box, 0, mask))
    bars.push(rect("activation", a.box, 0, p.color("\{family.name()}-fill"), stroke=p.color("\{family.name()}-stroke")))
  }
  let messages : Array[@writer.Node] = []
  for m in g.messages {
    let family = m.family
    let stroke = if m.message.line == Dotted && family != Security { arrow } else { p.color("\{family.name()}-stroke") }
    let attrs = [("class", "message f-\{family.name()}"), ("data-index", m.index.to_string()), ("data-from", m.sender), ("data-to", m.receiver),
      ("data-sender-head", head_name(m.message.sender_head)), ("data-receiver-head", head_name(m.message.receiver_head))]
    metadata(attrs, m.message.link, m.message.properties.bind(props => props.description))
    let d = StringBuilder()
    for i, s in m.path {
      if i == 0 { d.write_string("M\{s.x1} \{s.y1}") }
      d.write_string(" L\{s.x2} \{s.y2}")
    }
    let line_attrs = [("class", "m-line"), ("d", d.to_string()), ("fill", "none"), ("stroke", stroke), ("stroke-width", "1.4")]
    if m.message.line == Dotted { line_attrs.push(("stroke-dasharray", "4 4")) }
    for (end, head) in [("start", m.message.sender_head), ("end", m.message.receiver_head)] {
      if head != NoHead && head != Central(NoHead) {
        let id = "head-\{m.index}-\{end}"
        let edge = if end == "start" { m.path[0] } else { m.path[m.path.length() - 1] }
        let reversed = if end == "start" { edge.x2 > edge.x1 } else { edge.x2 < edge.x1 }
        defs.push(marker(id, head, stroke, reversed))
        line_attrs.push(("marker-\{end}", "url(#\{id})"))
      }
    }
    let children = [@writer.element("path", attrs=line_attrs), rect("mask", m.chip, 3, mask), p.label("m-label", m.label)]
    for (id, head, y) in [(m.sender, m.message.sender_head, m.y), (m.receiver, m.message.receiver_head, m.path[m.path.length() - 1].y2)] {
      if head is Central(_) {
        for part in g.participants {
          if part.id == id { children.push(@writer.element("circle", attrs=[("class", "central-connection"), ("cx", part.lifeline_x.to_string()), ("cy", y.to_string()), ("r", "5"), ("fill", mask), ("stroke", stroke), ("stroke-width", "1.4")])) }
        }
      }
    }
    if m.sublabel is Some(l) { children.push(p.label("sublabel", l)) }
    messages.push(@writer.element("g", attrs~, children~))
  }
  let notes : Array[@writer.Node] = []
  for n in g.notes {
    notes.push(rect("note-mask", n.box, 4, mask))
    notes.push(rect("note", n.box, 4, p.color("external-fill"), stroke=p.color("external-stroke")))
    notes.push(p.label("note-label", n.label))
  }
  let participants : Array[@writer.Node] = []
  for part in g.participants {
    let attrs = [("class", "participant f-\{part.family.name()}"), ("data-id", part.id)]
    metadata(attrs, part.participant.link, part.participant.properties.bind(props => props.description))
    let children = [rect("mask", part.box, 6, mask), rect("box", part.box, 6, p.color("\{part.family.name()}-fill"), stroke=p.color("\{part.family.name()}-stroke")), p.label("p-label", part.label)]
    if part.sublabel is Some(l) { children.push(p.label("sublabel", l)) }
    participants.push(@writer.element("g", attrs~, children~))
  }
  if g.title_box is Some(b) { foregrounds.push(rect("mask", b, 3, mask)) }
  if g.title is Some(l) { foregrounds.push(p.label("diagram-title", l)) }
  let root = @writer.element("svg", attrs=[("xmlns", "http://www.w3.org/2000/svg"), ("viewBox", "0 0 \{g.width} \{g.height}"),
    ("width", g.width.to_string()), ("height", g.height.to_string()), ("role", "img"), ("data-diagram", "sequence")], children=[
    @writer.element("title", children=[@writer.text(sequence.accessibility_title.unwrap_or(sequence.title.unwrap_or("Sequence diagram")))]),
    @writer.element("desc", children=[@writer.text(sequence.accessibility_description.unwrap_or(description(sequence, g.messages.length())))]),
    @writer.element("defs", children=defs),
    rect("bg", { x: 0, y: 0, width: g.width, height: g.height }, 0, bg),
    @writer.element("g", attrs=[("class", "bands")], children=backgrounds),
    @writer.element("g", attrs=[("class", "lifelines")], children=lifelines),
    @writer.element("g", attrs=[("class", "activations")], children=bars),
    @writer.element("g", attrs=[("class", "messages")], children=messages),
    @writer.element("g", attrs=[("class", "notes")], children=notes),
    @writer.element("g", attrs=[("class", "participants")], children=participants),
    @writer.element("g", attrs=[("class", "annotations")], children=foregrounds),
  ])
  let palettes = match mode { Adaptive => [("dark", theme.dark), ("light", theme.light)]; Fixed(Dark) => [("dark", theme.dark)]; Fixed(Light) => [("light", theme.light)] }
  for (name, palette) in palettes {
    for f in @layout.check(g, palette) { diagnostics.push(@diagrams.Diagnostic::warning("svg/legibility", "\{name}: \{f.message}")) }
  }
  let prefix = match id_prefix {
    Some(value) => {
      let encoded = StringBuilder()
      for c in value { encoded.write_string("\{c.to_int().to_string(radix=16)}-") }
      "seq-u-\{encoded.to_string()}"
    }
    None => {
      // FNV-1a over UTF-16 units keeps IDs deterministic on every backend.
      // Hash the complete unscoped output, including both palettes and mode.
      let source = @writer.to_string(root)
      let mut hash : UInt64 = 14695981039346656037
      for i in 0.. String {
  paint(sequence, theme, mode, id_prefix).0
}

///|
/// A renderer that writes one standalone SVG document.
pub struct SvgRenderer { theme : @theme.Theme; mode : Mode; id_prefix : String? }

///|
pub fn SvgRenderer::new(theme? : @theme.Theme = @theme.Theme::classic(), mode? : Mode = Adaptive, id_prefix? : String) -> SvgRenderer { { theme, mode, id_prefix } }

///|
pub impl @render.Renderer for SvgRenderer with render(self, diagram) {
  match diagram { Sequence(s) => {
    let (svg, diagnostics) = paint(s, self.theme, self.mode, self.id_prefix)
    @render.Rendered::ok(svg, "image/svg+xml", diagnostics~)
  } }
}

///|
pub extend SvgRenderer with @render.Renderer::{render}

///|
pub extend Variant with Eq::{not_equal, equal}

///|
pub extend Variant with @debug.Debug::{to_repr}

///|
pub extend Mode with Eq::{not_equal, equal}

///|
pub extend Mode with @debug.Debug::{to_repr}

///|
/// Rewrite only generated identifiers and fragment references, never user text.
/// Scope the stylesheet too, so adjacent SVGs can have different custom themes.
fn scope_svg(node : @writer.Node, prefix : String, theme : @theme.Theme) -> @writer.Node {
  match node {
    Text(_) => node
    Element(name, attributes, children) => {
      let attrs = attributes.map(pair => {
        let (key, value) = pair
        let value = if key == "id" { prefix + value }
          else if key == "marker-start" || key == "marker-end" {
            "url(#\{prefix}\{value.view(start_offset=5, end_offset=value.length() - 1)})"
          } else { value }
        (key, value)
      }).iter().to_array()
      if name == "svg" { attrs.push(("id", prefix + "diagram")) }
      let children = if name == "style" {
        [@writer.text(style_sheet(theme, selector="svg#\{prefix}diagram"))]
      } else { children.map(c => scope_svg(c, prefix, theme)).iter().to_array() }
      @writer.element(name, attrs~, children~)
    }
  }
}