///|
/// One part of a parsed message template: literal text or a named hole.
priv enum Part {
  Literal(String)
  Hole(String)
}

///|
/// Parse a message template into literal text and named holes.
///
/// `{{` is a literal `{` and `}}` is a literal `}`. A `{`, one or more name
/// characters (ASCII letters, digits and `_`) and a `}` make a hole. Each other
/// brace is literal text: an unclosed `{`, an empty `{}`, a name with another
/// character, and a single `}`. A parse never fails.
fn parse_template(template : String) -> Array[Part] {
  let chars = template.to_array()
  let parts = []
  let text = StringBuilder()
  let flush = () => {
    if !text.is_empty() {
      parts.push(Literal(text.to_string()))
      text.reset()
    }
  }
  let mut i = 0
  while i < chars.length() {
    let char = chars[i]
    if char == '{' && i + 1 < chars.length() && chars[i + 1] == '{' {
      text.write_char('{')
      i += 2
    } else if char == '}' && i + 1 < chars.length() && chars[i + 1] == '}' {
      text.write_char('}')
      i += 2
    } else if char == '{' {
      let mut end = i + 1
      while end < chars.length() && is_hole_name_char(chars[end]) {
        end += 1
      }
      if end > i + 1 && end < chars.length() && chars[end] == '}' {
        flush()
        parts.push(Hole(String::from_array(chars[i + 1:end])))
        i = end + 1
      } else {
        text.write_char('{')
        i += 1
      }
    } else {
      text.write_char(char)
      i += 1
    }
  }
  flush()
  parts
}

///|
/// An ASCII letter, an ASCII digit or `_`.
fn is_hole_name_char(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') ||
  (char >= 'A' && char <= 'Z') ||
  (char >= '0' && char <= '9') ||
  char == '_'
}

///|
/// Pair the holes of `parts` with `args` in order. A hole with no argument gives
/// no property, and an argument with no hole is dropped. Each value is a copy, so
/// a later change to an argument does not change the event.
fn bind_properties(parts : Array[Part], args : Array[Json]) -> Array[(String, Json)] {
  let properties = []
  for part in parts {
    guard part is Hole(name) else { continue }
    guard properties.length() < args.length() else { break }
    properties.push((name, copy_json(args[properties.length()])))
  }
  properties
}

///|
/// The text of a property value in a message: a JSON string without quotes, and
/// each other value as compact JSON.
fn render_value(value : Json) -> String {
  match value {
    String(text) => text
    _ => value.stringify()
  }
}

///|
/// The message text: the template with each hole replaced by the text of its
/// property value. A hole whose name has no property stays as its literal text.
/// When a name occurs in more than one hole, each of them shows the first value.
pub fn LogEvent::message(self : LogEvent) -> String {
  let out = StringBuilder()
  for part in parse_template(self.template) {
    match part {
      Literal(text) => out.write_string(text)
      Hole(name) =>
        match self.find(name) {
          Some(value) => out.write_string(render_value(value))
          None => out.write_string("{\{name}}")
        }
    }
  }
  out.to_string()
}