///|
/// Project every Mermaid arrow onto independent line and end values.
fn arrow_ends(m : @ast.MessageStatement) -> (@model.LineStyle, @model.Head, @model.Head) {
  let line : @model.LineStyle = if m.arrow.contains("--") { Dotted } else { Solid }
  let mut sender : @model.Head = NoHead
  let mut receiver : @model.Head = NoHead
  match m.arrow {
    "->>" | "-->>" => receiver = Arrow
    "<<->>" | "<<-->>" => { sender = Arrow; receiver = Arrow }
    "->" | "-->" => receiver = OpenArrow
    "-x" | "--x" => receiver = Cross
    "-)" | "--)" => receiver = Async
    "-|\\" | "--|\\" => receiver = HalfArrow(upper=true, filled=true)
    "-|/" | "--|/" => receiver = HalfArrow(upper=false, filled=true)
    "-\\\\" | "--\\\\" => receiver = HalfArrow(upper=true, filled=false)
    "-//" | "--//" => receiver = HalfArrow(upper=false, filled=false)
    "/|-" | "/|--" => sender = HalfArrow(upper=true, filled=true)
    "\\|-" | "\\|--" => sender = HalfArrow(upper=false, filled=true)
    "//-" | "//--" => sender = HalfArrow(upper=true, filled=false)
    "\\\\-" | "\\\\--" => sender = HalfArrow(upper=false, filled=false)
    _ => ()
  }
  if m.central_before { sender = Central(sender) }
  if m.central_after { receiver = Central(receiver) }
  (line, sender, receiver)
}

///|
fn participant_kind(text : String) -> @model.ParticipantKind? {
  match text {
    "participant" => Some(Participant)
    "actor" => Some(Actor)
    "boundary" => Some(Boundary)
    "control" => Some(Control)
    "entity" => Some(Entity)
    "database" => Some(Database)
    "collections" => Some(Collections)
    "queue" => Some(Queue)
    "service" => Some(Service)
    "resource" => Some(Resource)
    "client" => Some(Client)
    _ => None
  }
}

///|
/// Read the scalar type and alias fields of Mermaid's flow-style metadata.
/// JSON strings use JSON escapes; YAML single-quoted and plain scalars are
/// accepted too. Separators inside a quoted scalar are part of its value.
fn metadata_fields(text : String) -> Map[String, String]? {
  let fields = Map([])
  let entries = []
  let mut start = 0
  let mut quote = 0
  let mut escaped = false
  let mut depth = 0
  let mut i = 0
  while i < text.length() {
    let c = text.code_unit_at(i).to_int()
    if quote != 0 {
      if escaped { escaped = false }
      else if c == 92 && quote == 34 { escaped = true }
      else if c == quote {
        if quote == 39 && i + 1 < text.length() && text.code_unit_at(i + 1).to_int() == 39 {
          i += 1
        } else { quote = 0 }
      }
    } else if c == 34 || c == 39 { quote = c }
    else if c == 123 || c == 91 { depth += 1 }
    else if c == 125 || c == 93 { depth -= 1 }
    else if (c == 44 || c == 10) && depth == 0 {
      entries.push(text.view(start_offset=start, end_offset=i).to_owned())
      start = i + 1
    }
    i += 1
  }
  guard quote == 0 && depth == 0 else { return None }
  entries.push(text.view(start_offset=start).to_owned())
  fn scalar(raw : String) -> String? {
    let raw = raw.trim().to_owned()
    if raw.has_prefix("\"") {
      match json_value(raw) { Some(String(s)) => Some(s); _ => None }
    } else if raw.has_prefix("'") && raw.has_suffix("'") && raw.length() >= 2 {
      Some(raw.view(start_offset=1, end_offset=raw.length() - 1).to_owned().replace_all(old="''", new="'"))
    } else if raw != "" && !raw.has_prefix("{") && !raw.has_prefix("[") {
      Some(raw)
    } else { None }
  }
  for entry in entries {
    if entry.trim() == "" { continue }
    guard entry.find(":") is Some(at) else { return None }
    guard scalar(entry.view(end_offset=at).to_owned()) is Some(key) else { return None }
    if key == "type" || key == "alias" {
      guard scalar(entry.view(start_offset=at + 1).to_owned()) is Some(value) else { return None }
      fields[key] = value
    }
  }
  Some(fields)
}

///|
fn participant_metadata(config : String?) -> (@model.ParticipantKind?, String?)? {
  guard config is Some(text) else { return Some((None, None)) }
  guard metadata_fields(text) is Some(fields) else { return None }
  let kind = match fields.get("type") {
    Some(text) => match participant_kind(text) { Some(k) => Some(k); None => return None }
    None => None
  }
  Some((kind, fields.get("alias")))
}

///|
/// Separate the optional leading CSS colour from the box title. Keeping the
/// spelling lets the renderer parse and diagnose the colour in its own layer.
fn box_data(text : String) -> (String, String?) {
  let text = text.trim().to_owned()
  let lower = text.to_lower()
  let mut at = text.length()
  for i = 0; i < text.length(); i = i + 1 {
    if text.code_unit_at(i) == 32 || text.code_unit_at(i) == 9 { at = i; break }
  }
  let colour_function = lower.has_prefix("rgb(") || lower.has_prefix("rgba(") || lower.has_prefix("hsl(") || lower.has_prefix("hsla(")
  if colour_function {
    at = text.find(")").map(i => i + 1).unwrap_or(at)
  }
  let first = text.view(end_offset=at).to_owned()
  if colour_names.contains(first.to_lower()) || first.has_prefix("#") || (colour_function && first.has_suffix(")")) {
    (text.view(start_offset=at).trim().to_owned(), Some(first))
  } else { (text, None) }
}

///|
let colour_names : ReadOnlyArray[String] = [
  "aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque", "black",
  "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue", "chartreuse", "chocolate",
  "coral", "cornflowerblue", "cornsilk", "crimson", "cyan", "darkblue", "darkcyan", "darkgoldenrod",
  "darkgray", "darkgreen", "darkgrey", "darkkhaki", "darkmagenta", "darkolivegreen", "darkorange", "darkorchid",
  "darkred", "darksalmon", "darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise", "darkviolet",
  "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick", "floralwhite", "forestgreen",
  "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod", "gray", "green", "greenyellow",
  "grey", "honeydew", "hotpink", "indianred", "indigo", "ivory", "khaki", "lavender",
  "lavenderblush", "lawngreen", "lemonchiffon", "lightblue", "lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray",
  "lightgreen", "lightgrey", "lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray", "lightslategrey",
  "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta", "maroon", "mediumaquamarine",
  "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen", "mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred",
  "midnightblue", "mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive",
  "olivedrab", "orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise", "palevioletred",
  "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple", "rebeccapurple",
  "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown", "seagreen", "seashell",
  "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey", "snow", "springgreen",
  "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet", "wheat",
  "white", "whitesmoke", "yellow", "yellowgreen", "transparent", "currentcolor", "accentcolor", "accentcolortext",
  "activetext", "buttonborder", "buttonface", "buttontext", "canvas", "canvastext", "field", "fieldtext",
  "graytext", "highlight", "highlighttext", "linktext", "mark", "marktext", "selecteditem", "selecteditemtext",
  "visitedtext", "activeborder", "activecaption", "appworkspace", "background", "buttonhighlight", "buttonshadow", "captiontext",
  "inactiveborder", "inactivecaption", "inactivecaptiontext", "infobackground", "infotext", "menu", "menutext", "scrollbar",
  "threeddarkshadow", "threedface", "threedhighlight", "threedlightshadow", "threedshadow", "window", "windowframe", "windowtext",
  "inherit", "initial", "unset", "revert", "revert-layer",
]